├── .travis.yml
├── test
├── bonus-features
│ ├── component-this.html
│ ├── component-subcomponent.jade
│ ├── component-this.jade
│ ├── component-this-mixin.html
│ ├── component-composition.html
│ ├── component-this-each.html
│ ├── component-this-each.jade
│ ├── component-composition.jade
│ ├── component-this-mixin.jade
│ └── partial-application.jade
├── test-client-syntax-error.js
├── test-client.js
├── download-jade-tests.js
├── mock-dom.js
└── index.js
├── README.md
├── .gitignore
├── lib
├── compile-file.js
├── compile.js
├── utils
│ ├── is-template-literal.js
│ ├── jade-fix-style.js
│ ├── jade-join-classes.js
│ ├── jade-fix-attrs.js
│ ├── set-locals.js
│ ├── jade-merge.js
│ ├── acorn-transform.js
│ ├── java-script-compressor.js
│ └── compiler.js
├── parse-file.js
├── compile-file-client.js
├── compile-client.js
├── parse.js
└── browserify.js
├── index.js
├── LICENSE
└── package.json
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.10"
4 |
--------------------------------------------------------------------------------
/test/bonus-features/component-this.html:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------