diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c4183eb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.module-cache
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9032760
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (C) 2014 Luca Antiga http://lantiga.github.io
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f495eef
--- /dev/null
+++ b/README.md
@@ -0,0 +1,366 @@
+
+# React.hiccup: React 0% JSX, 100% hiccup
+
+React.hiccup is a complete replacement for [React](http://facebook.github.io/react/) under the form of a [sweet.js](http://sweetjs.org) macro.
+
+Dig React but can't take JSX? React.hiccup to the rescue.
+
+
+## Syntax
+
+React.hiccup syntax is heavily inspired by [hiccup](https://github.com/weavejester/hiccup), a popular [Clojure](http://clojure.org) HTML rendering library.
+
+In short, the syntax for a React.hiccup element is
+
+```js
+hiccup [tag#id.class1.class2 {attributes} child1 child2 ...]
+```
+
+e.g.
+
+```js
+hiccup [div#foo.bar.baz {some: "property", another: this.props.property} [p "A child element"] "Child text"]
+```
+
+where the id, classes, attributes and children are all optional. The className can be also specified
+among the attributes, in this case it will be merged with the class names given after the tag.
+
+A child can be a variable identifier
+
+```js
+var comment = "Some comment";
+hiccup [div#foo.bar.baz "The comment is: " comment]
+```
+
+but in case it is anything more complex, e.g. an expression, it needs to be surrounded by parentheses
+
+```js
+hiccup [div#foo.bar.baz "The comment is: " (this.state.comment)]
+```
+
+or
+
+```js
+var one_or_two = 1;
+var comment1 = "First comment";
+var comment2 = "Second comment";
+hiccup [div#foo.bar.baz "The comment is: " (one_or_two == 1 ? comment1 : comment2 )]
+```
+
+Note that this is not required in the attributes.
+
+
+## Complimentary rclass macro
+
+React.hiccup also comes with an optional macro for declaring a React class
+
+```js
+rclass FooBar = {
+ render: function() { ... }
+}
+```
+
+expands to (omitting the sweet.js gensym)
+
+```js
+var FooBar = React.createClass({
+ render: function() { ... }
+});
+```
+
+while
+
+```js
+rclass window.FooBar = {
+ render: function() { ... }
+}
+```
+
+expands to
+
+```js
+window.FooBar = React.createClass({
+ render: function() { ... }
+});
+```
+
+
+## Get it
+
+First install [sweet.js](http://sweetjs.org) if you don't have it already
+
+ $ npm install -g sweet.js
+
+Then get in your project directory
+
+ $ wget https://raw2.github.com/lantiga/react.hiccup/master/react_hiccup.sjs
+
+All set. Now to compile a React.hiccup js file into a plain js file do
+
+ $ sjs -m ./react_hiccup.sjs -o foo_build.js foo.js
+
+To watch the file and have it automatically compiled at every change
+
+ $ sjs -m ./react_hiccup.sjs -o foo_build.js -w foo.js
+
+(this appears to be currently broken in sweet.js)
+
+
+## Examples
+
+### React frontpage examples
+
+Here's how [React frontpage examples](http://facebook.github.io/react/) can be
+written using React.hiccup.
+
+#### A Simple Component
+
+JSX:
+
+```js
+/** @jsx React.DOM */
+var HelloMessage = React.createClass({
+ render: function() {
+ return
{'Hello ' + this.props.name}
;
+ }
+});
+
+React.renderComponent(, mountNode);
+```
+
+React.hiccup:
+
+```js
+var HelloMessage = React.createClass({
+ render: function() {
+ return hiccup [div ('Hello' + this.props.name)];
+ }
+});
+
+React.renderComponent(hiccup [HelloMessage {name: "John"}], mountNode);
+```
+
+or, using the rclass macro (we'll use it in the remainder of the examples)
+
+```js
+rclass HelloMessage = {
+ render: function() {
+ return hiccup [div ('Hello' + this.props.name)];
+ }
+}
+
+React.renderComponent(hiccup [HelloMessage {name: "John"}], mountNode);
+```
+
+#### A Stateful Component
+
+React.js
+
+```js
+var Timer = React.createClass({
+ getInitialState: function() {
+ return {secondsElapsed: 0};
+ },
+ tick: function() {
+ this.setState({secondsElapsed: this.state.secondsElapsed + 1});
+ },
+ componentDidMount: function() {
+ this.interval = setInterval(this.tick, 1000);
+ },
+ componentWillUnmount: function() {
+ clearInterval(this.interval);
+ },
+ render: function() {
+ return React.DOM.div({},
+ 'Seconds Elapsed: ', this.state.secondsElapsed
+ );
+ }
+});
+
+React.renderComponent(Timer({}), mountNode);
+```
+
+React.hiccup
+
+```js
+var Timer = React.createClass({
+ getInitialState: function() {
+ return {secondsElapsed: 0};
+ },
+ tick: function() {
+ this.setState({secondsElapsed: this.state.secondsElapsed + 1});
+ },
+ componentDidMount: function() {
+ this.interval = setInterval(this.tick, 1000);
+ },
+ componentWillUnmount: function() {
+ clearInterval(this.interval);
+ },
+ render: function() {
+ return hiccup [div 'Seconds Elapsed: ' (this.state.secondsElapsed)]
+ }
+});
+
+React.renderComponent(hiccup [Timer], mountNode);
+```
+
+#### An Application
+
+JSX:
+
+```js
+/** @jsx React.DOM */
+var TodoList = React.createClass({
+ render: function() {
+ var createItem = function(itemText) {
+ return {itemText};
+ };
+ return {this.props.items.map(createItem)}
;
+ }
+});
+var TodoApp = React.createClass({
+ getInitialState: function() {
+ return {items: [], text: ''};
+ },
+ onChange: function(e) {
+ this.setState({text: e.target.value});
+ },
+ handleSubmit: function(e) {
+ e.preventDefault();
+ var nextItems = this.state.items.concat([this.state.text]);
+ var nextText = '';
+ this.setState({items: nextItems, text: nextText});
+ },
+ render: function() {
+ return (
+
+
TODO
+
+
+
+ );
+ }
+});
+React.renderComponent(, mountNode);
+```
+
+React.hiccup
+
+```
+rclass TodoList = {
+ render: function() {
+ var createItem = function(itemText) {
+ return hiccup [li itemText];
+ };
+ return hiccup [ul (this.props.items.map(createItem))];
+ }
+}
+rclass TodoApp = {
+ getInitialState: function() {
+ return {items: [], text: ''};
+ },
+ onChange: function(e) {
+ this.setState({text: e.target.value});
+ },
+ handleSubmit: function(e) {
+ e.preventDefault();
+ var nextItems = this.state.items.concat([this.state.text]);
+ var nextText = '';
+ this.setState({items: nextItems, text: nextText});
+ },
+ render: function() {
+ return hiccup
+ [div
+ [h3 "TODO"]
+ [TodoList {items: this.state.items}]
+ [form {onSubmit: this.handleSubmit}
+ [input {onChange: this.onChange, value: this.state.text}]
+ [button ('Add #' + (this.state.items.length + 1))]]];
+ }
+}
+React.renderComponent(hiccup [TodoApp], mountNode);
+```
+
+#### A Component Using External Plugins
+
+JSX:
+
+```js
+/** @jsx React.DOM */
+
+var converter = new Showdown.converter();
+
+var MarkdownEditor = React.createClass({
+ getInitialState: function() {
+ return {value: 'Type some *markdown* here!'};
+ },
+ handleChange: function() {
+ this.setState({value: this.refs.textarea.getDOMNode().value});
+ },
+ render: function() {
+ return (
+
+ );
+ }
+});
+
+React.renderComponent(, mountNode);
+```
+
+React.hiccup:
+
+```js
+
+var converter = new Showdown.converter();
+
+rclass MarkdownEditor = {
+ getInitialState: function() {
+ return {value: 'Type some *markdown* here!'};
+ },
+ handleChange: function() {
+ this.setState({value: this.refs.textarea.getDOMNode().value});
+ },
+ render: function() {
+ return hiccup
+ [div.MarkdownEditor
+ [h3 "Input"]
+ [textarea {onChange: this.handleChange,
+ ref: "textarea",
+ defaultValue: this.state.value}]
+ [h3 "Output"]
+ div.content {dangerouslySetInnerHTML: {__html: converter.makeHtml(this.state.value)}}];
+ }
+}
+
+React.renderComponent(hiccup [MarkdownEditor], mountNode);
+```
+
+### React tutorial
+
+For something more involved you can take a look at the [React tutorial](http://facebook.github.io/react/docs/tutorial.html).
+
+Here's the code in [JSX](https://raw2.github.com/lantiga/react.hiccup/master/tutorial/tutorial.jsx), and
+here's the same code in [React.hiccup](https://raw2.github.com/lantiga/react.hiccup/master/tutorial/tutorial.jsx).
+
+## License
+
+MIT license http://www.opensource.org/licenses/mit-license.php/
+
+Copyright (C) 2014 Luca Antiga http://lantiga.github.io
+
diff --git a/react_hiccup.sjs b/react_hiccup.sjs
new file mode 100644
index 0000000..999e13d
--- /dev/null
+++ b/react_hiccup.sjs
@@ -0,0 +1,152 @@
+/**
+ * React.hiccup hiccup notation for React.js
+ * MIT license http://www.opensource.org/licenses/mit-license.php/
+ * Copyright (C) 2014 Luca Antiga http://lantiga.github.io
+ */
+
+macro _restargs {
+ rule { () } => { null }
+ rule { ($first) } => { hiccup $first }
+ rule { ($first $rest ...) } => { hiccup $first, _restargs ($rest ...) }
+}
+
+macro _tag {
+ case { _ $t } => {
+ var knownTags = {
+ a:true, abbr:true, address:true, area:true, article:true, aside:true, audio:true,
+ b:true, base:true, bdi:true, bdo:true, big:true, blockquote:true, body:true, br:true,
+ butto:true, canva:true, caption:true, cite:true, code:true, col:true, colgroup:true,
+ data:true, datalist:true, dd:true, del:true, details:true, dfn:true,
+ div:true, dl:true, dt:true, em:true, embed:true, fieldset:true, figcaption:true,
+ figure:true, footer:true, form:true, h1:true, h2:true, h3:true, h4:true, h5:true, h6:true,
+ head:true, header:true, hr:true, html:true, i:true, iframe:true, img:true, input:true,
+ ins:true, kbd:true, keygen:true, label:true, legend:true, li:true, link:true, main:true,
+ map:true, mark:true, menu:true, menuitem:true, meta:true, meter:true, nav:true, noscript:true,
+ object:true, ol:true, optgroup:true, option:true, output:true,
+ p:true, param:true, pre:true, progress:true, q:true, rp:true, rt:true, ruby:true, s:true,
+ samp:true, script:true, section:true, select:true, small:true, source:true,
+ span:true, strong:true, style:true, sub:true, summary:true, sup:true, table:true,
+ tbody:true, td:true, textarea:true, tfoot:true, th:true, thead:true, time:true,
+ title:true, tr:true, track:true, u:true, ul:true, "var":true, video:true, wbr:true,
+ circle:true, g:true, line:true, path:true, polygon:true, polyline:true, rect:true,
+ svg:true, text:true
+ };
+ var tagStr = unwrapSyntax(#{$t});
+ if (knownTags[tagStr]) {
+ return #{React.DOM.$t}
+ }
+ return #{$t}
+ }
+}
+
+macro _args {
+
+ case { _ $x ... } => {
+
+ var tokens = #{$x ...}.map(function(el) { return unwrapSyntax(el); });
+
+ var offset = 0;
+
+ var id = "", className = "";
+ if (tokens[0] == '#') {
+ tokens.shift();
+ id = tokens.shift();
+ offset += 2;
+ }
+ while (tokens[0] == '.') {
+ tokens.shift();
+ className += " " + tokens.shift();
+ offset += 2;
+ }
+
+ var hmap;
+ if (tokens.length > 0 && tokens[0].value == '{}') {
+ hmap = #{$x ...}[offset];
+ offset += 1;
+ }
+
+ letstx $id = [makeValue(id)],
+ $c = [makeValue(className)];
+
+ var children = #{$x ...}.slice(offset);
+ letstx $children ... = children;
+
+ if (hmap === undefined && children.length == 0) {
+ if (id != "" && className != "") {
+ return #{ {className: $c, id: $id} }
+ }
+ else if (id == "" && className == "") {
+ return #{ null }
+ }
+ else if (className != "") {
+ return #{ {className: $c} }
+ }
+ else {
+ return #{ {id: $id} }
+ }
+ }
+
+ if (hmap === undefined) {
+ if (id != "" && className != "") {
+ return #{ {className: $c, id: $id}, _restargs($children ...) }
+ }
+ else if (id == "" && className == "") {
+ return #{ null, _restargs($children ...) }
+ }
+ else if (className != "") {
+ return #{ {className: $c}, _restargs($children ...) }
+ }
+ else {
+ return #{ {id: $id}, _restargs($children ...) }
+ }
+ }
+
+ var classNameIndex;
+
+ for (var i=0; i {
+ _tag $t(_args $args ...)
+ }
+
+ rule { $x } => { $x }
+}
+
+macro rclass {
+ rule { $n.$m(.)... = $o:expr } => { $n.$m(.)... = React.createClass($o); }
+ rule { $n:ident = $o:expr } => { var $n = React.createClass($o); }
+}
+
+export hiccup;
+
+export rclass;
+
+// TODO
+// * better error handling in _args
+// * allow attribute object to be an identifier; in this case merge className and id using a function
+// * allow property access without brackets by improving _restargs
+
diff --git a/react_hiccup_test.js b/react_hiccup_test.js
new file mode 100644
index 0000000..de2a93e
--- /dev/null
+++ b/react_hiccup_test.js
@@ -0,0 +1,46 @@
+
+hiccup [div]
+hiccup [div#foo]
+hiccup [div.bar]
+hiccup [div.bar.baz]
+hiccup [div#foo.bar.baz]
+
+hiccup [div {foo: 'bar'}]
+hiccup [div#foo {foo: 'bar'}]
+hiccup [div.bar {foo: 'bar'}]
+hiccup [div.bar.baz {foo: 'bar'}]
+hiccup [div#foo.bar.baz {foo: 'bar'}]
+
+hiccup [div "text"]
+hiccup [div#foo "text"]
+hiccup [div.bar "text"]
+hiccup [div.bar.baz "text"]
+hiccup [div#foo.bar.baz "text"]
+hiccup [div#foo.bar.baz "text" [p#biz.bus "text2"]]
+hiccup [div#foo.bar.baz [FooBar#biz.bus {bla: "bee", blu: "fooba", className: "secco"} "text2"] "text" 2 1.0]
+hiccup [div#foo.bar.baz [FooBar#biz.bus {bla: "bee"} "text2"] "text" 2 1.0]
+hiccup [p#biz.bus {bla: "bee"} "text2"]
+
+hiccup [div#foo.bar.baz [FooBar.bus {bla: "bee", blu: "fooba", className: "secco"} "text2"] "text" 2 1.0]
+hiccup [div#id "text" "text2"]
+
+hiccup [p {"a": 1, "b": 2} [div [p "a"]] "b" "c" ]
+hiccup [p {"a": 1, "b": 2} [FooBar [div "a"]] ]
+hiccup [div.foo "a"]
+hiccup [div 1]
+hiccup [div]
+
+hiccup [p "a" "b" "c" ]
+
+hiccup [p {a: 1, b: 2} "b" "c" ]
+hiccup [p "a" "b" "c" ]
+
+hiccup [p ("a"+"b") "c" ]
+hiccup [p ("a"+"b") (this.state.foobar) ]
+
+rclass Foobar = { render: function() { console.log('a'); } }
+hiccup [Foobar "text"]
+
+rclass window.Foobaz = { render: function() { console.log('a'); } }
+hiccup [Foobaz "text"]
+
diff --git a/react_tutorial/comments.json b/react_tutorial/comments.json
new file mode 100644
index 0000000..fdfce10
--- /dev/null
+++ b/react_tutorial/comments.json
@@ -0,0 +1,4 @@
+[
+ {"author": "Pete Hunt", "text": "This is one comment"},
+ {"author": "Jordan Walke", "text": "This is *another* comment"}
+]
diff --git a/react_tutorial/tutorial.html b/react_tutorial/tutorial.html
new file mode 100644
index 0000000..c1d1516
--- /dev/null
+++ b/react_tutorial/tutorial.html
@@ -0,0 +1,14 @@
+
+
+ Hello React
+
+
+
+
+
+
+
+
+
+
diff --git a/react_tutorial/tutorial.jsx b/react_tutorial/tutorial.jsx
new file mode 100644
index 0000000..ec42cc8
--- /dev/null
+++ b/react_tutorial/tutorial.jsx
@@ -0,0 +1,93 @@
+/** @jsx React.DOM */
+
+var data = [
+ {author: "Pete Hunt", text: "This is one comment"},
+ {author: "Jordan Walke", text: "This is *another* comment"}
+];
+
+var converter = new Showdown.converter();
+
+var Comment = React.createClass({
+ render: function() {
+ var rawMarkup = converter.makeHtml(this.props.children.toString());
+ return(
+
+
+ {this.props.author}
+
+
+
+ );
+ }
+});
+
+var CommentList = React.createClass({
+ render: function() {
+ var commentNodes = this.props.data.map(function (comment,i) {
+ return {comment.text};
+ });
+ return (
+
+ {commentNodes}
+
+ );
+ }
+});
+
+var CommentForm = React.createClass({
+ handleSubmit: function() {
+ var author = this.refs.author.getDOMNode().value.trim();
+ var text = this.refs.text.getDOMNode().value.trim();
+ this.props.onCommentSubmit({author:author, text:text});
+ this.refs.author.getDOMNode().value = '';
+ this.refs.text.getDOMNode().value = '';
+ return false;
+ },
+ render: function() {
+ return (
+
+ );
+ }
+});
+
+var CommentBox = React.createClass({
+ loadCommentsFromServer: function() {
+ $.ajax({
+ url: this.props.url,
+ success: function(data) {
+ this.setState({data: data});
+ }.bind(this)
+ });
+ },
+ handleCommentSubmit: function(comment) {
+ var comments = this.state.data;
+ var newComments = comments.concat([comment]);
+ this.setState({data: newComments});
+ },
+ getInitialState: function() {
+ return {data: []};
+ },
+ componentWillMount: function() {
+ this.loadCommentsFromServer();
+ setInterval(this.loadCommentsFromServer, this.props.pollInterval);
+ },
+ render: function() {
+ return (
+
+
Comments
+
+
+
+ );
+ }
+});
+
+React.renderComponent(
+ ,
+ document.getElementById('content')
+);
+
diff --git a/react_tutorial/tutorial.sjs b/react_tutorial/tutorial.sjs
new file mode 100644
index 0000000..559fd0c
--- /dev/null
+++ b/react_tutorial/tutorial.sjs
@@ -0,0 +1,78 @@
+
+var data = [
+ {author: "Pete Hunt", text: "This is one comment"},
+ {author: "Jordan Walke", text: "This is *another* comment"}
+];
+
+var converter = new Showdown.converter();
+
+rclass Comment = {
+ render: function() {
+ var rawMarkup = converter.makeHtml(this.props.children.toString());
+ return hiccup
+ [div.comment
+ [h2.commentAuthor (this.props.author)]
+ [span {dangerouslySetInnerHTML:({__html: rawMarkup})}]];
+ }
+}
+
+rclass CommentList = {
+ render: function() {
+ var commentNodes = this.props.data.map(function (comment,i) {
+ return hiccup [Comment {key: i, author: comment.author} (comment.text)];
+ });
+ return hiccup [div.commentList commentNodes];
+ }
+}
+
+rclass CommentForm = {
+ handleSubmit: function() {
+ var author = this.refs.author.getDOMNode().value.trim();
+ var text = this.refs.text.getDOMNode().value.trim();
+ this.props.onCommentSubmit({author:author, text:text});
+ this.refs.author.getDOMNode().value = '';
+ this.refs.text.getDOMNode().value = '';
+ return false;
+ },
+ render: function() {
+ return hiccup
+ [form.commentForm {onSubmit: this.handleSubmit}
+ [input {type: "text", placeholder: "Your name", ref: "author"}]
+ [input {type: "text", placeholder: "Say something...", ref: "text"}]
+ [input {type: "submit", value:"Post"}]]
+ }
+}
+
+rclass CommentBox = {
+ loadCommentsFromServer: function() {
+ $.ajax({
+ url: this.props.url,
+ success: function(data) {
+ this.setState({data: data});
+ }.bind(this)
+ });
+ },
+ handleCommentSubmit: function(comment) {
+ var comments = this.state.data;
+ var newComments = comments.concat([comment]);
+ this.setState({data: newComments});
+ },
+ getInitialState: function() {
+ return {data: []};
+ },
+ componentWillMount: function() {
+ this.loadCommentsFromServer();
+ setInterval(this.loadCommentsFromServer, this.props.pollInterval);
+ },
+ render: function() {
+ return hiccup [div.commentBox
+ [h1 "Comments"]
+ [CommentList {data: this.state.data}]
+ [CommentForm {onCommentSubmit: this.handleCommentSubmit}]]}
+}
+
+React.renderComponent(
+ hiccup [CommentBox {url: "comments.json", pollInterval: 2000}],
+ document.getElementById('content')
+);
+