├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── lib ├── browserify.js ├── compile-client.js ├── compile-file-client.js ├── compile-file.js ├── compile.js ├── parse-file.js ├── parse.js └── utils │ ├── acorn-transform.js │ ├── compiler.js │ ├── is-template-literal.js │ ├── jade-fix-attrs.js │ ├── jade-fix-style.js │ ├── jade-join-classes.js │ ├── jade-merge.js │ ├── java-script-compressor.js │ └── set-locals.js ├── package.json └── test ├── bonus-features ├── component-composition.html ├── component-composition.jade ├── component-subcomponent.jade ├── component-this-each.html ├── component-this-each.jade ├── component-this-mixin.html ├── component-this-mixin.jade ├── component-this.html ├── component-this.jade └── partial-application.jade ├── download-jade-tests.js ├── index.js ├── mock-dom.js ├── test-client-syntax-error.js └── test-client.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | pids 10 | logs 11 | results 12 | npm-debug.log 13 | node_modules 14 | /test/output 15 | /test/jade 16 | coverage 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Forbes Lindesay 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project has now been replaced by https://github.com/pugjs/babel-plugin-transform-react-pug please use that for any future projects. 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isTemplateLiteral = require('./lib/utils/is-template-literal.js'); 4 | var browserify = require('./lib/browserify'); 5 | var compile = require('./lib/compile'); 6 | var compileFile = require('./lib/compile-file'); 7 | var compileClient = require('./lib/compile-client'); 8 | var compileFileClient = require('./lib/compile-file-client'); 9 | 10 | exports = (module.exports = browserifySupport); 11 | function browserifySupport(options, extra) { 12 | if (isTemplateLiteral(options)) { 13 | return compile(options.raw[0]); 14 | } else { 15 | return browserify.apply(this, arguments); 16 | } 17 | } 18 | 19 | exports.compile = compile; 20 | exports.compileFile = compileFile; 21 | exports.compileClient = compileClient; 22 | exports.compileFileClient = compileFileClient; 23 | -------------------------------------------------------------------------------- /lib/browserify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Transform = require('stream').Transform; 4 | var PassThrough = require('stream').PassThrough; 5 | var staticModule = require('static-module'); 6 | var resolve = require('resolve'); 7 | var path = require('path'); 8 | var stringify = require('js-stringify'); 9 | var isTemplateLiteral = require('./utils/is-template-literal.js'); 10 | var acornTransform = require('./utils/acorn-transform.js'); 11 | var compileClient = require('./compile-client.js'); 12 | var compileFileClient = require('./compile-file-client.js'); 13 | 14 | module.exports = browserify; 15 | function browserify(options, extra) { 16 | if (typeof options === 'string') { 17 | var filename = options; 18 | options = extra || {}; 19 | return makeStream(function (source) { 20 | return transform(filename, source, options); 21 | }); 22 | } else { 23 | options = options || {}; 24 | return function (filename, extra) { 25 | extra = extra || {}; 26 | Object.keys(options).forEach(function (key) { 27 | if (typeof extra[key] === 'undefined') { 28 | extra[key] = options[key]; 29 | } 30 | }); 31 | return makeStream(function (source) { 32 | return transform(filename, source, options); 33 | }); 34 | }; 35 | } 36 | } 37 | 38 | function makeStream(fn) { 39 | var src = ''; 40 | var stream = new Transform(); 41 | stream._transform = function (chunk, encoding, callback) { 42 | src += chunk; 43 | callback(); 44 | }; 45 | stream._flush = function (callback) { 46 | try { 47 | var res = fn(src); 48 | res.on('data', this.push.bind(this)); 49 | res.on('error', callback); 50 | res.on('end', callback.bind(null, null)); 51 | } catch (err) { 52 | callback(err); 53 | } 54 | }; 55 | return stream; 56 | } 57 | 58 | function makeClientRequire(filename) { 59 | function cr(path) { 60 | return require(cr.resolve(path)); 61 | } 62 | cr.resolve = function (path) { 63 | return resolve.sync(path, { 64 | basedir: path.dirname(filename) 65 | }); 66 | }; 67 | return cr; 68 | } 69 | 70 | function makeStaticImplementation(filename, options) { 71 | function staticImplementation(templateLiteral) { 72 | if (isTemplateLiteral(templateLiteral)) { 73 | return staticCompileImplementation(templateLiteral.raw[0]); 74 | } else { 75 | return '(function () { throw new Error("Invalid client side argument to react-jade"); }())'; 76 | } 77 | } 78 | function staticCompileImplementation(jadeSrc, localOptions) { 79 | localOptions = localOptions || {}; 80 | for (var key in options) { 81 | if ((key in options) && !(key in localOptions)) 82 | localOptions[key] = options[key]; 83 | } 84 | localOptions.filename = localOptions.filename || filename; 85 | localOptions.outputFile = filename; 86 | return compileClient(jadeSrc, localOptions); 87 | } 88 | function staticCompileFileImplementation(jadeFile, localOptions) { 89 | localOptions = localOptions || {}; 90 | for (var key in options) { 91 | if ((key in options) && !(key in localOptions)) 92 | localOptions[key] = options[key]; 93 | } 94 | localOptions.outputFile = filename; 95 | return compileFileClient(jadeFile, localOptions); 96 | } 97 | staticImplementation.compile = staticCompileImplementation; 98 | staticImplementation.compileFile = staticCompileFileImplementation; 99 | return staticImplementation; 100 | } 101 | 102 | // compile filename and return a readable stream 103 | function transform(filename, source, options) { 104 | function templateToJs(template) { 105 | return '(function () {' + 106 | 'var quasi = ' + stringify(template.slice(0)) + ';' + 107 | 'quasi.raw = ' + stringify(template.raw.slice(0)) + ';' + 108 | 'return quasi;}())'; 109 | } 110 | 111 | if (/\.json$/.test(filename)) { 112 | var stream = new PassThrough(); 113 | stream.end(source); 114 | return stream; 115 | } 116 | 117 | source = acornTransform(source, { 118 | TaggedTemplateExpression: function (node) { 119 | var cooked = node.quasi.quasis.map(function (q) { 120 | return q.value.cooked; 121 | }); 122 | cooked.raw = node.quasi.quasis.map(function (q) { 123 | return q.value.raw; 124 | }); 125 | var quasi = templateToJs(cooked); 126 | 127 | var expressions = node.quasi.expressions.map(acornTransform.getSource); 128 | 129 | acornTransform.setSource(node, acornTransform.getSource(node.tag) + '(' + 130 | [quasi].concat(expressions).join(', ') + ')'); 131 | } 132 | }); 133 | // var $__0 = Object.freeze(Object.defineProperties(["\ndiv\n h1 Page not found"], {raw: {value: Object.freeze(["\ndiv\n h1 Page not found"])}})); 134 | // var notFound = jade($__0); 135 | function isObjectDot(node, property) { 136 | return node.type === 'CallExpression' && node.callee.type === 'MemberExpression' && 137 | node.callee.object.type === 'Identifier' && node.callee.object.name === 'Object' && 138 | node.callee.computed === false && node.callee.property.type === 'Identifier' && 139 | node.callee.property.name === property; 140 | } 141 | function isArrayOfStrings(node) { 142 | return node.type === 'ArrayExpression' && node.elements.every(function (el) { 143 | return el.type === 'Literal' && typeof el.value === 'string'; 144 | }); 145 | } 146 | function isKeyedObject(node, key) { 147 | return node.type === 'ObjectExpression' && node.properties.length === 1 && 148 | node.properties[0].computed === false && node.properties[0].key.type === 'Identifier' && 149 | node.properties[0].key.name === key; 150 | } 151 | function isTraceuredTemplateLiteral(node) { 152 | if (isObjectDot(node, 'freeze') && node.arguments.length === 1 && isObjectDot(node.arguments[0], 'defineProperties')) { 153 | var args = node.arguments[0].arguments; 154 | if (isArrayOfStrings(args[0]) && isKeyedObject(args[1], 'raw')) { 155 | var raw = args[1].properties[0].value; 156 | if (isKeyedObject(raw, 'value')) { 157 | raw = raw.properties[0].value; 158 | if (isObjectDot(raw, 'freeze') && raw.arguments.length === 1 && isArrayOfStrings(raw.arguments[0])) { 159 | return Function('', 'return ' + acornTransform.getSource(node))(); 160 | } 161 | } 162 | } 163 | } 164 | } 165 | 166 | var literals = {}; 167 | source = acornTransform(source, { 168 | VariableDeclaration: function (node) { 169 | node.declarations.forEach(function (declaration) { 170 | if (declaration.id.type === 'Identifier' && declaration.id.name[0] === '$' && declaration.init) { 171 | var value = isTraceuredTemplateLiteral(declaration.init); 172 | if (value) { 173 | literals[declaration.id.name] = value; 174 | acornTransform.setSource(declaration.init, 'undefined'); 175 | } 176 | } 177 | }); 178 | }, 179 | Identifier: function (node) { 180 | if (node.name[0] === '$' && node.name in literals) { 181 | acornTransform.setSource(node, templateToJs(literals[node.name])); 182 | } 183 | } 184 | }); 185 | 186 | var makeStatic = staticModule({ 'react-jade': makeStaticImplementation(filename, options) }, { 187 | vars: { 188 | __dirname: path.dirname(filename), 189 | __filename: path.resolve(filename), 190 | path: path, 191 | require: makeClientRequire(filename) 192 | } 193 | }); 194 | makeStatic.end(source); 195 | return makeStatic; 196 | } 197 | -------------------------------------------------------------------------------- /lib/compile-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var parse = require('./parse'); 5 | 6 | var reactRuntimePath; 7 | 8 | try { 9 | reactRuntimePath = require.resolve('react'); 10 | } catch (ex) { 11 | reactRuntimePath = false; 12 | } 13 | 14 | module.exports = compileClient; 15 | function compileClient(str, options){ 16 | options = options || { filename: '' }; 17 | var react = options.outputFile ? path.relative(path.dirname(options.outputFile), reactRuntimePath) : reactRuntimePath; 18 | 19 | if (options.globalReact || !reactRuntimePath) { 20 | return '(function (React) {\n ' + 21 | parse(str, options).split('\n').join('\n ') + 22 | '\n}(React))'; 23 | } else { 24 | return '(function (React) {\n ' + 25 | parse(str, options).split('\n').join('\n ') + 26 | '\n}(typeof React !== "undefined" ? React : require("' + react.replace(/^([^\.])/, './$1').replace(/\\/g, '/') + '")))'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/compile-file-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var compileClient = require('./compile-client'); 6 | 7 | module.exports = compileFileClient; 8 | function compileFileClient(filename, options) { 9 | var str = fs.readFileSync(filename, 'utf8').toString(); 10 | var options = options || {}; 11 | options.filename = path.resolve(filename); 12 | return compileClient(str, options); 13 | } 14 | -------------------------------------------------------------------------------- /lib/compile-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var parseFile = require('./parse-file'); 5 | 6 | module.exports = compileFile; 7 | function compileFile(filename, options) { 8 | return Function('React', parseFile(filename, options))(React); 9 | } 10 | -------------------------------------------------------------------------------- /lib/compile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | var parse = require('./parse'); 5 | 6 | module.exports = compile; 7 | function compile(str, options){ 8 | options = options || { filename: '' }; 9 | return Function('React', parse(str, options))(React); 10 | } 11 | -------------------------------------------------------------------------------- /lib/parse-file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var parse = require('./parse'); 6 | 7 | module.exports = parseFile; 8 | function parseFile(filename, options) { 9 | var str = fs.readFileSync(filename, 'utf8').toString(); 10 | var options = options || {}; 11 | options.filename = path.resolve(filename); 12 | return parse(str, options); 13 | } 14 | -------------------------------------------------------------------------------- /lib/parse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var assert = require('assert'); 5 | var uglify = require('uglify-js'); 6 | var Parser = require('jade/lib/parser.js'); 7 | var jade = require('jade/lib/runtime.js'); 8 | var addWith = require('with'); 9 | var Compiler = require('./utils/compiler.js'); 10 | var JavaScriptCompressor = require('./utils/java-script-compressor.js'); 11 | 12 | var jade_join_classes = fs.readFileSync(__dirname + '/utils/jade-join-classes.js', 'utf8'); 13 | var jade_fix_style = fs.readFileSync(__dirname + '/utils/jade-fix-style.js', 'utf8'); 14 | var jade_fix_attrs = fs.readFileSync(__dirname + '/utils/jade-fix-attrs.js', 'utf8'); 15 | var jade_merge = fs.readFileSync(__dirname + '/utils/jade-merge.js', 'utf8'); 16 | var setLocals = fs.readFileSync(__dirname + '/utils/set-locals.js', 'utf8'); 17 | 18 | module.exports = parse; 19 | function parse(str, options) { 20 | var options = options || {}; 21 | var parser = new Parser(str, options.filename, options); 22 | var tokens; 23 | try { 24 | // Parse 25 | tokens = parser.parse(); 26 | } catch (err) { 27 | parser = parser.context(); 28 | jade.rethrow(err, parser.filename, parser.lexer.lineno, parser.input); 29 | } 30 | var compiler = new Compiler(tokens); 31 | 32 | var src = compiler.compile(); 33 | src = [ 34 | jade_join_classes + ';', 35 | jade_fix_style + ';', 36 | jade_fix_attrs + ';', 37 | jade_merge + ';', 38 | 'var jade_mixins = {};', 39 | 'var jade_interp;', 40 | src 41 | ].join('\n') 42 | 43 | var ast = uglify.parse(';(function () {' + src + '}.call(this));', { 44 | filename: options.filename 45 | }); 46 | 47 | ast.figure_out_scope(); 48 | ast = ast.transform(uglify.Compressor({ 49 | sequences: false, // join consecutive statemets with the “comma operator" 50 | properties: true, // optimize property access: a["foo"] → a.foo 51 | dead_code: true, // discard unreachable code 52 | unsafe: true, // some unsafe optimizations (see below) 53 | conditionals: true, // optimize if-s and conditional expressions 54 | comparisons: true, // optimize comparisonsx 55 | evaluate: true, // evaluate constant expressions 56 | booleans: true, // optimize boolean expressions 57 | loops: true, // optimize loops 58 | unused: true, // drop unused variables/functions 59 | hoist_funs: true, // hoist function declarations 60 | hoist_vars: false, // hoist variable declarations 61 | if_return: true, // optimize if-s followed by return/continue 62 | join_vars: false, // join var declarations 63 | cascade: true, // try to cascade `right` into `left` in sequences 64 | side_effects: true, // drop side-effect-free statements 65 | warnings: false, // warn about potentially dangerous optimizations/code 66 | global_defs: {} // global definitions)); 67 | })); 68 | 69 | ast = ast.transform(new JavaScriptCompressor()); 70 | 71 | src = ast.body[0].body.expression.expression.body.map(function (statement) { 72 | return statement.print_to_string({ 73 | beautify: true, 74 | comments: true, 75 | indent_level: 2 76 | }); 77 | }).join('\n'); 78 | src = addWith('locals || {}', src, [ 79 | 'tags', 80 | 'React', 81 | 'Array', 82 | 'undefined' 83 | ]); 84 | var js = 'var fn = function (locals) {' + 85 | 'var tags = [];' + 86 | src + 87 | 'if (tags.length === 1 && !Array.isArray(tags[0])) { return tags.pop() };' + 88 | 'tags.unshift("div", null);' + 89 | 'return React.createElement.apply(React, tags);' + 90 | '}'; 91 | 92 | // Check that the compiled JavaScript code is valid thus far. 93 | // uglify-js throws very cryptic errors when it fails to parse code. 94 | try { 95 | Function('', js); 96 | } catch (ex) { 97 | console.log(js); 98 | throw ex; 99 | } 100 | 101 | var ast = uglify.parse(js + ';\nfn.locals = ' + setLocals + ';', { 102 | filename: options.filename 103 | }); 104 | js = ast.print_to_string({ 105 | beautify: true, 106 | comments: true, 107 | indent_level: 2 108 | }); 109 | return js + ';\nreturn fn;'; 110 | } 111 | -------------------------------------------------------------------------------- /lib/utils/acorn-transform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var acorn = require('acorn'); 4 | var walk = require('acorn/dist/walk'); 5 | 6 | module.exports = transform; 7 | function transform(src, walker) { 8 | try { 9 | var ast = acorn.parse(src, { 10 | ecmaVersion: 6, 11 | allowReturnOutsideFunction: true, 12 | allowImportExportEverywhere: true, 13 | allowHashBang: true 14 | }); 15 | } catch (ex) { 16 | if (typeof ex.loc === 'object' && typeof ex.loc.line === 'number' && typeof ex.loc.column === 'number') { 17 | var lines = src.split(/\n/g); 18 | 19 | ex.message += '\n\n | ' + (lines[ex.loc.line - 2] || '') + 20 | '\n> | ' + (lines[ex.loc.line - 1] || '') + 21 | '\n | ' + (lines[ex.loc.line] || ''); 22 | } 23 | throw ex; 24 | } 25 | src = src.split(''); 26 | 27 | function getSource(node) { 28 | return src.slice(node.start, node.end).join(''); 29 | } 30 | function setSource(node, str) { 31 | for (var i = node.start; i < node.end; i++) { 32 | src[i] = ''; 33 | } 34 | src[node.start] = str; 35 | } 36 | module.exports.getSource = getSource; 37 | module.exports.setSource = setSource; 38 | 39 | walk.ancestor(ast, walker); 40 | 41 | return src.join(''); 42 | } 43 | -------------------------------------------------------------------------------- /lib/utils/compiler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var constantinople = require('constantinople'); 5 | var ent = require('ent'); 6 | var uglify = require('uglify-js'); 7 | var React = require('react'); 8 | var stringify = require('js-stringify'); 9 | 10 | var joinClasses = Function('', 'return ' + fs.readFileSync(__dirname + '/jade-join-classes.js', 'utf8'))(); 11 | var fixStyle = Function('', 'return ' + fs.readFileSync(__dirname + '/jade-fix-style.js', 'utf8'))(); 12 | 13 | function isConstant(str) { 14 | return constantinople(str); 15 | } 16 | function toConstant(str) { 17 | return constantinople.toConstant(str); 18 | } 19 | 20 | module.exports = Compiler; 21 | function Compiler(node) { 22 | this.node = node; 23 | this.mixins = {}; 24 | this.dynamicMixins = false; 25 | } 26 | 27 | Compiler.prototype.compile = function(){ 28 | this.buf = []; 29 | this.visit(this.node); 30 | 31 | if (!this.dynamicMixins) { 32 | // if there are no dynamic mixins we can remove any un-used mixins 33 | var mixinNames = Object.keys(this.mixins); 34 | for (var i = 0; i < mixinNames.length; i++) { 35 | var mixin = this.mixins[mixinNames[i]]; 36 | if (!mixin.used) { 37 | for (var x = 0; x < mixin.instances.length; x++) { 38 | for (var y = mixin.instances[x].start; y < mixin.instances[x].end; y++) { 39 | this.buf[y] = ''; 40 | } 41 | } 42 | } 43 | } 44 | } 45 | return this.buf.join('\n'); 46 | }; 47 | Compiler.prototype.visit = function(node){ 48 | if (typeof this['visit' + node.type] !== 'function') { 49 | throw new Error(node.type + ' is not supported'); 50 | } 51 | return this['visit' + node.type](node); 52 | } 53 | Compiler.prototype.visitBlock = function(block){ 54 | for (var i = 0; i < block.nodes.length; i++) { 55 | this.visit(block.nodes[i]); 56 | } 57 | } 58 | Compiler.prototype.visitCode = function (code) { 59 | if (code.block && code.buffer) { 60 | throw new Error('Not Implemented'); 61 | } 62 | if (code.buffer && !code.escape) { 63 | this.buf.push('tags.push(React.createElement("div", {dangerouslySetInnerHTML:{__html: ' + code.val + '}}))'); 64 | } else if (code.buffer) { 65 | this.buf.push('tags.push(' + code.val + ')'); 66 | } else { 67 | this.buf.push(code.val); 68 | if (code.block) { 69 | this.buf.push('{'); 70 | this.visit(code.block); 71 | this.buf.push('}'); 72 | } 73 | } 74 | }; 75 | Compiler.prototype.visitComment = function (comment) { 76 | this.buf.push('\n//' + comment.val + '\n'); 77 | }; 78 | Compiler.prototype.visitBlockComment = function (comment) { 79 | this.buf.push('/*'); 80 | this.buf.push(comment.val); 81 | this.visit(comment.block); 82 | this.buf.push('*/'); 83 | }; 84 | Compiler.prototype.visitEach = function (each) { 85 | this.buf.push('' 86 | + '// iterate ' + each.obj + '\n' 87 | + ';tags.push(function(){\n' 88 | + ' var tags = [];\n' 89 | + ' var $$obj = ' + each.obj + ';\n' 90 | + ' if (\'number\' == typeof $$obj.length) {\n'); 91 | 92 | if (each.alternative) { 93 | this.buf.push(' if ($$obj.length) {'); 94 | } 95 | 96 | this.buf.push('for (var ' + each.key + ' = 0, $$l = $$obj.length; ' + each.key + ' < $$l; ' + each.key + '++) {\n' 97 | + 'var ' + each.val + ' = $$obj[' + each.key + '];\n'); 98 | 99 | this.visit(each.block); 100 | this.buf.push('}'); 101 | 102 | if (each.alternative) { 103 | this.buf.push(' } else {'); 104 | this.visit(each.alternative); 105 | this.buf.push(' }'); 106 | } 107 | 108 | this.buf.push('' 109 | + ' } else {\n' 110 | + ' var $$l = 0;\n' 111 | + ' for (var ' + each.key + ' in $$obj) {\n' 112 | + ' $$l++;' 113 | + ' var ' + each.val + ' = $$obj[' + each.key + '];\n'); 114 | 115 | this.visit(each.block); 116 | this.buf.push('}'); 117 | 118 | if (each.alternative) { 119 | this.buf.push('if ($$l === 0) {'); 120 | this.visit(each.alternative); 121 | this.buf.push('}'); 122 | } 123 | 124 | this.buf.push('}'); 125 | 126 | this.buf.push('return tags;'); 127 | this.buf.push('}.call(this));'); 128 | }; 129 | Compiler.prototype.visitLiteral = function (literal) { 130 | if (/[<>&]/.test(literal.str)) { 131 | throw new Error('Plain Text cannot contain "<" or ">" or "&" in react-jade'); 132 | } else if (literal.str.length !== 0) { 133 | this.buf.push('tags.push(' + stringify(literal.str) + ')'); 134 | } 135 | }; 136 | Compiler.prototype.visitMixinBlock = function(block){ 137 | this.buf.push('block && (tags = tags.concat(block.call(this)));'); 138 | }; 139 | 140 | 141 | Compiler.prototype.visitMixin = function(mixin) { 142 | var name = 'jade_mixins['; 143 | var args = mixin.args || ''; 144 | var block = mixin.block; 145 | var attrs = mixin.attrs; 146 | var attrsBlocks = mixin.attributeBlocks; 147 | var pp = this.pp; 148 | var dynamic = mixin.name[0]==='#'; 149 | var key = mixin.name; 150 | if (dynamic) this.dynamicMixins = true; 151 | name += (dynamic ? mixin.name.substr(2,mixin.name.length-3):'"'+mixin.name+'"')+']'; 152 | 153 | this.mixins[key] = this.mixins[key] || {used: false, instances: []}; 154 | if (mixin.call) { 155 | this.mixins[key].used = true; 156 | //if (pp) this.buf.push("jade_indent.push('" + Array(this.indents + 1).join(' ') + "');") 157 | if (block || attrs.length || attrsBlocks.length) { 158 | 159 | this.buf.push('tags = tags.concat(' + name + '.call(this, {'); 160 | 161 | if (block) { 162 | this.buf.push('block: function(){'); 163 | this.buf.push('var tags = [];'); 164 | // Render block with no indents, dynamically added when rendered 165 | this.visit(mixin.block); 166 | this.buf.push('return tags;'); 167 | 168 | if (attrs.length || attrsBlocks.length) { 169 | this.buf.push('},'); 170 | } else { 171 | this.buf.push('}'); 172 | } 173 | } 174 | 175 | if (attrsBlocks.length) { 176 | if (attrs.length) { 177 | var val = getAttributes(attrs); 178 | attrsBlocks.unshift(val); 179 | } 180 | this.buf.push('attributes: jade_merge([' + attrsBlocks.join(',') + '])'); 181 | } else if (attrs.length) { 182 | var val = getAttributes(attrs); 183 | this.buf.push('attributes: ' + val); 184 | } 185 | 186 | if (args) { 187 | this.buf.push('}, ' + args + '));'); 188 | } else { 189 | this.buf.push('}));'); 190 | } 191 | 192 | } else { 193 | this.buf.push('tags = tags.concat(' + name + '.call(this, {}'); 194 | if (args) { 195 | this.buf.push(', ' + args + '));'); 196 | } else { 197 | this.buf.push('));'); 198 | } 199 | } 200 | } else { 201 | var mixin_start = this.buf.length; 202 | args = args ? args.split(',') : []; 203 | var rest; 204 | if (args.length && /^\.\.\./.test(args[args.length - 1].trim())) { 205 | rest = args.pop().trim().replace(/^\.\.\./, ''); 206 | } 207 | this.buf.push(name + ' = function(jade_mixin_options'); 208 | if (args.length) this.buf.push(',' + args.join(',')); 209 | this.buf.push('){'); 210 | this.buf.push('var block = (jade_mixin_options && jade_mixin_options.block), attributes = (jade_mixin_options && jade_mixin_options.attributes) || {};'); 211 | if (rest) { 212 | this.buf.push('var ' + rest + ' = [];'); 213 | this.buf.push('for (jade_interp = ' + (args.length + 1) + '; jade_interp < arguments.length; jade_interp++) {'); 214 | this.buf.push(' ' + rest + '.push(arguments[jade_interp]);'); 215 | this.buf.push('}'); 216 | } 217 | this.buf.push('var tags = [];'); 218 | this.visit(block); 219 | this.buf.push('return tags;'); 220 | this.buf.push('};'); 221 | var mixin_end = this.buf.length; 222 | this.mixins[key].instances.push({start: mixin_start, end: mixin_end}); 223 | } 224 | }; 225 | 226 | Compiler.prototype.visitTag = function (tag) { 227 | var name = tag.name; 228 | if (/^[a-z]/.test(tag.name) && !tag.buffer) { 229 | name = '"' + name + '"'; 230 | } 231 | this.buf.push('tags.push(React.createElement.apply(React, ['+name); 232 | 233 | 234 | if (tag.name === 'textarea' && tag.code && tag.code.buffer && tag.code.escape) { 235 | tag.attrs.push({ 236 | name: 'value', 237 | val: tag.code.val 238 | }); 239 | tag.code = null; 240 | } 241 | var attrs; 242 | if (tag.attributeBlocks.length) { 243 | attrs = 'jade_fix_attrs(jade_merge([' + getAttributes(tag.attrs) + ',' + tag.attributeBlocks.join(',') + ']))'; 244 | } else { 245 | attrs = getAttributes(tag.attrs, true); 246 | } 247 | this.buf.push(',' + attrs + ']'); 248 | if (tag.code || (tag.block && tag.block.nodes.length)) { 249 | this.buf.push('.concat(function () { var tags = [];'); 250 | if (tag.code) this.visitCode(tag.code); 251 | this.visit(tag.block); 252 | this.buf.push('return tags;}.call(this))'); 253 | } 254 | this.buf.push('))'); 255 | }; 256 | Compiler.prototype.visitText = function (text) { 257 | if (/[<>&]/.test(text.val.replace(/&((#\d+)|#[xX]([A-Fa-f0-9]+)|([^;\W]+));?/g, ''))) { 258 | throw new Error('Plain Text cannot contain "<" or ">" or "&" in react-jade'); 259 | } else if (text.val.length !== 0) { 260 | text.val = ent.decode(text.val); 261 | this.buf.push('tags.push(' + stringify(text.val) + ')'); 262 | } 263 | }; 264 | 265 | function getAttributes(attrs, fixAttributeNames){ 266 | var buf = []; 267 | var classes = []; 268 | 269 | attrs.forEach(function(attr){ 270 | var key = attr.name; 271 | if (fixAttributeNames && key === 'for') key = 'htmlFor'; 272 | if (fixAttributeNames && key === 'maxlength') key = 'maxLength'; 273 | if (key.substr(0, 2) === 'on') { 274 | var ast = uglify.parse('jade_interp = (' + attr.val + ')'); 275 | var val = ast.body[0].body.right; 276 | if (val.TYPE === 'Call') { 277 | if (val.expression.TYPE !== 'Dot' && val.expression.TYPE !== 'Sub') { 278 | val.expression = new uglify.AST_Dot({ 279 | expression: val.expression, 280 | property: 'bind' 281 | }); 282 | val.args.unshift(new uglify.AST_Null({})); 283 | attr.val = val.print_to_string(); 284 | } else if ((val.expression.TYPE === 'Dot' && val.expression.property !== 'bind') || 285 | val.expression.TYPE == 'Sub') { 286 | var obj = val.expression.expression; 287 | val.expression.expression = new uglify.AST_SymbolRef({name: 'jade_interp'}); 288 | val.expression = new uglify.AST_Dot({ 289 | expression: val.expression, 290 | property: 'bind' 291 | }); 292 | val.args.unshift(new uglify.AST_SymbolRef({name: 'jade_interp'})); 293 | val = new uglify.AST_Seq({ 294 | car: new uglify.AST_Assign({ 295 | operator: '=', 296 | left: new uglify.AST_SymbolRef({name: 'jade_interp'}), 297 | right: obj 298 | }), 299 | cdr: val 300 | }); 301 | attr.val = '(' + val.print_to_string() + ')'; 302 | } 303 | } 304 | } 305 | if (/Link$/.test(key)) { 306 | // transform: valueLink = this.state.name 307 | // into: valueLink = {value: this.state.name,requestChange:function(v){ this.setState({name:v})}.bind(this)} 308 | var ast = uglify.parse('jade_interp = (' + attr.val + ')'); 309 | var val = ast.body[0].body.right; 310 | if (val.TYPE === 'Dot' && val.expression.TYPE === 'Dot' && 311 | val.expression.expression.TYPE === 'This' && val.expression.property === 'state') { 312 | attr.val = '{value:this.state.' + val.property + ',' + 313 | 'requestChange:function(v){this.setState({' + val.property + ':v})}.bind(this)}'; 314 | } 315 | } 316 | if (key === 'class') { 317 | classes.push(attr.val); 318 | } else if (key === 'style') { 319 | if (isConstant(attr.val)) { 320 | var val = toConstant(attr.val); 321 | buf.push(stringify(key) + ': ' + stringify(fixStyle(val))); 322 | } else { 323 | buf.push(stringify(key) + ': jade_fix_style(' + attr.val + ')'); 324 | } 325 | } else if (isConstant(attr.val)) { 326 | var val = toConstant(attr.val); 327 | buf.push(stringify(key) + ': ' + stringify(val)); 328 | } else { 329 | buf.push(stringify(key) + ': ' + attr.val); 330 | } 331 | }); 332 | if (classes.length) { 333 | if (classes.every(isConstant)) { 334 | classes = stringify(joinClasses(classes.map(toConstant))); 335 | } else { 336 | classes = 'jade_join_classes([' + classes.join(',') + '])'; 337 | } 338 | if (classes.length) 339 | buf.push('"' + (fixAttributeNames ? 'className' : 'class') + '": ' + classes); 340 | } 341 | return '{' + buf.join(',') + '}'; 342 | } 343 | -------------------------------------------------------------------------------- /lib/utils/is-template-literal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = isTemplateLiteral; 4 | function isTemplateLiteral(str) { 5 | return str && typeof str === 'object' && 6 | str.raw && typeof str.raw === 'object' && 7 | str.raw.length === 1 && typeof str.raw[0] === 'string'; 8 | } 9 | -------------------------------------------------------------------------------- /lib/utils/jade-fix-attrs.js: -------------------------------------------------------------------------------- 1 | function jade_fix_attrs(attrs) { 2 | attrs = attrs || {}; 3 | if ('for' in attrs) { 4 | attrs.htmlFor = attrs['for']; 5 | delete attrs['for']; 6 | } 7 | if ('maxlength' in attrs) { 8 | attrs.maxLength = attrs['maxlength']; 9 | delete attrs['maxlength']; 10 | } 11 | if ('class' in attrs) { 12 | attrs.className = attrs['class']; 13 | delete attrs['class']; 14 | } 15 | return attrs; 16 | } 17 | -------------------------------------------------------------------------------- /lib/utils/jade-fix-style.js: -------------------------------------------------------------------------------- 1 | function jade_fix_style(style) { 2 | return typeof style === "string" ? style.split(";").filter(function (str) { 3 | return str.split(":").length > 1; 4 | }).reduce(function (obj, style) { 5 | obj[style.split(":")[0]] = style.split(":").slice(1).join(":"); return obj; 6 | }, {}) : style; 7 | } 8 | -------------------------------------------------------------------------------- /lib/utils/jade-join-classes.js: -------------------------------------------------------------------------------- 1 | function jade_join_classes(val) { 2 | return (Array.isArray(val) ? val.map(jade_join_classes) : 3 | (val && typeof val === "object") ? Object.keys(val).filter(function (key) { return val[key]; }) : 4 | [val] 5 | ).filter(function (val) { return val != null && val !== ""; }).join(" "); 6 | } 7 | -------------------------------------------------------------------------------- /lib/utils/jade-merge.js: -------------------------------------------------------------------------------- 1 | function jade_merge(a, b) { 2 | if (arguments.length === 1) { 3 | var attrs = a[0]; 4 | for (var i = 1; i < a.length; i++) { 5 | attrs = jade_merge(attrs, a[i]); 6 | } 7 | return attrs; 8 | } 9 | 10 | for (var key in b) { 11 | if (key === 'class') { 12 | a[key] = jade_join_classes([a[key], b[key]]); 13 | } else if (key === 'style') { 14 | a[key] = jade_fix_style(a[key]) || {}; 15 | b[key] = jade_fix_style(b[key]) || {}; 16 | for (var style in b[key]) { 17 | a[key][style] = b[key][style]; 18 | } 19 | } else { 20 | a[key] = b[key]; 21 | } 22 | } 23 | 24 | return a; 25 | }; 26 | -------------------------------------------------------------------------------- /lib/utils/java-script-compressor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * This file is contains a customised UglifyJS compressor. The advantage of having this is 5 | * that we can generate slow, ugly but correct code and simply run it through this to clean 6 | * it up. It's far simpler to just use `array.push` to add elements, but often you know the 7 | * list of elements up front and it's better to just use them as an array. 8 | */ 9 | var uglify = require('uglify-js'); 10 | 11 | module.exports = Compressor; 12 | function Compressor(options) { 13 | uglify.TreeTransformer.call(this, this.before, this.after); 14 | }; 15 | 16 | Compressor.prototype = Object.create(uglify.TreeTransformer.prototype); 17 | 18 | Compressor.prototype.after = function(node) { 19 | if (isVacuousFunction(node)) { 20 | var fn = node.expression.expression; 21 | if (fn.body.length > 0 && fn.body[0].TYPE === 'Return') { 22 | return fn.body[0].value; 23 | } 24 | } 25 | if (node.TYPE === 'Function') { 26 | var returnStatement = getReturnStatement(node); 27 | if (returnStatement) { 28 | node.body = [returnStatement]; 29 | return node; 30 | } 31 | } 32 | if (isConcattedArray(node)) { 33 | node.expression.expression.elements = node.expression.expression.elements 34 | .concat(node.args[0].elements); 35 | return node.expression.expression; 36 | } 37 | 38 | if (isConstantApply(node)) { 39 | node.expression = node.expression.expression; 40 | node.args = node.args[1].elements; 41 | } 42 | }; 43 | 44 | // [...].concat([...]) 45 | function isConcattedArray(node) { 46 | return node.TYPE === 'Call' && node.expression.TYPE === 'Dot' && 47 | node.expression.property === 'concat' && node.expression.expression.TYPE === 'Array' && 48 | node.args.length === 1 && node.args[0].TYPE === 'Array'; 49 | } 50 | 51 | // function () { ... }.call(this) 52 | function isVacuousFunction(node) { 53 | return node.TYPE === 'Call' && node.expression.TYPE === 'Dot' && 54 | node.expression.property === 'call' && node.expression.expression.TYPE === 'Function' && 55 | node.expression.expression.argnames.length == 0 && node.args.length === 1 && 56 | node.args[0].TYPE === 'This'; 57 | } 58 | 59 | // Foo.bar.apply(Foo, [...]) 60 | function isConstantApply(node) { 61 | return node.TYPE === 'Call' && node.expression.TYPE === 'Dot' && 62 | node.expression.property === 'apply' && node.expression.expression.TYPE === 'Dot' && 63 | node.expression.expression.expression.TYPE === 'SymbolRef' && node.args.length === 2 && 64 | node.args[0].TYPE === 'SymbolRef' && 65 | node.args[0].name === node.expression.expression.expression.name && 66 | node.args[1].TYPE === 'Array'; 67 | } 68 | 69 | // foo.push(...) 70 | function isArrayPush(node, name) { 71 | return node.TYPE === 'SimpleStatement' && node.body.TYPE === 'Call' && 72 | node.body.expression.TYPE === 'Dot' && node.body.expression.property === 'push' && 73 | node.body.expression.expression.TYPE === 'SymbolRef' && node.body.expression.expression.name === name; 74 | } 75 | 76 | // for `function () { var arr = []; arr.push(...); arr.push(...); return arr;}` get `[..., ...]` 77 | function getReturnStatement(node) { 78 | var nodes = node.body; 79 | if (nodes.length === 0) return new uglify.AST_Undefined({}); 80 | if (nodes[0].TYPE === 'Var' && nodes[0].definitions.length === 1 && nodes[0].definitions[0].value.TYPE === 'Array') { 81 | var name = nodes[0].definitions[0].name.name; 82 | var array = nodes[0].definitions[0].value; 83 | var elements = array.elements; 84 | for (var i = 1; isArrayPush(nodes[i], name) && i < nodes.length; i++) { 85 | elements = elements.concat(nodes[i].body.args); 86 | } 87 | if (nodes[i].TYPE === 'Return' && nodes[i].value.TYPE === 'SymbolRef' && nodes[i].value.name === name) { 88 | array.elements = elements; 89 | nodes[i].value = array; 90 | return nodes[i]; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/utils/set-locals.js: -------------------------------------------------------------------------------- 1 | function setLocals(locals) { 2 | var render = this; 3 | function newRender(additionalLocals) { 4 | var newLocals = {}; 5 | for (var key in locals) { 6 | newLocals[key] = locals[key]; 7 | } 8 | if (additionalLocals) { 9 | for (var key in additionalLocals) { 10 | newLocals[key] = additionalLocals[key]; 11 | } 12 | } 13 | return render.call(this, newLocals); 14 | } 15 | newRender.locals = setLocals; 16 | return newRender; 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-jade", 3 | "version": "2.5.0", 4 | "description": "Compile Jade to React JavaScript", 5 | "keywords": [], 6 | "dependencies": { 7 | "acorn": "^1.1.0", 8 | "constantinople": "^3.0.1", 9 | "ent": "^2.2.0", 10 | "jade": "1.9.2", 11 | "js-stringify": "^1.0.1", 12 | "resolve": "^1.1.6", 13 | "static-module": "^1.1.2", 14 | "uglify-js": "^2.4.21", 15 | "with": "^5.0.0" 16 | }, 17 | "devDependencies": { 18 | "es6ify": "^1.6.0", 19 | "gethub": "^2.0.1", 20 | "htmlparser2": "^3.8.2", 21 | "istanbul": "^0.3.14", 22 | "marked": "^0.3.3", 23 | "react": "^0.14.0", 24 | "react-dom": "^0.14.0", 25 | "rimraf": "^2.3.3", 26 | "testit": "^2.0.2", 27 | "unescape-html": "^1.0.0" 28 | }, 29 | "peerDependencies": { 30 | "react": ">=0.12.0 <0.15.0" 31 | }, 32 | "scripts": { 33 | "test": "node test/download-jade-tests.js && node test/index.js && npm run coverage", 34 | "coverage": "istanbul cover test" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/jadejs/react-jade.git" 39 | }, 40 | "author": "ForbesLindesay", 41 | "license": "MIT" 42 | } -------------------------------------------------------------------------------- /test/bonus-features/component-composition.html: -------------------------------------------------------------------------------- 1 |
2 |

Jade

3 | 8 |
9 | -------------------------------------------------------------------------------- /test/bonus-features/component-composition.jade: -------------------------------------------------------------------------------- 1 | div 2 | h1= this.props.title 3 | ul 4 | each item, index in this.props.items 5 | SubComponent(key="item"+index, item=item) 6 | -------------------------------------------------------------------------------- /test/bonus-features/component-subcomponent.jade: -------------------------------------------------------------------------------- 1 | li= this.props.item 2 | 3 | -------------------------------------------------------------------------------- /test/bonus-features/component-this-each.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /test/bonus-features/component-this-each.jade: -------------------------------------------------------------------------------- 1 | ul 2 | each item, index in this.props.list 3 | li(key=item, class=this.props.title + index%2)= this.props.title + ':' + item 4 | -------------------------------------------------------------------------------- /test/bonus-features/component-this-mixin.html: -------------------------------------------------------------------------------- 1 |
2 | param:Jade 3 | props:Jade 4 |
5 | 6 | -------------------------------------------------------------------------------- /test/bonus-features/component-this-mixin.jade: -------------------------------------------------------------------------------- 1 | mixin show1(message) 2 | span= 'param:' + message 3 | 4 | mixin show2() 5 | span= 'props:'+ this.props.title 6 | 7 | div 8 | +show1(this.props.title) 9 | +show2() 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/bonus-features/component-this.html: -------------------------------------------------------------------------------- 1 |

Jade

2 | -------------------------------------------------------------------------------- /test/bonus-features/component-this.jade: -------------------------------------------------------------------------------- 1 | div 2 | h1= this.props.title 3 | 4 | -------------------------------------------------------------------------------- /test/bonus-features/partial-application.jade: -------------------------------------------------------------------------------- 1 | - var click = view.click.bind(view); 2 | 3 | button(onClick=click('Click Me 0!')) Click Me 0! 4 | button(onClick=view.click('Click Me 1!')) Click Me 1! 5 | button(onClick=view['click']('Click Me 2!')) Click Me 2! 6 | -------------------------------------------------------------------------------- /test/download-jade-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var gethub = require('gethub'); 5 | var jadeVersion = require('jade/package.json').version; 6 | var downloadedVersion = ''; 7 | 8 | try { 9 | downloadedVersion = fs.readFileSync(__dirname + '/jade/version.txt', 'utf8'); 10 | } catch (ex) { 11 | // ignore non-existant version.txt file 12 | } 13 | 14 | if (downloadedVersion !== jadeVersion) { 15 | gethub('visionmedia', 'jade', jadeVersion, __dirname + '/jade', function (err) { 16 | if (err) throw err; 17 | fs.writeFileSync(__dirname + '/jade/version.txt', jadeVersion); 18 | }); 19 | } -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var assert = require('assert'); 5 | var test = require('testit'); 6 | var rimraf = require('rimraf').sync; 7 | var htmlparser = require('htmlparser2'); 8 | var unescapeHtml = require('unescape-html'); 9 | var mockDom = require('./mock-dom.js'); 10 | var jade = require('../'); 11 | var React = require('react'); 12 | var ReactDOM = require('react-dom/server') 13 | 14 | var outputDir = __dirname + '/output'; 15 | var inputDir = __dirname + '/jade/test/cases'; 16 | var bonusDir = __dirname + '/bonus-features'; 17 | 18 | rimraf(outputDir); 19 | fs.mkdirSync(outputDir); 20 | try { 21 | fs.statSync(inputDir); 22 | } catch (ex) { 23 | throw new Error('You must first download jade before you can run tests. This is done automatically if you use "npm test" to run tests.'); 24 | } 25 | 26 | fs.readdirSync(inputDir).filter(function (name) { 27 | return /\.jade$/.test(name) && 28 | !/doctype/.test(name) && 29 | !/filter/.test(name) && 30 | !/case/.test(name) && 31 | 'xml.jade' !== name && 32 | 'scripts.non-js.jade' !== name && 33 | 'html.jade' !== name && 34 | 'html5.jade' !== name && 35 | 'escape-test.jade' !== name && 36 | 'attrs.unescaped.jade' !== name && 37 | 'regression.784.jade' !== name && 38 | 'tags.self-closing.jade' !== name && 39 | 'interpolation.escape.jade' !== name && 40 | 'each.else.jade' !== name && 41 | 'includes.jade' !== name && 42 | 'code.iteration.jade' !== name && 43 | 'code.escape.jade' !== name && 44 | 'blockquote.jade' !== name && 45 | 'attrs-data.jade' !== name && 46 | 'blocks-in-blocks.jade' !== name && 47 | 'blocks-in-if.jade' !== name; 48 | }).forEach(function (name) { 49 | name = name.replace(/\.jade$/, ''); 50 | test(name, function () { 51 | var src = fs.readFileSync(inputDir + '/' + name + '.jade', 'utf8'); 52 | var expected = htmlparser.parseDOM(fs.readFileSync(inputDir + '/' + name + '.html', 'utf8')); 53 | fs.writeFileSync(outputDir + '/' + name + '.jade', src); 54 | var js = jade.compileFileClient(inputDir + '/' + name + '.jade', { 55 | outputFile: outputDir + '/' + name + '.js', 56 | basedir: inputDir 57 | }); 58 | fs.writeFileSync(outputDir + '/' + name + '.js', js); 59 | mockDom.mock(); 60 | var fn = jade.compileFile(inputDir + '/' + name + '.jade', { 61 | outputFile: outputDir + '/' + name + '.js', 62 | basedir: inputDir 63 | }); 64 | var actual = fn({title: 'Jade'}); 65 | var hasDiv = expected.filter(function(element) { return element.type !== 'text' }).length !== 1; 66 | actual = hasDiv ? actual.children : actual; 67 | mockDom.reset(); 68 | 69 | if (domToString(expected) !== domToString(actual)) { 70 | fs.writeFileSync(outputDir + '/' + name + '.expected.dom', domToString(expected) + '\n'); 71 | fs.writeFileSync(outputDir + '/' + name + '.actual.dom', domToString(actual) + '\n'); 72 | assert(domToString(expected) === domToString(actual), 'Expected output dom to match expected dom (see /test/output/' + name + '.actual.dom and /test/output/' + name + '.expected.dom for details.'); 73 | } 74 | }); 75 | }); 76 | 77 | function domToString(dom, indent) { 78 | if (Array.isArray(dom)) { 79 | return joinStrings(dom).map(function (child) { 80 | return domToString(child, indent); 81 | }).join('\n'); 82 | } 83 | indent = indent || ''; 84 | if (dom.attribs) { 85 | var sortedAttribs = {}; 86 | Object.keys(dom.attribs).sort().forEach(function (key) { 87 | sortedAttribs[key] = unescapeHtml(dom.attribs[key]); 88 | }); 89 | dom.attribs = sortedAttribs; 90 | } 91 | if (dom.attribs && dom.attribs.style) { 92 | dom.attribs.style = dom.attribs.style.split(';').sort().join(';'); 93 | } 94 | if (dom.type === 'script' || dom.type === 'style' || dom.type === 'tag' && (dom.name === 'script' || dom.name === 'style')) { 95 | return indent + dom.name + ' ' + JSON.stringify(dom.attribs); 96 | } else if (dom.type === 'tag') { 97 | return indent + dom.name + ' ' + JSON.stringify(dom.attribs) + joinStrings(dom.children).map(function (child) { 98 | return '\n' + domToString(child, indent + ' '); 99 | }).join(''); 100 | } else if (typeof dom === 'string') { 101 | return indent + JSON.stringify(dom + ''); 102 | } 103 | return indent + '[' + dom.type + ']'; 104 | } 105 | function joinStrings(elements) { 106 | var result = []; 107 | for (var i = 0; i < elements.length; i++) { 108 | var el = elements[i]; 109 | if (el === null || el === undefined) el = ''; 110 | if (el.type === 'text') { 111 | el = el.data; 112 | } 113 | if (typeof el !== 'function' && typeof el !== 'object') { 114 | el = (el + '').replace(/\s+/g, ''); 115 | } 116 | if (el.type === 'comment' || (typeof el === 'string' && el === '')) { 117 | // ignore 118 | } else if (typeof el === 'string' && typeof result[result.length - 1] === 'string') { 119 | result[result.length - 1] = (result[result.length - 1] + el).replace(/\s+/g, ''); 120 | } else { 121 | result.push(el); 122 | } 123 | } 124 | return result; 125 | } 126 | 127 | test('bonus-features/partial-application.jade', function () { 128 | var fn = jade.compileFile(__dirname + '/bonus-features/partial-application.jade'); 129 | fs.writeFileSync(__dirname + '/output/partial-application.js', jade.compileFileClient(__dirname + '/bonus-features/partial-application.jade')); 130 | function click() { 131 | throw new Error('click should never actually get called'); 132 | } 133 | var i = 0; 134 | var view = { click: click }; 135 | click.bind = function (self, val) { 136 | if (i === 0) { 137 | assert(self === view); 138 | assert(arguments.length === 1); 139 | } else if (i === 1) { 140 | assert(self === null); 141 | assert(val === 'Click Me 0!'); 142 | } else if (i === 2) { 143 | assert(self === view); 144 | assert(val === 'Click Me 1!'); 145 | } else if (i === 3) { 146 | assert(self === view); 147 | assert(val === 'Click Me 2!'); 148 | } 149 | i++; 150 | return click; 151 | }; 152 | fn({ view: view }); 153 | assert(i === 4); 154 | }); 155 | 156 | fs.readdirSync(bonusDir).filter(function (name) { 157 | return /\.jade$/.test(name) && 158 | /component-this/.test(name) 159 | }).forEach(function(name) { 160 | name = name.replace(/\.jade$/, ''); 161 | test(name, function () { 162 | var fn = jade.compileFile(bonusDir + '/' + name + '.jade'); 163 | var c = React.createClass({ render: fn }); 164 | var html = ReactDOM.renderToStaticMarkup(React.createElement(c, { title: 'Jade', list: ['a', 'b', 'c']})); 165 | 166 | var actual = htmlparser.parseDOM(html); 167 | var expected = htmlparser.parseDOM(fs.readFileSync(bonusDir + '/' + name + '.html', 'utf8')); 168 | if (domToString(expected) !== domToString(actual)) { 169 | fs.writeFileSync(outputDir + '/' + name + '.expected.dom', domToString(expected) + '\n'); 170 | fs.writeFileSync(outputDir + '/' + name + '.actual.dom', domToString(actual) + '\n'); 171 | assert(domToString(expected) === domToString(actual), 'Expected output dom to match expected dom (see /test/output/' + name + '.actual.dom and /test/output/' + name + '.expected.dom for details.'); 172 | } 173 | }); 174 | }); 175 | 176 | test('bonus-features/component-composition.jade', function () { 177 | 178 | var name = 'component-composition'; 179 | 180 | var render1 = jade.compileFile(bonusDir + '/' + 'component-subcomponent' + '.jade'); 181 | var SubComponent= React.createClass({ render: render1 }); 182 | 183 | var render2 = jade.compileFile(bonusDir + '/' + name + '.jade').locals({SubComponent: SubComponent}); 184 | var c = React.createClass({ 185 | render: render2 186 | }); 187 | 188 | var html = ReactDOM.renderToStaticMarkup(React.createElement(c, { title: 'Jade', items: [ 'a', 'b', 'c' ]})); 189 | 190 | var actual = htmlparser.parseDOM(html); 191 | var expected = htmlparser.parseDOM(fs.readFileSync(bonusDir + '/' + name + '.html', 'utf8')); 192 | if (domToString(expected) !== domToString(actual)) { 193 | fs.writeFileSync(outputDir + '/' + name + '.expected.dom', domToString(expected) + '\n'); 194 | fs.writeFileSync(outputDir + '/' + name + '.actual.dom', domToString(actual) + '\n'); 195 | assert(domToString(expected) === domToString(actual), 'Expected output dom to match expected dom (see /test/output/' + name + '.actual.dom and /test/output/' + name + '.expected.dom for details.'); 196 | } 197 | }); 198 | 199 | test('bonus-features/browserify', function (done) { 200 | fs.createReadStream(require.resolve('./test-client.js')) 201 | .pipe(jade(require.resolve('./test-client.js'))) 202 | .pipe(fs.createWriteStream(__dirname + '/output/test-client.js')) 203 | .on('close', function () { 204 | require('./output/test-client.js'); 205 | done(); 206 | }); 207 | }); 208 | test('bonus-features/browserify after es6ify', function (done) { 209 | fs.createReadStream(require.resolve('./test-client.js')) 210 | .pipe(require('es6ify')(require.resolve('./test-client.js'))) 211 | .pipe(jade(require.resolve('./test-client.js'))) 212 | .pipe(fs.createWriteStream(__dirname + '/output/test-client-es6ify.js')) 213 | .on('close', function () { 214 | require('./output/test-client-es6ify.js'); 215 | done(); 216 | }); 217 | }); 218 | 219 | 220 | test('bonus-features/browserify - error reporting', function (done) { 221 | fs.createReadStream(require.resolve('./test-client-syntax-error.js')) 222 | .pipe(jade(require.resolve('./test-client.js'))) 223 | .on('error', function (err) { 224 | assert(/var templateA \= jade\`/.test(err.message)); 225 | return done(); 226 | }).resume(); 227 | }); 228 | 229 | test('bonus-features/browserify - pass through JSON', function (done) { 230 | fs.createReadStream(require.resolve('../package.json')) 231 | .pipe(jade(require.resolve('../package.json'))) 232 | .pipe(fs.createWriteStream(__dirname + '/output/test-client-pass-through.json')) 233 | .on('close', function () { 234 | require('./output/test-client-pass-through'); 235 | done(); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /test/mock-dom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react'); 4 | 5 | var tags = Object.keys(React.DOM); 6 | var originalValues = tags.map(function (tag) { return React.DOM[tag]; }); 7 | var originalCreateElement = React.createElement; 8 | 9 | exports.mock = mock; 10 | function mock() { 11 | for (var i = 0; i < tags.length; i++) { 12 | React.DOM[tags[i]] = mockFor(tags[i]); 13 | } 14 | React.createElement = function() { 15 | var args = Array.prototype.slice.call(arguments, 0); 16 | var tag = args.shift(); 17 | return mockFor(tag).apply(null, args); 18 | } 19 | function mockFor(name) { 20 | return function (attribs) { 21 | var children = Array.prototype.slice.call(arguments, 1); 22 | var sortedAttribs = {}; 23 | if (attribs) { 24 | if ('class' in attribs) throw new Error('Cannot have an attribute named "class", perhaps you meant "className"'); 25 | if ('className' in attribs) { 26 | attribs['class'] = attribs.className; 27 | delete attribs.className; 28 | } 29 | if (attribs['class'] === '') delete attribs['class']; 30 | if (attribs['style']) { 31 | if (typeof attribs['style'] !== 'object') { 32 | throw new Error('Cannot have anything other than an object as the "style"'); 33 | } 34 | attribs['style'] = Object.keys(attribs.style).sort().map(function (key) { 35 | return key + ':' + attribs['style'][key]; 36 | }).join(';'); 37 | } 38 | Object.keys(attribs).sort().forEach(function (key) { 39 | if (attribs[key] === true) { 40 | sortedAttribs[key] = key; 41 | } else if (attribs[key] === false || attribs[key] === null || attribs[key] === undefined) { 42 | } else { 43 | sortedAttribs[key] = attribs[key] + ''; 44 | } 45 | }); 46 | } 47 | return { 48 | type: 'tag', 49 | name: name, 50 | attribs: sortedAttribs, 51 | children: Array.isArray(children) ? children : (children ? [children] : []) 52 | }; 53 | } 54 | } 55 | } 56 | 57 | exports.reset = reset; 58 | function reset() { 59 | for (var i = 0; i < tags.length; i++) { 60 | React.DOM[tags[i]] = originalValues[i]; 61 | } 62 | React.createElement = originalCreateElement; 63 | } 64 | -------------------------------------------------------------------------------- /test/test-client-syntax-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var ReactDOM= require('react-dom/server'); 5 | var jade = require('react-jade'); 6 | 7 | var test = /^\
Some Text\<\/div\>$/; 8 | 9 | var templateA = jade` 10 | #container Some Text 11 | ; 12 | assert(test.test(ReactDOM.renderToString(templateA()))); 13 | 14 | var templateB = jade.compile('#container Some Text'); 15 | assert(test.test(ReactDOM.renderToString(templateB()))); 16 | -------------------------------------------------------------------------------- /test/test-client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var React = require('react'); 5 | var ReactDOM= require('react-dom/server'); 6 | var jade = require('react-jade'); 7 | 8 | var test = /^\
Some Text\<\/div\>$/; 9 | 10 | var templateA = jade` 11 | #container Some Text 12 | `; 13 | assert(test.test(ReactDOM.renderToString(templateA()))); 14 | 15 | var templateB = jade.compile('#container Some Text'); 16 | assert(test.test(ReactDOM.renderToString(templateB()))); 17 | --------------------------------------------------------------------------------