├── .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 | ;;;
240 | ;;; 241 | ;;; 242 | ;;; 243 | ;;;
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 | --------------------------------------------------------------------------------