├── .gitignore
├── README.rst
├── example
├── base.css
├── code.lisp
└── package.lisp
├── license.txt
├── package.lisp
├── parenscriptx.asd
└── parenscriptx.lisp
/.gitignore:
--------------------------------------------------------------------------------
1 | \#*
2 | .\#*
3 | *~
4 | *.fasl
5 | data/data.lisp
6 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Parenscript X
2 | =============
3 |
4 | A simple tool for aiding with creating React UIs in parenscript
5 | ---------------------------------------------------------------
6 |
7 | Hello World (:use parenscript parenscriptx cl-who):
8 |
9 | .. code:: lisp
10 |
11 | (with-html-output-to-string (stream nil :prologue "")
12 | (:html
13 | (:head
14 | (:title "Hello React")
15 | (:script :src "https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react.js")
16 | (:script :src "https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react-dom.min.js")
17 | (:body
18 | (:div :id "content")
19 | (:script
20 | (ps-to-stream stream
21 | (defreact *hello-world
22 | render (lambda () (htm
23 | (:div (+ "Hello " (@ this props name) "!")))))
24 | (chain *React-d-o-m (render
25 | (htm
26 | (:*hello-world :name "World"))
27 | (chain document (get-element-by-id "content"))))))))))
28 |
29 | This allows cl-who style elements inside parenscript. The symbol
30 | ``cl-who:htm`` is used to introduce html literals into javascript.
31 | You must prefix all react class names with an asterisk; parenscript
32 | will turn ``*camel-case-name`` into ``CamelCaseName`` so this follows
33 | the convention of initial capital letters for react classes, and
34 | allows for HTML to introduce new element names without needing to
35 | update this library.
36 |
37 | Lastly ``defreact`` is a convenience wrapper for generating "var Foo =
38 | React.createClass(...)" forms; the body is alternating key/value pairs for the
39 | react object that is created.
40 |
41 | There is `a more full-featured example using hunchentoot`__; lines starting
42 | with ``;;;`` are the equivalent javascript code. This is based off of what
43 | used to be the first tutorial on the react home page.
44 |
45 | .. __: example/code.lisp
46 |
--------------------------------------------------------------------------------
/example/base.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: #fff;
3 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
4 | font-size: 15px;
5 | line-height: 1.7;
6 | margin: 0;
7 | padding: 30px;
8 | }
9 |
10 | a {
11 | color: #4183c4;
12 | text-decoration: none;
13 | }
14 |
15 | a:hover {
16 | text-decoration: underline;
17 | }
18 |
19 | code {
20 | background-color: #f8f8f8;
21 | border: 1px solid #ddd;
22 | border-radius: 3px;
23 | font-family: "Bitstream Vera Sans Mono", Consolas, Courier, monospace;
24 | font-size: 12px;
25 | margin: 0 2px;
26 | padding: 0px 5px;
27 | }
28 |
29 | h1, h2, h3, h4 {
30 | font-weight: bold;
31 | margin: 0 0 15px;
32 | padding: 0;
33 | }
34 |
35 | h1 {
36 | border-bottom: 1px solid #ddd;
37 | font-size: 2.5em;
38 | font-weight: bold;
39 | margin: 0 0 15px;
40 | padding: 0;
41 | }
42 |
43 | h2 {
44 | border-bottom: 1px solid #eee;
45 | font-size: 2em;
46 | }
47 |
48 | h3 {
49 | font-size: 1.5em;
50 | }
51 |
52 | h4 {
53 | font-size: 1.2em;
54 | }
55 |
56 | p, ul {
57 | margin: 15px 0;
58 | }
59 |
60 | ul {
61 | padding-left: 30px;
62 | }
--------------------------------------------------------------------------------
/example/code.lisp:
--------------------------------------------------------------------------------
1 | (in-package :parenscriptx/example)
2 |
3 | ;;; Helper macro for this-preserving lambda
4 | (ps:defpsmacro tlambda (args &body b)
5 | `(chain
6 | (lambda ,args ,@b)
7 | (bind this)))
8 |
9 |
10 |
11 | (defun start-server ()
12 | (hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 4242)))
13 |
14 | (defun encode-json-list-to-string (list)
15 | (let ((json::*json-list-encoder-fn* #'json::encode-json-list-explicit-encoder))
16 | (json:encode-json-to-string (cons :list list))))
17 |
18 | (hunchentoot:define-easy-handler (comments :uri "/comments.json") ()
19 | (setf (hunchentoot:content-type*) "application/json")
20 | (let ((comments
21 | (if (probe-file "comments.json")
22 | (mapcar #'alexandria:alist-hash-table
23 | (with-open-file (file "comments.json"
24 | :direction :input)
25 | (cl-json:decode-json file)))
26 | (list))))
27 | (when (eql (hunchentoot:request-method*) :post)
28 | (with-open-file (file "comments.json" :direction :output
29 | :if-exists :supersede)
30 | (cl-json:encode-json
31 | (setf comments
32 | (append
33 | comments
34 | (list (alexandria:alist-hash-table (hunchentoot:post-parameters*)))))
35 | file)))
36 | (encode-json-list-to-string comments)))
37 |
38 | (push (hunchentoot:create-static-file-dispatcher-and-handler
39 | "/css/base.css"
40 | (asdf:system-relative-pathname :parenscriptx/example "example/base.css")
41 | "text/css") hunchentoot:*dispatch-table*)
42 |
43 | (hunchentoot:define-easy-handler (index :uri "/index.html") ()
44 | (with-html-output-to-string (stream nil :prologue "")
45 | (:html
46 | (:head
47 | (:title "Hello React")
48 | (:link :rel "stylesheet" :href "css/base.css")
49 | (:script :src "https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react.js")
50 | (:script :src "https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js")
51 | (:script :src "https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react-dom.min.js")
52 | (:script :src "https://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"))
53 | (:body
54 | (:div :id "content")
55 | (:script
56 | (ps-to-stream stream
57 | ;;; var converter = new Showdown.converter();
58 | (var *converter* (new (chain *showdown (converter))))
59 |
60 | ;;;var Comment = React.createClass({
61 | ;;; render: function() {
62 | ;;; var rawMarkup = converter.makeHtml(this.props.children.toString());
63 | ;;; return (
64 | ;;;
65 | ;;;
66 | ;;; {this.props.author}
67 | ;;;
68 | ;;;
69 | ;;;
70 | ;;; );
71 | ;;; }
72 | ;;; });
73 | (defreact *comment
74 | render (lambda ()
75 | (let ((raw-markup
76 | (chain *converter* (make-html (chain this props children (to-string))))))
77 | (htm
78 | (:div
79 | :class-name "comment"
80 | (:h2
81 | :class-name "comment-author"
82 | (@ this props author))
83 | (:span :dangerously-set-inner-h-t-m-l
84 | (create __html raw-markup)))))))
85 |
86 | ;;;var CommentBox = React.createClass({
87 | ;;; loadCommentsFromServer: function() {
88 | ;;; $.ajax({
89 | ;;; url: this.props.url,
90 | ;;; dataType: 'json',
91 | ;;; success: function(data) {
92 | ;;; this.setState({data: data});
93 | ;;; }.bind(this),
94 | ;;; error: function(xhr, status, err) {
95 | ;;; console.error(this.props.url, status, err.toString());
96 | ;;; }.bind(this)
97 | ;;; });
98 | ;;; },
99 | ;;; handleCommentSubmit: function(comment) {
100 | ;;; var comments = this.state.data;
101 | ;;; comments.push(comment);
102 | ;;; this.setState({data: comments}, function() {
103 | ;;; // `setState` accepts a callback. To avoid (improbable) race condition,
104 | ;;; // `we'll send the ajax request right after we optimistically set the new
105 | ;;; // `state.
106 | ;;; $.ajax({
107 | ;;; url: this.props.url,
108 | ;;; dataType: 'json',
109 | ;;; type: 'POST',
110 | ;;; data: comment,
111 | ;;; success: function(data) {
112 | ;;; this.setState({data: data});
113 | ;;; }.bind(this),
114 | ;;; error: function(xhr, status, err) {
115 | ;;; console.error(this.props.url, status, err.toString());
116 | ;;; }.bind(this)
117 | ;;; });
118 | ;;; });
119 | ;;; },
120 | ;;; getInitialState: function() {
121 | ;;; return {data: []};
122 | ;;; },
123 | ;;; componentDidMount: function() {
124 | ;;; this.loadCommentsFromServer();
125 | ;;; setInterval(this.loadCommentsFromServer, this.props.pollInterval);
126 | ;;; },
127 | ;;; render: function() {
128 | ;;; return (
129 | ;;;
130 | ;;;
Comments
131 | ;;;
132 | ;;;
133 | ;;;
134 | ;;; );
135 | ;;; }
136 | ;;; });
137 |
138 | (defreact *comment-box
139 | load-comments-from-server (lambda ()
140 | (chain $
141 | (ajax
142 | (create
143 | url (@ this props url)
144 | data-type "json"
145 | success (tlambda (data)
146 | (chain this (set-state (create data data))))
147 | error (tlambda (xhr status err)
148 | (chain console
149 | (error
150 | (@ this props url)
151 | status
152 | (chain err (to-string)))))))))
153 | handle-comment-submit (lambda (comment)
154 | (let ((comments (@ this state data)))
155 | (chain comments (push comment))
156 | (chain this
157 | (set-state
158 | (create data comments)
159 | (lambda ()
160 | (chain $
161 | (ajax
162 | (create
163 | url (@ this props url)
164 | data-type :json
165 | type "POST"
166 | data comment
167 | success
168 | (tlambda (data)
169 | (chain this
170 | (set-state
171 | (create data data))))
172 | error
173 | (tlambda (xhr status error)
174 | (chain console
175 | (error (@ this props url)
176 | status
177 | (chain err to-string))))))))))))
178 | get-initial-state (lambda () (create data (array)))
179 | component-did-mount (lambda ()
180 | (chain this (load-comments-from-server))
181 | (set-interval (@ this load-comments-from-server)
182 | (@ this props poll-interval)))
183 | render (lambda ()
184 | (htm
185 | (:div :class-name "commentBox"
186 | (:h1 "Comments")
187 | (:*comment-list :data (@ this state data))
188 | (:*comment-form :on-comment-submit
189 | (@ this handle-comment-submit))))))
190 |
191 | ;;;var CommentList = React.createClass({
192 | ;;; render: function() {
193 | ;;; var commentNodes = this.props.data.map(function(comment, index) {
194 | ;;; return (
195 | ;;; // `key` is a React-specific concept and is not mandatory for the
196 | ;;; // purpose of this tutorial. if you're curious, see more here:
197 | ;;; // http://facebook.github.io/react/docs/multiple-components.html#dynamic-children
198 | ;;;
199 | ;;; {comment.text}
200 | ;;;
201 | ;;; );
202 | ;;; });
203 | ;;; return (
204 | ;;;
205 | ;;; {commentNodes}
206 | ;;;
207 | ;;; );
208 | ;;; }
209 | ;;; });
210 |
211 | (defreact *comment-list
212 | render (lambda ()
213 | (let ((comment-nodes (chain
214 | this props data
215 | (map (lambda (comment index)
216 | (htm
217 | (:*comment
218 | :author (@ comment author)
219 | :key index
220 | (@ comment text))))))))
221 | (htm
222 | (:div
223 | comment-nodes)))))
224 |
225 | ;;;var CommentForm = React.createClass({
226 | ;;; handleSubmit: function(e) {
227 | ;;; e.preventDefault();
228 | ;;; var author = React.findDOMNode(this.refs.author).value.trim();
229 | ;;; var text = React.findDOMNode(this.refs.text).value.trim();
230 | ;;; if (!text || !author) {
231 | ;;; return;
232 | ;;; }
233 | ;;; this.props.onCommentSubmit({author: author, text: text});
234 | ;;; React.findDOMNode(this.refs.author).value = '';
235 | ;;; React.findDOMNode(this.refs.text).value = '';
236 | ;;; },
237 | ;;; render: function() {
238 | ;;; return (
239 | ;;;
244 | ;;; );
245 | ;;; }
246 | ;;; });
247 | (defreact *comment-form
248 | handle-submit (lambda (e)
249 | (let ((author (chain *react (find-d-o-m-node (@ this refs author))
250 | value (trim)))
251 | (text (chain *react (find-d-o-m-node (@ this refs text))
252 | value (trim))))
253 | (chain e (prevent-default))
254 | (when (and text author)
255 | (chain this props (on-comment-submit
256 | (create author author text text)))
257 | (setf
258 | (chain *react (find-d-o-m-node (@ this refs author)) value)
259 | ""
260 | (chain *react (find-d-o-m-node (@ this refs text)) value)
261 | ""))))
262 | render (lambda ()
263 | (htm
264 | (:form
265 | :class-name "commentForm"
266 | :on-submit (@ this handle-submit)
267 | (:input :type "text"
268 | :placeholder "Your name"
269 | :ref "author")
270 | (:input :type "text"
271 | :placeholder "Say something..."
272 | :ref "text")
273 | (:input :type "submit" :value "Post")))))
274 |
275 | ;;;React.render(
276 | ;;; ,
277 | ;;; document.getElementById('content')
278 | ;;; );
279 |
280 | (chain *React-d-o-m (render
281 | (htm
282 | (:*comment-box :url "comments.json"
283 | :poll-interval 2000))
284 | (chain document (get-element-by-id "content"))))))))))
285 |
--------------------------------------------------------------------------------
/example/package.lisp:
--------------------------------------------------------------------------------
1 | (defpackage :parenscriptx/example
2 | (:use :cl :cl-who :parenscript :parenscriptx))
3 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright © 2015 Jason Miller
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/package.lisp:
--------------------------------------------------------------------------------
1 | ;;;; package.lisp
2 |
3 | (defpackage #:parenscriptx
4 | (:use #:cl #:cl-who)
5 | (:import-from #:cl-who #:htm)
6 | (:export #:{ #:htm #:defreact))
7 |
8 |
--------------------------------------------------------------------------------
/parenscriptx.asd:
--------------------------------------------------------------------------------
1 | ;;;; parenscriptx.asd
2 |
3 | (asdf:defsystem #:parenscriptx
4 | :description "Tool for generating React XJS templates"
5 | :author "Jason Miller "
6 | :license "MIT/X11"
7 | :depends-on (#:cl-who
8 | #:parenscript
9 | #:alexandria
10 | #:split-sequence)
11 | :serial t
12 | :components ((:file "package")
13 | (:file "parenscriptx")))
14 |
15 | (asdf:defsystem #:parenscriptx/example
16 | :description "Example from React tutorial"
17 | :author "Jason Miller "
18 | :license "MIT/X11"
19 | :depends-on (#:parenscriptx
20 | #:cl-who
21 | #:parenscript
22 | #:split-sequence
23 | #:cl-json
24 | #:hunchentoot)
25 | :serial t
26 | :components ((:module "example"
27 | :components
28 | ((:file "package")
29 | (:file "code")))))
30 |
--------------------------------------------------------------------------------
/parenscriptx.lisp:
--------------------------------------------------------------------------------
1 | ;;;; parenscriptx.lisp
2 |
3 | (in-package #:parenscriptx)
4 | (declaim (optimize (speed 3) (space 0) (debug 0)))
5 |
6 | ;;; "parenscriptx" goes here. Hacks and glory await!
7 |
8 | (defun split-tag-parts (tree)
9 | (loop with tag = (car tree)
10 | for rest on (cdr tree) by #'cddr
11 | while (keywordp (car rest))
12 | if (cadr rest)
13 | collect (ps::encode-js-identifier (string (car rest))) into attrs
14 | and
15 | collect (cadr rest)
16 | into attrs
17 | finally (return (values tag attrs rest))))
18 |
19 | (defun html-element-p (keyword)
20 | (notany #'upper-case-p (ps::encode-js-identifier (string keyword))))
21 |
22 | (parenscript:defpsmacro htm (&body trees)
23 | (if (> (length trees) 1)
24 | `(progn ,@(mapcar #'psx-htm-item trees))
25 | (psx-htm-item (car trees))))
26 |
27 | (defun psx-htm-item (tree)
28 | (if (and (consp tree) (keywordp (car tree)))
29 | (multiple-value-bind (tag attrs body)
30 | (split-tag-parts tree)
31 | `(ps:chain
32 | *react
33 | (create-element
34 | ,(if (html-element-p tag)
35 | (ps::encode-js-identifier (string tag))
36 | (intern (string tag) *package*))
37 | (ps:create ,@attrs)
38 | ,@(loop for item in body
39 | collect `(htm ,item)))))
40 | tree))
41 |
42 | (ps:defpsmacro defreact (name &rest args)
43 | `(ps:var ,name
44 | (ps:chain *react (create-class (ps:create 'display-name ,(ps::encode-js-identifier (string name)) ,@args)))))
45 |
46 |
47 | ;;; The following two macros are for backwards-compatibility
48 | ;;; It used to be required that parenscript inside htm be
49 | ;;; enclosed in a { macro.
50 | (parenscript:defpsmacro { (b) b)
51 |
52 | (parenscript:defpsmacro cl-who:esc (item)
53 | item)
54 |
--------------------------------------------------------------------------------