├── .gitignore
├── .travis.yml
├── README.md
├── lib
├── ReactDOM.js
├── index.js
└── parseOptions.js
├── license
├── package.json
├── scripts
└── react-dom.js
└── test
└── test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | browser/
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "5"
4 | script: npm test
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # handlebars-react [![NPM Version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][david-image]][david-url]
2 |
3 | > Compile Handlebars templates to [React](https://facebook.github.io/react/).
4 |
5 | Compile this:
6 | ```handlebars
7 |
8 | text1
9 | {{variable1}}
10 | {{#if variable2}}text2{{else}}text3{{/if}}
11 | text4
12 |
13 | ```
14 | into this:
15 | ```js
16 | React.DOM.div(null,
17 | "text1",
18 | this.props.variable1,
19 | this.props.variable2 ? React.DOM.span(null,
20 | "text2"
21 | ) : "text3",
22 | React.DOM.span({"data-attr":(this.props.variable3 ? "value1" : "") + " value2"},
23 | "text4"
24 | )
25 | );
26 | ```
27 |
28 |
29 | ## Installation
30 | [Node.js](http://nodejs.org/) `>= 5` is required; `< 5.0` will need an ES6 compiler. ~~Type this at the command line:~~
31 | ```shell
32 | npm install handlebars-react
33 | ```
34 |
35 |
36 | ## Usage
37 |
38 | ### Server/Browserify
39 | ```js
40 | var HandlebarsReact = require("handlebars-react");
41 |
42 | new HandlebarsReact(options)
43 | .compile("{{title}}
")
44 | .then(result => console.log("done!"));
45 | ```
46 |
47 | ### UMD/AMD/etc
48 | Accessible via `define()` or `window.HandlebarsReact`.
49 |
50 |
51 | ## Options
52 |
53 | ### options.beautify
54 | Type: `Boolean`
55 | Default value: `false`
56 | When `true`, output will be formatted for increased legibility.
57 |
58 | ### options.env
59 | Type: `String`
60 | Default value: `undefined`
61 | [Option presets](https://github.com/stevenvachon/handlebars-react/blob/master/lib/parseOptions.js) for your target environment: `"development"` or `"production"`. Preset options can be overridden.
62 |
63 | ### options.normalizeWhitespace
64 | Type: `Boolean`
65 | Default value: `false`
66 | See [handlebars-html-parser](https://github.com/stevenvachon/handlebars-html-parser).
67 |
68 | ### options.processCSS
69 | Type: `Boolean`
70 | Default value: `false`
71 | See [handlebars-html-parser](https://github.com/stevenvachon/handlebars-html-parser).
72 |
73 | ### options.processJS
74 | Type: `Boolean`
75 | Default value: `false`
76 | See [handlebars-html-parser](https://github.com/stevenvachon/handlebars-html-parser).
77 |
78 | ### options.useDomMethods
79 | Type: `Boolean`
80 | Default value: `false`
81 | When `true`, available `React.DOM` convenience functions will be used instead of `React.createElement()`.
82 |
83 |
84 | ## Roadmap Features
85 | * `convertHbsComments` to JavaScript block comments (or HTML comments?)
86 | * `convertHtmlComments` to JavaScript block comments
87 | * `ignoreComments` option when React supports such ([react#2810](https://github.com/facebook/react/issues/2810))
88 | * `trimWhitespace` option to remove spaces between elements (` a word ` to `a word`)?
89 |
90 |
91 | ## Changelog
92 | * 0.0.1–0.0.16 pre-releases
93 |
94 |
95 | [npm-image]: https://img.shields.io/npm/v/handlebars-react.svg
96 | [npm-url]: https://npmjs.org/package/handlebars-react
97 | [travis-image]: https://img.shields.io/travis/stevenvachon/handlebars-react.svg
98 | [travis-url]: https://travis-ci.org/stevenvachon/handlebars-react
99 | [david-image]: https://img.shields.io/david/stevenvachon/handlebars-react.svg
100 | [david-url]: https://david-dm.org/stevenvachon/handlebars-react
101 |
--------------------------------------------------------------------------------
/lib/ReactDOM.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // Generated via ../scripts/react-dom.js
4 | module.exports =
5 | {
6 | "a": true,
7 | "abbr": true,
8 | "address": true,
9 | "area": true,
10 | "article": true,
11 | "aside": true,
12 | "audio": true,
13 | "b": true,
14 | "base": true,
15 | "bdi": true,
16 | "bdo": true,
17 | "big": true,
18 | "blockquote": true,
19 | "body": true,
20 | "br": true,
21 | "button": true,
22 | "canvas": true,
23 | "caption": true,
24 | "cite": true,
25 | "code": true,
26 | "col": true,
27 | "colgroup": true,
28 | "data": true,
29 | "datalist": true,
30 | "dd": true,
31 | "del": true,
32 | "details": true,
33 | "dfn": true,
34 | "dialog": true,
35 | "div": true,
36 | "dl": true,
37 | "dt": true,
38 | "em": true,
39 | "embed": true,
40 | "fieldset": true,
41 | "figcaption": true,
42 | "figure": true,
43 | "footer": true,
44 | "form": true,
45 | "h1": true,
46 | "h2": true,
47 | "h3": true,
48 | "h4": true,
49 | "h5": true,
50 | "h6": true,
51 | "head": true,
52 | "header": true,
53 | "hgroup": true,
54 | "hr": true,
55 | "html": true,
56 | "i": true,
57 | "iframe": true,
58 | "img": true,
59 | "input": true,
60 | "ins": true,
61 | "kbd": true,
62 | "keygen": true,
63 | "label": true,
64 | "legend": true,
65 | "li": true,
66 | "link": true,
67 | "main": true,
68 | "map": true,
69 | "mark": true,
70 | "menu": true,
71 | "menuitem": true,
72 | "meta": true,
73 | "meter": true,
74 | "nav": true,
75 | "noscript": true,
76 | "object": true,
77 | "ol": true,
78 | "optgroup": true,
79 | "option": true,
80 | "output": true,
81 | "p": true,
82 | "param": true,
83 | "picture": true,
84 | "pre": true,
85 | "progress": true,
86 | "q": true,
87 | "rp": true,
88 | "rt": true,
89 | "ruby": true,
90 | "s": true,
91 | "samp": true,
92 | "script": true,
93 | "section": true,
94 | "select": true,
95 | "small": true,
96 | "source": true,
97 | "span": true,
98 | "strong": true,
99 | "style": true,
100 | "sub": true,
101 | "summary": true,
102 | "sup": true,
103 | "table": true,
104 | "tbody": true,
105 | "td": true,
106 | "textarea": true,
107 | "tfoot": true,
108 | "th": true,
109 | "thead": true,
110 | "time": true,
111 | "title": true,
112 | "tr": true,
113 | "track": true,
114 | "u": true,
115 | "ul": true,
116 | "var": true,
117 | "video": true,
118 | "wbr": true,
119 | "circle": true,
120 | "clipPath": true,
121 | "defs": true,
122 | "ellipse": true,
123 | "g": true,
124 | "image": true,
125 | "line": true,
126 | "linearGradient": true,
127 | "mask": true,
128 | "path": true,
129 | "pattern": true,
130 | "polygon": true,
131 | "polyline": true,
132 | "radialGradient": true,
133 | "rect": true,
134 | "stop": true,
135 | "svg": true,
136 | "text": true,
137 | "tspan": true
138 | };
139 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var parseOptions = require("./parseOptions");
3 | var ReactDOM = require("./ReactDOM");
4 |
5 | var HandlebarsHtmlParser = require("handlebars-html-parser");
6 | var postcss = require("postcss");
7 | var postcssJs = require("postcss-js");
8 |
9 | var eachNode = HandlebarsHtmlParser.each;
10 | var NodeType = HandlebarsHtmlParser.type;
11 |
12 |
13 |
14 | function compiler(options)
15 | {
16 | this.options = options = parseOptions(options);
17 |
18 | this.parser = new HandlebarsHtmlParser(
19 | {
20 | normalizeWhitespace: options.normalizeWhitespace,
21 | processCSS: options.processCSS,
22 | processJS: options.processJS
23 | });
24 | }
25 |
26 |
27 |
28 | compiler.prototype.compile = function(str)
29 | {
30 | var parserState;
31 |
32 | var compilerState =
33 | {
34 | // React.DOM… or React.createElement per element in stack
35 | // Stack indexed by parent tag depth -- first index is a "document" node (root/top-level nodes container)
36 | areDomMethods: [false]
37 | };
38 |
39 | var result = [];
40 |
41 | return this.parser.parse(str)
42 | .then( eachNode((node, state) =>
43 | {
44 | // Parent scope access
45 | parserState = state;
46 |
47 | switch (node.type)
48 | {
49 | case NodeType.HBS_EXPRESSION_END:
50 | {
51 | break;
52 | }
53 | case NodeType.HBS_EXPRESSION_START:
54 | {
55 | break;
56 | }
57 |
58 |
59 | case NodeType.HBS_HASH_END:
60 | {
61 | break;
62 | }
63 | case NodeType.HBS_HASH_START:
64 | {
65 | break;
66 | }
67 |
68 |
69 | case NodeType.HBS_HASH_KEY_END:
70 | {
71 | break;
72 | }
73 | case NodeType.HBS_HASH_KEY_START:
74 | {
75 | break;
76 | }
77 |
78 |
79 | case NodeType.HBS_HASH_VALUE_END:
80 | {
81 | break;
82 | }
83 | case NodeType.HBS_HASH_VALUE_START:
84 | {
85 | break;
86 | }
87 |
88 |
89 | case NodeType.HBS_PART_END:
90 | {
91 | break;
92 | }
93 | case NodeType.HBS_PART_START:
94 | {
95 | break;
96 | }
97 |
98 |
99 | case NodeType.HBS_PATH:
100 | {
101 | break;
102 | }
103 |
104 |
105 | case NodeType.HBS_TAG_END:
106 | {
107 | break;
108 | }
109 | case NodeType.HBS_TAG_START:
110 | {
111 | /*if (state.isTag === false)
112 | {
113 | if (node.closing !== true)
114 | {
115 | incrementTop(state.hbsCounts);
116 | }
117 | }*/
118 |
119 | break;
120 | }
121 |
122 |
123 | case NodeType.HTML_ATTR_END:
124 | {
125 | break;
126 | }
127 | case NodeType.HTML_ATTR_START:
128 | {
129 | if (numAttributes(parserState) <= 1)
130 | {
131 | if (isDomMethod(compilerState) === false)
132 | {
133 | // React.createElement("tag",
134 | result.push(",");
135 | }
136 |
137 | // React.createElement("tag", {
138 | // React.DOM.tag({
139 | result.push("{");
140 | }
141 | else
142 | {
143 | // React.createElement("tag", {attr:"value",
144 | // React.DOM.tag({attr:value,
145 | result.push(",");
146 | }
147 |
148 | break;
149 | }
150 |
151 |
152 | case NodeType.HTML_ATTR_NAME_END:
153 | {
154 | break;
155 | }
156 | case NodeType.HTML_ATTR_NAME_START:
157 | {
158 | break;
159 | }
160 |
161 |
162 | case NodeType.HTML_ATTR_VALUE_END:
163 | {
164 | break;
165 | }
166 | case NodeType.HTML_ATTR_VALUE_START:
167 | {
168 | result.push(":");
169 | break;
170 | }
171 |
172 |
173 | case NodeType.HTML_COMMENT_END:
174 | {
175 | break;
176 | }
177 | case NodeType.HTML_COMMENT_START:
178 | {
179 | break;
180 | }
181 |
182 |
183 | // …>
184 | case NodeType.HTML_TAG_END:
185 | {
186 | if (parserState.isClosingTag === true)
187 | {
188 | if (numAttributes(parserState)>0 && numChildren(parserState)<=0)
189 | {
190 | result.push("}");
191 | }
192 |
193 | result.push(")");
194 |
195 | compilerState.areDomMethods.pop();
196 | }
197 |
198 | break;
199 | }
200 | // <…
201 | case NodeType.HTML_TAG_START:
202 | {
203 | if (parserState.isClosingTag === false)
204 | {
205 | compilerState.areDomMethods.push(false);
206 |
207 | beforeChild(parserState, compilerState, result, true);
208 |
209 | result.push("React.createElement(");
210 | }
211 |
212 | break;
213 | }
214 |
215 |
216 | case NodeType.HTML_TAG_NAME_END:
217 | {
218 | break;
219 | }
220 | case NodeType.HTML_TAG_NAME_START:
221 | {
222 | break;
223 | }
224 |
225 |
226 | case NodeType.LITERAL:
227 | {
228 | if (parserState.isTag === true)
229 | {
230 | if (parserState.isTagName === true)
231 | {
232 | if (parserState.isClosingTag === false)
233 | {
234 | if (this.options.useDomMethods === true)
235 | {
236 | // If tag name has a `React.DOM` function
237 | if (ReactDOM[node.value] === true)
238 | {
239 | // Change stack's top value
240 | compilerState.areDomMethods[compilerState.areDomMethods.length-1] = true;
241 |
242 | // Change last/previous result index
243 | result[result.length-1] = "React.DOM." + node.value + "(";
244 |
245 | // Done -- no more code in this `case` will run
246 | break;
247 | }
248 | }
249 |
250 | // React.createElement("tag"
251 | result.push('"'+ node.value +'"');
252 | }
253 | // Else: closing tag name excluded from result
254 | }
255 | else if (parserState.isAttribute === true)
256 | {
257 | if (parserState.isAttributeName === true)
258 | {
259 | // React.createElement("tag", {"attr"
260 | // React.DOM.tag({"attr"
261 | result.push( transformAttributeName(node.value) );
262 | }
263 | else if (parserState.isAttributeValue === true)
264 | {
265 | // TODO :: support `href="javscript:code()"`
266 | /*if (parserState.isEventAttribute === true)
267 | {
268 | // React.createElement("tag", {"onsomething":"code()"
269 | // React.DOM.tag({"onsomething":"code()"
270 | result.push( transformScript(node.value, this.options) );
271 | }
272 | else*/ if (parserState.isStyleAttribute === true)
273 | {
274 | // React.createElement("tag", {"style":{…}
275 | // React.DOM.tag({"style":{…}
276 | result.push( transformInlineStyles(node.value, this.options) );
277 | }
278 | else
279 | {
280 | // React.createElement("tag", {"attr":"value"
281 | // React.DOM.tag({"attr":"value"
282 | result.push( safeString(node.value) );
283 | }
284 | }
285 | }
286 | }
287 | else
288 | {
289 | beforeChild(parserState, compilerState, result);
290 |
291 | if (parserState.isWithinScriptTag === true)
292 | {
293 | // React.createElement("script", …, "script()"
294 | // TODO :: only do so if mimetype is "text/javascript", "" or undefined
295 | result.push( transformScript(node.value, this.options) );
296 | }
297 | else if (parserState.isWithinStyleTag === true)
298 | {
299 | // React.createElement("style", …, "style:sheet"
300 | // TODO :: only do so if mimetype is "text/css", "" or undefined
301 | result.push( transformStylesheet(node.value, this.options) );
302 | }
303 | else
304 | {
305 | //if (typeof node.value==="string" || node.value instanceof String===true)
306 | //{
307 | // React.createElement("tag", …, "text"
308 | // React.DOM.tag(…, "text"
309 | result.push( safeString(node.value) );
310 | //}
311 | //else
312 | //{
313 | // Support for null, undefined, numbers
314 | // result.push(node.value);
315 | //}
316 | }
317 | }
318 |
319 | break;
320 | }
321 |
322 |
323 | default:
324 | {
325 | // oops?
326 | }
327 | }
328 | }))
329 | .then(program =>
330 | {
331 | // If more than one top-level node
332 | if (numChildren(parserState) > 1)
333 | {
334 | if (this.options.multipleTopLevelNodes === true)
335 | {
336 | // Contain comma-separated list in an Array
337 | result.unshift("[");
338 | result.push("]");
339 | }
340 | else
341 | {
342 | throw new Error(numChildren(parserState) + " top-level nodes detected. Only 1 is currently supported by React.");
343 | }
344 | }
345 |
346 | //console.log(str);
347 | //console.log(program);
348 | //console.log(result);
349 | result = finalize(result, this.options);
350 | //console.log(result);
351 |
352 | return result;
353 | });
354 | };
355 |
356 |
357 |
358 | //::: PRIVATE FUNCTIONS
359 |
360 |
361 |
362 | function beforeChild(parserState, compilerState, result, checkParent)
363 | {
364 | var _isDomMethod = checkParent!==true ? isDomMethod(compilerState) : isParentDomMethod(compilerState);
365 | var _numAttributes = checkParent!==true ? numAttributes(parserState) : numParentAttributes(parserState);
366 | var _numChildren = checkParent!==true ? numChildren(parserState) : numParentChildren(parserState);
367 |
368 | var _isTopLevelChild = checkParent!==true ? parserState.childCounts.length>1 : parserState.childCounts.length>2;
369 |
370 | if (_isTopLevelChild === true)
371 | {
372 | if (_numAttributes <= 0)
373 | {
374 | if (_numChildren <= 1)
375 | {
376 | if (_isDomMethod !== true)
377 | {
378 | // React.createElement("tag",
379 | result.push(",");
380 | }
381 |
382 | // React.createElement("tag", null,
383 | // React.DOM.tag(null,
384 | result.push("null");
385 | result.push(",");
386 | }
387 | else
388 | {
389 | // React.createElement("tag", {"attr":"value"}, sibling,
390 | // React.DOM.tag({"attr":"value"}, sibling,
391 | result.push(",");
392 | }
393 | }
394 | else
395 | {
396 | // React.createElement("tag", {"attr":"value"},
397 | // React.DOM.tag({"attr":"value"},
398 | result.push("}");
399 | result.push(",");
400 | }
401 | }
402 | // If top-level node with siblings
403 | else if (_numChildren > 1)
404 | {
405 | // React.createElement(…),
406 | // React.DOM.tag(…),
407 | // "text",
408 | result.push(",");
409 | }
410 | }
411 |
412 |
413 |
414 | function finalize(result, options)
415 | {
416 | var js = options.prefix + result.join("") + options.suffix;
417 |
418 | // Check that the compiled code is valid
419 | try
420 | {
421 | Function("", js);
422 | }
423 | catch (error)
424 | {
425 | console.log(js);
426 | throw error;
427 | }
428 |
429 | if (options.beautify === true)
430 | {
431 | js = HandlebarsHtmlParser.beautifyJS(js);
432 | }
433 |
434 | return js;
435 | }
436 |
437 |
438 |
439 | function getLast(stack)
440 | {
441 | return stack[stack.length - 1];
442 | }
443 |
444 |
445 |
446 | function getSecondLast(stack)
447 | {
448 | return stack[stack.length - 2];
449 | }
450 |
451 |
452 |
453 | function isDomMethod(compilerState)
454 | {
455 | return getLast(compilerState.areDomMethods);
456 | }
457 |
458 |
459 |
460 | function isParentDomMethod(compilerState)
461 | {
462 | var result = getSecondLast(compilerState.areDomMethods);
463 |
464 | if (result === undefined) result = -1;
465 |
466 | return result;
467 | }
468 |
469 |
470 |
471 | function numAttributes(parserState)
472 | {
473 | return getLast(parserState.attrCounts);
474 | }
475 |
476 |
477 |
478 | function numChildren(parserState)
479 | {
480 | return getLast(parserState.childCounts);
481 | }
482 |
483 |
484 |
485 | function numParentAttributes(parserState)
486 | {
487 | var result = getSecondLast(parserState.attrCounts);
488 |
489 | if (result === undefined) result = -1;
490 |
491 | return result;
492 | }
493 |
494 |
495 |
496 | function numParentChildren(parserState)
497 | {
498 | var result = getSecondLast(parserState.childCounts);
499 |
500 | if (result === undefined) result = -1;
501 |
502 | return result;
503 | }
504 |
505 |
506 |
507 | function safeString(string)
508 | {
509 | // Converts whitespace, unicode chars, adds/escapes quotes, etc
510 | return JSON.stringify(string);
511 | }
512 |
513 |
514 |
515 | function transformAttributeName(attrName)
516 | {
517 | // TODO :: is this necessary?
518 | // TODO :: find a lib for this as there're more?
519 | switch (attrName)
520 | {
521 | case "class":
522 | {
523 | attrName = "className";
524 | break;
525 | }
526 | case "for":
527 | {
528 | attrName = "htmlFor";
529 | break;
530 | }
531 | default:
532 | {
533 | // TODO :: camel-case it?
534 | }
535 | }
536 |
537 | return '"'+ attrName +'"';
538 | }
539 |
540 |
541 |
542 | function transformInlineStyles(styles, options)
543 | {
544 | return JSON.stringify( postcssJs.objectify( postcss.parse(styles) ) );
545 | }
546 |
547 |
548 |
549 | function transformScript(script, options)
550 | {
551 | return safeString(script);
552 | }
553 |
554 |
555 |
556 | function transformStylesheet(stylesheet, options)
557 | {
558 | return safeString(stylesheet);
559 | }
560 |
561 |
562 |
563 | module.exports = compiler;
564 |
--------------------------------------------------------------------------------
/lib/parseOptions.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var defaultOptions =
4 | {
5 | beautify: false,
6 | multipleTopLevelNodes: false,
7 | normalizeWhitespace: false,
8 | prefix: "",
9 | processCSS: false,
10 | processJS: false,
11 | suffix: "",
12 | useDomMethods: false
13 | };
14 |
15 |
16 |
17 | function parseOptions(customOptions)
18 | {
19 | var presetOptions;
20 |
21 | if (customOptions != null)
22 | {
23 | // Presets
24 | switch (customOptions.env)
25 | {
26 | case "development":
27 | {
28 | presetOptions =
29 | {
30 | beautify: true,
31 | normalizeWhitespace: true,
32 | processCSS: true,
33 | useDomMethods: true
34 | };
35 | break;
36 | }
37 | case "production":
38 | {
39 | presetOptions =
40 | {
41 | normalizeWhitespace: true,
42 | processCSS: true,
43 | processJS: true
44 | // TODO :: does `useDomMethods` gzip smaller?
45 | };
46 | break;
47 | }
48 | }
49 | }
50 |
51 | return Object.assign({}, defaultOptions, presetOptions, customOptions);
52 | }
53 |
54 |
55 |
56 | module.exports = parseOptions;
57 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Steven Vachon (svachon.com)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "handlebars-react",
3 | "description": "Compile Handlebars templates to React.",
4 | "version": "0.0.16",
5 | "license": "MIT",
6 | "homepage": "https://github.com/stevenvachon/handlebars-react",
7 | "author": {
8 | "name": "Steven Vachon",
9 | "email": "contact@svachon.com",
10 | "url": "http://www.svachon.com/"
11 | },
12 | "main": "lib",
13 | "repository": {
14 | "type": "git",
15 | "url": "git://github.com/stevenvachon/handlebars-react.git"
16 | },
17 | "bugs": {
18 | "url": "https://github.com/stevenvachon/handlebars-react/issues"
19 | },
20 | "dependencies": {
21 | "handlebars-html-parser": "git://github.com/stevenvachon/handlebars-html-parser.git",
22 | "postcss": "^5.0.17",
23 | "postcss-js": "~0.1.2",
24 | "react": "0.14.7"
25 | },
26 | "devDependencies": {
27 | "browserify": "^13.0.0",
28 | "chai": "^3.5.0",
29 | "chai-as-promised": "^5.2.0",
30 | "mkdirp": "~0.5.1",
31 | "mocha": "^2.4.5",
32 | "uglify-js": "^2.6.2"
33 | },
34 | "engines": {
35 | "node": ">=5"
36 | },
37 | "scripts": {
38 | "browserify": "npm dedupe && mkdirp browser && npm run browserify-full && npm run browserify-lite",
39 | "browserify-full": "browserify lib/ --standalone HandlebarsReact --exclude any-promise | uglifyjs --compress --mangle -o browser/handlebars-react.min.js",
40 | "browserify-lite": "browserify lib/ --standalone HandlebarsReact --exclude any-promise --exclude autoprefixer --exclude cssnano --exclude uglify-js | uglifyjs --compress --mangle -o browser/handlebars-react-lite.min.js",
41 | "install": "npm run react-dom",
42 | "react-dom": "node scripts/react-dom lib/ReactDOM.js",
43 | "test": "npm run test-server",
44 | "test-browser": "npm run browserify",
45 | "test-server": "mocha test/ --reporter spec --check-leaks --bail --no-exit"
46 | },
47 | "files": [
48 | "lib",
49 | "scripts",
50 | "license"
51 | ],
52 | "keywords": [
53 | "handlebars",
54 | "mustache",
55 | "react",
56 | "template",
57 | "view"
58 | ]
59 | }
60 |
--------------------------------------------------------------------------------
/scripts/react-dom.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | /*
3 | Generate a file containing React.DOM.* functions.
4 |
5 | This allows the compiler to not depend on React,
6 | which is especially important when running in a
7 | browser.
8 | */
9 | var fs = require("fs");
10 | var path = require("path");
11 | var React = require("react");
12 |
13 | var count,key,output,target;
14 |
15 | target = process.argv[2];
16 | if (target === undefined) throw Error("target not defined: npm run react-dom path/to/target.js");
17 | target = path.resolve(target);
18 |
19 | output = '"use strict";\n\n';
20 | output += '// Generated via ' + path.relative(path.dirname(target), __filename) + '\n';
21 | output += 'module.exports = \n';
22 | output += '{';
23 |
24 | count = 0;
25 |
26 | for (key in React.DOM)
27 | {
28 | if (count++ > 0) output += ',';
29 |
30 | output += '\n\t"'+ key +'": true';
31 | }
32 |
33 | output += '\n};\n';
34 |
35 | fs.writeFileSync(target, output);
36 |
37 | console.log("File written:\n" + target);
38 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var compiler = require("../lib");
3 | var options = require("../lib/parseOptions");
4 |
5 | var chai = require("chai");
6 | var expect = chai.expect;
7 |
8 | chai.use( require("chai-as-promised") );
9 |
10 |
11 |
12 | // https://facebook.github.io/react/jsx-compiler.html
13 |
14 |
15 |
16 | describe("Basic HTML", () =>
17 | {
18 | describe("with one top-level node", () =>
19 | {
20 | it("should be supported", () =>
21 | {
22 | var result = new compiler( options() ).compile('');
23 | var expectedResult = 'React.createElement("tag")';
24 |
25 | return expect(result).to.eventually.deep.equal(expectedResult);
26 | });
27 |
28 |
29 |
30 | it("should support an attribute", () =>
31 | {
32 | var result = new compiler( options() ).compile('');
33 | var expectedResult = 'React.createElement("tag",{"attr":"value"})';
34 |
35 | return expect(result).to.eventually.deep.equal(expectedResult);
36 | });
37 |
38 |
39 |
40 | it("should support attributes", () =>
41 | {
42 | var result = new compiler( options() ).compile('');
43 | var expectedResult = 'React.createElement("tag",{"attr1":"value1","attr-2":"value2"})';
44 |
45 | return expect(result).to.eventually.deep.equal(expectedResult);
46 | });
47 |
48 |
49 |
50 | it("should support attributes and text content", () =>
51 | {
52 | var result = new compiler( options() ).compile('text');
53 | var expectedResult = 'React.createElement("tag",{"attr1":"value1","attr-2":"value2"},"text")';
54 |
55 | //console.log( require("uglify-js").minify(result,{fromString:true}).code );
56 |
57 | return expect(result).to.eventually.deep.equal(expectedResult);
58 | });
59 |
60 |
61 |
62 | it("should support nested tags", () =>
63 | {
64 | var result = new compiler( options() ).compile('text');
65 | var expectedResult = 'React.createElement("tag",null,React.createElement("tag"),"text",React.createElement("tag"))';
66 |
67 | return expect(result).to.eventually.deep.equal(expectedResult);
68 | });
69 |
70 |
71 |
72 | it("should support nested tags (#2)", () =>
73 | {
74 | var result = new compiler( options() ).compile('texttext');
75 | var expectedResult = 'React.createElement("tag",null,"text",React.createElement("tag"),"text")';
76 |
77 | return expect(result).to.eventually.deep.equal(expectedResult);
78 | });
79 |
80 |
81 |
82 | it("should support nested tags and a convenience function", () =>
83 | {
84 | var result = new compiler( options({ useDomMethods:true }) ).compile('text
');
85 | var expectedResult = 'React.DOM.div(null,React.createElement("tag"),"text",React.createElement("tag"))';
86 |
87 | return expect(result).to.eventually.deep.equal(expectedResult);
88 | });
89 |
90 |
91 |
92 | it("should support nested tags and a convenience function (#2)", () =>
93 | {
94 | var result = new compiler( options({ useDomMethods:true }) ).compile('texttext
');
95 | var expectedResult = 'React.DOM.div(null,"text",React.createElement("tag"),"text")';
96 |
97 | return expect(result).to.eventually.deep.equal(expectedResult);
98 | });
99 |
100 |
101 |
102 | it("should support nested tags and a convenience function (#3)", () =>
103 | {
104 | var result = new compiler( options({ useDomMethods:true }) ).compile('');
105 | var expectedResult = 'React.DOM.div(null,React.DOM.div(null,"text"),React.createElement("tag",null,"text"))';
106 |
107 | return expect(result).to.eventually.deep.equal(expectedResult);
108 | });
109 | });
110 |
111 |
112 |
113 | // NOTE :: this is not supported by React, but it's here for completeness
114 | describe("with multiple top-level nodes", () =>
115 | {
116 | it("should be supported", () =>
117 | {
118 | var result = new compiler( options({ multipleTopLevelNodes:true }) ).compile('');
119 | var expectedResult = '[React.createElement("tag"),React.createElement("tag")]';
120 |
121 | return expect(result).to.eventually.deep.equal(expectedResult);
122 | });
123 |
124 |
125 |
126 | it("should support attributes and text content", () =>
127 | {
128 | var result = new compiler( options({ multipleTopLevelNodes:true }) ).compile('text text');
129 | var expectedResult = '[React.createElement("tag",{"attr":"value"},"text")," ",React.createElement("tag",{"attr1":"value1","attr-2":"value2"},"text")]';
130 |
131 | return expect(result).to.eventually.deep.equal(expectedResult);
132 | });
133 |
134 |
135 |
136 | it("should support nested tags", () =>
137 | {
138 | var result = new compiler( options({ multipleTopLevelNodes:true, useDomMethods:true }) ).compile('text texttext');
139 | var expectedResult = '[React.createElement("tag",null,React.createElement("tag"),"text",React.createElement("tag"))," ",React.createElement("tag",null,"text",React.createElement("tag"),"text")]';
140 |
141 | return expect(result).to.eventually.deep.equal(expectedResult);
142 | });
143 |
144 |
145 |
146 | it("should support nested tags and a convenience function", () =>
147 | {
148 | var result = new compiler( options({ multipleTopLevelNodes:true, useDomMethods:true }) ).compile('text
texttext
');
149 | var expectedResult = '[React.DOM.div(null,React.createElement("tag"),"text",React.createElement("tag"))," ",React.DOM.div(null,"text",React.createElement("tag"),"text")]';
150 |
151 | return expect(result).to.eventually.deep.equal(expectedResult);
152 | });
153 | });
154 |
155 |
156 |
157 | describe("edge cases", () =>
158 | {
159 | it("should support text content with special characters", () =>
160 | {
161 | var result = new compiler( options() ).compile('"text©© "');
162 | var expectedResult = 'React.createElement("tag",null,"\\\"text©© \\\"")';
163 |
164 | return expect(result).to.eventually.deep.equal(expectedResult);
165 | });
166 |
167 |
168 |
169 | it("should support ');
172 | var expectedResult = 'React.createElement("script",null,"function a(arg){ b(arg,\\\"arg\\\") }")';
173 |
174 | return expect(result).to.eventually.deep.equal(expectedResult);
175 | });
176 |
177 |
178 |
179 | it("should support unrecognized ');
182 | var expectedResult = 'React.createElement("script",{"type":"text/template"},"text")';
183 |
184 | return expect(result).to.eventually.deep.equal(expectedResult);
185 | });
186 |
187 |
188 |
189 | it("should support ');
192 | var expectedResult = 'React.createElement("style",null,"html { background-color:gray }")';
193 |
194 | return expect(result).to.eventually.deep.equal(expectedResult);
195 | });
196 |
197 |
198 |
199 | it("should support style attributes", () =>
200 | {
201 | var result = new compiler( options() ).compile('');
202 | var expectedResult = 'React.createElement("div",{"style":{"backgroundColor":"gray"}})';
203 |
204 | return expect(result).to.eventually.deep.equal(expectedResult);
205 | });
206 | });
207 |
208 |
209 |
210 | describe("options", () =>
211 | {
212 | it("beautify = true", () =>
213 | {
214 | var result = new compiler( options({ beautify:true }) ).compile('text');
215 |
216 | var expectedResult = '';
217 | expectedResult += 'React.createElement("tag", {\n';
218 | expectedResult += ' attr1: "value1",\n';
219 | expectedResult += ' "attr-2": "value2"\n';
220 | expectedResult += '}, "text");';
221 |
222 | return expect(result).to.eventually.deep.equal(expectedResult);
223 | });
224 |
225 |
226 |
227 | it("normalizeWhitespace = true", () =>
228 | {
229 | var result = new compiler( options({ normalizeWhitespace:true }) ).compile('text©© ');
230 | var expectedResult = 'React.createElement("tag",null,"text©© ")'; // non-breaking space remains
231 |
232 | return expect(result).to.eventually.deep.equal(expectedResult);
233 | });
234 |
235 |
236 |
237 | it("processCSS = true", () =>
238 | {
239 | var result = new compiler( options({ processCSS:true }) ).compile('');
240 | var expectedResult = 'React.createElement("style",null,"div{property:value}")';
241 |
242 | return expect(result).to.eventually.deep.equal(expectedResult);
243 | });
244 |
245 |
246 |
247 | it("processJS = true", () =>
248 | {
249 | var result = new compiler( options({ processJS:true }) ).compile('');
250 | var expectedResult = 'React.createElement("script",null,"function funcA(n){funcB(n,\\\"arg\\\")}")';
251 |
252 | return expect(result).to.eventually.deep.equal(expectedResult);
253 | });
254 |
255 |
256 |
257 | // `multipleTopLevelNodes` is tested above
258 | // `useDomMethods` is tested above
259 | });
260 | });
261 |
--------------------------------------------------------------------------------