├── .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 |
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 |
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 | [](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 |
9 | THIS MUST NOT BE GENERATED BECAUSE THE PARENT IS MARKED AS A SKIP BLOCK (skipBlockAttributeMarker='data-partial-id') AND emptySkipBlocks = true
10 |
9 | THIS MUST NOT BE GENERATED BECAUSE THE PARENT IS MARKED AS A SKIP BLOCK (skipBlockAttributeMarker='data-partial-id') AND emptySkipBlocks = true
10 |
10 | THIS MUST NOT BE GENERATED BECAUSE THE PARENT IS MARKED AS A SKIP BLOCK (skipBlockAttributeMarker='data-partial-id') AND emptySkipBlocks = true
11 |
19 | THIS MUST NOT BE GENERATED BECAUSE THE PARENT IS MARKED AS A SKIP BLOCK (skipBlockAttributeMarker='data-partial-id') AND emptySkipBlocks = true
20 |
19 | THIS MUST NOT BE GENERATED BECAUSE THE PARENT IS MARKED AS A SKIP BLOCK (skipBlockAttributeMarker='data-partial-id') AND emptySkipBlocks = true
20 |
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 |
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 "AB" 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 | }
--------------------------------------------------------------------------------