├── .gitignore ├── LICENSE ├── README.md ├── examples ├── README.md ├── backbone │ ├── build.js │ ├── index.html │ ├── scripts │ │ └── templates.js │ └── templates │ │ └── template.html ├── basic │ ├── build.js │ ├── index.html │ ├── scripts │ │ └── templates.js │ └── templates │ │ ├── _template-partial.html │ │ ├── template-1.html │ │ ├── template-2.html │ │ ├── template-3.html │ │ ├── template-4.html │ │ ├── template-5.html │ │ └── template-6.html ├── lib │ ├── precompile.hbs │ └── precompile.js └── simple │ ├── build.js │ ├── index.html │ ├── scripts │ └── templates.js │ └── templates │ └── template.html ├── package.json └── src ├── handlebars ├── index.js ├── instrumented-compiler.js ├── instrumented-runtime.js └── runtime.js ├── index.js └── transpiler ├── attributes-collector.js ├── backends └── idom.js ├── dataset-collector.js ├── index.js └── shared ├── constants.js ├── opcodes.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Davide Mancuso 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | incremental-bars 2 | ============= 3 | 4 | incremental-bars provides [incremental-dom](https://github.com/google/incremental-dom) backend backend support to [handlebars](http://www.handlebarsjs.com) templates. 5 | 6 | - compile ANY standard Handlebars template to output DOM instead of strings 7 | - patch only the DOM parts that changed upon each render 8 | - support all features native to Handlebars 9 | - integrate easily with existing / legacy code 10 | 11 | Rationale 12 | ---------- 13 | 14 | Handlebars templates are awesome and used in countless applications everywhere - but they generate strings, not DOM, and that makes re-rendering fragments fairly expensive and not suitable for in-place DOM patching (popularized by [React](https://facebook.github.io/react/), [Virtual Dom](https://github.com/Matt-Esch/virtual-dom), [incremental-dom](https://github.com/google/incremental-dom), etc.). 15 | 16 | There are ways to make Handlebars build DOM rather than strings ([HtmlBars](https://github.com/tildeio/htmlbars) is the first that comes to mind). However it is rather hard to find something that: 17 | 18 | - supports all cool features native to Handlebars (partials, custom block helpers, decorators etc.) 19 | - it's framework-agnostic and therefore easy to integrate with existing plumbing 20 | - allows reuse of templates that have already been written and deployed 21 | - does not attempt to rewrite the whole Handlebars 22 | 23 | The idea is not to make Handlebars understand DOM - which is a complex task and ends up re-writing most of the Handlebars library or making it hard to maintain / upgrade - but rather to let Handlebars do what it does best and simply change the input fed into it and adjust a little its output. The fact Handlebars is entirely html-agnostic (it does not make any assumption of how the input looks like so it has no clue about tags, attributes etc.) is just perfect to achieve this. 24 | 25 | This package is essentially composed of 3 main parts: 26 | 27 | - A moustache-aware HTML parser/converter that creates an intermediate representation of the input as a linear sequence of "instructions" for each HTML tag. This intermediate representation is backend-agnostic (incremental-dom, virtual-dom, etc.). 28 | - An emitter/transpiler that understands the intermediate representation and is capable of generating instructions for the target backend (incremental-dom, virtual-dom, etc.) from the input sequence in a string format compatible with the way Handlebars tokenizes its input. 29 | - A custom Handlebars JavascriptCompiler extension that generates outputs that can be executed at runtime 30 | 31 | [incremental-dom](https://github.com/google/incremental-dom) was chosen because it's a beautiful, fast and dead-simple library which has already a notion of "sequence of instructions" which map really well with the above approach (to the extent that the "intermediate representation" is just a little bit more than a list of idom-like meta-instructions). 32 | 33 | > The main purpose of this library is to be used as a build-time tool to generate optimized precompiled templates. 34 | > Although it would be possible to package it to run in a browser (but some modifications are needed), and therefore 35 | > use it as a runtime compiler, you should expect some inevitable size and performance overhead due to the additional 36 | > internalization steps required to parse and process the HTML input. So that's not recommended. 37 | 38 | Installing 39 | ---------- 40 | 41 | npm install incremental-bars 42 | 43 | `require('incremental-bars')` returns a regular Handlebars object that you can use as normal to (pre)compile templates. 44 | You specify a transpiler mode other than the standard `'html'` by passing a `transpilerOptions` hash to `compile` or `precompile` as described in the next sections. 45 | 46 | Head over to the [examples](https://github.com/atomictag/incremental-bars/tree/master/examples) to get an idea of what the library does. 47 | 48 | Usage 49 | ----- 50 | 51 | Compiling / precompiling templates using incremental-bars is syntactically identical to their standard Handlebars equivalents. 52 | Only 2 things are necessary: 53 | 54 | 1. you must `require('incremental-bars')` instead of `require('handlebars')` 55 | 2. you must add a `transpilerOptions` hash (described below) to the options passed to `Handlebars.compile` and `Handlebars.precompile` if you want to use a special backend 56 | 57 | This is an example snippet for programmatic usage with the `idom` backend: 58 | 59 | ```javascript 60 | var Handlebars = require('incremental-bars'); 61 | var html = '
hello: {{ world}} [{{ @backend }}] {{ type }}'; 62 | var templateFn = Handlebars.compile(html, { /* Handlebars options, */ transpilerOptions : { backend : 'idom' }}); 63 | ... 64 | 65 | // when you are ready to render: 66 | IncrementalDOM.patch(someElement, templateFn, someData); 67 | ``` 68 | 69 | Of course `Handlebars.precompile` works the same way (more info on that below). Here's a [RunKit]( https://runkit.com/593aa1f1727bdc0012e02621/593aa1f1727bdc0012e02622) to try it out yourself. 70 | 71 | Check out the [examples](https://github.com/atomictag/incremental-bars/tree/master/examples) for some inspiration. 72 | 73 | > NOTE: `Handlebars.compile` is not very useful with backends other than the default `html` in a Node.js environment since executing te template function 74 | > of DOM-patching backends requires, obviously, a DOM environment. For the incremental-dom server-side rendering you can check out [incremental-dom-to-string](https://github.com/paolocaminiti/incremental-dom-to-string) 75 | 76 | There is currently no CLI but that's easy to add (or you can roll your own). 77 | The [precompile script](https://github.com/atomictag/incremental-bars/blob/master/examples/lib/precompile.js) is a good starting point. 78 | 79 | #### transpilerOptions 80 | 81 | Full example with description (this is the default for the ìdom backend): 82 | 83 | ```javascript 84 | var transpilerOptions = { 85 | minifyInput : true, // OPTIONAL: minify input removing whitespaces and carriage returns (default is true) 86 | backend : 'idom', // REQUIRED: Suppoorted backends: idom, html (to use default Handlebars) 87 | functionMap : { // OPTIONAL: What function names should be generated for the various opcodes for this backend (see shared/opcodes.js). Defaults: 88 | 'elementOpen' : 'IncrementalDOM.elementOpen', 89 | 'elementClose' : 'IncrementalDOM.elementClose', 90 | 'elementVoid' : 'IncrementalDOM.elementVoid', 91 | 'text' : 'IncrementalDOM.text', 92 | 'elementOpenStart' : 'IncrementalDOM.elementOpenStart', 93 | 'elementOpenEnd' : 'IncrementalDOM.elementOpenEnd', 94 | 'attr' : 'IncrementalDOM.attr', 95 | 'skip' : 'IncrementalDOM.skip' 96 | }, 97 | hoistedStatics : {}, // OPTIONAL (undefined). An object that will hold hoisted static string references (falsy value to disable) 98 | generateKeysForStaticEl : false, // OPTIONAL (false). Whether keys should be auto-generated for elements with only static properties (not recommended) 99 | generateKeysForAllEl : true, // OPTIONAL (true). Whether keys should be auto-generated for ALL elements (recommended, takes precedence over generateKeysForStaticEl) 100 | skipBlockAttributeMarker : 'data-partial-id', // OPTIONAL (data-partial-id). The attribute marker for elements that need to generate a 'skip' instruction. Can be a string or an array of strings. (falsy to disable) 101 | emptySkipBlocks : true, // OPTIONAL (true). Whether instructions within skip blocks should be ignored / not be generated 102 | safeMergeSelfClosing : true, // OPTIONAL (true). Whether it is safe to merge open / close on ALL tags (true because this is safe with idom backends) 103 | } 104 | ``` 105 | 106 | NOTE: if no transpilerOptions (or no supported 'backend' identifier) are passed to compile / precompile, Handlebars behaves as normal (HTML strings are produced): 107 | 108 | Precompiling Templates 109 | ---------------------- 110 | 111 | Similarly to its `compile` equivalent, this is essentially the same as calling `Handlebars.precompile` with an extra options hash. 112 | 113 | ```javascript 114 | var Handlebars = require('incremental-bars'); 115 | var html = '
hello: {{ world}} [{{ @backend }}] {{ type }}'; 116 | var templateData = Handlebars.precompile(html, { /* Handlebars options, */ transpilerOptions : { backend : 'idom' }}); 117 | ... 118 | // when you are ready to render: 119 | IncrementalDOM.patch(someElement, templateFn, someData); 120 | ``` 121 | 122 | > `hoistedStatics` requires a special note. This is particularly useful if you precompile a bunch of templates and generate a single file 123 | > with all of them. In order to minimize the duplication of static strings used within the templates (e.g. class attributes), if hoistedStatics 124 | > is an `Object` the generation output is analyzed and statics are injected in the hoistedStatics object and replaced with symbolic (= variables) 125 | > references within the emitted code. An example here helps best: 126 | 127 | ```javascript 128 | var tmpl1 = '
FIRST TEMPLATE
' 129 | var tmpl2 = '
SECOND TEMPLATE
' 130 | var statics = {}; 131 | 132 | var templateData1 = Handlebars.precompile(tmpl1, { transpilerOptions : { backend : 'idom', hoistedStatics : statics }}); 133 | var templateData2 = Handlebars.precompile(tmpl2, { transpilerOptions : { backend : 'idom', hoistedStatics : statics }}); 134 | 135 | /* 136 | the "statics" object now contains: 137 | { 138 | _1 : ["class", "A B C"] 139 | } 140 | and code generated by the respective templates references __$._1 instead 141 | of the original strings. (__$ is the actual name of the variable that needs 142 | to be used when dumping the statics object into a variable. 143 | This can be changed with the *secret* option "staticsName") 144 | */ 145 | ``` 146 | 147 | Look up the /examples folder. I'll definitely add an example on how to use this. 148 | 149 | Compatibility 150 | ------------- 151 | 152 | Pretty much everything you can do with Handlebars (as of this time of writing at version 4.0.10) you can do with incremental-bars. This includes partials, default and custom block helpers, decorators etc. The core of this library has been tested in production in various projects - including a full productive mobile banking application - without issues. 153 | 154 | #### Block helpers returning HTML strings 155 | 156 | Bear in mind that helpers returning html strings will no longer work and might actually cause a runtime error depending on where they are used within the html template. This is not a bug, but rather an expected result of the fact helpers are now executed in between incremental dom instructions. I actually never really use helpers to output HTML (which seems an anti-pattern to me), but in case you do, don't despair. You can write backend-independent helpers by emplyoying the following runtime check: 157 | 158 | Handlebars.registerHelper('myHelper', function() { 159 | var options = arguments[arguments.length - 1]; // Last argument is 'options' 160 | var backend = options.data && options.data.backend; // 'backend' is set by the incremental-bars runtime 161 | if(backend === 'idom') { /* incremental-dom version ... */ } 162 | else { /* default html-string version ... */ } 163 | }); 164 | 165 | Because the backend identifier is stored in the `data` object, it is accessibile from within templates as `{{ @backend }}`. 166 | 167 | #### Conditional rendering 168 | 169 | [Conditional rendering and iterations](http://google.github.io/incremental-dom/#conditional-rendering/array-of-items) pose a challenge to DOM patchers as it is necessary to instruct them on what elements can be recycled and which need to be created from scratch. For example, a simple list like `{ items : [1, 2, 3, 4, 5] }` and the following template: 170 | 171 | {{#each items}} 172 |
{{ this }}
173 | {{/each}} 174 | 175 | will only output `
5
` as elements are recycled on every cycle. To generate one  `div` per item it is necessary to instruct incremental-dom that each item needs a different element, which requires a unique `key` to be set upon each iteration. This library makes this simple via the `key` attribute, i.e.: 176 | 177 | {{#each items}} 178 |
{{ this }}
179 | {{/each}} 180 | 181 | correctly generates: 182 | 183 |
1
184 |
2
185 |
3
186 |
4
187 |
5
188 | 189 | #### Direct DOM manipulation (AKA jQuery) 190 | 191 | One cool feature about this library, and one that comes literally for free thanks to incremental-dom, is the ability to use JQuery & co. in conjunction with the DOM patching, something that is a big no-no for other virtual-dom implementations. Perhaps the most notable difference with other DOM libraries is that whatever is modified (e.g. by jQuery) that is not visible to incremental-dom will be kept in the elements after each render - and not be reverted to the known state. Adding elements to the DOM, conversely, won't survive a re-render cycle (but that's desirable, I guess). Just be aware that DOM manipulations done by JQuery are not necessarily reset after the template is re-rendered because the elements are not thrashed until really needed (unlike traditional Handlebars which destroys the current DOM sub-tree and builds a new one from scratch upon each render). 192 | 193 | #### Nested DOM subtrees 194 | 195 | Speaking of JQuery and, in general, frameworks that allow nesting of dynamic elements or independent "views" with their own template / rendering routine (a typical case for example with $el.append(...) or Backbone extensions like [Backbone.Layout](https://github.com/tbranyen/backbone.layoutmanager)), the typical requirement is to keep "alien" DOM elements within a generated subtree when it is re-rendered. However, the default behaviour of incremental-dom would be to get rid of each foreign subtree when a node is patched. This library provides a convention to inform the code generator that a subtree should be left where it is and skipped. The way to do that is with the (configurable) attribute `data-partial-id`, e.g. 196 | 197 | 198 |
199 | ... 200 | 201 | Any elements that are appended within `child` are preserved when `parent`is re-rendered, which is a very handy way to nest things together in a sort of "component" fashion. Partials are of course not affected by this as they are natively supported by incremental-bars in the same way they are on standard Handlebars. 202 | 203 | ### Known Issues 204 | 205 | 1. Don't do this ugliness (conditionally open tags not always matched by a closing tag): 206 | 207 | {{#if something}} 208 |
209 | {{else}} 210 |
211 | {{/if}} 212 | BLAH BLAH BLAH 213 |
214 | 215 | but do this (or similar) instead: 216 | 217 |
218 | BLAH BLAH BLAH 219 |
220 | 221 | 2. Moustached attributes like: 222 | 223 |
224 | BLAH BLAH BLAH 225 |
226 | 227 | must return an attribute NAME, i.e. any string (or any falsy value), therefore values like: 228 | 229 | attribute = 'class="something" style="width:10px"'; 230 | 231 | will fail because that is not a valid attribute name. 232 | It is however totally possible to create an ad-hoc 'attribute' helper that does that directly on the DOM element. 233 | This is again something I never do as in my opionion returning html artifacts from templates smells of anti-pattern. 234 | 235 | 236 | Found other problems? Have a request? 237 | 238 | File an [issue](https://github.com/atomictag/incremental-bars/issues) and I'll have a look at it. 239 | 240 | ### Extensions and future work 241 | 242 | - Do not run any code related to the static parts on re-render, as outlined [here](https://github.com/tildeio/htmlbars/issues/405#issuecomment-132413502) 243 | 244 | - Intelligently re-render only the dynamic parts of the template that are affected by an actual change in the data passed to the template (i.e. execute only the instructions that are strictly needed to resync the output with a change of state) 245 | 246 | Most of the above has already been developed and used in a custom variant of this library, but that's not so dependency-free as the parts published here, so it would require investing time that I don't really have to generalize the inner workings. 247 | 248 | 249 | ### License 250 | 251 | incremental-bars is released under the MIT license. 252 | 253 | About 254 | ----- 255 | 256 | made with :smiling_imp: by oneoverzero GmbH (OOZ) 257 | 258 | [![oneoverzero GmbH](http://oneoverzero.net/assets/img/logo.png)](http://oneoverzero.net) 259 | 260 | 261 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | incremental-bars examples 2 | ============= 3 | 4 | The folder `lib`: contains the build script to precompile templates. 5 | The build script picks all templates in a specified folder and output a script file for inclusion in a HTML page. 6 | 7 | Available examples: 8 | - folder `simple`: the simplest possible example. A super minimal starting point to fiddle with with the library. 9 | - folder `basic`: simple examples with purposely trivial templates to familiarize with the library. 10 | - folder `backbone`: simple backbone integration. A "classic" re-render-on-user-input case where incremental-bars is very useful. 11 | 12 | NOTE: If templates are changed/added to an example project, run `node build.js` again to re-generate the script with the pre-compiled templates. 13 | -------------------------------------------------------------------------------- /examples/backbone/build.js: -------------------------------------------------------------------------------- 1 | require('../lib/precompile')({ 2 | rootPath : __dirname, // Absolute path or the root folder from where src and dest are resolved 3 | srcDir : 'templates', // Folder where template sources live, relative to rootPath. This is not recursive. 4 | destDir : 'scripts', // Folder where to put the generated precompileds script, relative to rootPath 5 | outputFileName : 'templates.js', // Name of the output file script, generated in the destDir folder 6 | }); -------------------------------------------------------------------------------- /examples/backbone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | incremental-bars backbone example 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 49 | -------------------------------------------------------------------------------- /examples/backbone/scripts/templates.js: -------------------------------------------------------------------------------- 1 | (function(Hbs) { 2 | 3 | // Precompiled templates are collected in Handlebars.template 4 | Hbs.templates = Hbs.templates || {}; 5 | 6 | // Handlebars.template override. Required so the backend type becomes 7 | // available to templates and helpers as html and options.data.backend 8 | 9 | var original_template = Hbs.template; 10 | Hbs.template = function template_instrumented(spec) { 11 | var templateFn = original_template(spec); 12 | if(templateFn) { 13 | var fn = templateFn, backend = spec.compiler[2] || "html"; 14 | templateFn = function(context, options) { 15 | options = options || {}; 16 | options.data = options.data || {}; 17 | options.data.backend = backend; 18 | return fn(context, options); 19 | }; 20 | // Backend also available as a property of the template function 21 | templateFn.backend = backend; 22 | } 23 | return templateFn; 24 | }; 25 | 26 | // Generated statics block 27 | 28 | var __$ = { 29 | __name : "__$", 30 | _S : " ", 31 | _1554569446 : ["type","text","name","userInput"], 32 | }; 33 | 34 | // Generated function map 35 | var _o = IncrementalDOM.elementOpen, 36 | _v = IncrementalDOM.elementVoid, 37 | _c = IncrementalDOM.elementClose, 38 | _t = IncrementalDOM.text, 39 | _os = IncrementalDOM.elementOpenStart, 40 | _oe = IncrementalDOM.elementOpenEnd, 41 | _at = IncrementalDOM.attr, 42 | _s = IncrementalDOM.skip; 43 | 44 | 45 | // ================== Generated precompiled templates ================== 46 | 47 | /* ------------------ template [ template ] ------------------ */ 48 | Hbs.templates["template"] = Hbs.template({"1":function(container,depth0,helpers,partials,data) { 49 | var helper; 50 | 51 | return (_t(" You typed: ") & _o("strong", "idom-13", null) & _t("" 52 | + ((helper = (helper = helpers.userInput || (depth0 != null ? depth0.userInput : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"userInput","hash":{},"data":data}) : helper)) 53 | + "") & _c("strong")); 54 | },"3":function(container,depth0,helpers,partials,data) { 55 | return (_t(" Please type something! ")); 56 | },"compiler":[7,">= 4.0.0","idom"],"main":function(container,depth0,helpers,partials,data) { 57 | var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}); 58 | 59 | return (_o("div", "idom-6", null) & _t(__$._S) & _o("h3", "idom-7", null) & _t("Simple Backbone Integration") & _c("h3") & _t(__$._S) & _o("div", "idom-8", null) & _t(" Input some text in the input field. When the input change the whole View is re-rendered. ") & _c("div") & _t(__$._S) & _o("div", "idom-9", null) & _t(__$._S) & _o("p", "idom-10", null) & _t(__$._S) & _v("input", "idom-11", __$._1554569446, "value", "" 60 | + ((helper = (helper = helpers.userInput || (depth0 != null ? depth0.userInput : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(alias1,{"name":"userInput","hash":{},"data":data}) : helper)) 61 | + "") & _t(__$._S) & _c("p") & _t(__$._S) & _o("p", "idom-12", null)) 62 | + ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.userInput : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.program(3, data, 0),"data":data})) != null ? stack1 : "") 63 | + (_t(__$._S) & _c("p") & _t(__$._S) & _c("div") & _t(__$._S) & _c("div")); 64 | },"useData":true}); 65 | 66 | 67 | 68 | })(window.Handlebars); -------------------------------------------------------------------------------- /examples/backbone/templates/template.html: -------------------------------------------------------------------------------- 1 |
2 |

Simple Backbone Integration

3 |
4 | Input some text in the input field. When the input change the whole View is re-rendered. 5 |
6 |
7 |

8 | 9 |

10 |

11 | {{#if userInput}} 12 | You typed: {{ userInput }} 13 | {{ else }} 14 | Please type something! 15 | {{/if}} 16 |

17 |
18 |
-------------------------------------------------------------------------------- /examples/basic/build.js: -------------------------------------------------------------------------------- 1 | require('../lib/precompile')({ 2 | rootPath : __dirname, // Absolute path or the root folder from where src and dest are resolved 3 | srcDir : 'templates', // Folder where template sources live, relative to rootPath. This is not recursive. 4 | destDir : 'scripts', // Folder where to put the generated precompileds script, relative to rootPath 5 | outputFileName : 'templates.js', // Name of the output file script, generated in the destDir folder 6 | }); -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | incremental-bars example 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 46 | -------------------------------------------------------------------------------- /examples/basic/scripts/templates.js: -------------------------------------------------------------------------------- 1 | (function(Hbs) { 2 | 3 | // Precompiled templates are collected in Handlebars.template 4 | Hbs.templates = Hbs.templates || {}; 5 | 6 | // Handlebars.template override. Required so the backend type becomes 7 | // available to templates and helpers as html and options.data.backend 8 | 9 | var original_template = Hbs.template; 10 | Hbs.template = function template_instrumented(spec) { 11 | var templateFn = original_template(spec); 12 | if(templateFn) { 13 | var fn = templateFn, backend = spec.compiler[2] || "html"; 14 | templateFn = function(context, options) { 15 | options = options || {}; 16 | options.data = options.data || {}; 17 | options.data.backend = backend; 18 | return fn(context, options); 19 | }; 20 | // Backend also available as a property of the template function 21 | templateFn.backend = backend; 22 | } 23 | return templateFn; 24 | }; 25 | 26 | // Generated statics block 27 | 28 | var __$ = { 29 | __name : "__$", 30 | _S : " ", 31 | _1614377996 : ["class","content"], 32 | _2650412886 : ["class","content__title"], 33 | _3222510870 : ["class","content__body"], 34 | _458214356 : ["data-partial-id","my_partial_holder"], 35 | }; 36 | 37 | // Generated function map 38 | var _o = IncrementalDOM.elementOpen, 39 | _v = IncrementalDOM.elementVoid, 40 | _c = IncrementalDOM.elementClose, 41 | _t = IncrementalDOM.text, 42 | _os = IncrementalDOM.elementOpenStart, 43 | _oe = IncrementalDOM.elementOpenEnd, 44 | _at = IncrementalDOM.attr, 45 | _s = IncrementalDOM.skip; 46 | 47 | 48 | // ================== Generated precompiled templates ================== 49 | 50 | /* ------------------ template [ _template-partial ] ------------------ */ 51 | Hbs.templates["_template-partial"] = Hbs.template({"compiler":[7,">= 4.0.0","idom"],"main":function(container,depth0,helpers,partials,data) { 52 | var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function"; 53 | 54 | return (_o("div", "idom-3", null) & _t(__$._S) & _o("strong", "idom-4", null) & _t("I am a partial in this " 55 | + ((helper = (helper = helpers.world || (depth0 != null ? depth0.world : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"world","hash":{},"data":data}) : helper)) 56 | + " and my backend is \"" 57 | + ((helper = (helper = helpers.backend || (data && data.backend)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"backend","hash":{},"data":data}) : helper)) 58 | + "\"") & _c("strong") & _t(__$._S) & _c("div")); 59 | },"useData":true}); 60 | 61 | /* ------------------ template [ template-1 ] ------------------ */ 62 | Hbs.templates["template-1"] = Hbs.template({"compiler":[7,">= 4.0.0","idom"],"main":function(container,depth0,helpers,partials,data) { 63 | var helper; 64 | 65 | return (_o("div", "idom-6", __$._1614377996) & _t(__$._S) & _o("div", "idom-7", __$._2650412886) & _t(__$._S) & _o("h3", "idom-8", null) & _t("template-1 (context)") & _c("h3") & _t(__$._S) & _c("div") & _t(__$._S) & _o("div", "idom-9", __$._3222510870) & _t(__$._S) & _o("div", "idom-10", null) & _t("hello: " 66 | + ((helper = (helper = helpers.world || (depth0 != null ? depth0.world : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"world","hash":{},"data":data}) : helper)) 67 | + "") & _c("div") & _t(__$._S) & _c("div") & _t(__$._S) & _o("div", "idom-11", __$._458214356) & _s() & _c("div") & _t(__$._S) & _c("div") & _t(__$._S) & _v("hr", "idom-12", null)); 68 | },"useData":true}); 69 | 70 | /* ------------------ template [ template-2 ] ------------------ */ 71 | Hbs.templates["template-2"] = Hbs.template({"compiler":[7,">= 4.0.0","idom"],"main":function(container,depth0,helpers,partials,data) { 72 | var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function"; 73 | 74 | return (_o("div", "idom-15", __$._1614377996) & _t(__$._S) & _o("div", "idom-16", __$._2650412886) & _t(__$._S) & _o("h3", "idom-17", null) & _t("template-2 (context + data)") & _c("h3") & _t(__$._S) & _c("div") & _t(__$._S) & _o("div", "idom-18", __$._3222510870) & _t(__$._S) & _o("div", "idom-19", null) & _t("hello: " 75 | + ((helper = (helper = helpers.world || (depth0 != null ? depth0.world : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"world","hash":{},"data":data}) : helper)) 76 | + ". My backend is " 77 | + ((helper = (helper = helpers.backend || (data && data.backend)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"backend","hash":{},"data":data}) : helper)) 78 | + "") & _c("div") & _t(__$._S) & _c("div") & _t(__$._S) & _o("div", "idom-20", __$._458214356) & _s() & _c("div") & _t(__$._S) & _c("div") & _t(__$._S) & _v("hr", "idom-21", null)); 79 | },"useData":true}); 80 | 81 | /* ------------------ template [ template-3 ] ------------------ */ 82 | Hbs.templates["template-3"] = Hbs.template({"compiler":[7,">= 4.0.0","idom"],"main":function(container,depth0,helpers,partials,data) { 83 | var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function"; 84 | 85 | return (_o("div", "idom-25", __$._1614377996) & _t(__$._S) & _o("div", "idom-26", __$._2650412886) & _t(__$._S) & _o("h3", "idom-27", null) & _t("template-3 (context + data + partial)") & _c("h3") & _t(__$._S) & _c("div") & _t(__$._S) & _o("div", "idom-28", __$._3222510870) & _t(__$._S) & _o("div", "idom-29", null) & _t("hello: " 86 | + ((helper = (helper = helpers.world || (depth0 != null ? depth0.world : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"world","hash":{},"data":data}) : helper)) 87 | + ". My backend is " 88 | + ((helper = (helper = helpers.backend || (data && data.backend)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"backend","hash":{},"data":data}) : helper)) 89 | + "") & _c("div")) 90 | + ((stack1 = container.invokePartial(partials["_template-partial"],depth0,{"name":"_template-partial","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") 91 | + (_t(__$._S) & _c("div") & _t(__$._S) & _o("div", "idom-30", __$._458214356) & _s() & _c("div") & _t(__$._S) & _c("div") & _t(__$._S) & _v("hr", "idom-31", null)); 92 | },"usePartial":true,"useData":true}); 93 | 94 | /* ------------------ template [ template-4 ] ------------------ */ 95 | Hbs.templates["template-4"] = Hbs.template({"1":function(container,depth0,helpers,partials,data) { 96 | var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function"; 97 | 98 | return (_t(__$._S) & _o("li", "" 99 | + ((helper = (helper = helpers.index || (data && data.index)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"index","hash":{},"data":data}) : helper)) 100 | + "", null, "key", "" 101 | + ((helper = (helper = helpers.index || (data && data.index)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"index","hash":{},"data":data}) : helper)) 102 | + "", "style", "" 103 | + ((stack1 = helpers["if"].call(alias1,1,{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") 104 | + "") & _t(" " 105 | + ((helper = (helper = helpers.index || (data && data.index)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"index","hash":{},"data":data}) : helper)) 106 | + " " 107 | + container.lambda(depth0, depth0) 108 | + " ")) 109 | + ((stack1 = helpers["if"].call(alias1,(data && data.first),{"name":"if","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") 110 | + (_t(__$._S)) 111 | + ((stack1 = helpers["if"].call(alias1,(data && data.last),{"name":"if","hash":{},"fn":container.program(9, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") 112 | + (_t(__$._S) & _c("li")); 113 | },"2":function(container,depth0,helpers,partials,data) { 114 | var stack1; 115 | 116 | return "color:" 117 | + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.index),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.program(5, data, 0),"data":data})) != null ? stack1 : "") 118 | + ""; 119 | },"3":function(container,depth0,helpers,partials,data) { 120 | return "navy"; 121 | },"5":function(container,depth0,helpers,partials,data) { 122 | return "red"; 123 | },"7":function(container,depth0,helpers,partials,data) { 124 | return (_t("(first)")); 125 | },"9":function(container,depth0,helpers,partials,data) { 126 | return (_t("(last)")); 127 | },"compiler":[7,">= 4.0.0","idom"],"main":function(container,depth0,helpers,partials,data) { 128 | var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function"; 129 | 130 | return (_o("div", "idom-47", __$._1614377996) & _t(__$._S) & _o("div", "idom-48", __$._2650412886) & _t(__$._S) & _o("h3", "idom-49", null) & _t("template-4 (context + data + partial + #each + #if)") & _c("h3") & _t(__$._S) & _c("div") & _t(__$._S) & _o("div", "idom-50", __$._3222510870) & _t(__$._S) & _o("div", "idom-51", null) & _t("hello: " 131 | + ((helper = (helper = helpers.world || (depth0 != null ? depth0.world : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"world","hash":{},"data":data}) : helper)) 132 | + ". My backend is " 133 | + ((helper = (helper = helpers.backend || (data && data.backend)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"backend","hash":{},"data":data}) : helper)) 134 | + "") & _c("div")) 135 | + ((stack1 = container.invokePartial(partials["_template-partial"],depth0,{"name":"_template-partial","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") 136 | + (_t(__$._S) & _o("div", "idom-52", null) & _t(__$._S) & _o("ul", "idom-53", null) & _t(__$._S)) 137 | + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.items : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") 138 | + (_t(__$._S) & _c("ul") & _t(__$._S) & _c("div") & _t(__$._S) & _c("div") & _t(__$._S) & _o("div", "idom-54", __$._458214356) & _s() & _c("div") & _t(__$._S) & _c("div") & _t(__$._S) & _v("hr", "idom-55", null)); 139 | },"usePartial":true,"useData":true}); 140 | 141 | /* ------------------ template [ template-5 ] ------------------ */ 142 | Hbs.templates["template-5"] = Hbs.template({"1":function(container,depth0,helpers,partials,data) { 143 | var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function"; 144 | 145 | return (_t(__$._S) & _o("li", "" 146 | + ((helper = (helper = helpers.index || (data && data.index)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"index","hash":{},"data":data}) : helper)) 147 | + "", null, "key", "" 148 | + ((helper = (helper = helpers.index || (data && data.index)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"index","hash":{},"data":data}) : helper)) 149 | + "", "style", "" 150 | + ((stack1 = helpers["if"].call(alias1,1,{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") 151 | + "") & _t(" " 152 | + (helpers.customHelper || (depth0 && depth0.customHelper) || alias2).call(alias1,(data && data.index),{"name":"customHelper","hash":{},"data":data}) 153 | + " " 154 | + container.lambda(depth0, depth0) 155 | + " ")) 156 | + ((stack1 = helpers["if"].call(alias1,(data && data.first),{"name":"if","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") 157 | + (_t(__$._S)) 158 | + ((stack1 = helpers["if"].call(alias1,(data && data.last),{"name":"if","hash":{},"fn":container.program(9, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") 159 | + (_t(__$._S) & _c("li")); 160 | },"2":function(container,depth0,helpers,partials,data) { 161 | var stack1; 162 | 163 | return "color:" 164 | + ((stack1 = helpers["if"].call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.index),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.program(5, data, 0),"data":data})) != null ? stack1 : "") 165 | + ""; 166 | },"3":function(container,depth0,helpers,partials,data) { 167 | return "navy"; 168 | },"5":function(container,depth0,helpers,partials,data) { 169 | return "red"; 170 | },"7":function(container,depth0,helpers,partials,data) { 171 | return (_t("(first)")); 172 | },"9":function(container,depth0,helpers,partials,data) { 173 | return (_t("(last)")); 174 | },"compiler":[7,">= 4.0.0","idom"],"main":function(container,depth0,helpers,partials,data) { 175 | var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function"; 176 | 177 | return (_o("div", "idom-71", __$._1614377996) & _t(__$._S) & _o("div", "idom-72", __$._2650412886) & _t(__$._S) & _o("h3", "idom-73", null) & _t("template-5 (context + data + partial + #each + #if + customHelper)") & _c("h3") & _t(__$._S) & _c("div") & _t(__$._S) & _o("div", "idom-74", __$._3222510870) & _t(__$._S) & _o("div", "idom-75", null) & _t("hello: " 178 | + ((helper = (helper = helpers.world || (depth0 != null ? depth0.world : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"world","hash":{},"data":data}) : helper)) 179 | + ". My backend is " 180 | + ((helper = (helper = helpers.backend || (data && data.backend)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"backend","hash":{},"data":data}) : helper)) 181 | + "") & _c("div")) 182 | + ((stack1 = container.invokePartial(partials["_template-partial"],depth0,{"name":"_template-partial","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") 183 | + (_t(__$._S) & _o("div", "idom-76", null) & _t(__$._S) & _o("ul", "idom-77", null) & _t(__$._S)) 184 | + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.items : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") 185 | + (_t(__$._S) & _c("ul") & _t(__$._S) & _c("div") & _t(__$._S) & _c("div") & _t(__$._S) & _o("div", "idom-78", __$._458214356) & _s() & _c("div") & _t(__$._S) & _c("div") & _t(__$._S) & _v("hr", "idom-79", null)); 186 | },"usePartial":true,"useData":true}); 187 | 188 | /* ------------------ template [ template-6 ] ------------------ */ 189 | Hbs.templates["template-6"] = Hbs.template({"1":function(container,depth0,helpers,partials,data) { 190 | var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function"; 191 | 192 | return (_t(__$._S) & _o("li", "" 193 | + ((helper = (helper = helpers.index || (data && data.index)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"index","hash":{},"data":data}) : helper)) 194 | + "", null, "key", "" 195 | + ((helper = (helper = helpers.index || (data && data.index)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"index","hash":{},"data":data}) : helper)) 196 | + "", "style", "" 197 | + ((stack1 = helpers["if"].call(alias1,1,{"name":"if","hash":{},"fn":container.program(2, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") 198 | + "") & _t(" " 199 | + (helpers.customHelper || (depth0 && depth0.customHelper) || alias2).call(alias1,(data && data.index),{"name":"customHelper","hash":{},"data":data}) 200 | + " " 201 | + container.lambda(depth0, depth0) 202 | + " ")) 203 | + ((stack1 = helpers["if"].call(alias1,(data && data.first),{"name":"if","hash":{},"fn":container.program(7, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") 204 | + (_t(__$._S)) 205 | + ((stack1 = helpers["if"].call(alias1,(data && data.last),{"name":"if","hash":{},"fn":container.program(9, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") 206 | + (_t(__$._S) & _c("li")); 207 | },"2":function(container,depth0,helpers,partials,data) { 208 | var stack1; 209 | 210 | return "color:" 211 | + ((stack1 = (helpers.customBlockHelper || (depth0 && depth0.customBlockHelper) || helpers.helperMissing).call(depth0 != null ? depth0 : (container.nullContext || {}),(data && data.index),{"name":"customBlockHelper","hash":{},"fn":container.program(3, data, 0),"inverse":container.program(5, data, 0),"data":data})) != null ? stack1 : "") 212 | + ""; 213 | },"3":function(container,depth0,helpers,partials,data) { 214 | return "red"; 215 | },"5":function(container,depth0,helpers,partials,data) { 216 | return "navy"; 217 | },"7":function(container,depth0,helpers,partials,data) { 218 | return (_t("(first)")); 219 | },"9":function(container,depth0,helpers,partials,data) { 220 | return (_t("(last)")); 221 | },"compiler":[7,">= 4.0.0","idom"],"main":function(container,depth0,helpers,partials,data) { 222 | var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function"; 223 | 224 | return (_o("div", "idom-95", __$._1614377996) & _t(__$._S) & _o("div", "idom-96", __$._2650412886) & _t(__$._S) & _o("h3", "idom-97", null) & _t("template-6 (context + data + partial + #each + #if + customHelper + customBlockHelper)") & _c("h3") & _t(__$._S) & _c("div") & _t(__$._S) & _o("div", "idom-98", __$._3222510870) & _t(__$._S) & _o("div", "idom-99", null) & _t("hello: " 225 | + ((helper = (helper = helpers.world || (depth0 != null ? depth0.world : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"world","hash":{},"data":data}) : helper)) 226 | + ". My backend is " 227 | + ((helper = (helper = helpers.backend || (data && data.backend)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"backend","hash":{},"data":data}) : helper)) 228 | + "") & _c("div")) 229 | + ((stack1 = container.invokePartial(partials["_template-partial"],depth0,{"name":"_template-partial","data":data,"helpers":helpers,"partials":partials,"decorators":container.decorators})) != null ? stack1 : "") 230 | + (_t(__$._S) & _o("div", "idom-100", null) & _t(__$._S) & _o("ul", "idom-101", null) & _t(__$._S)) 231 | + ((stack1 = helpers.each.call(alias1,(depth0 != null ? depth0.items : depth0),{"name":"each","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "") 232 | + (_t(__$._S) & _c("ul") & _t(__$._S) & _c("div") & _t(__$._S) & _c("div") & _t(__$._S) & _o("div", "idom-102", __$._458214356) & _s() & _c("div") & _t(__$._S) & _c("div") & _t(__$._S) & _v("hr", "idom-103", null)); 233 | },"usePartial":true,"useData":true}); 234 | 235 | 236 | // ================== Auto-register partials ================== 237 | Hbs.registerPartial("_template-partial", Hbs.templates["_template-partial"] ); 238 | 239 | })(window.Handlebars); -------------------------------------------------------------------------------- /examples/basic/templates/_template-partial.html: -------------------------------------------------------------------------------- 1 |
2 | I am a partial in this {{ world }} and my backend is "{{ @backend }}" 3 |
-------------------------------------------------------------------------------- /examples/basic/templates/template-1.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

template-1 (context)

4 |
5 |
6 |
hello: {{ world}}
7 |
8 |
9 | THIS MUST NOT BE GENERATED BECAUSE THE PARENT IS MARKED AS A SKIP BLOCK (skipBlockAttributeMarker='data-partial-id') AND emptySkipBlocks = true 10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /examples/basic/templates/template-2.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

template-2 (context + data)

4 |
5 |
6 |
hello: {{ world}}. My backend is {{ @backend }}
7 |
8 |
9 | THIS MUST NOT BE GENERATED BECAUSE THE PARENT IS MARKED AS A SKIP BLOCK (skipBlockAttributeMarker='data-partial-id') AND emptySkipBlocks = true 10 |
11 |
12 |
-------------------------------------------------------------------------------- /examples/basic/templates/template-3.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

template-3 (context + data + partial)

4 |
5 |
6 |
hello: {{ world}}. My backend is {{ @backend }}
7 | {{> _template-partial }} 8 |
9 |
10 | THIS MUST NOT BE GENERATED BECAUSE THE PARENT IS MARKED AS A SKIP BLOCK (skipBlockAttributeMarker='data-partial-id') AND emptySkipBlocks = true 11 |
12 |
13 |
-------------------------------------------------------------------------------- /examples/basic/templates/template-4.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

template-4 (context + data + partial + #each + #if)

4 |
5 |
6 |
hello: {{ world}}. My backend is {{ @backend }}
7 | {{> _template-partial }} 8 |
9 |
    10 | {{#each items}} 11 |
  • 12 | {{ @index }} {{ this }} {{#if @first}}(first){{/if}} {{#if @last}}(last){{/if}} 13 |
  • 14 | {{/each}} 15 |
16 |
17 |
18 |
19 | THIS MUST NOT BE GENERATED BECAUSE THE PARENT IS MARKED AS A SKIP BLOCK (skipBlockAttributeMarker='data-partial-id') AND emptySkipBlocks = true 20 |
21 |
22 |
-------------------------------------------------------------------------------- /examples/basic/templates/template-5.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

template-5 (context + data + partial + #each + #if + customHelper)

4 |
5 |
6 |
hello: {{ world}}. My backend is {{ @backend }}
7 | {{> _template-partial }} 8 |
9 |
    10 | {{#each items}} 11 |
  • 12 | {{ customHelper @index }} {{ this }} {{#if @first}}(first){{/if}} {{#if @last}}(last){{/if}} 13 |
  • 14 | {{/each}} 15 |
16 |
17 |
18 |
19 | THIS MUST NOT BE GENERATED BECAUSE THE PARENT IS MARKED AS A SKIP BLOCK (skipBlockAttributeMarker='data-partial-id') AND emptySkipBlocks = true 20 |
21 |
22 |
-------------------------------------------------------------------------------- /examples/basic/templates/template-6.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

template-6 (context + data + partial + #each + #if + customHelper + customBlockHelper)

4 |
5 |
6 |
hello: {{ world}}. My backend is {{ @backend }}
7 | {{> _template-partial }} 8 |
9 |
    10 | {{#each items}} 11 |
  • 12 | {{ customHelper @index }} {{ this }} {{#if @first}}(first){{/if}} {{#if @last}}(last){{/if}} 13 |
  • 14 | {{/each}} 15 |
16 |
17 |
18 |
19 | THIS MUST NOT BE GENERATED BECAUSE THE PARENT IS MARKED AS A SKIP BLOCK (skipBlockAttributeMarker='data-partial-id') AND emptySkipBlocks = true 20 |
21 |
22 |
-------------------------------------------------------------------------------- /examples/lib/precompile.hbs: -------------------------------------------------------------------------------- 1 | (function(Hbs) { 2 | 3 | // Precompiled templates are collected in Handlebars.template 4 | Hbs.templates = Hbs.templates || {}; 5 | 6 | // Handlebars.template override. Required so the backend type becomes 7 | // available to templates and helpers as {{ @backend }} and options.data.backend 8 | 9 | var original_template = Hbs.template; 10 | Hbs.template = function template_instrumented(spec) { 11 | var templateFn = original_template(spec); 12 | if(templateFn) { 13 | var fn = templateFn, backend = spec.compiler[2] || "html"; 14 | templateFn = function(context, options) { 15 | options = options || {}; 16 | options.data = options.data || {}; 17 | options.data.backend = backend; 18 | return fn(context, options); 19 | }; 20 | // Backend also available as a property of the template function 21 | templateFn.backend = backend; 22 | } 23 | return templateFn; 24 | }; 25 | 26 | {{#if statics.__name }} 27 | // Generated statics block 28 | 29 | var {{ statics.__name }} = { 30 | {{#each statics}} 31 | {{ @key }} : {{{stringify this }}}, 32 | {{/each}} 33 | }; 34 | {{/if}} 35 | 36 | {{#if functionMap}} 37 | // Generated function map 38 | {{#each functionMap}} 39 | {{#if @first}}var {{^}} {{/if}}{{ this}} = {{mapOpcode @key ../backend}}{{#if @last}};{{else}}, {{/if}} 40 | {{/each}} 41 | {{/if}} 42 | 43 | {{#if templates}} 44 | 45 | // ================== Generated precompiled templates ================== 46 | 47 | {{#each templates}} 48 | /* ------------------ template [ {{ @key }} ] ------------------ */ 49 | Hbs.templates["{{ @key }}"] = Hbs.template({{{ this }}}); 50 | 51 | {{/each}} 52 | {{else}} 53 | /* ------------------ [ NO TEMPLATES ] ------------------ */ 54 | {{/if}} 55 | 56 | {{#if partials.length}} 57 | // ================== Auto-register partials ================== 58 | {{#each partials}} 59 | Hbs.registerPartial("{{ this }}", Hbs.templates["{{ this }}"] ); 60 | {{/each}} 61 | {{/if}} 62 | 63 | })(window.Handlebars); -------------------------------------------------------------------------------- /examples/lib/precompile.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore')._, 2 | fs = require('fs'), 3 | path = require('path'), 4 | Handlebars = require('../../src/handlebars'); 5 | 6 | var DEFAULT_HANDLEBARS_OPTIONS = {}; 7 | 8 | var DEFAULT_TRANSPILER_OPTIONS = { 9 | debug : false, 10 | minifyInput : true, // OPTIONAL (true): minify input removing whitespaces and carriage returns 11 | backend : 'idom', // REQUIRED: Suppoorted backends: idom, html (to use default Handlebars) 12 | functionMap : { // OPTIONAL: What function names should be generated for the various opcodes for this backend (see shared/opcodes.js). 13 | 'elementOpen' : '_o', 14 | 'elementVoid' : '_v', 15 | 'elementClose' : '_c', 16 | 'text' : '_t', 17 | 'elementOpenStart' : '_os', 18 | 'elementOpenEnd' : '_oe', 19 | 'attr' : '_at', 20 | 'skip' : '_s' 21 | }, 22 | hoistedStatics : {}, // OPTIONAL (undefined). An object that will hold hoisted static string references (falsy value to disable) 23 | generateKeysForStaticEl : false, // OPTIONAL (false). Whether keys should be auto-generated for elements with only static properties (not recommended) 24 | generateKeysForAllEl : true, // OPTIONAL (true). Whether keys should be auto-generated for ALL elements (recommended, takes precedence over generateKeysForStaticEl) 25 | skipBlockAttributeMarker : 'data-partial-id', // OPTIONAL (data-partial-id). The attribute marker for elements that need to generate a 'skip' instruction (falsy to disable) 26 | emptySkipBlocks : true, // OPTIONAL (true). Whether instructions within skip blocks should be ignored / not be generated 27 | safeMergeSelfClosing : true, // OPTIONAL (true). Whether it is safe to merge open / close on ALL tags (true because this is safe with idom backends) 28 | }; 29 | 30 | var DEFAULT_BUILD_OPTIONS = { 31 | rootPath : process.cwd(), // Absolute path or the root folder from where src and dest are resolved 32 | srcDir : 'templates', // Folder where template sources live, relative to rootPath. This is not recursive. 33 | destDir : 'scripts', // Name of the output file script, generated in the destDir folder 34 | inputFileNames : undefined, // Array of files to precompile. Leave undefined to precompile all templates in srcPath 35 | outputFileName : 'templates.js', // Name of the output file script 36 | partialPrefix : '_', // String prefix to identify template partials (for auto-registration). Set to undefined to disable partials auto-registration. 37 | handlebarsOptions : DEFAULT_HANDLEBARS_OPTIONS, // Handlebars options. Provide your own if DEFAULT_HANDLEBARS_OPTIONS (empty by default) is not what you want. 38 | transpilerOptions : DEFAULT_TRANSPILER_OPTIONS, // Transpiler options. Provide your own if DEFAULT_TRANSPILER_OPTIONS is not what you want. 39 | } 40 | 41 | // Get all (valid) template paths in srcDir (or specified in inputFileNames) 42 | var getTemplatePaths = function(options, callback) { 43 | var inputFileNames = options.inputFileNames; 44 | var templatesPath = path.resolve(options.rootPath, options.srcDir); 45 | var templatesPaths = []; 46 | if(inputFileNames) { 47 | templatesPaths = inputFileNames.map(function(fileName) { 48 | return path.resolve(templatesPath, fileName); 49 | }); 50 | callback(null, templatesPaths); 51 | } else { 52 | fs.readdir(templatesPath, function(err, entries) { 53 | if(err) callback(err); 54 | else { 55 | // Done callback invoked after all templates have been compiled 56 | var done = _.after(entries.length, function() { 57 | callback(null, templatesPaths); 58 | }); 59 | // Loop through all template file names 60 | entries.forEach(function(entry) { 61 | var filePath = path.resolve(templatesPath, entry); 62 | fs.stat(filePath, function(err, stats) { 63 | if(err) callback(err); 64 | else { 65 | if(stats.isFile()) templatesPaths.push(filePath); 66 | done(); 67 | } 68 | }); 69 | }); 70 | } 71 | }); 72 | } 73 | } 74 | 75 | // Precompile all templates in rootPath/srcDir. callback is invoked with an object containing 76 | var precompileTemplates = function(options, callback) { 77 | var templatesPath = path.resolve(options.rootPath, options.srcDir); 78 | getTemplatePaths(options, function(err, templatesPaths) { 79 | if(err) callback(err); 80 | else if(templatesPaths.length === 0) { 81 | callback(null, [], []); 82 | } 83 | else { 84 | var templates = {}, partials = []; 85 | var precompileOptions = _.chain(options.handlebarsOptions).clone().extend({ transpilerOptions : options.transpilerOptions }).value(); 86 | // Done callback invoked after all templates have been compiled 87 | var done = _.after(templatesPaths.length, function() { 88 | callback(null, templates, partials); 89 | }); 90 | templatesPaths.forEach(function(templatePath) { 91 | fs.readFile(templatePath, function(err, fileData) { 92 | if(err) callback(err); 93 | else { 94 | try { 95 | console.log('> precompiling', templatePath); 96 | var precompiledData = Handlebars.precompile(fileData.toString(), precompileOptions); 97 | var templateName = templatePath.match(/([^/]*)\./)[1]; 98 | templates[templateName] = precompiledData; 99 | if(templateName.indexOf('_') === 0) { 100 | partials.push(templateName); 101 | } 102 | done(); 103 | } catch(e) { 104 | callback(e); 105 | } 106 | } 107 | }); 108 | }); 109 | } 110 | }); 111 | } 112 | 113 | // We use Handlebars to generate the body of the script file containing all precompiled templates and statics 114 | // For convenience this also adds the `Handlebars.template` override as in src/handlebars/instrumented-runtime.js 115 | // Templates are looked up as `Handlebars.templates[templateName]`. See precompile.hbs. 116 | var compileScriptTemplate = function(templates, partials, options, callback) { 117 | fs.readFile(path.resolve(__dirname, 'precompile.hbs'), function(err, outputTemplate) { 118 | if(err) callback(err); 119 | else { 120 | var transpilerOptions = options.transpilerOptions; 121 | var outputFn = Handlebars.compile(outputTemplate.toString()); 122 | var outputString = outputFn({ 123 | templates : templates, 124 | partials : partials, 125 | backend : transpilerOptions.backend, 126 | statics : transpilerOptions.hoistedStatics, 127 | functionMap : transpilerOptions.functionMap 128 | }, { 129 | helpers : { 130 | stringify : function(o) { return typeof o === 'string' ? '"' + o + '"' : JSON.stringify(o); }, 131 | mapOpcode : function(opcode, backend, options) { 132 | if(backend === 'idom') { 133 | return 'IncrementalDOM.' + opcode; 134 | } else { 135 | return opcode; 136 | } 137 | } 138 | } 139 | }); 140 | callback(null, outputString); 141 | } 142 | }); 143 | } 144 | 145 | var writeScriptFile = function(outputString, options, callback) { 146 | var destDirPath = path.resolve(options.rootPath, options.destDir); 147 | var writeTemplateScript = function(err) { 148 | if(err) console.error(err); 149 | else { 150 | var outputFilePath = path.resolve(destDirPath, options.outputFileName); 151 | console.log('> writing precompiled templates script', outputFilePath); 152 | fs.writeFile(outputFilePath, outputString, callback); 153 | } 154 | } 155 | fs.stat(destDirPath, function(err, stat) { 156 | if(stat) { 157 | writeTemplateScript(); 158 | } else { 159 | fs.mkdir(destDirPath, writeTemplateScript); 160 | } 161 | }); 162 | } 163 | 164 | var precompile = function(options) { 165 | options = _.defaults(options || {}, DEFAULT_BUILD_OPTIONS); 166 | console.log('> rootPath is', options.rootPath); 167 | precompileTemplates(options, function(err, templates, partials) { 168 | if(err) console.error(err); 169 | else { 170 | compileScriptTemplate(templates, partials, options, function(err, outputString) { 171 | if(err) console.error(err); 172 | else { 173 | writeScriptFile(outputString, options, function(err) { 174 | if(err) console.error(err); 175 | else { 176 | console.log('> DONE.') 177 | } 178 | }) 179 | } 180 | }); 181 | } 182 | }); 183 | } 184 | 185 | module.exports = precompile; 186 | -------------------------------------------------------------------------------- /examples/simple/build.js: -------------------------------------------------------------------------------- 1 | require('../lib/precompile')({ 2 | rootPath : __dirname, // Absolute path or the root folder from where src and dest are resolved 3 | srcDir : 'templates', // Folder where template sources live, relative to rootPath. This is not recursive. 4 | destDir : 'scripts', // Folder where to put the generated precompileds script, relative to rootPath 5 | outputFileName : 'templates.js', // Name of the output file script, generated in the destDir folder 6 | }); -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | incremental-bars example 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 33 | -------------------------------------------------------------------------------- /examples/simple/scripts/templates.js: -------------------------------------------------------------------------------- 1 | (function(Hbs) { 2 | 3 | // Precompiled templates are collected in Handlebars.template 4 | Hbs.templates = Hbs.templates || {}; 5 | 6 | // Handlebars.template override. Required so the backend type becomes 7 | // available to templates and helpers as html and options.data.backend 8 | 9 | var original_template = Hbs.template; 10 | Hbs.template = function template_instrumented(spec) { 11 | var templateFn = original_template(spec); 12 | if(templateFn) { 13 | var fn = templateFn, backend = spec.compiler[2] || "html"; 14 | templateFn = function(context, options) { 15 | options = options || {}; 16 | options.data = options.data || {}; 17 | options.data.backend = backend; 18 | return fn(context, options); 19 | }; 20 | // Backend also available as a property of the template function 21 | templateFn.backend = backend; 22 | } 23 | return templateFn; 24 | }; 25 | 26 | // Generated statics block 27 | 28 | var __$ = { 29 | __name : "__$", 30 | _S : " ", 31 | _1614377996 : ["class","content"], 32 | }; 33 | 34 | // Generated function map 35 | var _o = IncrementalDOM.elementOpen, 36 | _v = IncrementalDOM.elementVoid, 37 | _c = IncrementalDOM.elementClose, 38 | _t = IncrementalDOM.text, 39 | _os = IncrementalDOM.elementOpenStart, 40 | _oe = IncrementalDOM.elementOpenEnd, 41 | _at = IncrementalDOM.attr, 42 | _s = IncrementalDOM.skip; 43 | 44 | 45 | // ================== Generated precompiled templates ================== 46 | 47 | /* ------------------ template [ template ] ------------------ */ 48 | Hbs.templates["template"] = Hbs.template({"compiler":[7,">= 4.0.0","idom"],"main":function(container,depth0,helpers,partials,data) { 49 | var helper; 50 | 51 | return (_o("div", "idom-2", __$._1614377996) & _t(__$._S) & _o("h3", "idom-3", null) & _t("Simple template") & _c("h3") & _t(__$._S) & _o("div", "idom-4", null) & _t(__$._S) & _o("strong", "idom-5", null) & _t("Hello " 52 | + ((helper = (helper = helpers.world || (depth0 != null ? depth0.world : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"world","hash":{},"data":data}) : helper)) 53 | + "") & _c("strong") & _t(__$._S) & _c("div") & _t(__$._S) & _o("div", "idom-6", null) & _t(__$._S) & _o("p", "idom-7", null) & _t("The simplest template ever!") & _v("br", "idom-8", null) & _t("Feel free to modify the file ") & _o("code", "idom-9", null) & _t("templates/template.html") & _c("code") & _t(" and experiment.") & _c("p") & _t(__$._S) & _o("p", "idom-10", null) & _t("After you modify the template, run again the ") & _o("code", "idom-11", null) & _t("node build.js") & _c("code") & _t(" script and reload the page.") & _c("p") & _t(__$._S) & _c("div") & _t(__$._S) & _c("div") & _t(__$._S) & _v("hr", "idom-12", null)); 54 | },"useData":true}); 55 | 56 | 57 | 58 | })(window.Handlebars); -------------------------------------------------------------------------------- /examples/simple/templates/template.html: -------------------------------------------------------------------------------- 1 |
2 |

Simple template

3 |
4 | Hello {{ world}} 5 |
6 |
7 |

The simplest template ever!
Feel free to modify the file templates/template.html and experiment.

8 |

After you modify the template, run again the node build.js script and reload the page.

9 |
10 |
11 |
12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "incremental-bars", 3 | "description": "incremental-dom backend support for handlebars templates", 4 | "version": "1.0.6", 5 | "keywords": [ 6 | "handlebars", 7 | "mustache", 8 | "template", 9 | "html", 10 | "incremental-dom" 11 | ], 12 | "maintainers": [ 13 | { 14 | "name": "Davide Mancuso", 15 | "email": "dm@ooz.ch", 16 | "web": "http://ooz.ch" 17 | } 18 | ], 19 | "bugs": { 20 | "web": "https://github.com/atomictag/incremental-bars/issues" 21 | }, 22 | "repository": [ 23 | { 24 | "type": "git", 25 | "url": "https://github.com/atomictag/incremental-bars.git" 26 | } 27 | ], 28 | "license": "MIT", 29 | "homepage": "https://github.com/atomictag/incremental-bars", 30 | "main" : "src/index.js", 31 | "files" : [ 32 | "src", 33 | "examples" 34 | ], 35 | "dependencies": { 36 | "handlebars": ">=4.3.0", 37 | "html-minifier": "^3.5.2", 38 | "htmlparser2" : "^3.9.2", 39 | "underscore" : "^1.8.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/handlebars/index.js: -------------------------------------------------------------------------------- 1 | var Handlebars = require('handlebars'); 2 | Handlebars = require('./instrumented-compiler')(Handlebars); 3 | module.exports = Handlebars; -------------------------------------------------------------------------------- /src/handlebars/instrumented-compiler.js: -------------------------------------------------------------------------------- 1 | var TemplateTranspiler = require('../transpiler'); 2 | var Runtime = require('./instrumented-runtime'); 3 | var DEFAULT_BACKEND = Runtime.DEFAULT_BACKEND; 4 | 5 | module.exports = function(Handlebars) { 6 | 7 | if(!Handlebars || typeof Handlebars.compile !== 'function') { 8 | throw new Error('Invalid Handlebars') 9 | } 10 | 11 | // Instrument Runtime part of Handlebars 12 | Handlebars = Runtime(Handlebars); 13 | 14 | // Original compiler 15 | var JavaScriptCompilerDefault = Handlebars.JavaScriptCompiler; 16 | 17 | // Constructor & prototype 18 | var JavaScriptCompilerInstrumented = function JavaScriptCompilerInstrumented() {}; 19 | JavaScriptCompilerInstrumented.prototype = new JavaScriptCompilerDefault(); 20 | JavaScriptCompilerInstrumented.prototype.constructor = JavaScriptCompilerInstrumented; 21 | 22 | // Multiple compilers are instantiated during the precompilation process via this.compiler(). 23 | JavaScriptCompilerInstrumented.prototype.compiler = JavaScriptCompilerInstrumented; 24 | 25 | // OVERRIDE: compile 26 | JavaScriptCompilerInstrumented.prototype.compile = function compile_instrumented(environment, options, context, asObject) { 27 | options = options || {}; 28 | if(options.backend == null) options.backend = DEFAULT_BACKEND; 29 | return JavaScriptCompilerDefault.prototype.compile.call(this, environment, options, context, asObject); 30 | }; 31 | // OVERRIDE: pushSource 32 | JavaScriptCompilerInstrumented.prototype.pushSource = function pushSource_instrumented(source) { 33 | if(this.options.backend != null && this.options.backend !== DEFAULT_BACKEND) { 34 | if (this.pendingContent) { 35 | // Push source without quoting 36 | this.source.push(this.appendToBuffer(this.pendingContent, this.pendingLocation)); 37 | this.pendingContent = undefined; 38 | } 39 | if (source) { 40 | this.source.push(source); 41 | } 42 | } else { 43 | JavaScriptCompilerDefault.prototype.pushSource.call(this, source); 44 | } 45 | }; 46 | // OVERRIDE: appendEscaped. This is technically not required since you can pass the option `noEscape` to Handlebars itself. 47 | // However this may be be obvious (see Issue #4 https://github.com/atomictag/incremental-bars/issues/4) 48 | JavaScriptCompilerInstrumented.prototype.appendEscaped = function appendEscaped_instrumented() { 49 | if(this.options.backend != null && this.options.backend !== DEFAULT_BACKEND) { 50 | this.pushSource(this.appendToBuffer([ this.popStack() ])); 51 | } else { 52 | JavaScriptCompilerDefault.prototype.appendEscaped.call(this); 53 | } 54 | }; 55 | // OVERRIDE: compilerInfo 56 | JavaScriptCompilerInstrumented.prototype.compilerInfo = function compilerInfo() { 57 | var revision = Handlebars.COMPILER_REVISION, 58 | versions = Handlebars.REVISION_CHANGES[revision]; 59 | // Push backend type as 3rd argument of the compiler info. 60 | // This is accessibile as Handlebars.compile(spec).compiler[2] 61 | // or in a precompiled template BEFORE the spec is passed to Handlebars.template 62 | return [revision, versions].concat(this.options.backend || DEFAULT_BACKEND); 63 | // ***** TODO: ensure this.options.backend is ALWAYS SET 64 | }; 65 | Handlebars.JavaScriptCompiler = JavaScriptCompilerInstrumented; 66 | 67 | // ==== COMPILE & PRECOMPILE 68 | 69 | var compile = Handlebars.compile, 70 | precompile = Handlebars.precompile; 71 | 72 | Handlebars.compile = function compile_instrumented (input, options) { 73 | options = options || {}; 74 | 75 | var transpilerOptions = options.transpilerOptions || {}, 76 | backend = transpilerOptions.backend, 77 | output; 78 | 79 | if(TemplateTranspiler.supportsBackend(backend)) { 80 | var transpiler = new TemplateTranspiler(input, transpilerOptions); 81 | transpiler.generate(transpilerOptions, function(error, program) { 82 | if(transpilerOptions.debug) { 83 | console.log('\n ------8<------\n', program.prettyPrint(), '\n ------>8------\n'); 84 | } 85 | options.backend = backend; 86 | output = compile(program.toString(), options); 87 | }); 88 | } else { 89 | output = compile(input, options); 90 | } 91 | return output; 92 | } 93 | 94 | Handlebars.precompile = function precompile_instrumented (input, options) { 95 | options = options || {}; 96 | 97 | var transpilerOptions = options.transpilerOptions || {}, 98 | backend = transpilerOptions.backend, 99 | output; 100 | 101 | if(TemplateTranspiler.supportsBackend(backend)) { 102 | var transpiler = new TemplateTranspiler(input, transpilerOptions); 103 | transpiler.generate(transpilerOptions, function(error, program) { 104 | if(transpilerOptions.debug) { 105 | console.log('\n ------8<------\n', program.prettyPrint(), '\n ------>8------\n'); 106 | } 107 | options.backend = backend; 108 | output = precompile(program.toString(), options); 109 | }); 110 | } else { 111 | output = precompile(input, options); 112 | } 113 | return output; 114 | } 115 | 116 | return Handlebars; 117 | }; -------------------------------------------------------------------------------- /src/handlebars/instrumented-runtime.js: -------------------------------------------------------------------------------- 1 | var DEFAULT_BACKEND = 'html'; 2 | var Runtime = function(Handlebars) { 3 | if(!Handlebars || typeof Handlebars.template !== 'function') { 4 | throw new Error('Invalid Handlebars') 5 | } 6 | var original_template = Handlebars.template; 7 | Handlebars.template = function template_instrumented(spec) { 8 | var templateFn = original_template(spec); 9 | if(templateFn) { 10 | var fn = templateFn, backend = spec.compiler[2] || DEFAULT_BACKEND; 11 | // Ensure data.backend is always set 12 | templateFn = function(context, options) { 13 | options = options || {}; 14 | options.data = options.data || {}; 15 | options.data.backend = backend; 16 | return fn(context, options); 17 | }; 18 | // Also set backend on the template function itself 19 | templateFn.backend = backend; 20 | } 21 | return templateFn; 22 | } 23 | return Handlebars; 24 | }; 25 | Runtime.DEFAULT_BACKEND = DEFAULT_BACKEND; 26 | module.exports = Runtime; -------------------------------------------------------------------------------- /src/handlebars/runtime.js: -------------------------------------------------------------------------------- 1 | var Handlebars = require('handlebars'); 2 | Handlebars = require('./instrumented-runtime')(Handlebars); 3 | module.exports = Handlebars; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Get full instrumented version of Handlebars 2 | module.exports = require('./handlebars'); -------------------------------------------------------------------------------- /src/transpiler/attributes-collector.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore')._; 2 | var Constants = require('./shared/constants'); 3 | 4 | var BLOCK_QUALIFIER = 'block'; 5 | var ATTRIBUTE_QUALIFIER = 'attribute'; 6 | 7 | var AttributesCollector = function(parseOptions) { 8 | this.key = null; 9 | this.bcount = 0; 10 | this.hasBlocks = false; 11 | this.staticAttrs = []; 12 | this.dynAttrs = []; 13 | this.hasSkipBlockAttribute = false; 14 | this.skipBlockAttributeMarker = Array.isArray(parseOptions.skipBlockAttributeMarker) ? parseOptions.skipBlockAttributeMarker : [parseOptions.skipBlockAttributeMarker]; 15 | }; 16 | AttributesCollector.prototype.pushPair = function(key, value) { 17 | var hasMoustache = key.indexOf(Constants.moustachePlaceholderPrefix) !== -1 || value.indexOf(Constants.moustachePlaceholderPrefix) !== -1; 18 | var isStatic = (!hasMoustache && this.bcount === 0); 19 | var qualifier = ATTRIBUTE_QUALIFIER; 20 | // skip block attribute can be static or dynamic 21 | if(this.skipBlockAttributeMarker.indexOf(key) !== -1) { 22 | this.hasSkipBlockAttribute = true; 23 | if(this.bcount > 0) { 24 | throw new Error('Unsupperted skip block ' + key + (value ? '=' + value : '' ) + ' inside a control block'); 25 | } 26 | } 27 | // Keys can be dynamic but not within blocks 28 | if(key === 'key') { 29 | this.key = value; 30 | if(this.bcount > 0) { 31 | throw new Error('Unsupperted key=' + value + ' inside a control block'); 32 | } 33 | } 34 | if(isStatic) { 35 | this.staticAttrs.push(key, value, qualifier, false, this.bcount); 36 | } else { 37 | var isBlock = false; 38 | if(hasMoustache && key.indexOf(Constants.moustachePlaceholderBlockPrefix) !== -1) { 39 | isBlock = true; 40 | this.hasBlocks = true; 41 | // TODO => sanity check value="" 42 | if(key.indexOf(Constants.moustachePlaceholderBlockStartPrefix) !== -1) { 43 | ++this.bcount; 44 | } else if(key.indexOf(Constants.moustachePlaceholderBlockEndPrefix) !== -1) { 45 | --this.bcount; 46 | } 47 | } 48 | this.hasBlocks = this.hasBlocks || isBlock; 49 | var qualifier = isBlock ? BLOCK_QUALIFIER : qualifier; 50 | this.dynAttrs.push(key, value, qualifier, hasMoustache, this.bcount); 51 | } 52 | }; 53 | AttributesCollector.prototype.getBcount = function() { 54 | return this.bcount; 55 | }; 56 | AttributesCollector.prototype.getKey = function() { 57 | return this.key; 58 | }; 59 | AttributesCollector.prototype.hasSkipBlock = function() { 60 | return this.hasSkipBlockAttribute; 61 | }; 62 | AttributesCollector.prototype.getStaticAttrsList = function() { 63 | return _.chain(this.staticAttrs) 64 | .groupBy(function(element, index){ 65 | return Math.floor(index/5); 66 | }) 67 | .toArray() 68 | .value(); 69 | }; 70 | AttributesCollector.prototype.getDynamicAttrsList = function() { 71 | var list = _.chain(this.dynAttrs) 72 | .groupBy(function(element, index){ 73 | return Math.floor(index/5); 74 | }) 75 | .toArray() 76 | .value(); 77 | var hasBlocks = this.hasBlocks; 78 | list.hasBlocks = function() { return hasBlocks; }; 79 | return list; 80 | }; 81 | AttributesCollector.prototype.dump = function() { 82 | console.log('staticAttrs:', JSON.stringify(this.getStaticAttrsList(), undefined, 2)); 83 | console.log('dynAttrs:', JSON.stringify(this.getDynamicAttrsList(), undefined, 2)); 84 | console.log('hasBlocks:', JSON.stringify(this.getDynamicAttrsList().hasBlocks(), undefined, 2)); 85 | console.log('key:', this.getKey()); 86 | console.log('hasSkipBlock:', this.hasSkipBlock()); 87 | console.log('bcount:', this.getBcount()); 88 | }; 89 | AttributesCollector.BLOCK_QUALIFIER = BLOCK_QUALIFIER; 90 | AttributesCollector.ATTRIBUTE_QUALIFIER = ATTRIBUTE_QUALIFIER; 91 | 92 | module.exports = AttributesCollector; 93 | -------------------------------------------------------------------------------- /src/transpiler/backends/idom.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore')._; 2 | 3 | var Utils = require('../shared/utils'); 4 | var Opcodes = require('../shared/opcodes'); 5 | 6 | // Mapping opcodes -> emitted functions. Overridable with options.functionMap 7 | var DEFAULT_FUNCTION_MAP = {}; 8 | DEFAULT_FUNCTION_MAP[Opcodes.ELEMENT_OPEN] = 'IncrementalDOM.elementOpen', 9 | DEFAULT_FUNCTION_MAP[Opcodes.ELEMENT_OPEN_START] = 'IncrementalDOM.elementOpenStart', 10 | DEFAULT_FUNCTION_MAP[Opcodes.ELEMENT_OPEN_END] = 'IncrementalDOM.elementOpenEnd', 11 | DEFAULT_FUNCTION_MAP[Opcodes.ELEMENT_VOID] = 'IncrementalDOM.elementVoid', 12 | DEFAULT_FUNCTION_MAP[Opcodes.ELEMENT_CLOSE] = 'IncrementalDOM.elementClose', 13 | DEFAULT_FUNCTION_MAP[Opcodes.TEXT] = 'IncrementalDOM.text', 14 | DEFAULT_FUNCTION_MAP[Opcodes.ATTRIBUTES] = 'IncrementalDOM.attr', 15 | DEFAULT_FUNCTION_MAP[Opcodes.SKIP] = 'IncrementalDOM.skip'; 16 | 17 | // Mapping opcodes -> emitter methods 18 | var METHOD_MAP = {}; 19 | METHOD_MAP[Opcodes.ELEMENT_OPEN] = 'elementOpen', 20 | METHOD_MAP[Opcodes.ELEMENT_OPEN_START] = 'elementOpenStart', 21 | METHOD_MAP[Opcodes.ELEMENT_OPEN_END] = 'elementOpenEnd', 22 | METHOD_MAP[Opcodes.ELEMENT_VOID] = 'elementVoid', 23 | METHOD_MAP[Opcodes.ELEMENT_CLOSE] = 'elementClose', 24 | METHOD_MAP[Opcodes.TEXT] = 'text', 25 | METHOD_MAP[Opcodes.ATTRIBUTES] = 'attr', 26 | METHOD_MAP[Opcodes.SKIP] = 'skip', 27 | METHOD_MAP[Opcodes.CONTROL] = 'control'; 28 | 29 | var IDomEmitter = module.exports = function(options) { 30 | this.options = _.defaults(options || {}, { 31 | hoistedStatics : false, // An object that will hold hoisted static string references 32 | generateKeysForStaticEl : false, // Whether keys should be auto-generated for elements with only static properties 33 | generateKeysForAllEl : true, // Whether keys should be auto-generated for ALL elements 34 | staticsName : '__$', // Name of the hoistedStatics object variable 35 | }); 36 | }; 37 | _.extend(IDomEmitter.prototype, { 38 | functionMap : DEFAULT_FUNCTION_MAP, 39 | formatAttrs : function(attrs, compact, trim) { 40 | if(compact) attrs = _.compact(attrs); 41 | return _.map(attrs, function(attr) { 42 | if(attr == undefined) { 43 | return 'null'; 44 | } else if(_.isArray(attr)) { 45 | return '[' + this.formatAttrs(attr) + ']'; 46 | } else { 47 | if(trim) attr = attr.trim(); 48 | return attr; 49 | } 50 | }, this).join(', '); 51 | }, 52 | opcodeToFn : function(opcode) { 53 | return (this.options.functionMap && this.options.functionMap[opcode]) || this.functionMap[opcode]; 54 | }, 55 | generate : function(desc, compact, trim) { 56 | return this.opcodeToFn(desc.opcode) + '(' + this.formatAttrs(desc.attrs, compact, trim) + ')'; 57 | }, 58 | // === BACKEND OPCODES 59 | elementOpen : function(desc) { 60 | return this.generate(desc, false, true); 61 | }, 62 | elementVoid : function(desc) { 63 | return this.generate(desc, false, true); 64 | }, 65 | elementOpenStart : function(desc) { 66 | return this.generate(desc, false, true); 67 | }, 68 | attr : function(desc) { 69 | return this.generate(desc, false, true); 70 | }, 71 | elementOpenEnd : function(desc) { 72 | return this.generate(desc, true, true); 73 | }, 74 | elementClose : function(desc) { 75 | return this.generate(desc, true, true); 76 | }, 77 | skip : function(desc) { 78 | return this.generate(desc, true, true); 79 | }, 80 | text : function(desc) { 81 | return this.generate(desc, true, false); 82 | }, 83 | // === CONTROL OPCODE 84 | control : function(desc) { 85 | return desc.attrs[0].trim(); 86 | }, 87 | // === HOISTING 88 | hoistStatics : function(descriptors) { 89 | var statics = this.options.hoistedStatics;; 90 | if(statics) { 91 | var __name = this.options.staticsName; 92 | var __singleSpaceVar = '_S'; 93 | if(!_.isObject(statics)) { 94 | throw new Error('`hoistedStatics` must be an object!'); 95 | } 96 | if(!statics['__name']) { 97 | statics['__name'] = __name; 98 | statics[__singleSpaceVar] = ' '; 99 | } 100 | _.each(descriptors, function(desc) { 101 | // TODO: good idea? bad idea? 102 | if([ Opcodes.TEXT ].indexOf(desc.opcode) !== -1) { 103 | if(desc.attrs[0] === '" "') { 104 | desc.attrs[0] = __name + '.' + __singleSpaceVar; 105 | } 106 | } else if([ Opcodes.ELEMENT_OPEN, Opcodes.ELEMENT_OPEN_START, Opcodes.ELEMENT_VOID ].indexOf(desc.opcode) !== -1) { 107 | var attrs = desc.attrs, data = desc.attrs[2], key; 108 | if(data) { 109 | // Unquote statics 110 | data = _.map(data, function(prop) { return Utils.trim(Utils.unquotedString(prop), true); }); 111 | 112 | // Lookup duplicates 113 | key = _.findKey(statics, function(props) { 114 | return _.isEqual(props, data); 115 | }); 116 | 117 | // If no key entry found, create one and set data as value 118 | if(!key) { 119 | key = '_' + Utils.hashCode(data); 120 | statics[key] = data; 121 | } 122 | 123 | // Replace the statics array with the variable lookup 124 | desc.attrs[2] = __name + '.' + key; 125 | } 126 | } 127 | }); 128 | } 129 | return statics || {}; 130 | }, 131 | processStatics : function(descriptors) { 132 | var autoGenerateKeysForAllElements = !!this.options.generateKeysForAllEl; 133 | var autoGenerateKeysForStaticsOnly = !autoGenerateKeysForAllElements && !!this.options.generateKeysForStaticEl; 134 | _.each(descriptors, function(desc) { 135 | if([ Opcodes.ELEMENT_OPEN, Opcodes.ELEMENT_OPEN_START, Opcodes.ELEMENT_VOID ].indexOf(desc.opcode) !== -1) { 136 | var attrs = desc.attrs, data = attrs[2], key; 137 | if(data) { 138 | // Format and sort properties. These are effectively pairs and values can be in any order, so the sorting is not mega trivial 139 | data = _.chain(data) 140 | // Unquote and trim all extra spaces. There might be some remote data-* case where spaces matter, but we don't care about those 141 | .map(function(prop) { return Utils.trim(Utils.unquotedString(prop), true); }) 142 | // Group in key-value pairs 143 | .groupBy(function(element, index){ return Math.floor(index/2); }).toArray() 144 | // Reorganize segments in the value based on their hash value 145 | .map(function(pairs) { 146 | // There are some attributes that should not be trivially re-arranged, like style & co. 147 | // Actually this optimisation is probably worth doing only for 'class' attributes anyways.. 148 | if(pairs[0] === 'class') { 149 | return [ pairs[0], _.chain(pairs[1].split(' ')).sortBy(function(seg) { return Utils.hashCode(seg); }).value().join(' ')]; 150 | } else { 151 | return [ pairs[0], pairs[1]] 152 | } 153 | }) 154 | // Sorting of pairs based on their hash value 155 | .sortBy(function(pairs) { return Utils.hashCode(pairs); }) 156 | // Flatten 157 | .flatten().value(); 158 | 159 | // NOTE: at this point the properties are still unquoted. 160 | 161 | // Should keys be auto-generated where possible? 162 | if(autoGenerateKeysForStaticsOnly) { 163 | // > http://google.github.io/incremental-dom/#rendering-dom/statics-array 164 | // `If the statics array is provided, you must also provide a key. 165 | // This ensures that an Element with the same tag but different 166 | // statics array is never re-used by Incremental DOM.` 167 | if(!attrs[1] && _.compact(desc.attrs.slice(3)).length === 0) { 168 | attrs[1] = Utils.quotedString(Utils.hashCode(data) + Utils.uniqueId('-')); 169 | } 170 | } 171 | 172 | // Apply new data 173 | attrs[2] = _.map(data, function(item) { return Utils.quotedString(item); }); 174 | } 175 | 176 | // Should keys be auto-generated for all elements? 177 | if(autoGenerateKeysForAllElements) { 178 | if(!attrs[1]) { 179 | attrs[1] = Utils.quotedString(Utils.uniqueId('idom-')); 180 | } 181 | } 182 | // > If this element is within a skip block (= view partial) it MUST have a key set 183 | // otherwise duplications will occur. This will never happen if emptySkipBlocks=true 184 | else if(!attrs[1] && desc.skipLevel > 0) { 185 | attrs[1] = Utils.quotedString(Utils.uniqueId('idom-')); 186 | } 187 | } 188 | }); 189 | return descriptors; 190 | }, 191 | hasDecorators : function(descriptors) { 192 | return !! _.chain(descriptors).where({ opcode : Opcodes.CONTROL}).find(function(desc) { 193 | var attr = desc.attrs[0]; 194 | return attr.match(/\{\{\s*#?\*/); 195 | }).value(); 196 | }, 197 | // ( + + ) 198 | emit : function(descriptors) { 199 | var GROUP_SEPARATOR = ' & '; // ' , ' is also possible but it pushes too much stuff on the stack 200 | var GROUP_CLOSING = ')'; // ', "")'; 201 | var emitted = '', 202 | prevIsControl = false, 203 | open = false, 204 | concatenateFirstGroup = false, 205 | statics; 206 | // 207 | descriptors = this.processStatics(descriptors); 208 | statics = this.hoistStatics(descriptors); 209 | /* 210 | In some cases with decorators it is necessary to join our output with a generated string. 211 | Specifically a case like this: 212 |
AAA
213 | {{* myDecorator 1 }} 214 |
BBB
215 | fails unless concatenateFirstGroup=true 216 | {{* myDecorator 1 }} 217 |
AAA
218 |
BBB
219 | works no problem, though 220 | */ 221 | concatenateFirstGroup = concatenateFirstGroup || this.hasDecorators(descriptors); 222 | // 223 | _.each(descriptors, function(desc, i) { 224 | var method = METHOD_MAP[desc.opcode], 225 | line = desc.emitted = this[method](desc), 226 | opcode = desc.opcode, 227 | isControl = opcode === Opcodes.CONTROL, 228 | join = (i > 0) && !prevIsControl && !isControl; 229 | 230 | // Control instruction. If a block is open, close it 231 | if(isControl && open) { 232 | emitted += GROUP_CLOSING 233 | open = false; 234 | } 235 | // Non-control instruction. If a block is closed, open it 236 | else if(!isControl && !open) { 237 | // The leading + fixes an issue with decorators 238 | emitted += (concatenateFirstGroup ? ' + ' : '') + '('; 239 | open = true; 240 | } 241 | 242 | // Non-first line or dom instruction not following a control instruction 243 | if(join) { 244 | emitted += GROUP_SEPARATOR + line; 245 | } 246 | // Control instruction 247 | else if(isControl) { 248 | emitted += line; 249 | } 250 | // First line or dom instruction following a control instruction 251 | else { 252 | emitted += line; 253 | } 254 | 255 | // Save the information that the last visited descriptor was a control instruction 256 | prevIsControl = isControl; 257 | }, this); 258 | // Close last block if still open. 259 | if(open) emitted += GROUP_CLOSING; 260 | return { 261 | statics : statics, 262 | toString : function() { 263 | return '' + emitted + ''; 264 | }, 265 | prettyPrint : function() { 266 | return (emitted).split(GROUP_SEPARATOR).join(' \n ' + GROUP_SEPARATOR) 267 | .split('}{').join('}\n{') 268 | .split('}(').join('}(\n ') 269 | .split('){').join(')\n{') 270 | .split('}} +').join('}}\n +') 271 | ; 272 | } 273 | }; 274 | } 275 | }); -------------------------------------------------------------------------------- /src/transpiler/dataset-collector.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore')._; 2 | 3 | var Utils = require('./shared/utils'); 4 | var Opcodes = require('./shared/opcodes'); 5 | 6 | // ============================================ 7 | // allow datasets around text blocks instead of collecting them in the parent? 8 | var ALLOW_TEXT_DATASETS = true; 9 | // ============================================ 10 | 11 | // DatasetCollector 12 | var DatasetCollector = function(level, elementOpen, openIndex) { 13 | this.level = level; 14 | this.children = []; 15 | this.open(elementOpen, openIndex); 16 | } 17 | DatasetCollector.prototype.open = function(elementOpen, openIndex) { 18 | this.elementOpen = elementOpen; 19 | this.levelOpenIndex = openIndex; 20 | this.collectedDatasets = undefined; 21 | this.collect(elementOpen); 22 | }; 23 | DatasetCollector.prototype.purgeChildrenDatasets = function(datasets) { 24 | var parentDatasets = datasets || this.collectedDatasets; 25 | _.each(this.children, function(child) { 26 | var childDatasets = child.collectedDatasets; 27 | Utils.purgeChildDatasets(parentDatasets, childDatasets); 28 | child.purgeChildrenDatasets(parentDatasets); 29 | }); 30 | } 31 | DatasetCollector.prototype.close = function(elementClose, closeIndex) { 32 | this.elementClose = elementClose; 33 | this.levelCloseIndex = closeIndex; 34 | // optimise elementVoid 35 | if(elementClose !== this.elementOpen) { 36 | this.collect(elementClose); 37 | } 38 | // remove datasets entries from children when they are superceded by the parent 39 | this.purgeChildrenDatasets(); 40 | // Compact datasets 41 | Utils.compactDatasets(this.collectedDatasets); 42 | return this.parent; 43 | }; 44 | DatasetCollector.prototype.collect = function(desc) { 45 | this.collectedDatasets = this.collectedDatasets || {}; 46 | this.collectedDatasets = Utils.mergeDatasets(this.collectedDatasets, desc.datasets, false); 47 | } 48 | DatasetCollector.prototype.addChild = function(level, elementOpen, openIndex) { 49 | var child = new DatasetCollector(level, elementOpen, openIndex); 50 | this.children.push(child); 51 | child.parent = this; 52 | return child; 53 | } 54 | DatasetCollector.prototype.dump = function() { 55 | if(!_.isEmpty(this.collectedDatasets)) { 56 | console.log(new Array(this.level * 2).join('\t'),'level',this.level,'would add dataset before', this.levelOpenIndex,'and after', this.levelCloseIndex, Utils.formatDatasets(this.collectedDatasets)); 57 | } else { 58 | console.log(new Array(this.level * 2).join('\t'),'level',this.level,'has no datasets to add'); 59 | } 60 | _.each(this.children, function(child) { child.dump(); }); 61 | }, 62 | DatasetCollector.prototype.getPatch = function() { 63 | var patch = {}; 64 | if(!_.isEmpty(this.collectedDatasets)) { 65 | patch[this.levelOpenIndex] = { datasetOpen : true, datasets : this.collectedDatasets, level : this.level }; 66 | // Handle cases where levelOpenIndex === levelCloseIndex 67 | patch[this.levelCloseIndex] = _.extend(patch[this.levelCloseIndex] || {}, { datasetClose : true }); 68 | } 69 | _.each(this.children, function(child) { 70 | patch = _.extend(patch, child.getPatch()); 71 | }); 72 | return patch; 73 | } 74 | DatasetCollector.process = function(descriptors) { 75 | var currentLevel = 0; 76 | var currentHead = undefined; 77 | var roots = [], patches = {}; 78 | var openOpcodes = [ Opcodes.ELEMENT_OPEN, Opcodes.ELEMENT_OPEN_START, Opcodes.ELEMENT_VOID]; 79 | // allow datasets around text blocks instead of collecting them in the parent? 80 | if(ALLOW_TEXT_DATASETS) { 81 | openOpcodes.push(Opcodes.TEXT); 82 | } 83 | var iterator = function (desc, index) { 84 | var opcode = desc.opcode; 85 | if(openOpcodes.indexOf(opcode) !== -1) { 86 | if(currentLevel === 0) { 87 | currentHead = new DatasetCollector(0, desc, index); 88 | roots.push(currentHead); 89 | ++currentLevel; 90 | } else { 91 | currentHead = currentHead.addChild(currentLevel, desc, index); 92 | ++currentLevel; 93 | } 94 | if(Opcodes.ELEMENT_VOID === opcode || Opcodes.TEXT === opcode ) { 95 | --currentLevel; 96 | currentHead = currentHead.close(desc, index); 97 | } 98 | } else if(Opcodes.ELEMENT_CLOSE === opcode) { 99 | --currentLevel; 100 | currentHead = currentHead.close(desc, index); 101 | } else { 102 | if(!currentHead && !_.isEmpty(desc.datasets)) { 103 | // This is a text or a control outside of a root (i.e. at level 0). 104 | // In the former case (only if 'text' opcodes are not treated as 'openOpcodes') 105 | // we can add a new root and add the descriptor with all text to it. 106 | // In the latter case we would have to find the closing control moustache 107 | // which is not all that trivial and probably useful only for (some) partials. 108 | // For those cases simple changes to the template are sufficient to fix the issue. 109 | if(desc.opcode === Opcodes.CONTROL) { 110 | throw new Error('Control blocks with datasets are not allowed at level 0'); 111 | } 112 | currentHead = new DatasetCollector(0, desc, index); 113 | roots.push(currentHead); 114 | currentHead = currentHead.close(desc, index); 115 | } else if(!_.isEmpty(desc.datasets)) { 116 | // Collect dataset 117 | currentHead.collect(desc); 118 | } 119 | } 120 | } 121 | 122 | // Scan descriptors and create roots 123 | _.each(descriptors, iterator); 124 | 125 | // Accumulate roots into patches 126 | _.each(roots, function(root) { 127 | // root.dump(); 128 | patches = _.extend(patches, root.getPatch()); 129 | }); 130 | 131 | // Return patches 132 | return patches; 133 | }; 134 | 135 | module.exports = { 136 | process : DatasetCollector.process 137 | }; -------------------------------------------------------------------------------- /src/transpiler/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore')._; 2 | var HtmlParser = require('htmlparser2').Parser; 3 | var HtmlMinify = require('html-minifier').minify; 4 | 5 | var Utils = require('./shared/utils'); 6 | var Opcodes = require('./shared/opcodes'); 7 | var Constants = require('./shared/constants'); 8 | var DatasetCollector = require('./dataset-collector'); 9 | var AttributesCollector = require('./attributes-collector'); 10 | 11 | var DEFAULT_OPTIONS = { 12 | minifyInput : true, // Minify input removing whitespaces and carriage returns 13 | skipBlockAttributeMarker : 'data-partial-id', // The attribute marker for elements that need to generate a Opcodes.SKIP instruction 14 | emptySkipBlocks : true, // Whether instructions within skip blocks should not be generated 15 | safeMergeSelfClosing : true, // Whether it is safe to merge open / close on ALL tags 16 | extractDatasets : [ ], // FUTURE FEATURE - DON'T USE 17 | datasetsControlBlockSuffix : 'datasets' // FUTURE FEATURE - DON'T USE 18 | }; 19 | 20 | 21 | var TemplateTranspiler = function(html, options) { 22 | this.html = html; 23 | this.options = _.defaults(options || {}, DEFAULT_OPTIONS); 24 | this.onreset(); 25 | } 26 | 27 | _.extend(TemplateTranspiler.prototype, { 28 | // Brackets matches are {2, 4} to take into account raw helper blocks (the regexps are fixed in patchMoustache) 29 | // Set to true to reuse the same placeholder to moustaches that are identical. Not needed for all practical cases but it's here for experimentation. 30 | // *** MUST BE FALSE otherwise
...
31 | // *** will not compute the attributes correctly. A warning is generated in patchMoustaches to notify this 32 | RECYCLE_MOUSTACHE_PLACEHOLDERS : false, 33 | // htmlparser2 options 34 | PARSER_OPTIONS : { decodeEntities: true, lowerCaseTags: true, recognizeSelfClosing: true, recognizeCDATA : true }, 35 | // Emitters registry (see registerBackend(type, emitter)) 36 | EMITTERS : { }, 37 | 38 | onreset : function() { 39 | this.moustacheMap = {}; 40 | }, 41 | getParser : function() { 42 | return HtmlParser; 43 | }, 44 | makePlaceholder : function(moustache) { 45 | var placeholder, mKey = Utils.uniqueId() + Constants.moustachePlaceholderSuffix; // Add a termination to prevent moustache-1 from matching moustache-10 etc. 46 | if(!!moustache.match(/\{{4}\s*[^\/]/)) { 47 | throw new Error('Raw helpers are not supported (found: ' + moustache + ' )') 48 | } else if(!!moustache.match(Constants.BLOCK_MOUSTACHE_START_RE)) { 49 | placeholder = Constants.moustachePlaceholderBlockStartPrefix; 50 | } else if(!!moustache.match(Constants.BLOCK_MOUSTACHE_ELSE_RE)) { 51 | placeholder = Constants.moustachePlaceholderBlockElsePrefix; 52 | } else if(!!moustache.match(Constants.BLOCK_MOUSTACHE_END_RE)) { 53 | placeholder = Constants.moustachePlaceholderBlockEndPrefix; 54 | } else if(!!moustache.match(Constants.BLOCK_MOUSTACHE_RE)) { 55 | placeholder = Constants.moustachePlaceholderBlockPrefix; 56 | } else { 57 | placeholder = Constants.moustachePlaceholderPrefix; 58 | } 59 | // The replace moustache key, taking care of padding either sides otherwise the HTML parser may fail its tokenization 60 | return ' ' + placeholder + mKey + ' '; 61 | }, 62 | normalizeMoustache : function(moustache) { 63 | // Remove all unneeded spaces 64 | moustache = Utils.trim(moustache, true); 65 | // Aim to compact the format: {{# xxx}} => {{#xxx}} 66 | moustache = moustache.replace(/(#|\/|>|~)\s*/g, '$1').replace(/\{\s*/g, '{').replace(/\s*(?=\})/g, ''); 67 | return moustache; 68 | }, 69 | extractDatasets : function(moustache) { 70 | var dataprefixes = this.options.extractDatasets, datasets = { }; 71 | if(dataprefixes && _.isArray(dataprefixes) && dataprefixes.length) { 72 | _.each(dataprefixes, function(dataprefix) { 73 | var datasetRe = new RegExp( dataprefix + '\\.?([.\\w]*)', 'g'); 74 | var matches = moustache.match(datasetRe); 75 | if(matches && matches.length) { 76 | var slot = []; 77 | _.each(matches, function(match) { 78 | match = match.replace(dataprefix, '').replace(/^\./, ''); 79 | match = match === '' ? '*' : match; 80 | slot.push(match); 81 | }); 82 | // Simple compaction 83 | if(slot.indexOf('*') !== -1) { 84 | slot = ['*']; 85 | } 86 | // Set in datasets 87 | datasets[dataprefix] = slot; 88 | } 89 | }); 90 | } 91 | return datasets; 92 | }, 93 | patchMoustaches : function(html) { 94 | var patchedHtml = html; 95 | var moustaches = html.match(Constants.ANY_MOUSTACHE_RE); 96 | if(moustaches && moustaches.length) { 97 | if(this.RECYCLE_MOUSTACHE_PLACEHOLDERS) { 98 | console.warn('*** RECYCLE_MOUSTACHE_PLACEHOLDERS is enabled. This may produce wrong dynamic attributes in some cases!') 99 | } 100 | _.each(moustaches, function(moustache) { 101 | var placeholder = this.makePlaceholder(moustache); 102 | // Build the replace expression 103 | var replaceExpression = this.RECYCLE_MOUSTACHE_PLACEHOLDERS ? new RegExp(Utils.escapeRegExp(moustache), 'gm') : moustache; 104 | var normalizedMoustache = this.normalizeMoustache(moustache); 105 | patchedHtml = patchedHtml.replace(replaceExpression, placeholder); 106 | this.moustacheMap[placeholder] = { 107 | regexp : new RegExp('\\s?' + placeholder.trim() + '\\s?', 'gi'), 108 | key : placeholder, 109 | value : normalizedMoustache, 110 | isBlock : (placeholder.indexOf(Constants.moustachePlaceholderBlockPrefix) !== -1), 111 | datasets: this.extractDatasets(moustache) 112 | }; 113 | }, this); 114 | } 115 | return patchedHtml; 116 | }, 117 | // Tokenize attribute on moustace boundaries. Double quote all non-moustace content 118 | tokenizeAttribute : function(attr) { 119 | var tokens = attr.split(Constants.ANY_MOUSTACHE_PLACEHOLDER_RE), buffer = ''; 120 | _.each(tokens, function(token) { 121 | if(token.match(Constants.ANY_MOUSTACHE_PLACEHOLDER_RE)) { 122 | buffer += token; 123 | } else { 124 | buffer += Utils.quotedString(token); 125 | } 126 | }, this); 127 | attr = buffer; 128 | return attr; 129 | }, 130 | // Unpatch attributes 131 | unpatchAttributes : function(desc, attrs) { 132 | // TODO OPTIMIZATION: escape attributes only if they are within Opcodes.TEXT instructions? 133 | attrs = attrs || desc.attrs; 134 | _.each(attrs, function(attr, index) { 135 | if(_.isArray(attr)) { 136 | // Apply unpatching in-situ 137 | this.unpatchAttributes(desc, attr); 138 | } else { 139 | // attr can be null or empty 140 | if(attr != undefined) { 141 | var isBlock = false; 142 | var tokenize = (desc.opcode !== Opcodes.CONTROL); 143 | if(tokenize) attr = this.tokenizeAttribute(attr); 144 | _.each(this.moustacheMap, function(moustacheValue, moustacheKey) { 145 | var moustacheKeyRe = moustacheValue.regexp; 146 | if(attr.match(moustacheKeyRe)) { 147 | attr = attr.replace(moustacheKeyRe, moustacheValue.value); 148 | isBlock = isBlock || moustacheValue.isBlock; // Track presence of blocks 149 | desc.datasets = Utils.mergeDatasets(desc.datasets, moustacheValue.datasets, false); 150 | } 151 | }); 152 | // Wrap tokenizable attributes that happen to contain a control block 153 | // in between another dummy control block. This way the HB compiler is forced 154 | // to generate a "program" frame rather than apply `invokeAmbiguous`, 155 | // which generates inline variables in some cases and that is not compatible 156 | // with function-based code generation (because we can't have variables defined inline). 157 | // ***** FIXES
158 | if(isBlock && tokenize) { 159 | attr = '""' + Utils.formatControlOpen('if', 1) + attr + Utils.formatControlClose('if') + '""'; 160 | } 161 | } 162 | attrs.splice(index, 1, attr); 163 | } 164 | }, this); 165 | }, 166 | unpatchMoustaches : function(descriptors) { 167 | var haveDataSets = false; 168 | _.each(descriptors, function(desc) { 169 | this.unpatchAttributes(desc); 170 | haveDataSets = haveDataSets || !_.isEmpty(desc.datasets); 171 | }, this); 172 | descriptors.haveDataSets = haveDataSets; 173 | return descriptors; 174 | }, 175 | // Process datasets 176 | processDataSets : function(descriptors) { 177 | // Short-cut descriptors with no data sets 178 | if(!descriptors.haveDataSets) { 179 | return descriptors; 180 | } 181 | 182 | // Process descriptors to produce patches 183 | var patches = DatasetCollector.process(descriptors); 184 | var patchedDescriptors = []; 185 | var datasetsControlBlockName = '_' + this.options.backend + '_' + this.options.datasetsControlBlockSuffix + '_'; 186 | 187 | // Produce a copy of descriptors interleaving the datasets control blocks 188 | _.each(descriptors, function(desc, index) { 189 | var patch = patches[index]; 190 | if(patch && patch.datasetOpen) { 191 | patchedDescriptors.push({ opcode : Opcodes.CONTROL, attrs : [ Utils.formatControlOpen(datasetsControlBlockName, Utils.quotedString(Utils.uniqueId('ds')) + Utils.formatDatasets(patch.datasets) + ' level=' + patch.level) ]}); 192 | } 193 | patchedDescriptors.push(desc); 194 | if(patch && patch.datasetClose) { 195 | patchedDescriptors.push({ opcode : Opcodes.CONTROL, attrs : [ Utils.formatControlClose(datasetsControlBlockName) ]}); 196 | } 197 | }); 198 | 199 | return patchedDescriptors; 200 | }, 201 | // Merge skip levels 202 | mergeSkipLevels : function(descriptors) { 203 | if(this.options.emptySkipBlocks) { 204 | var skipModeOn = false; 205 | var i = 0; 206 | while(i < descriptors.length - 1) { 207 | var desc = descriptors[i]; 208 | if(desc.opcode === Opcodes.SKIP) { 209 | skipModeOn = true; 210 | } else if(desc.skipLevel === 0) { 211 | skipModeOn = 0; 212 | } else if(skipModeOn) { 213 | descriptors.splice(i, 1); 214 | continue; 215 | } else { 216 | throw new Error('Inconsistent skip level state'); 217 | } 218 | i++; 219 | } 220 | } 221 | return descriptors; 222 | }, 223 | // Merge self closing tags where possible 224 | mergeSelfClosing : function(descriptors) { 225 | var i = 0; 226 | while(i < descriptors.length - 1) { 227 | var desc = descriptors[i]; 228 | if(desc.opcode === Opcodes.ELEMENT_OPEN && (this.options.safeMergeSelfClosing || Utils.isSelfClosingTag(desc.attrs[0]))) { 229 | var next = descriptors[i+1]; 230 | if(next && next.opcode === Opcodes.ELEMENT_CLOSE) { 231 | desc.opcode = Opcodes.ELEMENT_VOID; 232 | descriptors.splice(i+1, 1); 233 | } 234 | } 235 | i++; 236 | } 237 | return descriptors; 238 | }, 239 | // Merge contiguous text nodes 240 | mergeText : function(descriptors) { 241 | var i = 0; 242 | while(i < descriptors.length - 1) { 243 | var desc = descriptors[i]; 244 | if(desc.opcode === Opcodes.TEXT) { 245 | // Peek next descriptor 246 | var nextDesc = descriptors[i+1] 247 | if(nextDesc && nextDesc.opcode === Opcodes.TEXT) { 248 | nextDesc.attrs[0] = desc.attrs[0] + nextDesc.attrs[0]; 249 | descriptors.splice(i, 1); 250 | continue; 251 | } 252 | } 253 | i++; 254 | } 255 | return descriptors; 256 | }, 257 | mergeDescriptors : function(descriptors) { 258 | this.mergeSkipLevels(descriptors); 259 | this.mergeText(descriptors); 260 | return descriptors; 261 | }, 262 | prepareHtml : function() { 263 | if(this.html) { 264 | // Trim spaces (before comments so they can be used as trimmers :) 265 | var html = this.html.replace(Constants.TRIM_LEFT_RE, '$1~').replace(Constants.TRIM_RIGHT_RE, '~$1'); 266 | // Remove comments 267 | html = html.replace(Constants.COMMENT_BLOCK_MOUSTACHE_RE, '').replace(Constants.COMMENT_MOUSTACHE_RE, ''); 268 | // More trimming before and after leading/trailing tags (for noel views) 269 | html = html.replace(/^\s*\s*$/gm,'>'); 270 | if(this.options.minifyInput) { 271 | // Apply HtmlMinify to remove spaces and line breaks (so they don't end up as useless text nodes). 272 | // Note that mustaches inside
 tags may have their spacing messed up because of this, but it's for a good cause :)
273 |                 html = HtmlMinify(html, {
274 |                     collapseWhitespace : true,
275 |                     conservativeCollapse : true,
276 |                     preserveLineBreaks   : false,
277 |                     ignoreCustomFragments : [ /\{\{[\s\S]*?\}\}/ ]
278 |                 });
279 |             }
280 |             // Patch moustaches. This also normalizes them
281 |             html = this.patchMoustaches(html);
282 |             // Normalize moustaches enclosed used in tags, so they can be used as <{{ tag }}> ... <{{ /tag }}>
283 |             html = html.replace(new RegExp('(<\\/?)\\s*(' + Constants.moustachePlaceholderPrefix + '.*?' + Constants.moustachePlaceholderSuffix + ')\\s*(>)', 'gm'), '$1$2$3');
284 |             // Remove double spaces between a block and a mustache _block_   _value_ => _block_ _value_
285 |             // **** this works but it removes spaces too eagerly
286 |             // html = html.replace(/(moustache\-block.*?\-x)\s*(?=moustache\-.*?\-x)/gi, '$1 ');
287 |             // Remove all spaces between blocks _block_  _block_ => _block__block_
288 |             // **** this works but it compresses attribute names, which results in wrong bcounts
289 |             // html = html.replace(/(moustache\-block.*?\-x)\s*(?=moustache-block\-.*?\-x)/gi, '$1');
290 |             // Remove all spaces between a closing tag and a _block_ => 
_block_ =>
_block_ 291 | html = html.replace(/(<\/.*?>)\s*(?=moustache-block\-.*?\-x)/gi, '$1'); 292 | return html; 293 | } 294 | }, 295 | parse : function(parseOptions, callback) { 296 | // parseOptions.backend MAY contain the hint of the target emitter if invoked by .generate() 297 | // this is currently unused but it could be useful in the future to optimize the parsing for a given target. 298 | parseOptions = _.defaults(parseOptions || {}, this.options); 299 | 300 | var Parser = this.getParser(), 301 | html = this.prepareHtml() || '', 302 | descriptors = [], 303 | state = { skipLevel : 0, nestLevel : 0 }, // Stateful variables during the whole parsing process 304 | backendHint = parseOptions.backend, // UNUSED backend hint 305 | container = this; // Reference to `this` for closures 306 | 307 | // Throw if the Parser is not available 308 | if(!Parser) return callback && callback('Failed to instantiate htmlparser2. Check your dependencies (`npm install htmlparser2` on nodejs).'); 309 | 310 | // Warn if input is not available. Nothing bad will really happen. 311 | if(!html) console.warn('No input to parse.'); 312 | 313 | // Push descriptor from parser callbacks 314 | var pushDescriptor = function(opcode, attrs, state) { 315 | var desc = _.extend({ opcode : opcode, attrs : attrs }, state || {}, { datasets : {} }); 316 | descriptors.push(desc); 317 | } 318 | 319 | // Parse complete. Invoked when parsing completes, 320 | var parseComplete = function(descriptors) { 321 | descriptors = container.mergeDescriptors(descriptors); 322 | descriptors = container.unpatchMoustaches(descriptors); 323 | descriptors = container.processDataSets(descriptors); 324 | // Process self closing tags AFTER processing the data sets! 325 | descriptors = container.mergeSelfClosing(descriptors); 326 | return descriptors; 327 | } 328 | 329 | var attributesCollector = new AttributesCollector(parseOptions); 330 | 331 | var parser = new Parser({ 332 | onattribute : function(key, value) { 333 | //console.log('KEY:', key, 'VALUE:', value) 334 | // TODO: onattribute is called with ALL the attributes (including duplicates!) 335 | // that are found BEFORE onopentag is called. It is not called (as expected) 336 | // if there are no attributes to report. 337 | attributesCollector.pushPair(key, value); 338 | }, 339 | onopentag: function (name /*, attrs */) { 340 | 341 | // Initialize args array 342 | var parsedAttributes = attributesCollector, 343 | argsArray = [name, parsedAttributes.getKey(), null], 344 | staticList = parsedAttributes.getStaticAttrsList(), 345 | dynamicList = parsedAttributes.getDynamicAttrsList(); 346 | 347 | // Reset attributes parser 348 | attributesCollector = new AttributesCollector(parseOptions); 349 | 350 | // Increase nestLevel 351 | ++state.nestLevel; 352 | 353 | // Debug dump 354 | // parsedAttributes.dump(); 355 | if(parsedAttributes.getBcount() > 0) { 356 | parsedAttributes.dump(); 357 | throw new Error('Block count in attributes is not zero'); 358 | } 359 | 360 | // Process statics 361 | if(staticList.length) { 362 | var staticsArray = []; 363 | _.each(staticList, function(entry) { 364 | staticsArray.push(entry[0], entry[1]); 365 | }); 366 | argsArray[2] = staticsArray; 367 | } 368 | 369 | // Process dynamic 370 | if(dynamicList.length) { 371 | if(dynamicList.hasBlocks()) { 372 | pushDescriptor(Opcodes.ELEMENT_OPEN_START, argsArray, { skipLevel : state.skipLevel }); 373 | _.each(dynamicList, function(entry) { 374 | var qualifier = entry[2]; 375 | if(qualifier === AttributesCollector.BLOCK_QUALIFIER) { 376 | pushDescriptor(Opcodes.CONTROL, [ entry[0] ], { skipLevel : state.skipLevel }); 377 | } else if(qualifier === AttributesCollector.ATTRIBUTE_QUALIFIER) { 378 | pushDescriptor(Opcodes.ATTRIBUTES, [ entry[0], entry[1] ], { skipLevel : state.skipLevel }); 379 | } else { 380 | throw new Error('Unsupported qualifier ' + qualifier); 381 | } 382 | }); 383 | pushDescriptor(Opcodes.ELEMENT_OPEN_END, [ name ], { skipLevel : state.skipLevel }); 384 | } else { 385 | var dynamicArray = []; 386 | _.each(dynamicList, function(entry) { 387 | dynamicArray.push(entry[0], entry[1]); 388 | }); 389 | argsArray = argsArray.concat(dynamicArray); 390 | } 391 | } 392 | 393 | // Generate Opcodes.ELEMENT_OPEN if there are no dynamic blocks 394 | if(!dynamicList.hasBlocks()) { 395 | pushDescriptor(Opcodes.ELEMENT_OPEN, argsArray, { skipLevel : state.skipLevel }); 396 | } 397 | 398 | // Set skipLevel to 1 if this element is a partial holder not inside a skip block 399 | if(parsedAttributes.hasSkipBlock() && !state.skipLevel) { 400 | state.skipLevel = 1; 401 | pushDescriptor(Opcodes.SKIP, [], { skipLevel : state.skipLevel }); 402 | } 403 | // If the skip level is set (= this element is inside a skip block), increase the level 404 | else if(state.skipLevel) { 405 | // Increment skip level 406 | ++state.skipLevel; 407 | } 408 | // non-zero skip levels are preserved by 'ontext', decreased by one unit by 'onclosetag', and increased by one unit by further 'onopentag' 409 | 410 | }, 411 | ontext: function(text) { 412 | var segments = text.split(Constants.BLOCK_MOUSTACHE_PLACEHOLDER_RE); 413 | _.each(segments, function(segment) { 414 | // Need to allow empty text segments otherwise "A B" becomes "AB" 415 | if(segment /* && segment.trim() */) { 416 | var match = !!segment.match(Constants.BLOCK_MOUSTACHE_PLACEHOLDER_RE); 417 | var opcode = match ? Opcodes.CONTROL : Opcodes.TEXT; 418 | pushDescriptor(opcode, [ segment ], { skipLevel : state.skipLevel }); 419 | } 420 | }); 421 | }, 422 | onclosetag: function(name) { 423 | // Decrease nestLevel 424 | if(--state.nestLevel < 0 ) { 425 | throw new Error('Inconsistent nestLevel. Some tags were not correctly closed.'); 426 | } 427 | if(state.skipLevel) --state.skipLevel; 428 | pushDescriptor(Opcodes.ELEMENT_CLOSE, [ name ], { skipLevel : state.skipLevel }); 429 | // TODO manage situation where there are more closing tags that opening ones 430 | // (i.e. if multiple open are defined within separate control blocks) 431 | }, 432 | onerror : function(error) { 433 | callback && callback(error); 434 | callback = null; // Prevent further callback invocations 435 | parser.reset(); 436 | }, 437 | onreset : function() { 438 | container.onreset(); 439 | }, 440 | onend : function() { 441 | var error; 442 | if(state.nestLevel !== 0) { 443 | throw new Error('Inconsistent nestLevel at the end of parsing. Some tags were not correctly closed.'); 444 | } 445 | try { 446 | descriptors = parseComplete(descriptors); 447 | } catch(e) { 448 | error = e; 449 | } 450 | callback && callback(error, descriptors); 451 | callback = null; // Prevent further callback invocations 452 | parser.reset(); 453 | } 454 | }, this.PARSER_OPTIONS); 455 | 456 | // RUN PARSER 457 | parser.parseChunk(html); 458 | parser.done(); 459 | }, 460 | generate : function(options, callback) { 461 | var EMITTER = this.EMITTERS[options.backend]; 462 | if(!EMITTER) { 463 | var error = new Error('Emitter "' + options.backend + '" does not exist'); 464 | callback && callback(error); 465 | throw error; 466 | } 467 | this.parse({ backend : options.backend }, function(error, descriptors) { 468 | var program; 469 | if(descriptors && !error) { 470 | try { 471 | program = (new EMITTER(options)).emit(descriptors); 472 | } catch(e) { 473 | error = e; 474 | } 475 | } 476 | if(error) console.error(error); 477 | callback && callback(error, program); 478 | }); 479 | } 480 | }); 481 | 482 | TemplateTranspiler.registerBackend = function(type, emitter) { 483 | TemplateTranspiler.prototype.EMITTERS[type] = emitter; 484 | } 485 | TemplateTranspiler.supportsBackend = function(type) { 486 | return typeof TemplateTranspiler.prototype.EMITTERS[type] === 'function'; 487 | } 488 | 489 | // AUTO-REGISTER BACKEND EMITTERS 490 | var fs = require('fs'); 491 | var path = require('path'); 492 | var emittersPath = path.resolve(__dirname, 'backends'); 493 | var items = fs.readdirSync(emittersPath); 494 | for (var i=0; i|else|\*|\^)(?:\n|\r|.)*?~?\}{2,3})/gm, // match open-close-else moustache 6 | BLOCK_MOUSTACHE_START_RE : /(\{{2,3}~?[\s]*(?:#)(?:\n|\r|.)*?~?\}{2,3})/gm, // match open moustache 7 | BLOCK_MOUSTACHE_ELSE_RE : /(\{{2,3}~?[\s]*(?:else|\^)(?:\n|\r|.)*?~?\}{2,3})/gm, // match else moustache 8 | BLOCK_MOUSTACHE_END_RE : /(\{{2,3}~?[\s]*(?:\/)(?:\n|\r|.)*?~?\}{2,3})/gm, // match close moustache 9 | COMMENT_MOUSTACHE_RE : /(\{{2}~?!(?:.)*?~?\}{2})/g, // match all comments moustache 10 | COMMENT_BLOCK_MOUSTACHE_RE : /(\{{2}~?!(?:--)?(?:\n|\r|.)*?(?:--)~?\}{2})/gm, // match all comments moustache 11 | TRIM_LEFT_RE : /\s*(\{{2,3})\s*~/gm, // {{~ and {{{~ 12 | TRIM_RIGHT_RE : /~\s*(\}{2,3})\s*/gm, // ~}} and ~}}} 13 | // 14 | moustachePlaceholderPrefix : 'moustache-', 15 | moustachePlaceholderBlockPrefix : 'moustache-block-', 16 | moustachePlaceholderBlockStartPrefix : 'moustache-block-start-', 17 | moustachePlaceholderBlockElsePrefix : 'moustache-block-else-', 18 | moustachePlaceholderBlockEndPrefix : 'moustache-block-end-', 19 | moustachePlaceholderSuffix : '-x', 20 | // 21 | ANY_MOUSTACHE_PLACEHOLDER_RE : /(\s?moustache\-.*?\-x\s?)/gi, 22 | BLOCK_MOUSTACHE_PLACEHOLDER_RE : /(\s?moustache\-block.*?-x\s?)/gi 23 | } -------------------------------------------------------------------------------- /src/transpiler/shared/opcodes.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | ELEMENT_OPEN : 'elementOpen', 4 | ELEMENT_OPEN_START : 'elementOpenStart', 5 | ELEMENT_OPEN_END : 'elementOpenEnd', 6 | ELEMENT_CLOSE : 'elementClose', 7 | ELEMENT_VOID : 'elementVoid', 8 | ATTRIBUTES : 'attr', 9 | TEXT : 'text', 10 | SKIP : 'skip', 11 | CONTROL : 'control' 12 | } -------------------------------------------------------------------------------- /src/transpiler/shared/utils.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore')._; 2 | 3 | var Utils = module.exports = { 4 | // Self closing tags 5 | SELF_CLOSING_TAGS : ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'], 6 | // Check if a tag is (potentially) self closing (the descriptor may not be if it has attributes within control blocks) 7 | isSelfClosingTag : function(tag) { 8 | return Utils.SELF_CLOSING_TAGS.indexOf(tag) !== -1; 9 | }, 10 | stringify : function(input) { 11 | return (_.isObject(input) ? JSON.stringify(input) : input) + ''; 12 | }, 13 | quotedString : function(str) { 14 | return '"' + (str + '') 15 | .replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') 16 | .replace(/\r/g, '\\r').replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 17 | .replace(/\u2029/g, '\\u2029') + '"'; 18 | }, 19 | unquotedString : function(str) { 20 | return str && str.replace(/^"|"$/g, ''); 21 | }, 22 | trim : function(str, all) { 23 | return str && (all ? str.replace(/\s+/g,' ').replace(/^\s+|\s+$/g,'') : str).trim(); 24 | }, 25 | escapeRegExp : function(str) { 26 | return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string 27 | }, 28 | hashCode_DJB2 : function(str) { 29 | var hash = 0, i, chr, len; 30 | str = Utils.stringify(str); 31 | if (str.length === 0) return hash; 32 | for (i = 0, len = str.length; i < len; i++) { 33 | chr = str.charCodeAt(i); 34 | hash = ((hash << 5) - hash) + chr; 35 | hash |= 0; // Convert to 32bit integer 36 | } 37 | return hash; 38 | }, 39 | hashCode_FNV_1a : function (str, asString, seed) { 40 | var i, l, hval = (seed === undefined) ? 0x811c9dc5 : seed; 41 | str = Utils.stringify(str); 42 | for (i = 0, l = str.length; i < l; i++) { 43 | hval ^= str.charCodeAt(i); 44 | hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24); 45 | } 46 | if( asString ){ 47 | // Convert to 8 digit hex string 48 | return ("0000000" + (hval >>> 0).toString(16)).substr(-8); 49 | } 50 | return hval >>> 0; 51 | }, 52 | hashCode : function(str) { 53 | // Use hashCode_FNV_1a as default 54 | return Utils.hashCode_FNV_1a(str); 55 | }, 56 | // uninque ID generator 57 | uniqueId : function(prefix) { 58 | return _.uniqueId(prefix); 59 | }, 60 | // merge two datasets 61 | mergeDatasets : function(dest, toMerge, compact) { 62 | _.each(toMerge, function(dataset, key) { 63 | if(dest[key]) { 64 | dest[key] = dest[key].concat(dataset); 65 | } else { 66 | dest[key] = dataset; 67 | } 68 | }); 69 | if(compact) Utils.compactDatasets(dest); 70 | return dest; 71 | }, 72 | // compact a dataset 73 | compactDatasets : function(datasets) { 74 | _.each(datasets, function(dataset, key) { 75 | if(dataset.indexOf('*') !== -1) { 76 | dataset = ['*']; 77 | } else { 78 | // Sort in ascending order 79 | var sorted = _.sortBy(dataset, function(item) { return item.length }); 80 | // Filter out segments that have a matching parent 81 | dataset = _.reduce(sorted, function(memo, item, index) { 82 | var hasParentPath = false; 83 | if(index > 0) { 84 | for(var i=index-1; i >= 0; i--) { 85 | if((item + '.').indexOf(sorted[i] + '.') === 0) { 86 | hasParentPath = true; 87 | break; 88 | } 89 | } 90 | } 91 | if(!hasParentPath) { 92 | memo.push(item); 93 | } 94 | return memo; 95 | }, []); 96 | } 97 | datasets[key] = dataset; 98 | }); 99 | }, 100 | // Purge from a child dataset all entries that exist in the parent 101 | purgeChildDatasets : function(parentDatasets, childDatasets) { 102 | _.each(parentDatasets, function(parentDataset, key) { 103 | var childDataset = childDatasets[key]; 104 | if(childDataset && childDataset.length) { 105 | var purgedDataset = []; 106 | _.each(childDataset, function(item, index) { 107 | var hasParentPath = false; 108 | for(var i = 0; i < parentDataset.length; i++) { 109 | if(parentDataset[i] === '*' || (item + '.').indexOf(parentDataset[i] + '.') === 0) { 110 | hasParentPath = true; 111 | break; 112 | } 113 | } 114 | if(!hasParentPath) { 115 | purgedDataset.push(item); 116 | } 117 | }); 118 | if(purgedDataset.length) { 119 | childDatasets[key] = purgedDataset; 120 | } else { 121 | delete childDatasets[key]; 122 | } 123 | } 124 | }); 125 | }, 126 | formatDatasets : function(datasets) { 127 | var buffer = ''; 128 | _.each(datasets, function(dataset, key) { 129 | buffer += ' ' + key.replace(/^@/, '') + '="' + dataset.join(' ') + '"'; 130 | }); 131 | return buffer; 132 | }, 133 | formatControlOpen : function(name, body) { 134 | body = body ? ' ' + ('' + body).trim() : ''; 135 | return '{{#' + name + body + '}}'; 136 | }, 137 | formatControlClose : function(name) { 138 | return '{{/' + name + '}}'; 139 | }, 140 | } --------------------------------------------------------------------------------