├── .gitignore ├── test ├── fixtures │ ├── 02.jade │ ├── 01.jade │ ├── 03.jade │ ├── 13.jade │ ├── 06.jade │ ├── 05.jade │ ├── 07.jade │ ├── 01.jade.js │ ├── 04.jade │ ├── 19.jade │ ├── 14.jade │ ├── 16.jade │ ├── 18.jade │ ├── 02.jade.js │ ├── 08.jade │ ├── 10.jade │ ├── 20.jade │ ├── 03.jade.js │ ├── 17.jade │ ├── 22.jade │ ├── 06.jade.js │ ├── 09.jade │ ├── 01.jade.pretty.js │ ├── 04.jade.js │ ├── 13.jade.js │ ├── 02.jade.pretty.js │ ├── 05.jade.js │ ├── 07.jade.js │ ├── 16.jade.js │ ├── 19.jade.js │ ├── 22.jade.js │ ├── 11.jade │ ├── 18.jade.js │ ├── 08.jade.js │ ├── 14.jade.js │ ├── 20.jade.js │ ├── 17.jade.js │ ├── 09.jade.js │ ├── 03.jade.pretty.js │ ├── 06.jade.pretty.js │ ├── 10.jade.js │ ├── 12.jade │ ├── 13.jade.pretty.js │ ├── 04.jade.pretty.js │ ├── 07.jade.pretty.js │ ├── 16.jade.pretty.js │ ├── 15.jade │ ├── 18.jade.pretty.js │ ├── 19.jade.pretty.js │ ├── 22.jade.pretty.js │ ├── 20.jade.pretty.js │ ├── 08.jade.pretty.js │ ├── 17.jade.pretty.js │ ├── 05.jade.pretty.js │ ├── 14.jade.pretty.js │ ├── 09.jade.pretty.js │ ├── 15.jade.js │ ├── 10.jade.pretty.js │ ├── 11.jade.js │ ├── 12.jade.js │ ├── 15.jade.pretty.js │ ├── 11.jade.pretty.js │ ├── 12.jade.pretty.js │ ├── 21.jade │ ├── 21.jade.js │ └── 21.jade.pretty.js ├── helpers │ └── chai-compile.coffee └── compilation-test.coffee ├── src ├── main.coffee ├── class-expression-compiler.coffee ├── class-array-expression-compiler.coffee └── compiler.coffee ├── Gulpfile.coffee ├── package.json └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /test/fixtures/02.jade: -------------------------------------------------------------------------------- 1 | p 2 | -------------------------------------------------------------------------------- /test/fixtures/01.jade: -------------------------------------------------------------------------------- 1 | | Text 2 | -------------------------------------------------------------------------------- /test/fixtures/03.jade: -------------------------------------------------------------------------------- 1 | p Text 2 | -------------------------------------------------------------------------------- /test/fixtures/13.jade: -------------------------------------------------------------------------------- 1 | a: img 2 | -------------------------------------------------------------------------------- /test/fixtures/06.jade: -------------------------------------------------------------------------------- 1 | p= variable 2 | -------------------------------------------------------------------------------- /test/fixtures/05.jade: -------------------------------------------------------------------------------- 1 | ul 2 | li Text 3 | -------------------------------------------------------------------------------- /test/fixtures/07.jade: -------------------------------------------------------------------------------- 1 | p.customClass 2 | -------------------------------------------------------------------------------- /test/fixtures/01.jade.js: -------------------------------------------------------------------------------- 1 | function(){return "Text";} -------------------------------------------------------------------------------- /test/fixtures/04.jade: -------------------------------------------------------------------------------- 1 | p 2 | | Text 1 3 | | Text 2 -------------------------------------------------------------------------------- /test/fixtures/19.jade: -------------------------------------------------------------------------------- 1 | p(class=this.productType) 2 | -------------------------------------------------------------------------------- /test/fixtures/14.jade: -------------------------------------------------------------------------------- 1 | input(type="checkbox", checked) 2 | -------------------------------------------------------------------------------- /test/fixtures/16.jade: -------------------------------------------------------------------------------- 1 | p(class=['foo', 'bar', 'baz']) 2 | -------------------------------------------------------------------------------- /test/fixtures/18.jade: -------------------------------------------------------------------------------- 1 | p(class=['foo', local, 'baz']) 2 | -------------------------------------------------------------------------------- /test/fixtures/02.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.p(null);} -------------------------------------------------------------------------------- /test/fixtures/08.jade: -------------------------------------------------------------------------------- 1 | p.customClass1(class="customClass2") 2 | -------------------------------------------------------------------------------- /test/fixtures/10.jade: -------------------------------------------------------------------------------- 1 | ul 2 | li Text 1 3 | li Text 2 4 | -------------------------------------------------------------------------------- /test/fixtures/20.jade: -------------------------------------------------------------------------------- 1 | p.markup(class=['array', dynamic]) 2 | -------------------------------------------------------------------------------- /test/fixtures/03.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.p(null,"Text");} -------------------------------------------------------------------------------- /test/fixtures/17.jade: -------------------------------------------------------------------------------- 1 | p(class=['foo', this.classProp, 'baz']) 2 | -------------------------------------------------------------------------------- /test/fixtures/22.jade: -------------------------------------------------------------------------------- 1 | CustomTag(customAttribute="customValue") 2 | -------------------------------------------------------------------------------- /test/fixtures/06.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.p(null,variable);} -------------------------------------------------------------------------------- /test/fixtures/09.jade: -------------------------------------------------------------------------------- 1 | p(class="customClass", title="customTitle") 2 | -------------------------------------------------------------------------------- /test/fixtures/01.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return "Text"; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/04.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.p(null,"Text 1","Text 2");} -------------------------------------------------------------------------------- /test/fixtures/13.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.a(null,React.DOM.img(null));} -------------------------------------------------------------------------------- /test/fixtures/02.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.p(null); 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/05.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.ul(null,React.DOM.li(null,"Text"));} -------------------------------------------------------------------------------- /test/fixtures/07.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.p({"className":"customClass"});} -------------------------------------------------------------------------------- /test/fixtures/16.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.p({"className":"foo bar baz"});} -------------------------------------------------------------------------------- /test/fixtures/19.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.p({"className":this.productType});} -------------------------------------------------------------------------------- /test/fixtures/22.jade.js: -------------------------------------------------------------------------------- 1 | function(){return CustomTag({"customAttribute":"customValue"});} -------------------------------------------------------------------------------- /test/fixtures/11.jade: -------------------------------------------------------------------------------- 1 | ul 2 | each product in this.products 3 | li= product.name 4 | -------------------------------------------------------------------------------- /test/fixtures/18.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.p({"className":"foo "+local+" baz"});} -------------------------------------------------------------------------------- /test/fixtures/08.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.p({"className":"customClass1 customClass2"});} -------------------------------------------------------------------------------- /test/fixtures/14.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.input({"checked":true,"type":"checkbox"});} -------------------------------------------------------------------------------- /test/fixtures/20.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.p({"className":"markup array "+dynamic});} -------------------------------------------------------------------------------- /test/fixtures/17.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.p({"className":"foo "+this.classProp+" baz"});} -------------------------------------------------------------------------------- /test/fixtures/09.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.p({"className":"customClass","title":"customTitle"});} -------------------------------------------------------------------------------- /test/fixtures/03.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.p(null, 3 | "Text" 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/06.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.p(null, 3 | variable 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/10.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.ul(null,React.DOM.li(null,"Text 1"),React.DOM.li(null,"Text 2"));} -------------------------------------------------------------------------------- /test/fixtures/12.jade: -------------------------------------------------------------------------------- 1 | ul 2 | each product in products 3 | li 4 | | Product 5 | = product.name 6 | -------------------------------------------------------------------------------- /test/fixtures/13.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.a(null, 3 | React.DOM.img(null) 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/04.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.p(null, 3 | "Text 1", 4 | "Text 2" 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/07.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.p({ 3 | "className": "customClass" 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/16.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.p({ 3 | "className": "foo bar baz" 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/15.jade: -------------------------------------------------------------------------------- 1 | select(value="B") 2 | option(value="A") Apple 3 | option(value="B") Banana 4 | option(value="C") Cranberry 5 | -------------------------------------------------------------------------------- /test/fixtures/18.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.p({ 3 | "className": "foo "+local+" baz" 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/19.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.p({ 3 | "className": this.productType 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/22.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return CustomTag({ 3 | "customAttribute": "customValue" 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/20.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.p({ 3 | "className": "markup array "+dynamic 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/08.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.p({ 3 | "className": "customClass1 customClass2" 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/17.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.p({ 3 | "className": "foo "+this.classProp+" baz" 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/05.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.ul(null, 3 | React.DOM.li(null, 4 | "Text" 5 | ) 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/14.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.input({ 3 | "checked": true, 4 | "type": "checkbox" 5 | }); 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/09.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.p({ 3 | "className": "customClass", 4 | "title": "customTitle" 5 | }); 6 | } 7 | -------------------------------------------------------------------------------- /src/main.coffee: -------------------------------------------------------------------------------- 1 | jade = require('jade') 2 | compiler = require('./compiler') 3 | 4 | module.exports = (markup, options = {}) -> 5 | options.compiler = compiler 6 | jade.render(markup, options) 7 | -------------------------------------------------------------------------------- /test/fixtures/15.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.select({"value":"B"},React.DOM.option({"value":"A"},"Apple"),React.DOM.option({"value":"B"},"Banana"),React.DOM.option({"value":"C"},"Cranberry"));} -------------------------------------------------------------------------------- /test/fixtures/10.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.ul(null, 3 | React.DOM.li(null, 4 | "Text 1" 5 | ), 6 | React.DOM.li(null, 7 | "Text 2" 8 | ) 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/11.jade.js: -------------------------------------------------------------------------------- 1 | function(){function map(o,f){if('number'===typeof o.length)return o.map(f);var r=[],k,h={}.hasOwnProperty;for(k in o)h.call(o,k)&&r.push(f(k,o[k]));return r;}return React.DOM.ul(null,map(this.products,function(product,$index){return React.DOM.li(null,product.name);}));} -------------------------------------------------------------------------------- /test/fixtures/12.jade.js: -------------------------------------------------------------------------------- 1 | function(){function map(o,f){if('number'===typeof o.length)return o.map(f);var r=[],k,h={}.hasOwnProperty;for(k in o)h.call(o,k)&&r.push(f(k,o[k]));return r;}return React.DOM.ul(null,map(products,function(product,$index){return React.DOM.li(null,"Product",product.name);}));} -------------------------------------------------------------------------------- /Gulpfile.coffee: -------------------------------------------------------------------------------- 1 | gulp = require('gulp') 2 | mocha = require('gulp-mocha') 3 | 4 | gulp.task 'test', -> 5 | gulp.src(['test/*.coffee'], read: false) 6 | .pipe(mocha(reporter: 'spec')) 7 | 8 | gulp.task 'watch', -> 9 | gulp.watch ['src/**/*', 'test/**/*'], ['test'] 10 | 11 | gulp.task 'default', ['test'] 12 | -------------------------------------------------------------------------------- /test/helpers/chai-compile.coffee: -------------------------------------------------------------------------------- 1 | chai = require('chai') 2 | 3 | chai.use (chai, utils) -> 4 | Assertion = chai.Assertion 5 | 6 | Assertion.addMethod 'transform', (args...) -> 7 | transform = @_obj 8 | new Assertion(-> transform.apply(this, args)) 9 | 10 | Assertion.addMethod 'into', (expectedOutput) -> 11 | actualOutput = @_obj() 12 | new Assertion(actualOutput).to.equal(expectedOutput) 13 | -------------------------------------------------------------------------------- /test/fixtures/15.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.select({ 3 | "value": "B" 4 | }, 5 | React.DOM.option({ 6 | "value": "A" 7 | }, 8 | "Apple" 9 | ), 10 | React.DOM.option({ 11 | "value": "B" 12 | }, 13 | "Banana" 14 | ), 15 | React.DOM.option({ 16 | "value": "C" 17 | }, 18 | "Cranberry" 19 | ) 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /test/fixtures/11.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | function map (obj, fn) { 3 | if ('number' === typeof obj.length) return obj.map(fn); 4 | var result = [], key, hasProp = {}.hasOwnProperty; 5 | for (key in obj) hasProp.call(obj, key) && result.push(fn(key, obj[key])); 6 | return result; 7 | } 8 | 9 | return React.DOM.ul(null, 10 | map(this.products, function (product, $index) { 11 | return React.DOM.li(null, 12 | product.name 13 | ); 14 | } 15 | ) 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/12.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | function map (obj, fn) { 3 | if ('number' === typeof obj.length) return obj.map(fn); 4 | var result = [], key, hasProp = {}.hasOwnProperty; 5 | for (key in obj) hasProp.call(obj, key) && result.push(fn(key, obj[key])); 6 | return result; 7 | } 8 | 9 | return React.DOM.ul(null, 10 | map(products, function (product, $index) { 11 | return React.DOM.li(null, 12 | "Product", 13 | product.name 14 | ); 15 | } 16 | ) 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/class-expression-compiler.coffee: -------------------------------------------------------------------------------- 1 | UglifyJS = require('uglify-js') 2 | compressor = UglifyJS.Compressor(side_effects: false) 3 | 4 | module.exports = (expressionOrAst) -> 5 | if expressionOrAst instanceof UglifyJS.AST_Node 6 | ast = expressionOrAst 7 | else 8 | ast = UglifyJS.parse(expressionOrAst) 9 | 10 | ast.figure_out_scope() 11 | 12 | # Combine constant strings 13 | ast = ast.transform(compressor) 14 | 15 | # Trim trailing semi-colon 16 | expression = ast.print_to_string() 17 | return expression.slice(0, expression.length - 1) 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jade-react", 3 | "version": "0.2.0", 4 | "description": "Compile Jade to React components", 5 | "main": "src/main.coffee", 6 | "scripts": { 7 | "test": "gulp test --require coffee-script" 8 | }, 9 | "author": "Duncan Beevers", 10 | "license": "WTFPL", 11 | "dependencies": { 12 | "coffee-script": "~1.6.3", 13 | "jade": "~1.1.5", 14 | "uglify-js": "~2.4.12", 15 | "envify": "~0.2.0", 16 | "react": "~0.8.0" 17 | }, 18 | "devDependencies": { 19 | "gulp": "~3.5.0", 20 | "gulp-mocha": "~0.4.1", 21 | "chai": "~1.8.1" 22 | }, 23 | "directories": { 24 | "test": "test" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/duncanbeevers/jade-react.git" 29 | }, 30 | "keywords": [ 31 | "jade", 32 | "react", 33 | "jsx" 34 | ], 35 | "bugs": { 36 | "url": "https://github.com/duncanbeevers/jade-react/issues" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # jade-react 2 | 3 | Compile Jade templates to React de-sugared JSX. 4 | 5 | ```jade 6 | .container-fluid.readme 7 | .row 8 | h1= this.storeName 9 | ul 10 | each product in this.products 11 | li 12 | | Product 13 | = product.title 14 | ``` 15 | 16 | into 17 | 18 | ```javascript 19 | function () { 20 | function map (obj, fn) { 21 | if ('number' === typeof obj.length) return obj.map(fn); 22 | var result = [], key, hasProp = {}.hasOwnProperty; 23 | for (key in obj) hasProp.call(obj, key) && result.push(fn(key, obj[key])); 24 | return result; 25 | } 26 | 27 | return React.DOM.div({ 28 | "className": "container-fluid readme" 29 | }, 30 | React.DOM.div({ 31 | "className": "row" 32 | }, 33 | React.DOM.h1(null, 34 | this.storeName 35 | ), 36 | React.DOM.ul(null), 37 | map(this.products, function (product, $index) { 38 | return React.DOM.li(null, 39 | "Product", 40 | product.title 41 | ); 42 | } 43 | ) 44 | ) 45 | ); 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /test/fixtures/21.jade: -------------------------------------------------------------------------------- 1 | div 2 | a 3 | abbr 4 | address 5 | area 6 | article 7 | aside 8 | audio 9 | b 10 | base 11 | bdi 12 | bdo 13 | big 14 | blockquote 15 | body 16 | br 17 | button 18 | canvas 19 | caption 20 | cite 21 | code 22 | col 23 | colgroup 24 | data 25 | datalist 26 | dd 27 | del 28 | details 29 | dfn 30 | div 31 | dl 32 | dt 33 | em 34 | embed 35 | fieldset 36 | figcaption 37 | figure 38 | footer 39 | form 40 | h1 41 | h2 42 | h3 43 | h4 44 | h5 45 | h6 46 | head 47 | header 48 | hr 49 | html 50 | i 51 | iframe 52 | img 53 | input 54 | ins 55 | kbd 56 | keygen 57 | label 58 | legend 59 | li 60 | link 61 | main 62 | map 63 | mark 64 | menu 65 | menuitem 66 | meta 67 | meter 68 | nav 69 | noscript 70 | object 71 | ol 72 | optgroup 73 | option 74 | output 75 | p 76 | param 77 | pre 78 | progress 79 | q 80 | rp 81 | rt 82 | ruby 83 | s 84 | samp 85 | script 86 | section 87 | select 88 | small 89 | source 90 | span 91 | strong 92 | style 93 | sub 94 | summary 95 | sup 96 | table 97 | tbody 98 | td 99 | textarea 100 | tfoot 101 | th 102 | thead 103 | time 104 | title 105 | tr 106 | track 107 | u 108 | ul 109 | var 110 | video 111 | wbr 112 | circle 113 | g 114 | line 115 | path 116 | polyline 117 | rect 118 | svg 119 | text 120 | injection 121 | -------------------------------------------------------------------------------- /src/class-array-expression-compiler.coffee: -------------------------------------------------------------------------------- 1 | UglifyJS = require('uglify-js') 2 | expressionCompiler = require('./class-expression-compiler') 3 | 4 | # Insert a space string node between each extant pair of nodes 5 | # in the array. 6 | mingleSpaces = (previous, element, index) -> 7 | if previous.length 8 | previous.push new UglifyJS.AST_String(value: ' ') 9 | previous.push element 10 | return previous 11 | 12 | # Transforms an uglify AST representing a single Array 13 | # into an AST representing a series of concatenation/addition 14 | # operations suitable for css class names. 15 | # 16 | # ['a', 'b', 'c'] 17 | # 18 | # 'a'+' '+'b'+' '+'c' 19 | # 20 | uglifyArrayToStringConcatTransform = do -> 21 | newBody = undefined 22 | constructBinaryAdditions = (elements) -> 23 | right = elements[elements.length - 1] 24 | if elements.length > 2 25 | left = constructBinaryAdditions(elements.slice(0, elements.length - 1)) 26 | else 27 | left = elements[0] 28 | 29 | return new UglifyJS.AST_Binary 30 | left: left 31 | operator: '+' 32 | right: right 33 | 34 | before = (node, descend) -> 35 | if node instanceof UglifyJS.AST_Array 36 | elements = node.elements.reduce mingleSpaces, [] 37 | newBody = constructBinaryAdditions(elements) 38 | return newBody 39 | 40 | descend(node, this) 41 | return node 42 | 43 | new UglifyJS.TreeTransformer(before) 44 | 45 | module.exports = (expression) -> 46 | ast = UglifyJS.parse(expression) 47 | ast = ast.transform(uglifyArrayToStringConcatTransform) 48 | 49 | return expressionCompiler(ast) 50 | -------------------------------------------------------------------------------- /test/compilation-test.coffee: -------------------------------------------------------------------------------- 1 | chai = require('chai') 2 | expect = chai.expect 3 | 4 | # Load custom assertions 5 | require('./helpers/chai-compile') 6 | 7 | render = require('../src/main') 8 | 9 | describe 'compile', -> 10 | fs = require('fs') 11 | 12 | setupFixtureTests = (pretty) -> 13 | fixturesDir = 'test/fixtures/' 14 | fixtures = fs.readdirSync(fixturesDir) 15 | inputs = fixtures.filter (fixture) -> /\.jade$/.test(fixture) 16 | 17 | for inputFileName in inputs 18 | if pretty 19 | suffix = '.pretty.js' 20 | else 21 | suffix = '.js' 22 | 23 | outputFileName = inputFileName + suffix 24 | 25 | do (inputFileName, outputFileName) -> 26 | try 27 | markup = String(fs.readFileSync(fixturesDir + inputFileName)) 28 | output = String(fs.readFileSync(fixturesDir + outputFileName)) 29 | 30 | it 'compiles ' + inputFileName + ' to ' + outputFileName, -> 31 | options = pretty: pretty 32 | expect(render).to.transform(markup, options).into(output) 33 | catch setupError 34 | it 'failed to setup fixture test for file pair ' + inputFileName + '→' + outputFileName, -> 35 | expect(-> throw setupError).not.to.throw() 36 | 37 | setupFixtureTests(false) 38 | setupFixtureTests(true) 39 | 40 | it 'should not compile multiple root nodes', -> 41 | expect(render).transform('p\np\n').to.throw('Component may have no more than one root node') 42 | 43 | it 'should not compile doctype', -> 44 | expect(render).transform('doctype html').to.throw('Component may not have doctype tag') 45 | -------------------------------------------------------------------------------- /test/fixtures/21.jade.js: -------------------------------------------------------------------------------- 1 | function(){return React.DOM.div(null,React.DOM.a(null),React.DOM.abbr(null),React.DOM.address(null),React.DOM.area(null),React.DOM.article(null),React.DOM.aside(null),React.DOM.audio(null),React.DOM.b(null),React.DOM.base(null),React.DOM.bdi(null),React.DOM.bdo(null),React.DOM.big(null),React.DOM.blockquote(null),React.DOM.body(null),React.DOM.br(null),React.DOM.button(null),React.DOM.canvas(null),React.DOM.caption(null),React.DOM.cite(null),React.DOM.code(null),React.DOM.col(null),React.DOM.colgroup(null),React.DOM.data(null),React.DOM.datalist(null),React.DOM.dd(null),React.DOM.del(null),React.DOM.details(null),React.DOM.dfn(null),React.DOM.div(null),React.DOM.dl(null),React.DOM.dt(null),React.DOM.em(null),React.DOM.embed(null),React.DOM.fieldset(null),React.DOM.figcaption(null),React.DOM.figure(null),React.DOM.footer(null),React.DOM.form(null),React.DOM.h1(null),React.DOM.h2(null),React.DOM.h3(null),React.DOM.h4(null),React.DOM.h5(null),React.DOM.h6(null),React.DOM.head(null),React.DOM.header(null),React.DOM.hr(null),React.DOM.html(null),React.DOM.i(null),React.DOM.iframe(null),React.DOM.img(null),React.DOM.input(null),React.DOM.ins(null),React.DOM.kbd(null),React.DOM.keygen(null),React.DOM.label(null),React.DOM.legend(null),React.DOM.li(null),React.DOM.link(null),React.DOM.main(null),React.DOM.map(null),React.DOM.mark(null),React.DOM.menu(null),React.DOM.menuitem(null),React.DOM.meta(null),React.DOM.meter(null),React.DOM.nav(null),React.DOM.noscript(null),React.DOM.object(null),React.DOM.ol(null),React.DOM.optgroup(null),React.DOM.option(null),React.DOM.output(null),React.DOM.p(null),React.DOM.param(null),React.DOM.pre(null),React.DOM.progress(null),React.DOM.q(null),React.DOM.rp(null),React.DOM.rt(null),React.DOM.ruby(null),React.DOM.s(null),React.DOM.samp(null),React.DOM.script(null),React.DOM.section(null),React.DOM.select(null),React.DOM.small(null),React.DOM.source(null),React.DOM.span(null),React.DOM.strong(null),React.DOM.style(null),React.DOM.sub(null),React.DOM.summary(null),React.DOM.sup(null),React.DOM.table(null),React.DOM.tbody(null),React.DOM.td(null),React.DOM.textarea(null),React.DOM.tfoot(null),React.DOM.th(null),React.DOM.thead(null),React.DOM.time(null),React.DOM.title(null),React.DOM.tr(null),React.DOM.track(null),React.DOM.u(null),React.DOM.ul(null),React.DOM.var(null),React.DOM.video(null),React.DOM.wbr(null),React.DOM.circle(null),React.DOM.g(null),React.DOM.line(null),React.DOM.path(null),React.DOM.polyline(null),React.DOM.rect(null),React.DOM.svg(null),React.DOM.text(null),React.DOM.injection(null));} -------------------------------------------------------------------------------- /test/fixtures/21.jade.pretty.js: -------------------------------------------------------------------------------- 1 | function () { 2 | return React.DOM.div(null, 3 | React.DOM.a(null), 4 | React.DOM.abbr(null), 5 | React.DOM.address(null), 6 | React.DOM.area(null), 7 | React.DOM.article(null), 8 | React.DOM.aside(null), 9 | React.DOM.audio(null), 10 | React.DOM.b(null), 11 | React.DOM.base(null), 12 | React.DOM.bdi(null), 13 | React.DOM.bdo(null), 14 | React.DOM.big(null), 15 | React.DOM.blockquote(null), 16 | React.DOM.body(null), 17 | React.DOM.br(null), 18 | React.DOM.button(null), 19 | React.DOM.canvas(null), 20 | React.DOM.caption(null), 21 | React.DOM.cite(null), 22 | React.DOM.code(null), 23 | React.DOM.col(null), 24 | React.DOM.colgroup(null), 25 | React.DOM.data(null), 26 | React.DOM.datalist(null), 27 | React.DOM.dd(null), 28 | React.DOM.del(null), 29 | React.DOM.details(null), 30 | React.DOM.dfn(null), 31 | React.DOM.div(null), 32 | React.DOM.dl(null), 33 | React.DOM.dt(null), 34 | React.DOM.em(null), 35 | React.DOM.embed(null), 36 | React.DOM.fieldset(null), 37 | React.DOM.figcaption(null), 38 | React.DOM.figure(null), 39 | React.DOM.footer(null), 40 | React.DOM.form(null), 41 | React.DOM.h1(null), 42 | React.DOM.h2(null), 43 | React.DOM.h3(null), 44 | React.DOM.h4(null), 45 | React.DOM.h5(null), 46 | React.DOM.h6(null), 47 | React.DOM.head(null), 48 | React.DOM.header(null), 49 | React.DOM.hr(null), 50 | React.DOM.html(null), 51 | React.DOM.i(null), 52 | React.DOM.iframe(null), 53 | React.DOM.img(null), 54 | React.DOM.input(null), 55 | React.DOM.ins(null), 56 | React.DOM.kbd(null), 57 | React.DOM.keygen(null), 58 | React.DOM.label(null), 59 | React.DOM.legend(null), 60 | React.DOM.li(null), 61 | React.DOM.link(null), 62 | React.DOM.main(null), 63 | React.DOM.map(null), 64 | React.DOM.mark(null), 65 | React.DOM.menu(null), 66 | React.DOM.menuitem(null), 67 | React.DOM.meta(null), 68 | React.DOM.meter(null), 69 | React.DOM.nav(null), 70 | React.DOM.noscript(null), 71 | React.DOM.object(null), 72 | React.DOM.ol(null), 73 | React.DOM.optgroup(null), 74 | React.DOM.option(null), 75 | React.DOM.output(null), 76 | React.DOM.p(null), 77 | React.DOM.param(null), 78 | React.DOM.pre(null), 79 | React.DOM.progress(null), 80 | React.DOM.q(null), 81 | React.DOM.rp(null), 82 | React.DOM.rt(null), 83 | React.DOM.ruby(null), 84 | React.DOM.s(null), 85 | React.DOM.samp(null), 86 | React.DOM.script(null), 87 | React.DOM.section(null), 88 | React.DOM.select(null), 89 | React.DOM.small(null), 90 | React.DOM.source(null), 91 | React.DOM.span(null), 92 | React.DOM.strong(null), 93 | React.DOM.style(null), 94 | React.DOM.sub(null), 95 | React.DOM.summary(null), 96 | React.DOM.sup(null), 97 | React.DOM.table(null), 98 | React.DOM.tbody(null), 99 | React.DOM.td(null), 100 | React.DOM.textarea(null), 101 | React.DOM.tfoot(null), 102 | React.DOM.th(null), 103 | React.DOM.thead(null), 104 | React.DOM.time(null), 105 | React.DOM.title(null), 106 | React.DOM.tr(null), 107 | React.DOM.track(null), 108 | React.DOM.u(null), 109 | React.DOM.ul(null), 110 | React.DOM.var(null), 111 | React.DOM.video(null), 112 | React.DOM.wbr(null), 113 | React.DOM.circle(null), 114 | React.DOM.g(null), 115 | React.DOM.line(null), 116 | React.DOM.path(null), 117 | React.DOM.polyline(null), 118 | React.DOM.rect(null), 119 | React.DOM.svg(null), 120 | React.DOM.text(null), 121 | React.DOM.injection(null) 122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /src/compiler.coffee: -------------------------------------------------------------------------------- 1 | React = require('react') 2 | 3 | isString = (s) -> '\'' == s[0] || '"' == s[0] 4 | toString = (s) -> s.slice(1, s.length - 1) 5 | 6 | arrayCompiler = require('./class-array-expression-compiler') 7 | expressionCompiler = require('./class-expression-compiler') 8 | 9 | prettyMap = ' ' + """ 10 | function map (obj, fn) { 11 | if ('number' === typeof obj.length) return obj.map(fn); 12 | var result = [], key, hasProp = {}.hasOwnProperty; 13 | for (key in obj) hasProp.call(obj, key) && result.push(fn(key, obj[key])); 14 | return result; 15 | } 16 | """.split('\n').join('\n ') + '\n\n' 17 | 18 | terseMap = """ 19 | function map(o,f){if('number'===typeof o.length)return o.map(f);var r=[],k,h={}.hasOwnProperty;for(k in o)h.call(o,k)&&r.push(f(k,o[k]));return r;} 20 | """ 21 | 22 | pairSort = (a, b) -> 23 | if a[0] < b[0] 24 | -1 25 | else if a[0] > b[0] 26 | 1 27 | else 28 | 0 29 | 30 | partition = (collection, fn) -> 31 | left = [] 32 | right = [] 33 | for item in collection 34 | if fn(item) 35 | left.push item 36 | else 37 | right.push item 38 | [left, right] 39 | 40 | isArrayExpression = (expression) -> 41 | '[' == expression[0] && ']' == expression[expression.length - 1] 42 | 43 | joinArrays = (expression) -> 44 | if isArrayExpression(expression) 45 | expression + '.join(" ")' 46 | else 47 | expression 48 | 49 | Compiler = (node, options) -> 50 | compile: -> 51 | # Setup 52 | pretty = options.pretty 53 | depth = -1 54 | seenDepth0 = false 55 | parts = [] 56 | seenDepth0 = false 57 | needsMap = false 58 | continueIndenting = false 59 | 60 | visitTag = (tag) -> 61 | bufferExpression(indentToDepth()) 62 | if React.DOM[tag.name] 63 | bufferExpression('React.DOM.') 64 | bufferExpression(tag.name, '(') 65 | 66 | visitAttributes(tag.attrs, tag.attributeBlocks) 67 | 68 | depth += 1 69 | if 0 == depth && seenDepth0 70 | throw new Error('Component may have no more than one root node') 71 | seenDepth0 = true 72 | anyArgs = visitArgs(tag) 73 | depth -= 1 74 | 75 | if pretty 76 | if anyArgs 77 | bufferExpression('\n', indentToDepth(), ')') 78 | else 79 | bufferExpression(')') 80 | else 81 | bufferExpression(')') 82 | 83 | visitArgs = (node) -> 84 | len = node.block.nodes.length 85 | anyArgs = node.code || len 86 | if anyArgs 87 | if pretty 88 | bufferExpression(',\n') 89 | else 90 | bufferExpression(',') 91 | 92 | if node.code 93 | visitCode(node.code) 94 | 95 | for node, i in node.block.nodes 96 | 97 | visit(node) 98 | 99 | if i + 1 < len 100 | if pretty 101 | bufferExpression(',\n') 102 | else 103 | bufferExpression(',') 104 | 105 | return anyArgs 106 | 107 | visitBlock = (block) -> 108 | len = block.nodes.length 109 | for node, i in block.nodes 110 | visit(node) 111 | if i + 1 < len 112 | bufferExpression(' + \n') 113 | 114 | normalizeClassExpressions = (expressions) -> 115 | # If the expression is an array, we won't peer inside, 116 | # and we'll just invoke a join function on the whole expression. 117 | [arrayExpressions, expressions] = partition expressions, 118 | isArrayExpression 119 | 120 | [stringExpressions, expressions] = partition expressions, 121 | isString 122 | 123 | stringClassNames = stringExpressions.map(toString) 124 | stringClassNamesExpression = JSON.stringify(stringClassNames.join(' ')) 125 | 126 | if stringClassNames.length 127 | expressions.unshift(stringClassNamesExpression) 128 | 129 | if arrayExpressions.length 130 | for expression in arrayExpressions 131 | expressions.push(arrayCompiler(expression)) 132 | 133 | if 1 == expressions.length 134 | expression = expressions[0] 135 | else 136 | expression = expressionCompiler(expressions.join('+" "+')) 137 | 138 | return expression 139 | 140 | normalizeAttributes = (attrs) -> 141 | visited = {} 142 | classExpressions = [] 143 | normalized = {} 144 | 145 | for attr in attrs 146 | name = attr.name 147 | val = attr.val 148 | 149 | # `class` attributes are automatically treated as `className` 150 | if 'class' == name 151 | name = 'className' 152 | 153 | # Only `class`/`className` attributes are extensible. 154 | # Every other attribute blows up if its seen twice in a tag 155 | if 'className' != name && visited[name] 156 | throw new Error('Duplicate key ' + JSON.stringify(name) + ' is not allowed.') 157 | visited[name] = true 158 | 159 | # `className` is handled specially. 160 | # First, at compile time, constant class names are 161 | # gathered and concatenated, like so `foo baz` 162 | # Then dynamic components are added as run-time expressions, 163 | # concatenating themselves on to the constant component. 164 | if 'className' == name 165 | classExpressions.push val 166 | else 167 | normalized[name] = val 168 | 169 | # If we ever visited a `className` attribute, we need to 170 | # build its normalized value manually. 171 | if visited['className'] 172 | normalized['className'] = normalizeClassExpressions(classExpressions) 173 | 174 | return normalized 175 | 176 | visitAttributes = (attrs, attributeBlocks) -> 177 | unless attrs && attrs.length 178 | bufferExpression('null') 179 | return 180 | 181 | normalized = normalizeAttributes(attrs) 182 | 183 | pairs = [] 184 | for name, val of normalized 185 | pairs.push([name, val]) 186 | 187 | # Lexically sort by attribute name 188 | pairs.sort(pairSort) 189 | 190 | if pretty 191 | sep = ': ' 192 | else 193 | sep = ':' 194 | pairs = pairs.map (pair) -> 195 | [name, val] = pair 196 | JSON.stringify(name) + sep + val 197 | 198 | bufferExpression('{') 199 | if pretty 200 | depth += 2 201 | bufferExpression('\n', indentToDepth()) 202 | bufferExpression(pairs.join(',\n' + indentToDepth())) 203 | depth -= 1 204 | bufferExpression('\n', indentToDepth()) 205 | depth -= 1 206 | else 207 | bufferExpression(pairs.join(',')) 208 | bufferExpression('}') 209 | 210 | visitCode = (code) -> 211 | return unless code 212 | bufferExpression(indentToDepth(), code.val) 213 | 214 | visitText = (node) -> 215 | bufferExpression(indentToDepth(), JSON.stringify(node.val)) 216 | 217 | visitEach = (node) -> 218 | needsMap = true 219 | depth += 1 220 | bufferExpression(indentToDepth(), 'map(', node.obj) 221 | 222 | if pretty 223 | bufferExpression(', function (') 224 | else 225 | bufferExpression(',function(') 226 | bufferExpression(node.val) 227 | 228 | if pretty 229 | bufferExpression(', ') 230 | else 231 | bufferExpression(',') 232 | 233 | bufferExpression(node.key, ')') 234 | 235 | if pretty 236 | bufferExpression(' {\n') 237 | else 238 | bufferExpression('{') 239 | 240 | depth += 1 241 | bufferExpression(indentToDepth(), 'return ') 242 | continueIndenting = false 243 | for node in node.block.nodes 244 | visit(node) 245 | 246 | if pretty 247 | bufferExpression(';\n') 248 | else 249 | bufferExpression(';') 250 | 251 | depth -= 1 252 | if pretty 253 | bufferExpression(indentToDepth(), '}\n') 254 | else 255 | bufferExpression(indentToDepth(), '}') 256 | depth -= 1 257 | bufferExpression(indentToDepth(), ')') 258 | 259 | visitNodes = 260 | Text: visitText 261 | Tag: visitTag 262 | Block: visitBlock 263 | Each: visitEach 264 | Code: visitCode 265 | Doctype: -> throw new Error('Component may not have doctype tag') 266 | 267 | indentToDepth = -> 268 | return '' unless pretty 269 | if continueIndenting 270 | # depth 1 is implicit in function wrapper 271 | Array(depth + 3).join(' ') 272 | else 273 | continueIndenting = true 274 | '' 275 | 276 | # Open render function body 277 | bufferExpression = (strs...) -> parts = parts.concat(strs) 278 | visit = (node) -> visitNodes[node.type](node) 279 | visit(node) 280 | 281 | # Create the function wrapper. 282 | if pretty 283 | parts.unshift ' return ' 284 | if needsMap 285 | parts.unshift prettyMap 286 | parts.unshift 'function () {\n' 287 | else 288 | parts.unshift 'return ' 289 | if needsMap 290 | parts.unshift terseMap 291 | parts.unshift 'function(){' 292 | 293 | if pretty 294 | bufferExpression(';\n}\n') 295 | else 296 | bufferExpression(';}') 297 | 298 | # Map to jade machine instruction statements; 299 | toPush = (part) -> 'buf.push(' + JSON.stringify(part)+ ');' 300 | return parts.map(toPush).join('\n') 301 | 302 | module.exports = Compiler 303 | --------------------------------------------------------------------------------