├── .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('
text
text
'); 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 | --------------------------------------------------------------------------------