├── .gitignore
├── LICENSE
├── README.md
├── bin
├── itemplate.js
└── itemplate.min.js
├── examples
├── example - exception
│ └── index.html
├── example - format text
│ └── index.html
├── example - helpers
│ └── index.html
├── example - iterate over properties
│ └── index.html
├── example - simply
│ └── index.html
├── example - test
│ ├── index.css
│ ├── index.html
│ └── index.js
├── example - unwarped function
│ └── index.html
└── lib
│ └── incremental-dom.js
├── package.json
├── source
├── builder.js
├── itemplate.js
├── mode.js
├── options.js
├── parser.js
├── prepare.js
└── wrapper.js
├── test
├── README.md
├── test.html
└── test.js
├── webpack.config.js
└── webpack.config.release.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 |
29 | .idea/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 RAD.JS
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 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # idom-template
2 |
3 | > Now you can speed up and optimize your application, which once used standard templates by incremental DOM. You may read more about it [here](https://medium.com/google-developers/introducing-incremental-dom-e98f79ce2c5f).
4 |
5 | Library for converting your HTML or templates ([ejs](http://www.embeddedjs.com/)/[underscore templates](http://underscorejs.org/#template) or like) into [incremental-DOM by Google](http://google.github.io/incremental-dom/) rendering functions.
6 |
7 | *You can use this library with any framework with you want.*
8 |
9 | > New functionality is available. The documentation going to be updated, but now, see Tests.
10 |
11 | ###Example
12 | The simplest example of use looks like this, **html**:
13 |
14 | ```html
15 |
16 |
38 | ```
39 | **javascript**:
40 |
41 | ```javascript
42 | var templateData = {
43 | listTitle: "Olympic Volleyball Players",
44 | listItems: [
45 | {
46 | name: "Misty May-Treanor",
47 | hasOlympicGold: true
48 | },
49 | {
50 | name: "Kerri Walsh Jennings",
51 | hasOlympicGold: true
52 | },
53 | {
54 | name: "Jennifer Kessy",
55 | hasOlympicGold: false
56 | },
57 | {
58 | name: "April Ross",
59 | hasOlympicGold: false
60 | }
61 | ]
62 | };
63 |
64 | var templateStr = document.getElementById('underscore-template').innerHTML;
65 | var renderFn = itemplate.compile(templateStr, IncrementalDOM);
66 |
67 | IncrementalDOM.patch(document.querySelector('#container'), renderFn, templateData);
68 | ```
69 |
70 | In this case your template will be compiled by **idom-template** into the following JS function:
71 |
72 | ```javascript
73 | function (data) {
74 | var o = lib.elementOpen, c = lib.elementClose, t = lib.text, v = lib.elementVoid;
75 | o('div');
76 | t(data.listTitle);
77 | c('div');
78 | o('ul');
79 | var showFootnote = false;
80 | data.listItems.forEach(function (listItem, i) {
81 | o('li', null, null, 'class', 'row ' + (i % 2 == 1 ? ' even' : ''));
82 | t(listItem.name);
83 | if (listItem.hasOlympicGold) {
84 | showFootnote = true;
85 | o('em');
86 | t('*');
87 | c('em');
88 | }
89 | c('li');
90 | });
91 | c('ul');
92 | if (showFootnote) {
93 | o('p', null, null, 'style', 'font-size: 12px ;');
94 | o('em');
95 | t('* Olympic gold medalist');
96 | c('em');
97 | c('p');
98 | }
99 | }
100 | ```
101 | > Take note that the dependencies: *[lib](http://google.github.io/incremental-dom/)*(incremental-dom library) и *[helpers](#helpers)* are ejected with closure. The same closure contains the creation of *[static arrays](#static)*, when you use them for the array of static attributes.
102 | >
103 | > As opposed to underscore.js, **compilation of comments does not take place**!
104 |
105 | This one and more complicated examples can be viewed in the directory **[examples](https://github.com/Rapid-Application-Development-JS/itemplate/tree/master/examples)**.
106 |
107 | You may also compare the performance of BackboneJS, BackboneJS + incremental-dom, ReactJS 0.13.3: **[DEMO](http://rapid-application-development-js.github.io/itemplate/)**
108 |
109 | ## npm
110 | More over then include library directly, You also can use `npm` for installation:
111 |
112 | ```bash
113 | npm install idom-template --save
114 | ```
115 |
116 | ## Include
117 | ```html
118 |
119 |
120 | ```
121 |
122 | ## Use
123 |
124 | ```javascript
125 | var templateStr = document.getElementById('underscore-template').innerHTML;
126 | var renderFn = itemplate.compile(templateStr, IncrementalDOM);
127 |
128 | patch(containerElement, renderFn, templateData);
129 | ```
130 | > You should consider the following issues:
131 | >
132 | * You should be careful with the `'` symbol in templates; if it's mentioned in the text, it should be screened as `\'`. This will be fixed in further versions.
133 | * The data is transferred to the template as one object; so if you don't want to transfer data via closure in templates, you should work with one object that will be transferred as a [**parameter**](#parameterName) to `path`.
134 |
135 | ####unwrap
136 | Be careful, if the second parameter is absent during the compilation of the template, which means you won't transmit the link to the library:
137 |
138 | ```javascript
139 | var renderFn = itemplate.compile(templateStr[, IncrementalDOM]);
140 | ```
141 |
142 | In this case it's not a rendering function that will be compiled, it will be just a function without closure and wrapping:
143 |
144 | ```javascript
145 | function (data, lib, helpers){
146 | // throw error or something if not lib and helpers
147 | var o=lib.elementOpen,c=lib.elementClose,t=lib.text,v=lib.elementVoid;
148 | o('div', null, null, 'class', 'box');
149 | t( data.content );
150 | c('div');
151 | helpers['my-console']({data:7+8});
152 | }
153 | ```
154 |
155 | In this case you should call it and transmit compilation parameters as follows:
156 |
157 | ```javascript
158 | patch(document.getElementById('container'), function () {
159 | template(data, IncrementalDOM, customHelpers);
160 | });
161 | ```
162 | It allows to work with templates with more flexibility; for example, call several templates in one rendering function, or introduce additional logic, such as filtration etc.
163 |
164 | ###Options
165 | You may set compiling option as object to the library:
166 |
167 | ```javascript
168 | itemplate.options({
169 | //...
170 | });
171 | ```
172 | Where:
173 |
174 | * **parameterName** - name of the data object, which is transferred to the render function.
175 | * **template** (*interpolate*, *escape*, *evaluate*) - regular expression of your templates; you may change them, so that the compiler will process your template syntax.
176 | > Take note that compilation is carried out in the following order: *interpolate*, *escape*, *evaluate*. In further versions we plan to provide an opportunity of changing the sequence of template processing.
177 |
178 | * **escape**, **MAP** - regular expression and MAP for processing the html escaping expression.
179 |
180 | * **accessory** (*open*, *close*) - service lines for processing *interpolate*, *escape* templates; it's better not to modify them.
181 | * **staticKey** - attribute name for static attributes array generation in current tag. See [static attributes](#static).
182 |
183 |
184 | By default the options have the following values:
185 |
186 | ```javascript
187 | {
188 | BREAK_LINE: /(\r\n|\n|\r)\s{0,}/gm,
189 | // prepare options
190 | template: {
191 | evaluate: /<%([\s\S]+?)%>/g,
192 | interpolate: /<%=([\s\S]+?)%>/g,
193 | escape: /<%-([\s\S]+?)%>/g
194 | },
195 | order: ['interpolate', 'escape', 'evaluate'],
196 | evaluate: {
197 | name: 'script',
198 | open: ''
200 | },
201 | accessory: {
202 | open: '{%',
203 | close: '%}'
204 | },
205 | escape: /(&|<|>|")/g,
206 | MAP: {
207 | '&': '&',
208 | '<': '<',
209 | '>': '>',
210 | '"': '"'
211 | },
212 | // build options
213 | emptyString: true,
214 | staticKey: 'key',
215 | staticArray: 'static-array',
216 | nonStaticAttributes: ['id', 'name'],
217 | parameterName: 'data',
218 | parentParameterName: 'parent',
219 | renderContentFnName: 'content',
220 | // tags parse rules
221 | textSaveTags: ['pre', 'code'],
222 | voidRequireTags: ['input', 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'keygen', 'link', 'meta',
223 | 'param', 'source', 'track', 'wbr'],
224 | debug: false
225 | }
226 | ```
227 | You may modify any option.
228 |
229 | ###Static attributes
230 | Arrays of static attributes are used to [save memory](http://google.github.io/incremental-dom/#rendering-dom/statics-array).
231 |
232 | For generation of a static array, you should select the `static-array` attribute from element attriibutes and add it to the template tag.
233 |
234 | The value of this attribute will become the name of the static array:
235 |
236 | * in case the value is not specified, the array will be generated, and its name will be a unique generated line.
237 | * in case different tags contain the same name of the static array, the same array will be used for all of these elements. This generated array will be based on **all** static attributes of the **last** tag with this key in the template.
238 | * the name of the attribute can be changed in [options](#static_attr).
239 |
240 | **Example**
241 |
242 | If you compile the function of the following template:
243 |
244 | ```ejs
245 |
247 | <%= data.content %>
248 |
249 | ```
250 | You will get the result:
251 |
252 | ```javascript
253 | (function (lib, helpers) {
254 | var box_class = ['class', 'box', 'data-key', 'my-custom-key'];
255 | return function (data) {
256 | var o = lib.elementOpen, c = lib.elementClose, t = lib.text, v = lib.elementVoid;
257 | o('div', 'ZYjoAthjdzUz', box_class, 'style', 'top: ' + data.top + 'px; left: ' + data.left + 'px; background: rgb(0,0,' + data.color + ');');
258 | t(data.content);
259 | c('div');
260 | }
261 | })(IncrementalDOM, {
262 | // ... helpers object
263 | });
264 | ```
265 | It's important to understand that arrays of static attributes are unqiue for every **template**. If you use the same key name in different templates, there will be different arrays with different values.
266 |
267 | > Take note that:
268 | >
269 | * if you use an array of static attributes, a **[key](http://google.github.io/incremental-dom/#api/elementOpen)** for this element will be generated automatically.
270 |
271 | ###Helpers
272 | There is an option of injecting JS functions as a part of the compiled template.
273 |
274 | For that purpose we use **self-closing** tag (with `/`). All attributes of this tag will be moved to the JS function as a data object with keys, which are attributes of the tag.
275 |
276 | That is, upon registering the following helper:
277 |
278 | ```javascript
279 | itemplate.registerHelper('my-console', function (attrs) {
280 | console.log(attrs);
281 | });
282 | ```
283 | ...where the first parameter is the helper name, and the second one is the JS function.
284 |
285 | In this case, in order to call the helper it will suffice to indicate the tag is your template:
286 |
287 | ```ejs
288 |
289 |
290 |
291 | ```
292 | Every time the template is rendered, the registered function will be executed in the place where the given tag is inserted. This is what will be in the console:
293 |
294 | ```javascript
295 | {id: 'console_1', data: 15}
296 | ```
297 | This option can be used in the following ways:
298 |
299 | **Insertion of templates:**
300 |
301 | You may register any rendering function of incremental DOM as a helper. In this case external templates will be inserted into the rendering function.
302 |
303 | For example, having registered the following template as a helper:
304 |
305 | ```javascript
306 | var footnoteRenderFn = itemplate.compile(footnoteTemplate, IncrementalDOM);
307 | itemplate.registerHelper('my-footnote', footnoteRenderFn);
308 | ```
309 |
310 | You will be able to simply insert it in another template in the following way `.ejs`:
311 |
312 | ```ejs
313 |
314 | ```
315 |
316 | **Auxiliary logic of templates:**
317 |
318 | It's similarly possible to describe **synchronous** auxiliary logic using helpers.
319 |
320 | ```javascript
321 | var itemRenderFn = itemplate.compile(itemTemplate, IncrementalDOM);
322 | itemplate.registerHelper('my-list', function (attrs) {
323 | elementOpen('ul');
324 | _.each(attrs.listItems, function (listItem, i) {
325 | // render single list item
326 | itemRenderFn({
327 | i: i,
328 | name: listItem.name,
329 | hasOlympicGold: listItem.hasOlympicGold
330 | });
331 | });
332 | elementClose('ul');
333 | });
334 | ```
335 | The following line in your template will be responsible for full rendering of the `.ejs` list:
336 |
337 | ```ejs
338 |
339 | ```
340 | Examples of use of helpers can be viewed in the directory **[examples](https://github.com/Rapid-Application-Development-JS/itemplate/tree/master/examples)**
341 |
342 | > Please consider:
343 | >
344 | * data in `helpers` can be transmitted only via tag attributes. For example, the internal template will have access only to the data received via attributes.
345 | * `helpers` are not `web-components`, so they work only if you have registered *helper* beforehand, and then compiled the template that uses it.
--------------------------------------------------------------------------------
/bin/itemplate.js:
--------------------------------------------------------------------------------
1 | (function webpackUniversalModuleDefinition(root, factory) {
2 | if(typeof exports === 'object' && typeof module === 'object')
3 | module.exports = factory();
4 | else if(typeof define === 'function' && define.amd)
5 | define([], factory);
6 | else if(typeof exports === 'object')
7 | exports["itemplate"] = factory();
8 | else
9 | root["itemplate"] = factory();
10 | })(this, function() {
11 | return /******/ (function(modules) { // webpackBootstrap
12 | /******/ // The module cache
13 | /******/ var installedModules = {};
14 |
15 | /******/ // The require function
16 | /******/ function __webpack_require__(moduleId) {
17 |
18 | /******/ // Check if module is in cache
19 | /******/ if(installedModules[moduleId])
20 | /******/ return installedModules[moduleId].exports;
21 |
22 | /******/ // Create a new module (and put it into the cache)
23 | /******/ var module = installedModules[moduleId] = {
24 | /******/ exports: {},
25 | /******/ id: moduleId,
26 | /******/ loaded: false
27 | /******/ };
28 |
29 | /******/ // Execute the module function
30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
31 |
32 | /******/ // Flag the module as loaded
33 | /******/ module.loaded = true;
34 |
35 | /******/ // Return the exports of the module
36 | /******/ return module.exports;
37 | /******/ }
38 |
39 |
40 | /******/ // expose the modules object (__webpack_modules__)
41 | /******/ __webpack_require__.m = modules;
42 |
43 | /******/ // expose the module cache
44 | /******/ __webpack_require__.c = installedModules;
45 |
46 | /******/ // __webpack_public_path__
47 | /******/ __webpack_require__.p = "";
48 |
49 | /******/ // Load entry module and return exports
50 | /******/ return __webpack_require__(0);
51 | /******/ })
52 | /************************************************************************/
53 | /******/ ([
54 | /* 0 */
55 | /***/ function(module, exports, __webpack_require__) {
56 |
57 | var _options = __webpack_require__(1);
58 | var prepare = __webpack_require__(2);
59 | var Parser = __webpack_require__(3);
60 | var Builder = __webpack_require__(5);
61 |
62 | var wrapper = __webpack_require__(6).createWrapper();
63 | var builder = new Builder(wrapper);
64 | var parser = new Parser(builder);
65 |
66 | var helpers = {};
67 |
68 | var itemplate = {
69 | compile: function (string, library, scopedHelpers, rootKeys) {
70 | builder.reset();
71 | builder.set(
72 | Object.keys(helpers),
73 | scopedHelpers ? Object.keys(scopedHelpers) : [],
74 | rootKeys
75 | );
76 | wrapper.set(library, helpers, null, string);
77 | return parser.parseComplete(prepare(string));
78 | },
79 | options: function (options) {
80 | // mix options
81 | for (var key in options) {
82 | if (options.hasOwnProperty(key))
83 | _options[key] = options[key];
84 | }
85 | },
86 | registerHelper: function (name, fn) {
87 | helpers[name] = fn;
88 | },
89 | unregisterHelper: function (name) {
90 | delete helpers[name];
91 | }
92 | };
93 |
94 | Object.defineProperty(itemplate, 'helpers', {
95 | get: function () {
96 | return helpers;
97 | },
98 | set: function () {
99 | }
100 | });
101 |
102 | module.exports = itemplate;
103 |
104 | /***/ },
105 | /* 1 */
106 | /***/ function(module, exports) {
107 |
108 | var _options = {
109 | BREAK_LINE: /(\r\n|\n|\r)\s{0,}/gm,
110 | // prepare options
111 | template: {
112 | evaluate: /<%([\s\S]+?)%>/g,
113 | interpolate: /<%=([\s\S]+?)%>/g,
114 | escape: /<%-([\s\S]+?)%>/g
115 | },
116 | order: ['interpolate', 'escape', 'evaluate'],
117 | evaluate: {
118 | name: 'script',
119 | open: ''
121 | },
122 | accessory: {
123 | open: '{%',
124 | close: '%}'
125 | },
126 | escape: /(&|<|>|")/g,
127 | MAP: {
128 | '&': '&',
129 | '<': '<',
130 | '>': '>',
131 | '"': '"'
132 | },
133 | // build options
134 | emptyString: true,
135 | skipAttr: 'skip',
136 | staticKey: 'key',
137 | staticArray: 'static-array',
138 | nonStaticAttributes: ['id', 'name', 'ref'],
139 | binderPre: '::',
140 | helperPre: 'i-',
141 | parameterName: 'data',
142 | parentParameterName: 'parent',
143 | renderContentFnName: 'content',
144 | // tags parse rules
145 | textSaveTags: ['pre', 'code'],
146 | voidRequireTags: ['input', 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'keygen', 'link', 'meta',
147 | 'param', 'source', 'track', 'wbr'],
148 | debug: false
149 | };
150 |
151 | module.exports = _options;
152 |
153 | /***/ },
154 | /* 2 */
155 | /***/ function(module, exports, __webpack_require__) {
156 |
157 | var _options = __webpack_require__(1);
158 |
159 | function replacer(match, p1) {
160 | return _options.accessory.open + p1 + _options.accessory.close;
161 | }
162 |
163 | var methods = {
164 | evaluate: function (string) {
165 | return string.replace(_options.template.evaluate, function (match, p1) {
166 | return _options.evaluate.open + p1.replace(_options.BREAK_LINE, ' ').trim() + _options.evaluate.close;
167 | });
168 | },
169 | interpolate: function (string) {
170 | return string.replace(_options.template.interpolate, replacer);
171 | },
172 | escape: function (string) {
173 | return string.replace(_options.template.escape, replacer);
174 | }
175 | };
176 |
177 | function prepare(string) {
178 | var result = string;
179 | for (var i = 0; i < _options.order.length; i++) {
180 | result = methods[_options.order[i]](result);
181 | }
182 | return result;
183 | }
184 |
185 | module.exports = prepare;
186 |
187 | /***/ },
188 | /* 3 */
189 | /***/ function(module, exports, __webpack_require__) {
190 |
191 | var Mode = __webpack_require__(4);
192 |
193 | function Parser(builder) {
194 | this._builder = builder;
195 | this.reset();
196 | }
197 |
198 | //**Public**//
199 | Parser.prototype.reset = function () {
200 | this._state = {
201 | mode: Mode.Text,
202 | pos: 0,
203 | data: null,
204 | pendingText: null,
205 | pendingWrite: null,
206 | lastTag: null,
207 | isScript: false,
208 | needData: false,
209 | output: [],
210 | done: false
211 | };
212 | this._builder.reset();
213 | };
214 |
215 | Parser.prototype.parseChunk = function (chunk) {
216 | this._state.needData = false;
217 | this._state.data = (this._state.data !== null) ? this._state.data.substr(this.pos) + chunk : chunk;
218 | while (this._state.pos < this._state.data.length && !this._state.needData) {
219 | this._parse(this._state);
220 | }
221 | };
222 |
223 | Parser.prototype.parseComplete = function (data) {
224 | this.reset();
225 | this.parseChunk(data);
226 | return this.done();
227 | };
228 |
229 | Parser.prototype.done = function () {
230 | this._state.done = true;
231 | this._parse(this._state);
232 | this._flushWrite();
233 | return this._builder.done();
234 | };
235 |
236 | //**Private**//
237 | Parser.prototype._parse = function () {
238 | switch (this._state.mode) {
239 | case Mode.Text:
240 | return this._parseText(this._state);
241 | case Mode.Tag:
242 | return this._parseTag(this._state);
243 | case Mode.Attr:
244 | return this._parseAttr(this._state);
245 | case Mode.CData:
246 | return this._parseCData(this._state);
247 | case Mode.Doctype:
248 | return this._parseDoctype(this._state);
249 | case Mode.Comment:
250 | return this._parseComment(this._state);
251 | }
252 | };
253 |
254 | Parser.prototype._writePending = function (node) {
255 | if (!this._state.pendingWrite) {
256 | this._state.pendingWrite = [];
257 | }
258 | this._state.pendingWrite.push(node);
259 | };
260 |
261 | Parser.prototype._flushWrite = function () {
262 | if (this._state.pendingWrite) {
263 | for (var i = 0, len = this._state.pendingWrite.length; i < len; i++) {
264 | var node = this._state.pendingWrite[i];
265 | this._builder.write(node);
266 | }
267 | this._state.pendingWrite = null;
268 | }
269 | };
270 |
271 | Parser.prototype._write = function (node) {
272 | this._flushWrite();
273 | this._builder.write(node);
274 | };
275 |
276 | Parser._re_parseText_scriptClose = /<\s*\/\s*script/ig;
277 | Parser.prototype._parseText = function () {
278 | var state = this._state;
279 | var foundPos;
280 | if (state.isScript) {
281 | Parser._re_parseText_scriptClose.lastIndex = state.pos;
282 | foundPos = Parser._re_parseText_scriptClose.exec(state.data);
283 | foundPos = (foundPos) ? foundPos.index : -1;
284 | } else {
285 | foundPos = state.data.indexOf('<', state.pos);
286 | }
287 | var text = (foundPos === -1) ? state.data.substring(state.pos, state.data.length) : state.data.substring(state.pos, foundPos);
288 | if (foundPos < 0 && state.done) {
289 | foundPos = state.data.length;
290 | }
291 | if (foundPos < 0) {
292 | if (state.isScript) {
293 | state.needData = true;
294 | return;
295 | }
296 | if (!state.pendingText) {
297 | state.pendingText = [];
298 | }
299 | state.pendingText.push(state.data.substring(state.pos, state.data.length));
300 | state.pos = state.data.length;
301 | } else {
302 | if (state.pendingText) {
303 | state.pendingText.push(state.data.substring(state.pos, foundPos));
304 | text = state.pendingText.join('');
305 | state.pendingText = null;
306 | } else {
307 | text = state.data.substring(state.pos, foundPos);
308 | }
309 | if (text !== '') {
310 | this._write({type: Mode.Text, data: text});
311 | }
312 | state.pos = foundPos + 1;
313 | state.mode = Mode.Tag;
314 | }
315 | };
316 |
317 | Parser.re_parseTag = /\s*(\/?)\s*([^\s>\/]+)(\s*)\??(>?)/g;
318 | Parser.prototype._parseTag = function () {
319 | var state = this._state;
320 | Parser.re_parseTag.lastIndex = state.pos;
321 | var match = Parser.re_parseTag.exec(state.data);
322 |
323 | if (match) {
324 | if (!match[1] && match[2].substr(0, 3) === '!--') {
325 | state.mode = Mode.Comment;
326 | state.pos += 3;
327 | return;
328 | }
329 | if (!match[1] && match[2].substr(0, 8) === '![CDATA[') {
330 | state.mode = Mode.CData;
331 | state.pos += 8;
332 | return;
333 | }
334 | if (!match[1] && match[2].substr(0, 8) === '!DOCTYPE') {
335 | state.mode = Mode.Doctype;
336 | state.pos += 8;
337 | return;
338 | }
339 | if (!state.done && (state.pos + match[0].length) === state.data.length) {
340 | //We're at the and of the data, might be incomplete
341 | state.needData = true;
342 | return;
343 | }
344 | var raw;
345 | if (match[4] === '>') {
346 | state.mode = Mode.Text;
347 | raw = match[0].substr(0, match[0].length - 1);
348 | } else {
349 | state.mode = Mode.Attr;
350 | raw = match[0];
351 | }
352 | state.pos += match[0].length;
353 | var tag = {type: Mode.Tag, name: match[1] + match[2], raw: raw, position: Parser.re_parseTag.lastIndex };
354 | if (state.mode === Mode.Attr) {
355 | state.lastTag = tag;
356 | }
357 | if (tag.name.toLowerCase() === 'script') {
358 | state.isScript = true;
359 | } else if (tag.name.toLowerCase() === '/script') {
360 | state.isScript = false;
361 | }
362 | if (state.mode === Mode.Attr) {
363 | this._writePending(tag);
364 | } else {
365 | this._write(tag);
366 | }
367 | } else {
368 | state.needData = true;
369 | }
370 | };
371 |
372 | Parser.re_parseAttr_findName = /\s*([^=<>\s'"\/]+)\s*/g;
373 | Parser.prototype._parseAttr_findName = function () {
374 | // todo: parse {{ checked ? 'checked' : '' }} in input
375 | Parser.re_parseAttr_findName.lastIndex = this._state.pos;
376 | var match = Parser.re_parseAttr_findName.exec(this._state.data);
377 | if (!match) {
378 | return null;
379 | }
380 | if (this._state.pos + match[0].length !== Parser.re_parseAttr_findName.lastIndex) {
381 | return null;
382 | }
383 | return {
384 | match: match[0],
385 | name: match[1]
386 | };
387 | };
388 | Parser.re_parseAttr_findValue = /\s*=\s*(?:'([^']*)'|"([^"]*)"|([^'"\s\/>]+))\s*/g;
389 | Parser.re_parseAttr_findValue_last = /\s*=\s*['"]?(.*)$/g;
390 | Parser.prototype._parseAttr_findValue = function () {
391 | var state = this._state;
392 | Parser.re_parseAttr_findValue.lastIndex = state.pos;
393 | var match = Parser.re_parseAttr_findValue.exec(state.data);
394 | if (!match) {
395 | if (!state.done) {
396 | return null;
397 | }
398 | Parser.re_parseAttr_findValue_last.lastIndex = state.pos;
399 | match = Parser.re_parseAttr_findValue_last.exec(state.data);
400 | if (!match) {
401 | return null;
402 | }
403 | return {
404 | match: match[0],
405 | value: (match[1] !== '') ? match[1] : null
406 | };
407 | }
408 | if (state.pos + match[0].length !== Parser.re_parseAttr_findValue.lastIndex) {
409 | return null;
410 | }
411 | return {
412 | match: match[0],
413 | value: match[1] || match[2] || match[3]
414 | };
415 | };
416 | Parser.re_parseAttr_splitValue = /\s*=\s*['"]?/g;
417 | Parser.re_parseAttr_selfClose = /(\s*\/\s*)(>?)/g;
418 | Parser.prototype._parseAttr = function () {
419 | var state = this._state;
420 | var name_data = this._parseAttr_findName(state);
421 | if (!name_data || name_data.name === '?') {
422 | Parser.re_parseAttr_selfClose.lastIndex = state.pos;
423 | var matchTrailingSlash = Parser.re_parseAttr_selfClose.exec(state.data);
424 | if (matchTrailingSlash && matchTrailingSlash.index === state.pos) {
425 | if (!state.done && !matchTrailingSlash[2] && state.pos + matchTrailingSlash[0].length === state.data.length) {
426 | state.needData = true;
427 | return;
428 | }
429 | state.lastTag.raw += matchTrailingSlash[1];
430 | this._write({type: Mode.Tag, name: '/' + state.lastTag.name, raw: null});
431 | state.pos += matchTrailingSlash[1].length;
432 | }
433 | var foundPos = state.data.indexOf('>', state.pos);
434 | if (foundPos < 0) {
435 | if (state.done) {
436 | state.lastTag.raw += state.data.substr(state.pos);
437 | state.pos = state.data.length;
438 | return;
439 | }
440 | state.needData = true;
441 | } else {
442 | // state.lastTag = null;
443 | state.pos = foundPos + 1;
444 | state.mode = Mode.Text;
445 | }
446 | return;
447 | }
448 | if (!state.done && state.pos + name_data.match.length === state.data.length) {
449 | state.needData = true;
450 | return null;
451 | }
452 | state.pos += name_data.match.length;
453 | var value_data = this._parseAttr_findValue(state);
454 | if (value_data) {
455 | if (!state.done && state.pos + value_data.match.length === state.data.length) {
456 | state.needData = true;
457 | state.pos -= name_data.match.length;
458 | return;
459 | }
460 | state.pos += value_data.match.length;
461 | } else {
462 | if (state.data.indexOf(' ', state.pos - 1)) {
463 | value_data = {
464 | match: '',
465 | value: null
466 | };
467 |
468 | } else {
469 | Parser.re_parseAttr_splitValue.lastIndex = state.pos;
470 | if (Parser.re_parseAttr_splitValue.exec(state.data)) {
471 | state.needData = true;
472 | state.pos -= name_data.match.length;
473 | return;
474 | }
475 | value_data = {
476 | match: '',
477 | value: null
478 | };
479 | }
480 | }
481 | state.lastTag.raw += name_data.match + value_data.match;
482 |
483 | this._writePending({type: Mode.Attr, name: name_data.name, data: value_data.value});
484 | };
485 |
486 | Parser.re_parseCData_findEnding = /\]{1,2}$/;
487 | Parser.prototype._parseCData = function () {
488 | var state = this._state;
489 | var foundPos = state.data.indexOf(']]>', state.pos);
490 | if (foundPos < 0 && state.done) {
491 | foundPos = state.data.length;
492 | }
493 | if (foundPos < 0) {
494 | Parser.re_parseCData_findEnding.lastIndex = state.pos;
495 | var matchPartialCDataEnd = Parser.re_parseCData_findEnding.exec(state.data);
496 | if (matchPartialCDataEnd) {
497 | state.needData = true;
498 | return;
499 | }
500 | if (!state.pendingText) {
501 | state.pendingText = [];
502 | }
503 | state.pendingText.push(state.data.substr(state.pos, state.data.length));
504 | state.pos = state.data.length;
505 | state.needData = true;
506 | } else {
507 | var text;
508 | if (state.pendingText) {
509 | state.pendingText.push(state.data.substring(state.pos, foundPos));
510 | text = state.pendingText.join('');
511 | state.pendingText = null;
512 | } else {
513 | text = state.data.substring(state.pos, foundPos);
514 | }
515 | this._write({type: Mode.CData, data: text});
516 | state.mode = Mode.Text;
517 | state.pos = foundPos + 3;
518 | }
519 | };
520 |
521 | Parser.prototype._parseDoctype = function () {
522 | var state = this._state;
523 | var foundPos = state.data.indexOf('>', state.pos);
524 | if (foundPos < 0 && state.done) {
525 | foundPos = state.data.length;
526 | }
527 | if (foundPos < 0) {
528 | Parser.re_parseCData_findEnding.lastIndex = state.pos;
529 | if (!state.pendingText) {
530 | state.pendingText = [];
531 | }
532 | state.pendingText.push(state.data.substr(state.pos, state.data.length));
533 | state.pos = state.data.length;
534 | state.needData = true;
535 | } else {
536 | var text;
537 | if (state.pendingText) {
538 | state.pendingText.push(state.data.substring(state.pos, foundPos));
539 | text = state.pendingText.join('');
540 | state.pendingText = null;
541 | } else {
542 | text = state.data.substring(state.pos, foundPos);
543 | }
544 | this._write({type: Mode.Doctype, data: text});
545 | state.mode = Mode.Text;
546 | state.pos = foundPos + 1;
547 | }
548 | };
549 |
550 | Parser.re_parseComment_findEnding = /\-{1,2}$/;
551 | Parser.prototype._parseComment = function () {
552 | var state = this._state;
553 | var foundPos = state.data.indexOf('-->', state.pos);
554 | if (foundPos < 0 && state.done) {
555 | foundPos = state.data.length;
556 | }
557 | if (foundPos < 0) {
558 | Parser.re_parseComment_findEnding.lastIndex = state.pos;
559 | var matchPartialCommentEnd = Parser.re_parseComment_findEnding.exec(state.data);
560 | if (matchPartialCommentEnd) {
561 | state.needData = true;
562 | return;
563 | }
564 | if (!state.pendingText) {
565 | state.pendingText = [];
566 | }
567 | state.pendingText.push(state.data.substr(state.pos, state.data.length));
568 | state.pos = state.data.length;
569 | state.needData = true;
570 | } else {
571 | var text;
572 | if (state.pendingText) {
573 | state.pendingText.push(state.data.substring(state.pos, foundPos));
574 | text = state.pendingText.join('');
575 | state.pendingText = null;
576 | } else {
577 | text = state.data.substring(state.pos, foundPos);
578 | }
579 |
580 | this._write({type: Mode.Comment, data: text});
581 | state.mode = Mode.Text;
582 | state.pos = foundPos + 3;
583 | }
584 | };
585 |
586 | module.exports = Parser;
587 |
588 | /***/ },
589 | /* 4 */
590 | /***/ function(module, exports) {
591 |
592 | var Mode = {
593 | Text: 'text',
594 | Tag: 'tag',
595 | Attr: 'attr',
596 | CData: 'cdata',
597 | Doctype: 'doctype',
598 | Comment: 'comment'
599 | };
600 |
601 | module.exports = Mode;
602 |
603 | /***/ },
604 | /* 5 */
605 | /***/ function(module, exports, __webpack_require__) {
606 |
607 | /* private */
608 | var _options = __webpack_require__(1);
609 | var Mode = __webpack_require__(4);
610 | var Command = __webpack_require__(6).Command;
611 |
612 | var state; // current builder state
613 | var stack; // result builder
614 | var staticArraysHolder = {}; // holder for static arrays
615 | var wrapper; // external wrapper functionality
616 | var helpers; // keys for helpers
617 | var localComponentNames = []; // keys for local helpers
618 |
619 | var empty = '', quote = '"', comma = ', "', removable = '-%%&#__II-'; // auxiliary
620 |
621 | var nestingLevelInfo = {level: 0, skip: []};
622 |
623 | function isRootNode() {
624 | return nestingLevelInfo.level === 0;
625 | }
626 |
627 | function makeKey() {
628 | var text = new Array(12), possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgijklmnopqrstuvwxyz';
629 | for (var i = 0; i < 12; i++)
630 | text.push(possible.charAt(Math.floor(Math.random() * possible.length)));
631 |
632 | return text.join(empty);
633 | }
634 |
635 | function decodeAccessory(string, force) {
636 | var regex = new RegExp(_options.accessory.open + '|' + _options.accessory.close, 'g');
637 | var code;
638 | var isStatic = true, openStub, closeStub;
639 |
640 | if (string !== undefined)
641 | code = string.split(regex).map(function (piece, i) {
642 | openStub = '';
643 | closeStub = '';
644 |
645 | if (i % 2) {
646 | isStatic = false;
647 | piece = piece.trim();
648 | if (_options.emptyString && !force) { // undefined as empty string
649 | if (piece.indexOf(' ') !== -1) {
650 | openStub = '(';
651 | closeStub = ')';
652 | }
653 | return ' + (' + openStub + piece + closeStub + ' === undefined ? "" : '
654 | + openStub + piece + closeStub + ') + ';
655 | } else
656 | return ' + ' + piece + ' + ';
657 | } else {
658 | return JSON.stringify(piece);
659 | }
660 | }).join('');
661 | else
662 | code = '""';
663 |
664 | // micro-optimizations (remove appending empty strings)
665 | code = code.replace(/^"" \+ | \+ ""$/g, '').replace(/ \+ "" \+ /g, ' + ');
666 |
667 | return {value: code, isStatic: isStatic};
668 | }
669 |
670 | function formatText(text) {
671 | return text.trim()
672 | .replace(/(\d+);/g, function (match, dec) {
673 | return String.fromCharCode(dec);
674 | })
675 | .replace(_options.escape, function (m) {
676 | return _options.MAP[m];
677 | });
678 | }
679 |
680 | function prepareKey(command, attributes, useKeyCommand) {
681 | var result = empty, decode, stub;
682 | if ((command === Command.elementOpen || command === Command.elementVoid)) {
683 |
684 | if (attributes && attributes.hasOwnProperty(_options.staticKey)) {
685 | decode = decodeAccessory(attributes[_options.staticKey] || makeKey());
686 | delete attributes[_options.staticKey];
687 | } else if (useKeyCommand) {
688 | decode = {value: Command.getKey};
689 | } else {
690 | decode = {value: 'null'};
691 | }
692 | stub = (Object.keys(attributes).length > 0) ? ', ' : empty;
693 | result = ', ' + decode.value + stub;
694 | }
695 | return result;
696 | }
697 |
698 | function prepareAttr(command, attributes) {
699 | var result = empty, attr, decode, arrayStaticKey = false, isSkipped = false, skipCommand;
700 | if ((command === Command.elementOpen || command === Command.elementVoid) && Object.keys(attributes).length > 0) {
701 | if (attributes && attributes.hasOwnProperty(_options.staticArray)) {
702 | arrayStaticKey = attributes[_options.staticArray] || makeKey();
703 | staticArraysHolder[arrayStaticKey] = staticArraysHolder[arrayStaticKey] || {};
704 | delete attributes[_options.staticArray];
705 | }
706 |
707 | if (attributes && attributes.hasOwnProperty(_options.skipAttr)) {
708 | isSkipped = true;
709 | skipCommand = Command.startSkipContent(decodeAccessory(attributes[_options.skipAttr], true).value);
710 | delete attributes[_options.skipAttr];
711 | }
712 |
713 | result = arrayStaticKey || null;
714 | for (var key in attributes) {
715 | attr = attributes[key];
716 | attr = (attr === null) ? key : ((attr === undefined) ? '' : attr);
717 | decode = decodeAccessory(attr);
718 | if (decode.isStatic && (_options.nonStaticAttributes.indexOf(key) === -1)) {
719 | if (arrayStaticKey) {
720 | var value = formatText(attr);
721 | if (!staticArraysHolder[arrayStaticKey].hasOwnProperty(key)) {
722 | staticArraysHolder[arrayStaticKey][key] = value;
723 | } else if (staticArraysHolder[arrayStaticKey][key] !== value) {
724 | staticArraysHolder[arrayStaticKey][key] = removable;
725 | result += comma + key + '", "' + value + quote;
726 | }
727 | } else
728 | result += comma + key + '", "' + formatText(attr) + quote;
729 | } else {
730 | result += comma + key + '", ' + formatText(decode.value);
731 | }
732 | }
733 | }
734 | return {value: result, isSkipped: isSkipped, skip: skipCommand};
735 | }
736 |
737 | function unwrapStaticArrays(holder) {
738 | var result = {}, obj, key;
739 | for (var arrayName in holder) {
740 | obj = holder[arrayName];
741 | result[arrayName] = [];
742 |
743 | for (key in obj)
744 | if (obj[key] !== removable)
745 | result[arrayName].push(quote + key + quote, quote + obj[key] + quote);
746 | }
747 |
748 | return result;
749 | }
750 |
751 | function decodeAttrs(obj) {
752 | var result = ['{'];
753 | for (var key in obj)
754 | result.push(((result.length > 1) ? ',' : empty) + '\'' + key + '\'' + ':' + decodeAccessory(obj[key], true).value);
755 | result.push('}');
756 |
757 | return result.join(empty);
758 | }
759 |
760 | function camelCase(input) {
761 | return input.replace(/\s/g, '').replace(/-(.)/g, function (match, group1) {
762 | return group1.toUpperCase();
763 | });
764 | }
765 |
766 | function writeCommand(command, tag, attributes) {
767 | if (attributes && attributes.ref) {
768 | var refName = attributes.ref;
769 | delete attributes.ref;
770 | }
771 |
772 | var strKey = prepareKey(command, attributes);
773 | var strAttrs = prepareAttr(command, attributes);
774 |
775 | if (refName) {
776 | // i.e. ref[refName] = elementOpen(...)
777 | command = Command.saveRef(camelCase(decodeAccessory(refName, true).value), command);
778 | }
779 |
780 | stack.push(command + tag + quote + strKey + strAttrs.value + Command.close);
781 |
782 | // save skipped
783 | if (strAttrs.isSkipped) {
784 | stack.push(strAttrs.skip);
785 | nestingLevelInfo.skip.push(nestingLevelInfo.level);
786 | }
787 | }
788 |
789 | function writeText(text) {
790 | text = formatText(text);
791 | if (text.length > 0) {
792 | var decode = decodeAccessory(text);
793 | stack.push(Command.text + decode.value + Command.close);
794 | }
795 | }
796 |
797 | function helperOpen(helperName, attrs) {
798 | stack.push(Command.helpers + '["' + helperName + '"](' + decodeAttrs(attrs) + ', function ('
799 | + _options.parentParameterName + '){');
800 | }
801 |
802 | function helperClose() {
803 | stack.push('}.bind(this));');
804 | }
805 |
806 | function isHelperTag(tagName) {
807 | return localComponentNames.indexOf(tagName) !== -1
808 | || helpers.indexOf(tagName) !== -1
809 | || tagName.indexOf(_options.helperPre) === 0;
810 | }
811 |
812 | function binderOpen(helperName, attrs) {
813 | var fnName = helperName.replace(_options.binderPre, '');
814 | stack.push(Command.binder + '(' + fnName + ',' + decodeAttrs(attrs) + ', function ('
815 | + _options.parentParameterName + '){');
816 | }
817 |
818 | function binderClose() {
819 | stack.push('}.bind(this));');
820 | }
821 |
822 | function isTagBinded(tagName) {
823 | return tagName.indexOf(_options.binderPre) === 0;
824 | }
825 |
826 | // TODO: Clarify logic.
827 | // Seems like this method only opens state but named as 'CloseOpenState'
828 | // also seems like `isClosed` flags used only to detect elementVoid and it's a bit confusing
829 | // because sounds like it can be used to detect tags open or close state.
830 | function writeAndCloseOpenState(isClosed) {
831 | var isShouldClose = true;
832 |
833 | if (state.tag) {
834 | var isRoot = isRootNode();
835 |
836 | if (isHelperTag(state.tag)) { // helper case
837 | helperOpen(state.tag, state.attributes);
838 | isShouldClose = isClosed;
839 | } else if (isTagBinded(state.tag)) {
840 | binderOpen(state.tag, state.attributes);
841 | isShouldClose = isClosed;
842 | } else if (isClosed || _options.voidRequireTags.indexOf(state.tag) !== -1) { // void mode
843 | writeCommand(Command.elementVoid, state.tag, state.attributes, isRoot);
844 | nestingLevelInfo.level--;
845 | isShouldClose = false;
846 | } else if (state.tag !== _options.evaluate.name) { // standard mode
847 | writeCommand(Command.elementOpen, state.tag, state.attributes, isRoot);
848 | } // if we write code, do nothing
849 |
850 | nestingLevelInfo.level++;
851 | }
852 |
853 | // clear builder state for next tag
854 | state.tag = null;
855 | state.attributes = {};
856 |
857 | return isShouldClose; // should we close this tag: no if we have void element
858 | }
859 |
860 | /* public */
861 | function Builder(functionWrapper) {
862 | wrapper = functionWrapper;
863 | this.reset();
864 | }
865 |
866 | Builder.prototype.reset = function () {
867 | stack = [];
868 | state = {
869 | tag: null,
870 | attributes: {}
871 | };
872 | staticArraysHolder = {};
873 | nestingLevelInfo = {level: 0, skip: []};
874 | };
875 |
876 | Builder.prototype.set = function (helpersKeys, localNames) {
877 | helpers = helpersKeys;
878 | localComponentNames = localNames || [];
879 | };
880 |
881 | Builder.prototype.write = function (command) {
882 | var tag;
883 | switch (command.type) {
884 | case Mode.Tag:
885 | tag = command.name.replace('/', empty);
886 |
887 | if (command.name.indexOf('/') === 0) {
888 |
889 | // close tag case
890 | if (writeAndCloseOpenState(true) && tag !== _options.evaluate.name) {
891 | nestingLevelInfo.level--;
892 |
893 | // write end skip functionality
894 | if (nestingLevelInfo.level === nestingLevelInfo.skip[nestingLevelInfo.skip.length - 1]) {
895 | stack.push(Command.endSkipContent);
896 | nestingLevelInfo.skip.pop();
897 | }
898 |
899 | if (isHelperTag(tag))
900 | helperClose();
901 | else if (isTagBinded(tag))
902 | binderClose();
903 | else
904 | writeCommand(Command.elementClose, tag);
905 | }
906 | } else {
907 | // open tag case
908 | writeAndCloseOpenState();
909 | state.tag = tag;
910 | state.attributes = {};
911 | }
912 | break;
913 | case Mode.Attr: // push attribute in state
914 | state.attributes[command.name] = command.data;
915 | break;
916 | case Mode.Text: // write text
917 | tag = state.tag;
918 | writeAndCloseOpenState();
919 | if (tag === _options.evaluate.name) { // write code
920 | stack.push(formatText(command.data));
921 | } else {
922 | writeText(command.data);
923 | }
924 | break;
925 | case Mode.Comment: // write comments only in debug mode
926 | if (_options.debug)
927 | stack.push('\n// ' + command.data.replace(_options.BREAK_LINE, ' ') + '\n');
928 | break;
929 | }
930 | };
931 |
932 | Builder.prototype.done = function () {
933 | return wrapper(stack, unwrapStaticArrays(staticArraysHolder));
934 | };
935 |
936 | module.exports = Builder;
937 |
938 | /***/ },
939 | /* 6 */
940 | /***/ function(module, exports, __webpack_require__) {
941 |
942 | var _options = __webpack_require__(1);
943 |
944 | var Command = { // incremental DOM commands
945 | helpers: '_h',
946 | binder: '_b',
947 | elementOpen: '_o("',
948 | elementClose: '_c("',
949 | elementVoid: '_v("',
950 | saveRef: function (name, command) {
951 | return '_r[' + name + '] = ' + command;
952 | },
953 | text: '_t(',
954 | close: ');\n',
955 | startSkipContent: function (flag) {
956 | // compile static values
957 | flag = (flag === '"false"') ? false : flag;
958 | flag = (flag === '"true"') ? true : flag;
959 |
960 | return 'if(' + flag + '){_l.skip();}else{';
961 | },
962 | endSkipContent: '}'
963 | };
964 |
965 | function createWrapper() {
966 | var _library, _helpers, _fnName, _template;
967 | var glue = '';
968 | var eol = '\n';
969 |
970 | function wrapFn(body) {
971 | var returnValue = eol + ' return _r;';
972 |
973 | var prepareError = 'var TE=function(m,n,o){this.original=o;this.name=n;(o)?this.stack=this.original.stack:' +
974 | 'this.stack=null;this.message=o.message+m;};var CE=function(){};CE.prototype=Error.prototype;' +
975 | 'TE.prototype=new CE();TE.prototype.constructor=TE;';
976 |
977 | if (_options.debug) {
978 | return 'try {'
979 | + body +
980 | '} catch (err) {'
981 | + prepareError +
982 | 'throw new TE(' + JSON.stringify(_template) + ', err.name, err);' +
983 | '}'
984 | + returnValue;
985 | }
986 | return body + returnValue;
987 | }
988 |
989 | function wrapper(stack, holder) {
990 | var resultFn;
991 | var variables = [
992 | 'var _o = _l.elementOpen;',
993 | 'var _c = _l.elementClose;',
994 | 'var _v = _l.elementVoid;',
995 | 'var _t = _l.text;',
996 | 'var _r = {};',
997 | '_b = _b || function(fn, data, content){ return fn(data, content); };'
998 | ].join(eol) + eol;
999 |
1000 | for (var key in holder) { // collect static arrays for function
1001 | if (holder.hasOwnProperty(key))
1002 | variables += 'var ' + key + '=[' + holder[key] + '];';
1003 | }
1004 | var body = variables + wrapFn(stack.join(glue));
1005 |
1006 | if (_library) {
1007 | body = 'return function(' + _options.parameterName + ', ' + _options.renderContentFnName + ', _b){' + body + '};';
1008 | resultFn = (new Function('_l', '_h', body))(_library, _helpers);
1009 | } else {
1010 | resultFn = new Function(_options.parameterName, '_l', '_h', _options.renderContentFnName, '_b', body);
1011 | }
1012 | return resultFn;
1013 | }
1014 |
1015 | wrapper.set = function (library, helpers, fnName, template) {
1016 | _library = library;
1017 | _helpers = helpers;
1018 | _fnName = fnName;
1019 | _template = template;
1020 | };
1021 |
1022 | return wrapper;
1023 | }
1024 |
1025 | module.exports = {
1026 | createWrapper: createWrapper,
1027 | Command: Command
1028 | };
1029 |
1030 | /***/ }
1031 | /******/ ])
1032 | });
1033 | ;
--------------------------------------------------------------------------------
/bin/itemplate.min.js:
--------------------------------------------------------------------------------
1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.itemplate=t():e.itemplate=t()}(this,function(){return function(e){function t(a){if(n[a])return n[a].exports;var r=n[a]={exports:{},id:a,loaded:!1};return e[a].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){var a=n(1),r=n(2),s=n(3),i=n(5),o=n(6).createWrapper(),p=new i(o),u=new s(p),l={},d={compile:function(e,t,n,a){return p.reset(),p.set(Object.keys(l),n?Object.keys(n):[],a),o.set(t,l,null,e),u.parseComplete(r(e))},options:function(e){for(var t in e)e.hasOwnProperty(t)&&(a[t]=e[t])},registerHelper:function(e,t){l[e]=t},unregisterHelper:function(e){delete l[e]}};Object.defineProperty(d,"helpers",{get:function(){return l},set:function(){}}),e.exports=d},function(e,t){var n={BREAK_LINE:/(\r\n|\n|\r)\s{0,}/gm,template:{evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g},order:["interpolate","escape","evaluate"],evaluate:{name:"script",open:""},accessory:{open:"{%",close:"%}"},escape:/(&|<|>|")/g,MAP:{"&":"&","<":"<",">":">",""":'"'},emptyString:!0,skipAttr:"skip",staticKey:"key",staticArray:"static-array",nonStaticAttributes:["id","name","ref"],binderPre:"::",helperPre:"i-",parameterName:"data",parentParameterName:"parent",renderContentFnName:"content",textSaveTags:["pre","code"],voidRequireTags:["input","area","base","br","col","command","embed","hr","img","keygen","link","meta","param","source","track","wbr"],debug:!1};e.exports=n},function(e,t,n){function a(e,t){return s.accessory.open+t+s.accessory.close}function r(e){for(var t=e,n=0;ne;e++){var n=this._state.pendingWrite[e];this._builder.write(n)}this._state.pendingWrite=null}},a.prototype._write=function(e){this._flushWrite(),this._builder.write(e)},a._re_parseText_scriptClose=/<\s*\/\s*script/gi,a.prototype._parseText=function(){var e,t=this._state;t.isScript?(a._re_parseText_scriptClose.lastIndex=t.pos,e=a._re_parseText_scriptClose.exec(t.data),e=e?e.index:-1):e=t.data.indexOf("<",t.pos);var n=-1===e?t.data.substring(t.pos,t.data.length):t.data.substring(t.pos,e);if(0>e&&t.done&&(e=t.data.length),0>e){if(t.isScript)return void(t.needData=!0);t.pendingText||(t.pendingText=[]),t.pendingText.push(t.data.substring(t.pos,t.data.length)),t.pos=t.data.length}else t.pendingText?(t.pendingText.push(t.data.substring(t.pos,e)),n=t.pendingText.join(""),t.pendingText=null):n=t.data.substring(t.pos,e),""!==n&&this._write({type:r.Text,data:n}),t.pos=e+1,t.mode=r.Tag},a.re_parseTag=/\s*(\/?)\s*([^\s>\/]+)(\s*)\??(>?)/g,a.prototype._parseTag=function(){var e=this._state;a.re_parseTag.lastIndex=e.pos;var t=a.re_parseTag.exec(e.data);if(t){if(!t[1]&&"!--"===t[2].substr(0,3))return e.mode=r.Comment,void(e.pos+=3);if(!t[1]&&"![CDATA["===t[2].substr(0,8))return e.mode=r.CData,void(e.pos+=8);if(!t[1]&&"!DOCTYPE"===t[2].substr(0,8))return e.mode=r.Doctype,void(e.pos+=8);if(!e.done&&e.pos+t[0].length===e.data.length)return void(e.needData=!0);var n;">"===t[4]?(e.mode=r.Text,n=t[0].substr(0,t[0].length-1)):(e.mode=r.Attr,n=t[0]),e.pos+=t[0].length;var s={type:r.Tag,name:t[1]+t[2],raw:n,position:a.re_parseTag.lastIndex};e.mode===r.Attr&&(e.lastTag=s),"script"===s.name.toLowerCase()?e.isScript=!0:"/script"===s.name.toLowerCase()&&(e.isScript=!1),e.mode===r.Attr?this._writePending(s):this._write(s)}else e.needData=!0},a.re_parseAttr_findName=/\s*([^=<>\s'"\/]+)\s*/g,a.prototype._parseAttr_findName=function(){a.re_parseAttr_findName.lastIndex=this._state.pos;var e=a.re_parseAttr_findName.exec(this._state.data);return e?this._state.pos+e[0].length!==a.re_parseAttr_findName.lastIndex?null:{match:e[0],name:e[1]}:null},a.re_parseAttr_findValue=/\s*=\s*(?:'([^']*)'|"([^"]*)"|([^'"\s\/>]+))\s*/g,a.re_parseAttr_findValue_last=/\s*=\s*['"]?(.*)$/g,a.prototype._parseAttr_findValue=function(){var e=this._state;a.re_parseAttr_findValue.lastIndex=e.pos;var t=a.re_parseAttr_findValue.exec(e.data);return t?e.pos+t[0].length!==a.re_parseAttr_findValue.lastIndex?null:{match:t[0],value:t[1]||t[2]||t[3]}:e.done?(a.re_parseAttr_findValue_last.lastIndex=e.pos,t=a.re_parseAttr_findValue_last.exec(e.data),t?{match:t[0],value:""!==t[1]?t[1]:null}:null):null},a.re_parseAttr_splitValue=/\s*=\s*['"]?/g,a.re_parseAttr_selfClose=/(\s*\/\s*)(>?)/g,a.prototype._parseAttr=function(){var e=this._state,t=this._parseAttr_findName(e);if(t&&"?"!==t.name){if(!e.done&&e.pos+t.match.length===e.data.length)return e.needData=!0,null;e.pos+=t.match.length;var n=this._parseAttr_findValue(e);if(n){if(!e.done&&e.pos+n.match.length===e.data.length)return e.needData=!0,void(e.pos-=t.match.length);e.pos+=n.match.length}else if(e.data.indexOf(" ",e.pos-1))n={match:"",value:null};else{if(a.re_parseAttr_splitValue.lastIndex=e.pos,a.re_parseAttr_splitValue.exec(e.data))return e.needData=!0,void(e.pos-=t.match.length);n={match:"",value:null}}e.lastTag.raw+=t.match+n.match,this._writePending({type:r.Attr,name:t.name,data:n.value})}else{a.re_parseAttr_selfClose.lastIndex=e.pos;var s=a.re_parseAttr_selfClose.exec(e.data);if(s&&s.index===e.pos){if(!e.done&&!s[2]&&e.pos+s[0].length===e.data.length)return void(e.needData=!0);e.lastTag.raw+=s[1],this._write({type:r.Tag,name:"/"+e.lastTag.name,raw:null}),e.pos+=s[1].length}var i=e.data.indexOf(">",e.pos);if(0>i){if(e.done)return e.lastTag.raw+=e.data.substr(e.pos),void(e.pos=e.data.length);e.needData=!0}else e.pos=i+1,e.mode=r.Text}},a.re_parseCData_findEnding=/\]{1,2}$/,a.prototype._parseCData=function(){var e=this._state,t=e.data.indexOf("]]>",e.pos);if(0>t&&e.done&&(t=e.data.length),0>t){a.re_parseCData_findEnding.lastIndex=e.pos;var n=a.re_parseCData_findEnding.exec(e.data);if(n)return void(e.needData=!0);e.pendingText||(e.pendingText=[]),e.pendingText.push(e.data.substr(e.pos,e.data.length)),e.pos=e.data.length,e.needData=!0}else{var s;e.pendingText?(e.pendingText.push(e.data.substring(e.pos,t)),s=e.pendingText.join(""),e.pendingText=null):s=e.data.substring(e.pos,t),this._write({type:r.CData,data:s}),e.mode=r.Text,e.pos=t+3}},a.prototype._parseDoctype=function(){var e=this._state,t=e.data.indexOf(">",e.pos);if(0>t&&e.done&&(t=e.data.length),0>t)a.re_parseCData_findEnding.lastIndex=e.pos,e.pendingText||(e.pendingText=[]),e.pendingText.push(e.data.substr(e.pos,e.data.length)),e.pos=e.data.length,e.needData=!0;else{var n;e.pendingText?(e.pendingText.push(e.data.substring(e.pos,t)),n=e.pendingText.join(""),e.pendingText=null):n=e.data.substring(e.pos,t),this._write({type:r.Doctype,data:n}),e.mode=r.Text,e.pos=t+1}},a.re_parseComment_findEnding=/\-{1,2}$/,a.prototype._parseComment=function(){var e=this._state,t=e.data.indexOf("-->",e.pos);if(0>t&&e.done&&(t=e.data.length),0>t){a.re_parseComment_findEnding.lastIndex=e.pos;var n=a.re_parseComment_findEnding.exec(e.data);if(n)return void(e.needData=!0);e.pendingText||(e.pendingText=[]),e.pendingText.push(e.data.substr(e.pos,e.data.length)),e.pos=e.data.length,e.needData=!0}else{var s;e.pendingText?(e.pendingText.push(e.data.substring(e.pos,t)),s=e.pendingText.join(""),e.pendingText=null):s=e.data.substring(e.pos,t),this._write({type:r.Comment,data:s}),e.mode=r.Text,e.pos=t+3}},e.exports=a},function(e,t){var n={Text:"text",Tag:"tag",Attr:"attr",CData:"cdata",Doctype:"doctype",Comment:"comment"};e.exports=n},function(e,t,n){function a(){return 0===V.level}function r(){for(var e=new Array(12),t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgijklmnopqrstuvwxyz",n=0;12>n;n++)e.push(t.charAt(Math.floor(Math.random()*t.length)));return e.join(P)}function s(e,t){var n,a,r,s=new RegExp(k.accessory.open+"|"+k.accessory.close,"g"),i=!0;return n=void 0!==e?e.split(s).map(function(e,n){return a="",r="",n%2?(i=!1,e=e.trim(),k.emptyString&&!t?(-1!==e.indexOf(" ")&&(a="(",r=")")," + ("+a+e+r+' === undefined ? "" : '+a+e+r+") + "):" + "+e+" + "):JSON.stringify(e)}).join(""):'""',n=n.replace(/^"" \+ | \+ ""$/g,"").replace(/ \+ "" \+ /g," + "),{value:n,isStatic:i}}function i(e){return e.trim().replace(/(\d+);/g,function(e,t){return String.fromCharCode(t)}).replace(k.escape,function(e){return k.MAP[e]})}function o(e,t,n){var a,i,o=P;return e!==O.elementOpen&&e!==O.elementVoid||(t&&t.hasOwnProperty(k.staticKey)?(a=s(t[k.staticKey]||r()),delete t[k.staticKey]):a=n?{value:O.getKey}:{value:"null"},i=Object.keys(t).length>0?", ":P,o=", "+a.value+i),o}function p(e,t){var n,a,o,p=P,u=!1,l=!1;if((e===O.elementOpen||e===O.elementVoid)&&Object.keys(t).length>0){t&&t.hasOwnProperty(k.staticArray)&&(u=t[k.staticArray]||r(),E[u]=E[u]||{},delete t[k.staticArray]),t&&t.hasOwnProperty(k.skipAttr)&&(l=!0,o=O.startSkipContent(s(t[k.skipAttr],!0).value),delete t[k.skipAttr]),p=u||null;for(var d in t)if(n=t[d],n=null===n?d:void 0===n?"":n,a=s(n),a.isStatic&&-1===k.nonStaticAttributes.indexOf(d))if(u){var c=i(n);E[u].hasOwnProperty(d)?E[u][d]!==c&&(E[u][d]=j,p+=I+d+'", "'+c+N):E[u][d]=c}else p+=I+d+'", "'+i(n)+N;else p+=I+d+'", '+i(a.value)}return{value:p,isSkipped:l,skip:o}}function u(e){var t,n,a={};for(var r in e){t=e[r],a[r]=[];for(n in t)t[n]!==j&&a[r].push(N+n+N,N+t[n]+N)}return a}function l(e){var t=["{"];for(var n in e)t.push((t.length>1?",":P)+"'"+n+"':"+s(e[n],!0).value);return t.push("}"),t.join(P)}function d(e){return e.replace(/\s/g,"").replace(/-(.)/g,function(e,t){return t.toUpperCase()})}function c(e,t,n){if(n&&n.ref){var a=n.ref;delete n.ref}var r=o(e,n),i=p(e,n);a&&(e=O.saveRef(d(s(a,!0).value),e)),C.push(e+t+N+r+i.value+O.close),i.isSkipped&&(C.push(i.skip),V.skip.push(V.level))}function f(e){if(e=i(e),e.length>0){var t=s(e);C.push(O.text+t.value+O.close)}}function h(e,t){C.push(O.helpers+'["'+e+'"]('+l(t)+", function ("+k.parentParameterName+"){")}function g(){C.push("}.bind(this));")}function _(e){return-1!==S.indexOf(e)||-1!==w.indexOf(e)||0===e.indexOf(k.helperPre)}function m(e,t){var n=e.replace(k.binderPre,"");C.push(O.binder+"("+n+","+l(t)+", function ("+k.parentParameterName+"){")}function v(){C.push("}.bind(this));")}function x(e){return 0===e.indexOf(k.binderPre)}function y(e){var t=!0;if(b.tag){var n=a();_(b.tag)?(h(b.tag,b.attributes),t=e):x(b.tag)?(m(b.tag,b.attributes),t=e):e||-1!==k.voidRequireTags.indexOf(b.tag)?(c(O.elementVoid,b.tag,b.attributes,n),V.level--,t=!1):b.tag!==k.evaluate.name&&c(O.elementOpen,b.tag,b.attributes,n),V.level++}return b.tag=null,b.attributes={},t}function T(e){A=e,this.reset()}var b,C,A,w,k=n(1),D=n(4),O=n(6).Command,E={},S=[],P="",N='"',I=', "',j="-%%&#__II-",V={level:0,skip:[]};T.prototype.reset=function(){C=[],b={tag:null,attributes:{}},E={},V={level:0,skip:[]}},T.prototype.set=function(e,t){w=e,S=t||[]},T.prototype.write=function(e){var t;switch(e.type){case D.Tag:t=e.name.replace("/",P),0===e.name.indexOf("/")?y(!0)&&t!==k.evaluate.name&&(V.level--,V.level===V.skip[V.skip.length-1]&&(C.push(O.endSkipContent),V.skip.pop()),_(t)?g():x(t)?v():c(O.elementClose,t)):(y(),b.tag=t,b.attributes={});break;case D.Attr:b.attributes[e.name]=e.data;break;case D.Text:t=b.tag,y(),t===k.evaluate.name?C.push(i(e.data)):f(e.data);break;case D.Comment:k.debug&&C.push("\n// "+e.data.replace(k.BREAK_LINE," ")+"\n")}},T.prototype.done=function(){return A(C,u(E))},e.exports=T},function(e,t,n){function a(){function e(e){var t=p+" return _r;",n="var TE=function(m,n,o){this.original=o;this.name=n;(o)?this.stack=this.original.stack:this.stack=null;this.message=o.message+m;};var CE=function(){};CE.prototype=Error.prototype;TE.prototype=new CE();TE.prototype.constructor=TE;";return r.debug?"try {"+e+"} catch (err) {"+n+"throw new TE("+JSON.stringify(i)+", err.name, err);}"+t:e+t}function t(t,s){var i,u=["var _o = _l.elementOpen;","var _c = _l.elementClose;","var _v = _l.elementVoid;","var _t = _l.text;","var _r = {};","_b = _b || function(fn, data, content){ return fn(data, content); };"].join(p)+p;for(var l in s)s.hasOwnProperty(l)&&(u+="var "+l+"=["+s[l]+"];");var d=u+e(t.join(o));return n?(d="return function("+r.parameterName+", "+r.renderContentFnName+", _b){"+d+"};",i=new Function("_l","_h",d)(n,a)):i=new Function(r.parameterName,"_l","_h",r.renderContentFnName,"_b",d),i}var n,a,s,i,o="",p="\n";return t.set=function(e,t,r,o){n=e,a=t,s=r,i=o},t}var r=n(1),s={helpers:"_h",binder:"_b",elementOpen:'_o("',elementClose:'_c("',elementVoid:'_v("',saveRef:function(e,t){return"_r["+e+"] = "+t},text:"_t(",close:");\n",startSkipContent:function(e){return e='"false"'===e?!1:e,e='"true"'===e?!0:e,"if("+e+"){_l.skip();}else{"},endSkipContent:"}"};e.exports={createWrapper:a,Command:s}}])});
--------------------------------------------------------------------------------
/examples/example - exception/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | incremental-dom
6 |
7 |
8 |
9 |
18 |
19 |
20 |
21 |
43 |
73 |
74 |
--------------------------------------------------------------------------------
/examples/example - format text/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | incremental-dom
6 |
7 |
8 |
9 |
10 |
11 |
23 |
28 |
29 |
--------------------------------------------------------------------------------
/examples/example - helpers/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | incremental-dom helpers
6 |
7 |
8 |
9 |
19 |
20 |
21 |
22 |
23 |
31 |
32 |
45 |
46 |
65 |
66 |
156 |
157 |
--------------------------------------------------------------------------------
/examples/example - iterate over properties/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | incremental-dom
6 |
7 |
8 |
9 |
10 |
11 |
20 |
27 |
28 |
--------------------------------------------------------------------------------
/examples/example - simply/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | incremental-dom
6 |
7 |
8 |
9 |
18 |
19 |
20 |
21 |
41 |
69 |
70 |
--------------------------------------------------------------------------------
/examples/example - test/index.css:
--------------------------------------------------------------------------------
1 | button {
2 | font: bold 14px/14px Arial;
3 | margin-left: 10px;
4 | }
5 |
6 | #grid {
7 | margin: 10px;
8 | }
9 |
10 | .box-view {
11 | width: 20px; height: 20px;
12 | float: left;
13 | position: relative;
14 | margin: 8px;
15 | }
16 |
17 | .box {
18 | border-radius: 100px;
19 | width: 20px; height: 10px;
20 | padding: 5px 0;
21 | color: #fff;
22 | font: 10px/10px Arial;
23 | text-align: center;
24 | position: absolute;
25 | }
--------------------------------------------------------------------------------
/examples/example - test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | incremental-dom test
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
In the example with incremental DOM we used the itemplate library for the
25 | compilation of the underscorejs template.
26 |
27 |
-
28 |
Count items:
29 |
30 |
31 |
32 |
33 |
34 |
40 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/examples/example - test/index.js:
--------------------------------------------------------------------------------
1 | var N;
2 | var totalTime;
3 | var loopCount;
4 | var timeout;
5 | var $timing = $('#timing');
6 |
7 | //benchmark
8 | function benchmarkLoop(fn) {
9 | var startDate = new Date();
10 | fn();
11 | var endDate = new Date();
12 | totalTime += endDate - startDate;
13 | loopCount++;
14 | if (loopCount % 20 === 0) {
15 | $timing.text('Performed ' + loopCount + ' iterations in ' + totalTime + ' ms (average ' + (totalTime / loopCount).toFixed(2) + ' ms per loop).');
16 | }
17 | timeout = _.defer(benchmarkLoop, fn);
18 | }
19 |
20 | function benchmarkFlash() {
21 | totalTime = 0;
22 | loopCount = 0;
23 | clearTimeout(timeout);
24 | $timing.text('-');
25 | N = parseInt($('input').val(), 10);
26 | $('#grid').html('');
27 | }
28 |
29 | // ReactJS implementation
30 | var reactInit;
31 | (function () {
32 | var counter;
33 |
34 | var BoxView = React.createClass({
35 | render: function () {
36 | var count = this.props.count + 1;
37 | return (
38 | React.DOM.div(
39 | {className: "box-view"},
40 | React.DOM.div(
41 | {
42 | className: "box",
43 | style: {
44 | top: Math.ceil(Math.sin(count / 10) * 10),
45 | left: Math.ceil(Math.cos(count / 10) * 10),
46 | background: 'rgb(0, 0,' + count % 255 + ')'
47 | }
48 | },
49 | count % 100
50 | )
51 | )
52 | );
53 | }
54 | });
55 |
56 | var BoxesView = React.createClass({
57 | render: function () {
58 | var boxes = _.map(_.range(N), function (i) {
59 | return React.createElement(BoxView, {key: i, count: this.props.count});
60 | }, this);
61 | return React.DOM.div(null, boxes);
62 | }
63 | });
64 |
65 | function reactAnimate() {
66 | ReactDOM.render(React.createElement(BoxesView, {count: counter++}), document.getElementById('grid'));
67 | }
68 |
69 | reactInit = function () {
70 | counter = -1;
71 | benchmarkLoop(reactAnimate);
72 | };
73 |
74 | })();
75 |
76 | // BackboneJS implementation
77 | var backboneInit;
78 | (function () {
79 | var iDOM = true;
80 | var boxes;
81 |
82 | var Box = Backbone.Model.extend({
83 | defaults: {
84 | top: 0,
85 | left: 0,
86 | color: 0,
87 | content: 0,
88 | id: 0
89 | },
90 |
91 | initialize: function () {
92 | this.count = 0;
93 | },
94 |
95 | tick: function () {
96 | var count = this.count += 1;
97 | this.set({
98 | top: Math.ceil(Math.sin(count / 10) * 10),
99 | left: Math.ceil(Math.cos(count / 10) * 10),
100 | color: (count) % 255,
101 | content: count % 100,
102 | id: this.cid
103 | });
104 | }
105 | });
106 |
107 | var BoxView = Backbone.View.extend({
108 | className: 'box-view',
109 |
110 | itemplate: itemplate.compile($('#i-template').html(), IncrementalDOM),
111 |
112 | template: _.template($('#underscore-template').html()),
113 |
114 | initialize: function () {
115 | this.model.bind('change', this.render, this);
116 | },
117 |
118 | render: function () {
119 | if (iDOM) {
120 | IncrementalDOM.patch(this.el, this.itemplate, this.model.attributes);
121 | } else {
122 | this.$el.html(this.template(this.model.attributes));
123 | }
124 | return this;
125 | }
126 | });
127 |
128 | function backboneAnimate() {
129 | for (var i = 0, l = boxes.length; i < l; i++) {
130 | boxes[i].tick();
131 | }
132 | }
133 |
134 | backboneInit = function (incremental) {
135 | iDOM = incremental;
136 | boxes = _.map(_.range(N), function (i) {
137 | var box = new Box({number: i});
138 | var view = new BoxView({model: box});
139 | $('#grid').append(view.render().el);
140 | return box;
141 | });
142 | benchmarkLoop(backboneAnimate);
143 | };
144 |
145 | })();
146 |
147 | $('#backbone').click(function () {
148 | benchmarkFlash();
149 | backboneInit(false);
150 | });
151 |
152 | $('#incremental').click(function () {
153 | benchmarkFlash();
154 | backboneInit(true);
155 | });
156 |
157 | $('#react').click(function () {
158 | benchmarkFlash();
159 | reactInit();
160 | });
--------------------------------------------------------------------------------
/examples/example - unwarped function/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | incremental-dom helpers
6 |
7 |
8 |
9 |
10 |
11 |
12 |
18 |
19 |
40 |
41 |
--------------------------------------------------------------------------------
/examples/lib/incremental-dom.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * @license
4 | * Copyright 2015 The Incremental DOM Authors. All Rights Reserved.
5 | *
6 | * Licensed under the Apache License, Version 2.0 (the "License");
7 | * you may not use this file except in compliance with the License.
8 | * You may obtain a copy of the License at
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS-IS" BASIS,
14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | * See the License for the specific language governing permissions and
16 | * limitations under the License.
17 | */
18 |
19 | (function (global, factory) {
20 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
21 | typeof define === 'function' && define.amd ? define(['exports'], factory) :
22 | (factory((global.IncrementalDOM = global.IncrementalDOM || {})));
23 | }(this, function (exports) { 'use strict';
24 |
25 | /**
26 | * Copyright 2015 The Incremental DOM Authors. All Rights Reserved.
27 | *
28 | * Licensed under the Apache License, Version 2.0 (the "License");
29 | * you may not use this file except in compliance with the License.
30 | * You may obtain a copy of the License at
31 | *
32 | * http://www.apache.org/licenses/LICENSE-2.0
33 | *
34 | * Unless required by applicable law or agreed to in writing, software
35 | * distributed under the License is distributed on an "AS-IS" BASIS,
36 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
37 | * See the License for the specific language governing permissions and
38 | * limitations under the License.
39 | */
40 |
41 | /**
42 | * A cached reference to the hasOwnProperty function.
43 | */
44 | var hasOwnProperty = Object.prototype.hasOwnProperty;
45 |
46 | /**
47 | * A cached reference to the create function.
48 | */
49 | var create = Object.create;
50 |
51 | /**
52 | * Used to prevent property collisions between our "map" and its prototype.
53 | * @param {!Object} map The map to check.
54 | * @param {string} property The property to check.
55 | * @return {boolean} Whether map has property.
56 | */
57 | var has = function (map, property) {
58 | return hasOwnProperty.call(map, property);
59 | };
60 |
61 | /**
62 | * Creates an map object without a prototype.
63 | * @return {!Object}
64 | */
65 | var createMap = function () {
66 | return create(null);
67 | };
68 |
69 | /**
70 | * Keeps track of information needed to perform diffs for a given DOM node.
71 | * @param {!string} nodeName
72 | * @param {?string=} key
73 | * @constructor
74 | */
75 | function NodeData(nodeName, key) {
76 | /**
77 | * The attributes and their values.
78 | * @const {!Object}
79 | */
80 | this.attrs = createMap();
81 |
82 | /**
83 | * An array of attribute name/value pairs, used for quickly diffing the
84 | * incomming attributes to see if the DOM node's attributes need to be
85 | * updated.
86 | * @const {Array<*>}
87 | */
88 | this.attrsArr = [];
89 |
90 | /**
91 | * The incoming attributes for this Node, before they are updated.
92 | * @const {!Object}
93 | */
94 | this.newAttrs = createMap();
95 |
96 | /**
97 | * The key used to identify this node, used to preserve DOM nodes when they
98 | * move within their parent.
99 | * @const
100 | */
101 | this.key = key;
102 |
103 | /**
104 | * Keeps track of children within this node by their key.
105 | * {?Object}
106 | */
107 | this.keyMap = null;
108 |
109 | /**
110 | * Whether or not the keyMap is currently valid.
111 | * {boolean}
112 | */
113 | this.keyMapValid = true;
114 |
115 | /**
116 | * The node name for this node.
117 | * @const {string}
118 | */
119 | this.nodeName = nodeName;
120 |
121 | /**
122 | * @type {?string}
123 | */
124 | this.text = null;
125 | }
126 |
127 | /**
128 | * Initializes a NodeData object for a Node.
129 | *
130 | * @param {Node} node The node to initialize data for.
131 | * @param {string} nodeName The node name of node.
132 | * @param {?string=} key The key that identifies the node.
133 | * @return {!NodeData} The newly initialized data object
134 | */
135 | var initData = function (node, nodeName, key) {
136 | var data = new NodeData(nodeName, key);
137 | node['__incrementalDOMData'] = data;
138 | return data;
139 | };
140 |
141 | /**
142 | * Retrieves the NodeData object for a Node, creating it if necessary.
143 | *
144 | * @param {Node} node The node to retrieve the data for.
145 | * @return {!NodeData} The NodeData for this Node.
146 | */
147 | var getData = function (node) {
148 | var data = node['__incrementalDOMData'];
149 |
150 | if (!data) {
151 | var nodeName = node.nodeName.toLowerCase();
152 | var key = null;
153 |
154 | if (node instanceof Element) {
155 | key = node.getAttribute('key');
156 | }
157 |
158 | data = initData(node, nodeName, key);
159 | }
160 |
161 | return data;
162 | };
163 |
164 | /**
165 | * Copyright 2015 The Incremental DOM Authors. All Rights Reserved.
166 | *
167 | * Licensed under the Apache License, Version 2.0 (the "License");
168 | * you may not use this file except in compliance with the License.
169 | * You may obtain a copy of the License at
170 | *
171 | * http://www.apache.org/licenses/LICENSE-2.0
172 | *
173 | * Unless required by applicable law or agreed to in writing, software
174 | * distributed under the License is distributed on an "AS-IS" BASIS,
175 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
176 | * See the License for the specific language governing permissions and
177 | * limitations under the License.
178 | */
179 |
180 | /** @const */
181 | var symbols = {
182 | default: '__default',
183 |
184 | placeholder: '__placeholder'
185 | };
186 |
187 | /**
188 | * Applies an attribute or property to a given Element. If the value is null
189 | * or undefined, it is removed from the Element. Otherwise, the value is set
190 | * as an attribute.
191 | * @param {!Element} el
192 | * @param {string} name The attribute's name.
193 | * @param {?(boolean|number|string)=} value The attribute's value.
194 | */
195 | var applyAttr = function (el, name, value) {
196 | if (value == null) {
197 | el.removeAttribute(name);
198 | } else {
199 | el.setAttribute(name, value);
200 | }
201 | };
202 |
203 | /**
204 | * Applies a property to a given Element.
205 | * @param {!Element} el
206 | * @param {string} name The property's name.
207 | * @param {*} value The property's value.
208 | */
209 | var applyProp = function (el, name, value) {
210 | el[name] = value;
211 | };
212 |
213 | /**
214 | * Applies a style to an Element. No vendor prefix expansion is done for
215 | * property names/values.
216 | * @param {!Element} el
217 | * @param {string} name The attribute's name.
218 | * @param {*} style The style to set. Either a string of css or an object
219 | * containing property-value pairs.
220 | */
221 | var applyStyle = function (el, name, style) {
222 | if (typeof style === 'string') {
223 | el.style.cssText = style;
224 | } else {
225 | el.style.cssText = '';
226 | var elStyle = el.style;
227 | var obj = /** @type {!Object} */style;
228 |
229 | for (var prop in obj) {
230 | if (has(obj, prop)) {
231 | elStyle[prop] = obj[prop];
232 | }
233 | }
234 | }
235 | };
236 |
237 | /**
238 | * Updates a single attribute on an Element.
239 | * @param {!Element} el
240 | * @param {string} name The attribute's name.
241 | * @param {*} value The attribute's value. If the value is an object or
242 | * function it is set on the Element, otherwise, it is set as an HTML
243 | * attribute.
244 | */
245 | var applyAttributeTyped = function (el, name, value) {
246 | var type = typeof value;
247 |
248 | if (type === 'object' || type === 'function') {
249 | applyProp(el, name, value);
250 | } else {
251 | applyAttr(el, name, /** @type {?(boolean|number|string)} */value);
252 | }
253 | };
254 |
255 | /**
256 | * Calls the appropriate attribute mutator for this attribute.
257 | * @param {!Element} el
258 | * @param {string} name The attribute's name.
259 | * @param {*} value The attribute's value.
260 | */
261 | var updateAttribute = function (el, name, value) {
262 | var data = getData(el);
263 | var attrs = data.attrs;
264 |
265 | if (attrs[name] === value) {
266 | return;
267 | }
268 |
269 | var mutator = attributes[name] || attributes[symbols.default];
270 | mutator(el, name, value);
271 |
272 | attrs[name] = value;
273 | };
274 |
275 | /**
276 | * A publicly mutable object to provide custom mutators for attributes.
277 | * @const {!Object}
278 | */
279 | var attributes = createMap();
280 |
281 | // Special generic mutator that's called for any attribute that does not
282 | // have a specific mutator.
283 | attributes[symbols.default] = applyAttributeTyped;
284 |
285 | attributes[symbols.placeholder] = function () {};
286 |
287 | attributes['style'] = applyStyle;
288 |
289 | /**
290 | * Gets the namespace to create an element (of a given tag) in.
291 | * @param {string} tag The tag to get the namespace for.
292 | * @param {?Node} parent
293 | * @return {?string} The namespace to create the tag in.
294 | */
295 | var getNamespaceForTag = function (tag, parent) {
296 | if (tag === 'svg') {
297 | return 'http://www.w3.org/2000/svg';
298 | }
299 |
300 | if (getData(parent).nodeName === 'foreignObject') {
301 | return null;
302 | }
303 |
304 | return parent.namespaceURI;
305 | };
306 |
307 | /**
308 | * Creates an Element.
309 | * @param {Document} doc The document with which to create the Element.
310 | * @param {?Node} parent
311 | * @param {string} tag The tag for the Element.
312 | * @param {?string=} key A key to identify the Element.
313 | * @param {?Array<*>=} statics An array of attribute name/value pairs of the
314 | * static attributes for the Element.
315 | * @return {!Element}
316 | */
317 | var createElement = function (doc, parent, tag, key, statics) {
318 | var namespace = getNamespaceForTag(tag, parent);
319 | var el = undefined;
320 |
321 | if (namespace) {
322 | el = doc.createElementNS(namespace, tag);
323 | } else {
324 | el = doc.createElement(tag);
325 | }
326 |
327 | initData(el, tag, key);
328 |
329 | if (statics) {
330 | for (var i = 0; i < statics.length; i += 2) {
331 | updateAttribute(el, /** @type {!string}*/statics[i], statics[i + 1]);
332 | }
333 | }
334 |
335 | return el;
336 | };
337 |
338 | /**
339 | * Creates a Text Node.
340 | * @param {Document} doc The document with which to create the Element.
341 | * @return {!Text}
342 | */
343 | var createText = function (doc) {
344 | var node = doc.createTextNode('');
345 | initData(node, '#text', null);
346 | return node;
347 | };
348 |
349 | /**
350 | * Creates a mapping that can be used to look up children using a key.
351 | * @param {?Node} el
352 | * @return {!Object} A mapping of keys to the children of the
353 | * Element.
354 | */
355 | var createKeyMap = function (el) {
356 | var map = createMap();
357 | var children = el.children;
358 | var count = children.length;
359 |
360 | for (var i = 0; i < count; i += 1) {
361 | var child = children[i];
362 | var key = getData(child).key;
363 |
364 | if (key) {
365 | map[key] = child;
366 | }
367 | }
368 |
369 | return map;
370 | };
371 |
372 | /**
373 | * Retrieves the mapping of key to child node for a given Element, creating it
374 | * if necessary.
375 | * @param {?Node} el
376 | * @return {!Object} A mapping of keys to child Elements
377 | */
378 | var getKeyMap = function (el) {
379 | var data = getData(el);
380 |
381 | if (!data.keyMap) {
382 | data.keyMap = createKeyMap(el);
383 | }
384 |
385 | return data.keyMap;
386 | };
387 |
388 | /**
389 | * Retrieves a child from the parent with the given key.
390 | * @param {?Node} parent
391 | * @param {?string=} key
392 | * @return {?Node} The child corresponding to the key.
393 | */
394 | var getChild = function (parent, key) {
395 | return key ? getKeyMap(parent)[key] : null;
396 | };
397 |
398 | /**
399 | * Registers an element as being a child. The parent will keep track of the
400 | * child using the key. The child can be retrieved using the same key using
401 | * getKeyMap. The provided key should be unique within the parent Element.
402 | * @param {?Node} parent The parent of child.
403 | * @param {string} key A key to identify the child with.
404 | * @param {!Node} child The child to register.
405 | */
406 | var registerChild = function (parent, key, child) {
407 | getKeyMap(parent)[key] = child;
408 | };
409 |
410 | /**
411 | * Copyright 2015 The Incremental DOM Authors. All Rights Reserved.
412 | *
413 | * Licensed under the Apache License, Version 2.0 (the "License");
414 | * you may not use this file except in compliance with the License.
415 | * You may obtain a copy of the License at
416 | *
417 | * http://www.apache.org/licenses/LICENSE-2.0
418 | *
419 | * Unless required by applicable law or agreed to in writing, software
420 | * distributed under the License is distributed on an "AS-IS" BASIS,
421 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
422 | * See the License for the specific language governing permissions and
423 | * limitations under the License.
424 | */
425 |
426 | /** @const */
427 | var notifications = {
428 | /**
429 | * Called after patch has compleated with any Nodes that have been created
430 | * and added to the DOM.
431 | * @type {?function(Array)}
432 | */
433 | nodesCreated: null,
434 |
435 | /**
436 | * Called after patch has compleated with any Nodes that have been removed
437 | * from the DOM.
438 | * Note it's an applications responsibility to handle any childNodes.
439 | * @type {?function(Array)}
440 | */
441 | nodesDeleted: null
442 | };
443 |
444 | /**
445 | * Keeps track of the state of a patch.
446 | * @constructor
447 | */
448 | function Context() {
449 | /**
450 | * @type {(Array|undefined)}
451 | */
452 | this.created = notifications.nodesCreated && [];
453 |
454 | /**
455 | * @type {(Array|undefined)}
456 | */
457 | this.deleted = notifications.nodesDeleted && [];
458 | }
459 |
460 | /**
461 | * @param {!Node} node
462 | */
463 | Context.prototype.markCreated = function (node) {
464 | if (this.created) {
465 | this.created.push(node);
466 | }
467 | };
468 |
469 | /**
470 | * @param {!Node} node
471 | */
472 | Context.prototype.markDeleted = function (node) {
473 | if (this.deleted) {
474 | this.deleted.push(node);
475 | }
476 | };
477 |
478 | /**
479 | * Notifies about nodes that were created during the patch opearation.
480 | */
481 | Context.prototype.notifyChanges = function () {
482 | if (this.created && this.created.length > 0) {
483 | notifications.nodesCreated(this.created);
484 | }
485 |
486 | if (this.deleted && this.deleted.length > 0) {
487 | notifications.nodesDeleted(this.deleted);
488 | }
489 | };
490 |
491 | /**
492 | * Copyright 2015 The Incremental DOM Authors. All Rights Reserved.
493 | *
494 | * Licensed under the Apache License, Version 2.0 (the "License");
495 | * you may not use this file except in compliance with the License.
496 | * You may obtain a copy of the License at
497 | *
498 | * http://www.apache.org/licenses/LICENSE-2.0
499 | *
500 | * Unless required by applicable law or agreed to in writing, software
501 | * distributed under the License is distributed on an "AS-IS" BASIS,
502 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
503 | * See the License for the specific language governing permissions and
504 | * limitations under the License.
505 | */
506 |
507 | /**
508 | * Keeps track whether or not we are in an attributes declaration (after
509 | * elementOpenStart, but before elementOpenEnd).
510 | * @type {boolean}
511 | */
512 | var inAttributes = false;
513 |
514 | /**
515 | * Keeps track whether or not we are in an element that should not have its
516 | * children cleared.
517 | * @type {boolean}
518 | */
519 | var inSkip = false;
520 |
521 | /**
522 | * Makes sure that there is a current patch context.
523 | * @param {*} context
524 | */
525 | var assertInPatch = function (context) {
526 | if (!context) {
527 | throw new Error('Cannot call currentElement() unless in patch.');
528 | }
529 | };
530 |
531 | /**
532 | * Makes sure that keyed Element matches the tag name provided.
533 | * @param {!string} nodeName The nodeName of the node that is being matched.
534 | * @param {string=} tag The tag name of the Element.
535 | * @param {?string=} key The key of the Element.
536 | */
537 | var assertKeyedTagMatches = function (nodeName, tag, key) {
538 | if (nodeName !== tag) {
539 | throw new Error('Was expecting node with key "' + key + '" to be a ' + tag + ', not a ' + nodeName + '.');
540 | }
541 | };
542 |
543 | /**
544 | * Makes sure that a patch closes every node that it opened.
545 | * @param {?Node} openElement
546 | * @param {!Node|!DocumentFragment} root
547 | */
548 | var assertNoUnclosedTags = function (openElement, root) {
549 | if (openElement === root) {
550 | return;
551 | }
552 |
553 | var currentElement = openElement;
554 | var openTags = [];
555 | while (currentElement && currentElement !== root) {
556 | openTags.push(currentElement.nodeName.toLowerCase());
557 | currentElement = currentElement.parentNode;
558 | }
559 |
560 | throw new Error('One or more tags were not closed:\n' + openTags.join('\n'));
561 | };
562 |
563 | /**
564 | * Makes sure that the caller is not where attributes are expected.
565 | * @param {string} functionName
566 | */
567 | var assertNotInAttributes = function (functionName) {
568 | if (inAttributes) {
569 | throw new Error(functionName + '() can not be called between ' + 'elementOpenStart() and elementOpenEnd().');
570 | }
571 | };
572 |
573 | /**
574 | * Makes sure that the caller is not inside an element that has declared skip.
575 | * @param {string} functionName
576 | */
577 | var assertNotInSkip = function (functionName) {
578 | if (inSkip) {
579 | throw new Error(functionName + '() may not be called inside an element ' + 'that has called skip().');
580 | }
581 | };
582 |
583 | /**
584 | * Makes sure that the caller is where attributes are expected.
585 | * @param {string} functionName
586 | */
587 | var assertInAttributes = function (functionName) {
588 | if (!inAttributes) {
589 | throw new Error(functionName + '() can only be called after calling ' + 'elementOpenStart().');
590 | }
591 | };
592 |
593 | /**
594 | * Makes sure the patch closes virtual attributes call
595 | */
596 | var assertVirtualAttributesClosed = function () {
597 | if (inAttributes) {
598 | throw new Error('elementOpenEnd() must be called after calling ' + 'elementOpenStart().');
599 | }
600 | };
601 |
602 | /**
603 | * Makes sure that placeholders have a key specified. Otherwise, conditional
604 | * placeholders and conditional elements next to placeholders will cause
605 | * placeholder elements to be re-used as non-placeholders and vice versa.
606 | * @param {string} key
607 | */
608 | var assertPlaceholderKeySpecified = function (key) {
609 | if (!key) {
610 | throw new Error('elementPlaceholder() requires a key.');
611 | }
612 | };
613 |
614 | /**
615 | * Makes sure that tags are correctly nested.
616 | * @param {string} nodeName
617 | * @param {string} tag
618 | */
619 | var assertCloseMatchesOpenTag = function (nodeName, tag) {
620 | if (nodeName !== tag) {
621 | throw new Error('Received a call to close "' + tag + '" but "' + nodeName + '" was open.');
622 | }
623 | };
624 |
625 | /**
626 | * Makes sure that no children elements have been declared yet in the current
627 | * element.
628 | * @param {string} functionName
629 | * @param {?Node} previousNode
630 | */
631 | var assertNoChildrenDeclaredYet = function (functionName, previousNode) {
632 | if (previousNode !== null) {
633 | throw new Error(functionName + '() must come before any child ' + 'declarations inside the current element.');
634 | }
635 | };
636 |
637 | /**
638 | * Checks that a call to patchOuter actually patched the element.
639 | * @param {?Node} node The node requested to be patched.
640 | * @param {?Node} currentNode The currentNode after the patch.
641 | */
642 | var assertPatchElementNotEmpty = function (node, currentNode) {
643 | if (node === currentNode) {
644 | throw new Error('There must be exactly one top level call corresponding ' + 'to the patched element.');
645 | }
646 | };
647 |
648 | /**
649 | * Checks that a call to patchOuter actually patched the element.
650 | * @param {?Node} node The node requested to be patched.
651 | * @param {?Node} previousNode The previousNode after the patch.
652 | */
653 | var assertPatchElementNoExtras = function (node, previousNode) {
654 | if (node !== previousNode) {
655 | throw new Error('There must be exactly one top level call corresponding ' + 'to the patched element.');
656 | }
657 | };
658 |
659 | /**
660 | * Updates the state of being in an attribute declaration.
661 | * @param {boolean} value
662 | * @return {boolean} the previous value.
663 | */
664 | var setInAttributes = function (value) {
665 | var previous = inAttributes;
666 | inAttributes = value;
667 | return previous;
668 | };
669 |
670 | /**
671 | * Updates the state of being in a skip element.
672 | * @param {boolean} value
673 | * @return {boolean} the previous value.
674 | */
675 | var setInSkip = function (value) {
676 | var previous = inSkip;
677 | inSkip = value;
678 | return previous;
679 | };
680 |
681 | /** @type {?Context} */
682 | var context = null;
683 |
684 | /** @type {?Node} */
685 | var currentNode = undefined;
686 |
687 | /** @type {?Node} */
688 | var currentParent = undefined;
689 |
690 | /** @type {?Element|?DocumentFragment} */
691 | var root = undefined;
692 |
693 | /** @type {?Document} */
694 | var doc = undefined;
695 |
696 | /**
697 | * Sets up and restores a patch context, running the patch function with the
698 | * provided data.
699 | * @param {!Element|!DocumentFragment} node The Element or Document
700 | * where the patch should start.
701 | * @param {!function(T)} fn The patching function.
702 | * @param {T=} data An argument passed to fn.
703 | * @template T
704 | */
705 | var runPatch = function (node, fn, data) {
706 | var prevContext = context;
707 | var prevRoot = root;
708 | var prevDoc = doc;
709 | var prevCurrentNode = currentNode;
710 | var prevCurrentParent = currentParent;
711 | var previousInAttributes = false;
712 | var previousInSkip = false;
713 |
714 | context = new Context();
715 | root = node;
716 | doc = node.ownerDocument;
717 |
718 | if ('development' !== 'production') {
719 | previousInAttributes = setInAttributes(false);
720 | previousInSkip = setInSkip(false);
721 | }
722 |
723 | fn(data);
724 |
725 | if ('development' !== 'production') {
726 | assertVirtualAttributesClosed();
727 | setInAttributes(previousInAttributes);
728 | setInSkip(previousInSkip);
729 | }
730 |
731 | context.notifyChanges();
732 |
733 | context = prevContext;
734 | root = prevRoot;
735 | doc = prevDoc;
736 | currentNode = prevCurrentNode;
737 | currentParent = prevCurrentParent;
738 | };
739 |
740 | /**
741 | * Patches the document starting at node with the provided function. This
742 | * function may be called during an existing patch operation.
743 | * @param {!Element|!DocumentFragment} node The Element or Document
744 | * to patch.
745 | * @param {!function(T)} fn A function containing elementOpen/elementClose/etc.
746 | * calls that describe the DOM.
747 | * @param {T=} data An argument passed to fn to represent DOM state.
748 | * @template T
749 | */
750 | var patchInner = function (node, fn, data) {
751 | runPatch(node, function (data) {
752 | currentNode = node;
753 | currentParent = node.parentNode;
754 |
755 | enterNode();
756 | fn(data);
757 | exitNode();
758 |
759 | if ('development' !== 'production') {
760 | assertNoUnclosedTags(currentNode, node);
761 | }
762 | }, data);
763 | };
764 |
765 | /**
766 | * Patches an Element with the the provided function. Exactly one top level
767 | * element call should be made corresponding to `node`.
768 | * @param {!Element} node The Element where the patch should start.
769 | * @param {!function(T)} fn A function containing elementOpen/elementClose/etc.
770 | * calls that describe the DOM. This should have at most one top level
771 | * element call.
772 | * @param {T=} data An argument passed to fn to represent DOM state.
773 | * @template T
774 | */
775 | var patchOuter = function (node, fn, data) {
776 | runPatch(node, function (data) {
777 | currentNode = /** @type {!Element} */{ nextSibling: node };
778 | currentParent = node.parentNode;
779 |
780 | fn(data);
781 |
782 | if ('development' !== 'production') {
783 | assertPatchElementNotEmpty(node, currentNode.nextSibling);
784 | assertPatchElementNoExtras(node, currentNode);
785 | }
786 | }, data);
787 | };
788 |
789 | /**
790 | * Checks whether or not the current node matches the specified nodeName and
791 | * key.
792 | *
793 | * @param {?string} nodeName The nodeName for this node.
794 | * @param {?string=} key An optional key that identifies a node.
795 | * @return {boolean} True if the node matches, false otherwise.
796 | */
797 | var matches = function (nodeName, key) {
798 | var data = getData(currentNode);
799 |
800 | // Key check is done using double equals as we want to treat a null key the
801 | // same as undefined. This should be okay as the only values allowed are
802 | // strings, null and undefined so the == semantics are not too weird.
803 | return nodeName === data.nodeName && key == data.key;
804 | };
805 |
806 | /**
807 | * Aligns the virtual Element definition with the actual DOM, moving the
808 | * corresponding DOM node to the correct location or creating it if necessary.
809 | * @param {string} nodeName For an Element, this should be a valid tag string.
810 | * For a Text, this should be #text.
811 | * @param {?string=} key The key used to identify this element.
812 | * @param {?Array<*>=} statics For an Element, this should be an array of
813 | * name-value pairs.
814 | */
815 | var alignWithDOM = function (nodeName, key, statics) {
816 | if (currentNode && matches(nodeName, key)) {
817 | return;
818 | }
819 |
820 | var node = undefined;
821 |
822 | // Check to see if the node has moved within the parent.
823 | if (key) {
824 | node = getChild(currentParent, key);
825 | if (node && 'development' !== 'production') {
826 | assertKeyedTagMatches(getData(node).nodeName, nodeName, key);
827 | }
828 | }
829 |
830 | // Create the node if it doesn't exist.
831 | if (!node) {
832 | if (nodeName === '#text') {
833 | node = createText(doc);
834 | } else {
835 | node = createElement(doc, currentParent, nodeName, key, statics);
836 | }
837 |
838 | if (key) {
839 | registerChild(currentParent, key, node);
840 | }
841 |
842 | context.markCreated(node);
843 | }
844 |
845 | // If the node has a key, remove it from the DOM to prevent a large number
846 | // of re-orders in the case that it moved far or was completely removed.
847 | // Since we hold on to a reference through the keyMap, we can always add it
848 | // back.
849 | if (currentNode && getData(currentNode).key) {
850 | currentParent.replaceChild(node, currentNode);
851 | getData(currentParent).keyMapValid = false;
852 | } else {
853 | currentParent.insertBefore(node, currentNode);
854 | }
855 |
856 | currentNode = node;
857 | };
858 |
859 | /**
860 | * Clears out any unvisited Nodes, as the corresponding virtual element
861 | * functions were never called for them.
862 | */
863 | var clearUnvisitedDOM = function () {
864 | var node = currentParent;
865 | var data = getData(node);
866 | var keyMap = data.keyMap;
867 | var keyMapValid = data.keyMapValid;
868 | var child = node.lastChild;
869 | var key = undefined;
870 |
871 | if (child === currentNode && keyMapValid) {
872 | return;
873 | }
874 |
875 | if (data.attrs[symbols.placeholder] && node !== root) {
876 | if ('development' !== 'production') {
877 | console.warn('symbols.placeholder will be removed in Incremental DOM' + ' 0.5 use skip() instead');
878 | }
879 | return;
880 | }
881 |
882 | while (child !== currentNode) {
883 | node.removeChild(child);
884 | context.markDeleted( /** @type {!Node}*/child);
885 |
886 | key = getData(child).key;
887 | if (key) {
888 | delete keyMap[key];
889 | }
890 | child = node.lastChild;
891 | }
892 |
893 | // Clean the keyMap, removing any unusued keys.
894 | if (!keyMapValid) {
895 | for (key in keyMap) {
896 | child = keyMap[key];
897 | if (child.parentNode !== node) {
898 | context.markDeleted(child);
899 | delete keyMap[key];
900 | }
901 | }
902 |
903 | data.keyMapValid = true;
904 | }
905 | };
906 |
907 | /**
908 | * Changes to the first child of the current node.
909 | */
910 | var enterNode = function () {
911 | currentParent = currentNode;
912 | currentNode = null;
913 | };
914 |
915 | /**
916 | * Changes to the next sibling of the current node.
917 | */
918 | var nextNode = function () {
919 | if (currentNode) {
920 | currentNode = currentNode.nextSibling;
921 | } else {
922 | currentNode = currentParent.firstChild;
923 | }
924 | };
925 |
926 | /**
927 | * Changes to the parent of the current node, removing any unvisited children.
928 | */
929 | var exitNode = function () {
930 | clearUnvisitedDOM();
931 |
932 | currentNode = currentParent;
933 | currentParent = currentParent.parentNode;
934 | };
935 |
936 | /**
937 | * Makes sure that the current node is an Element with a matching tagName and
938 | * key.
939 | *
940 | * @param {string} tag The element's tag.
941 | * @param {?string=} key The key used to identify this element. This can be an
942 | * empty string, but performance may be better if a unique value is used
943 | * when iterating over an array of items.
944 | * @param {?Array<*>=} statics An array of attribute name/value pairs of the
945 | * static attributes for the Element. These will only be set once when the
946 | * Element is created.
947 | * @return {!Element} The corresponding Element.
948 | */
949 | var coreElementOpen = function (tag, key, statics) {
950 | nextNode();
951 | alignWithDOM(tag, key, statics);
952 | enterNode();
953 | return (/** @type {!Element} */currentParent
954 | );
955 | };
956 |
957 | /**
958 | * Closes the currently open Element, removing any unvisited children if
959 | * necessary.
960 | *
961 | * @return {!Element} The corresponding Element.
962 | */
963 | var coreElementClose = function () {
964 | if ('development' !== 'production') {
965 | setInSkip(false);
966 | }
967 |
968 | exitNode();
969 | return (/** @type {!Element} */currentNode
970 | );
971 | };
972 |
973 | /**
974 | * Makes sure the current node is a Text node and creates a Text node if it is
975 | * not.
976 | *
977 | * @return {!Text} The corresponding Text Node.
978 | */
979 | var coreText = function () {
980 | nextNode();
981 | alignWithDOM('#text', null, null);
982 | return (/** @type {!Text} */currentNode
983 | );
984 | };
985 |
986 | /**
987 | * Gets the current Element being patched.
988 | * @return {!Element}
989 | */
990 | var currentElement = function () {
991 | if ('development' !== 'production') {
992 | assertInPatch(context);
993 | assertNotInAttributes('currentElement');
994 | }
995 | return (/** @type {!Element} */currentParent
996 | );
997 | };
998 |
999 | /**
1000 | * Skips the children in a subtree, allowing an Element to be closed without
1001 | * clearing out the children.
1002 | */
1003 | var skip = function () {
1004 | if ('development' !== 'production') {
1005 | assertNoChildrenDeclaredYet('skip', currentNode);
1006 | setInSkip(true);
1007 | }
1008 | currentNode = currentParent.lastChild;
1009 | };
1010 |
1011 | /**
1012 | * The offset in the virtual element declaration where the attributes are
1013 | * specified.
1014 | * @const
1015 | */
1016 | var ATTRIBUTES_OFFSET = 3;
1017 |
1018 | /**
1019 | * Builds an array of arguments for use with elementOpenStart, attr and
1020 | * elementOpenEnd.
1021 | * @const {Array<*>}
1022 | */
1023 | var argsBuilder = [];
1024 |
1025 | /**
1026 | * @param {string} tag The element's tag.
1027 | * @param {?string=} key The key used to identify this element. This can be an
1028 | * empty string, but performance may be better if a unique value is used
1029 | * when iterating over an array of items.
1030 | * @param {?Array<*>=} statics An array of attribute name/value pairs of the
1031 | * static attributes for the Element. These will only be set once when the
1032 | * Element is created.
1033 | * @param {...*} const_args Attribute name/value pairs of the dynamic attributes
1034 | * for the Element.
1035 | * @return {!Element} The corresponding Element.
1036 | */
1037 | var elementOpen = function (tag, key, statics, const_args) {
1038 | if ('development' !== 'production') {
1039 | assertNotInAttributes('elementOpen');
1040 | assertNotInSkip('elementOpen');
1041 | }
1042 |
1043 | var node = coreElementOpen(tag, key, statics);
1044 | var data = getData(node);
1045 |
1046 | /*
1047 | * Checks to see if one or more attributes have changed for a given Element.
1048 | * When no attributes have changed, this is much faster than checking each
1049 | * individual argument. When attributes have changed, the overhead of this is
1050 | * minimal.
1051 | */
1052 | var attrsArr = data.attrsArr;
1053 | var newAttrs = data.newAttrs;
1054 | var attrsChanged = false;
1055 | var i = ATTRIBUTES_OFFSET;
1056 | var j = 0;
1057 |
1058 | for (; i < arguments.length; i += 1, j += 1) {
1059 | if (attrsArr[j] !== arguments[i]) {
1060 | attrsChanged = true;
1061 | break;
1062 | }
1063 | }
1064 |
1065 | for (; i < arguments.length; i += 1, j += 1) {
1066 | attrsArr[j] = arguments[i];
1067 | }
1068 |
1069 | if (j < attrsArr.length) {
1070 | attrsChanged = true;
1071 | attrsArr.length = j;
1072 | }
1073 |
1074 | /*
1075 | * Actually perform the attribute update.
1076 | */
1077 | if (attrsChanged) {
1078 | for (i = ATTRIBUTES_OFFSET; i < arguments.length; i += 2) {
1079 | newAttrs[arguments[i]] = arguments[i + 1];
1080 | }
1081 |
1082 | for (var _attr in newAttrs) {
1083 | updateAttribute(node, _attr, newAttrs[_attr]);
1084 | newAttrs[_attr] = undefined;
1085 | }
1086 | }
1087 |
1088 | return node;
1089 | };
1090 |
1091 | /**
1092 | * Declares a virtual Element at the current location in the document. This
1093 | * corresponds to an opening tag and a elementClose tag is required. This is
1094 | * like elementOpen, but the attributes are defined using the attr function
1095 | * rather than being passed as arguments. Must be folllowed by 0 or more calls
1096 | * to attr, then a call to elementOpenEnd.
1097 | * @param {string} tag The element's tag.
1098 | * @param {?string=} key The key used to identify this element. This can be an
1099 | * empty string, but performance may be better if a unique value is used
1100 | * when iterating over an array of items.
1101 | * @param {?Array<*>=} statics An array of attribute name/value pairs of the
1102 | * static attributes for the Element. These will only be set once when the
1103 | * Element is created.
1104 | */
1105 | var elementOpenStart = function (tag, key, statics) {
1106 | if ('development' !== 'production') {
1107 | assertNotInAttributes('elementOpenStart');
1108 | setInAttributes(true);
1109 | }
1110 |
1111 | argsBuilder[0] = tag;
1112 | argsBuilder[1] = key;
1113 | argsBuilder[2] = statics;
1114 | };
1115 |
1116 | /***
1117 | * Defines a virtual attribute at this point of the DOM. This is only valid
1118 | * when called between elementOpenStart and elementOpenEnd.
1119 | *
1120 | * @param {string} name
1121 | * @param {*} value
1122 | */
1123 | var attr = function (name, value) {
1124 | if ('development' !== 'production') {
1125 | assertInAttributes('attr');
1126 | }
1127 |
1128 | argsBuilder.push(name, value);
1129 | };
1130 |
1131 | /**
1132 | * Closes an open tag started with elementOpenStart.
1133 | * @return {!Element} The corresponding Element.
1134 | */
1135 | var elementOpenEnd = function () {
1136 | if ('development' !== 'production') {
1137 | assertInAttributes('elementOpenEnd');
1138 | setInAttributes(false);
1139 | }
1140 |
1141 | var node = elementOpen.apply(null, argsBuilder);
1142 | argsBuilder.length = 0;
1143 | return node;
1144 | };
1145 |
1146 | /**
1147 | * Closes an open virtual Element.
1148 | *
1149 | * @param {string} tag The element's tag.
1150 | * @return {!Element} The corresponding Element.
1151 | */
1152 | var elementClose = function (tag) {
1153 | if ('development' !== 'production') {
1154 | assertNotInAttributes('elementClose');
1155 | }
1156 |
1157 | var node = coreElementClose();
1158 |
1159 | if ('development' !== 'production') {
1160 | assertCloseMatchesOpenTag(getData(node).nodeName, tag);
1161 | }
1162 |
1163 | return node;
1164 | };
1165 |
1166 | /**
1167 | * Declares a virtual Element at the current location in the document that has
1168 | * no children.
1169 | * @param {string} tag The element's tag.
1170 | * @param {?string=} key The key used to identify this element. This can be an
1171 | * empty string, but performance may be better if a unique value is used
1172 | * when iterating over an array of items.
1173 | * @param {?Array<*>=} statics An array of attribute name/value pairs of the
1174 | * static attributes for the Element. These will only be set once when the
1175 | * Element is created.
1176 | * @param {...*} const_args Attribute name/value pairs of the dynamic attributes
1177 | * for the Element.
1178 | * @return {!Element} The corresponding Element.
1179 | */
1180 | var elementVoid = function (tag, key, statics, const_args) {
1181 | var node = elementOpen.apply(null, arguments);
1182 | elementClose.apply(null, arguments);
1183 | return node;
1184 | };
1185 |
1186 | /**
1187 | * Declares a virtual Element at the current location in the document that is a
1188 | * placeholder element. Children of this Element can be manually managed and
1189 | * will not be cleared by the library.
1190 | *
1191 | * A key must be specified to make sure that this node is correctly preserved
1192 | * across all conditionals.
1193 | *
1194 | * @param {string} tag The element's tag.
1195 | * @param {string} key The key used to identify this element.
1196 | * @param {?Array<*>=} statics An array of attribute name/value pairs of the
1197 | * static attributes for the Element. These will only be set once when the
1198 | * Element is created.
1199 | * @param {...*} const_args Attribute name/value pairs of the dynamic attributes
1200 | * for the Element.
1201 | * @return {!Element} The corresponding Element.
1202 | */
1203 | var elementPlaceholder = function (tag, key, statics, const_args) {
1204 | if ('development' !== 'production') {
1205 | assertPlaceholderKeySpecified(key);
1206 | console.warn('elementPlaceholder will be removed in Incremental DOM 0.5' + ' use skip() instead');
1207 | }
1208 |
1209 | elementOpen.apply(null, arguments);
1210 | skip();
1211 | return elementClose.apply(null, arguments);
1212 | };
1213 |
1214 | /**
1215 | * Declares a virtual Text at this point in the document.
1216 | *
1217 | * @param {string|number|boolean} value The value of the Text.
1218 | * @param {...(function((string|number|boolean)):string)} const_args
1219 | * Functions to format the value which are called only when the value has
1220 | * changed.
1221 | * @return {!Text} The corresponding text node.
1222 | */
1223 | var text = function (value, const_args) {
1224 | if ('development' !== 'production') {
1225 | assertNotInAttributes('text');
1226 | assertNotInSkip('text');
1227 | }
1228 |
1229 | var node = coreText();
1230 | var data = getData(node);
1231 |
1232 | if (data.text !== value) {
1233 | data.text = /** @type {string} */value;
1234 |
1235 | var formatted = value;
1236 | for (var i = 1; i < arguments.length; i += 1) {
1237 | /*
1238 | * Call the formatter function directly to prevent leaking arguments.
1239 | * https://github.com/google/incremental-dom/pull/204#issuecomment-178223574
1240 | */
1241 | var fn = arguments[i];
1242 | formatted = fn(formatted);
1243 | }
1244 |
1245 | node.data = formatted;
1246 | }
1247 |
1248 | return node;
1249 | };
1250 |
1251 | exports.patch = patchInner;
1252 | exports.patchInner = patchInner;
1253 | exports.patchOuter = patchOuter;
1254 | exports.currentElement = currentElement;
1255 | exports.skip = skip;
1256 | exports.elementVoid = elementVoid;
1257 | exports.elementOpenStart = elementOpenStart;
1258 | exports.elementOpenEnd = elementOpenEnd;
1259 | exports.elementOpen = elementOpen;
1260 | exports.elementClose = elementClose;
1261 | exports.elementPlaceholder = elementPlaceholder;
1262 | exports.text = text;
1263 | exports.attr = attr;
1264 | exports.symbols = symbols;
1265 | exports.attributes = attributes;
1266 | exports.applyAttr = applyAttr;
1267 | exports.applyProp = applyProp;
1268 | exports.notifications = notifications;
1269 |
1270 | }));
1271 |
1272 | //# sourceMappingURL=incremental-dom.js.map
1273 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "idom-template",
3 | "version": "1.1.3",
4 | "description": "Library for converting templates into Incremental DOM rendering functions",
5 | "main": "bin/itemplate.js",
6 | "scripts": {
7 | "test": "mocha-phantomjs test/test.html",
8 | "build": "webpack && webpack --config webpack.config.release.js"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git://github.com/Rapid-Application-Development-JS/itemplate.git"
13 | },
14 | "keywords": [
15 | "incremental-dom",
16 | "library",
17 | "ejs",
18 | "underscore",
19 | "template",
20 | "virtual-dom"
21 | ],
22 | "author": "Yuriy Luchaninov (http://mobidev.biz/)",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/Rapid-Application-Development-JS/itemplate/issues"
26 | },
27 | "devDependencies": {
28 | "chai": "^3.5.0",
29 | "chai-fuzzy": "^1.6.0",
30 | "fs": "0.0.2",
31 | "incremental-dom": "^0.4.1",
32 | "mocha": "^2.4.5",
33 | "mocha-phantomjs": "^4.0.2",
34 | "phantomjs": "^2.1.3",
35 | "webpack": "^1.12.9"
36 | },
37 | "contributors": [
38 | {
39 | "email": "y.luchaninov@mobidev.biz",
40 | "name": "Yuriy Luchaninov"
41 | },
42 | {
43 | "email": "a.trofimenko@mobidev.biz",
44 | "name": "Anton Trofimenko"
45 | }
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------
/source/builder.js:
--------------------------------------------------------------------------------
1 | /* private */
2 | var _options = require('./options');
3 | var Mode = require('./mode');
4 | var Command = require('./wrapper').Command;
5 |
6 | var state; // current builder state
7 | var stack; // result builder
8 | var staticArraysHolder = {}; // holder for static arrays
9 | var wrapper; // external wrapper functionality
10 | var helpers; // keys for helpers
11 | var localComponentNames = []; // keys for local helpers
12 |
13 | var empty = '', quote = '"', comma = ', "', removable = '-%%&#__II-'; // auxiliary
14 |
15 | var nestingLevelInfo = {level: 0, skip: []};
16 |
17 | function isRootNode() {
18 | return nestingLevelInfo.level === 0;
19 | }
20 |
21 | function makeKey() {
22 | var text = new Array(12), possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefgijklmnopqrstuvwxyz';
23 | for (var i = 0; i < 12; i++)
24 | text.push(possible.charAt(Math.floor(Math.random() * possible.length)));
25 |
26 | return text.join(empty);
27 | }
28 |
29 | function decodeAccessory(string, force) {
30 | var regex = new RegExp(_options.accessory.open + '|' + _options.accessory.close, 'g');
31 | var code;
32 | var isStatic = true, openStub, closeStub;
33 |
34 | if (string !== undefined)
35 | code = string.split(regex).map(function (piece, i) {
36 | openStub = '';
37 | closeStub = '';
38 |
39 | if (i % 2) {
40 | isStatic = false;
41 | piece = piece.trim();
42 | if (_options.emptyString && !force) { // undefined as empty string
43 | if (piece.indexOf(' ') !== -1) {
44 | openStub = '(';
45 | closeStub = ')';
46 | }
47 | return ' + (' + openStub + piece + closeStub + ' === undefined ? "" : '
48 | + openStub + piece + closeStub + ') + ';
49 | } else
50 | return ' + ' + piece + ' + ';
51 | } else {
52 | return JSON.stringify(piece);
53 | }
54 | }).join('');
55 | else
56 | code = '""';
57 |
58 | // micro-optimizations (remove appending empty strings)
59 | code = code.replace(/^"" \+ | \+ ""$/g, '').replace(/ \+ "" \+ /g, ' + ');
60 |
61 | return {value: code, isStatic: isStatic};
62 | }
63 |
64 | function formatText(text) {
65 | return text.trim()
66 | .replace(/(\d+);/g, function (match, dec) {
67 | return String.fromCharCode(dec);
68 | })
69 | .replace(_options.escape, function (m) {
70 | return _options.MAP[m];
71 | });
72 | }
73 |
74 | function prepareKey(command, attributes, useKeyCommand) {
75 | var result = empty, decode, stub;
76 | if ((command === Command.elementOpen || command === Command.elementVoid)) {
77 |
78 | if (attributes && attributes.hasOwnProperty(_options.staticKey)) {
79 | decode = decodeAccessory(attributes[_options.staticKey] || makeKey());
80 | delete attributes[_options.staticKey];
81 | } else if (useKeyCommand) {
82 | decode = {value: Command.getKey};
83 | } else {
84 | decode = {value: 'null'};
85 | }
86 | stub = (Object.keys(attributes).length > 0) ? ', ' : empty;
87 | result = ', ' + decode.value + stub;
88 | }
89 | return result;
90 | }
91 |
92 | function prepareAttr(command, attributes) {
93 | var result = empty, attr, decode, arrayStaticKey = false, isSkipped = false, skipCommand;
94 | if ((command === Command.elementOpen || command === Command.elementVoid) && Object.keys(attributes).length > 0) {
95 | if (attributes && attributes.hasOwnProperty(_options.staticArray)) {
96 | arrayStaticKey = attributes[_options.staticArray] || makeKey();
97 | staticArraysHolder[arrayStaticKey] = staticArraysHolder[arrayStaticKey] || {};
98 | delete attributes[_options.staticArray];
99 | }
100 |
101 | if (attributes && attributes.hasOwnProperty(_options.skipAttr)) {
102 | isSkipped = true;
103 | skipCommand = Command.startSkipContent(decodeAccessory(attributes[_options.skipAttr], true).value);
104 | delete attributes[_options.skipAttr];
105 | }
106 |
107 | result = arrayStaticKey || null;
108 | for (var key in attributes) {
109 | attr = attributes[key];
110 | attr = (attr === null) ? key : ((attr === undefined) ? '' : attr);
111 | decode = decodeAccessory(attr);
112 | if (decode.isStatic && (_options.nonStaticAttributes.indexOf(key) === -1)) {
113 | if (arrayStaticKey) {
114 | var value = formatText(attr);
115 | if (!staticArraysHolder[arrayStaticKey].hasOwnProperty(key)) {
116 | staticArraysHolder[arrayStaticKey][key] = value;
117 | } else if (staticArraysHolder[arrayStaticKey][key] !== value) {
118 | staticArraysHolder[arrayStaticKey][key] = removable;
119 | result += comma + key + '", "' + value + quote;
120 | }
121 | } else
122 | result += comma + key + '", "' + formatText(attr) + quote;
123 | } else {
124 | result += comma + key + '", ' + formatText(decode.value);
125 | }
126 | }
127 | }
128 | return {value: result, isSkipped: isSkipped, skip: skipCommand};
129 | }
130 |
131 | function unwrapStaticArrays(holder) {
132 | var result = {}, obj, key;
133 | for (var arrayName in holder) {
134 | obj = holder[arrayName];
135 | result[arrayName] = [];
136 |
137 | for (key in obj)
138 | if (obj[key] !== removable)
139 | result[arrayName].push(quote + key + quote, quote + obj[key] + quote);
140 | }
141 |
142 | return result;
143 | }
144 |
145 | function decodeAttrs(obj) {
146 | var result = ['{'];
147 | for (var key in obj)
148 | result.push(((result.length > 1) ? ',' : empty) + '\'' + key + '\'' + ':' + decodeAccessory(obj[key], true).value);
149 | result.push('}');
150 |
151 | return result.join(empty);
152 | }
153 |
154 | function camelCase(input) {
155 | return input.replace(/\s/g, '').replace(/-(.)/g, function (match, group1) {
156 | return group1.toUpperCase();
157 | });
158 | }
159 |
160 | function writeCommand(command, tag, attributes) {
161 | if (attributes && attributes.ref) {
162 | var refName = attributes.ref;
163 | delete attributes.ref;
164 | }
165 |
166 | var strKey = prepareKey(command, attributes);
167 | var strAttrs = prepareAttr(command, attributes);
168 |
169 | if (refName) {
170 | // i.e. ref[refName] = elementOpen(...)
171 | command = Command.saveRef(camelCase(decodeAccessory(refName, true).value), command);
172 | }
173 |
174 | stack.push(command + tag + quote + strKey + strAttrs.value + Command.close);
175 |
176 | // save skipped
177 | if (strAttrs.isSkipped) {
178 | stack.push(strAttrs.skip);
179 | nestingLevelInfo.skip.push(nestingLevelInfo.level);
180 | }
181 | }
182 |
183 | function writeText(text) {
184 | text = formatText(text);
185 | if (text.length > 0) {
186 | var decode = decodeAccessory(text);
187 | stack.push(Command.text + decode.value + Command.close);
188 | }
189 | }
190 |
191 | function helperOpen(helperName, attrs) {
192 | stack.push(Command.helpers + '["' + helperName + '"](' + decodeAttrs(attrs) + ', function ('
193 | + _options.parentParameterName + '){');
194 | }
195 |
196 | function helperClose() {
197 | stack.push('}.bind(this));');
198 | }
199 |
200 | function isHelperTag(tagName) {
201 | return localComponentNames.indexOf(tagName) !== -1
202 | || helpers.indexOf(tagName) !== -1
203 | || tagName.indexOf(_options.helperPre) === 0;
204 | }
205 |
206 | function binderOpen(helperName, attrs) {
207 | var fnName = helperName.replace(_options.binderPre, '');
208 | stack.push(Command.binder + '(' + fnName + ',' + decodeAttrs(attrs) + ', function ('
209 | + _options.parentParameterName + '){');
210 | }
211 |
212 | function binderClose() {
213 | stack.push('}.bind(this));');
214 | }
215 |
216 | function isTagBinded(tagName) {
217 | return tagName.indexOf(_options.binderPre) === 0;
218 | }
219 |
220 | // TODO: Clarify logic.
221 | // Seems like this method only opens state but named as 'CloseOpenState'
222 | // also seems like `isClosed` flags used only to detect elementVoid and it's a bit confusing
223 | // because sounds like it can be used to detect tags open or close state.
224 | function writeAndCloseOpenState(isClosed) {
225 | var isShouldClose = true;
226 |
227 | if (state.tag) {
228 | var isRoot = isRootNode();
229 |
230 | if (isHelperTag(state.tag)) { // helper case
231 | helperOpen(state.tag, state.attributes);
232 | isShouldClose = isClosed;
233 | } else if (isTagBinded(state.tag)) {
234 | binderOpen(state.tag, state.attributes);
235 | isShouldClose = isClosed;
236 | } else if (isClosed || _options.voidRequireTags.indexOf(state.tag) !== -1) { // void mode
237 | writeCommand(Command.elementVoid, state.tag, state.attributes, isRoot);
238 | nestingLevelInfo.level--;
239 | isShouldClose = false;
240 | } else if (state.tag !== _options.evaluate.name) { // standard mode
241 | writeCommand(Command.elementOpen, state.tag, state.attributes, isRoot);
242 | } // if we write code, do nothing
243 |
244 | nestingLevelInfo.level++;
245 | }
246 |
247 | // clear builder state for next tag
248 | state.tag = null;
249 | state.attributes = {};
250 |
251 | return isShouldClose; // should we close this tag: no if we have void element
252 | }
253 |
254 | /* public */
255 | function Builder(functionWrapper) {
256 | wrapper = functionWrapper;
257 | this.reset();
258 | }
259 |
260 | Builder.prototype.reset = function () {
261 | stack = [];
262 | state = {
263 | tag: null,
264 | attributes: {}
265 | };
266 | staticArraysHolder = {};
267 | nestingLevelInfo = {level: 0, skip: []};
268 | };
269 |
270 | Builder.prototype.set = function (helpersKeys, localNames) {
271 | helpers = helpersKeys;
272 | localComponentNames = localNames || [];
273 | };
274 |
275 | Builder.prototype.write = function (command) {
276 | var tag;
277 | switch (command.type) {
278 | case Mode.Tag:
279 | tag = command.name.replace('/', empty);
280 |
281 | if (command.name.indexOf('/') === 0) {
282 |
283 | // close tag case
284 | if (writeAndCloseOpenState(true) && tag !== _options.evaluate.name) {
285 | nestingLevelInfo.level--;
286 |
287 | // write end skip functionality
288 | if (nestingLevelInfo.level === nestingLevelInfo.skip[nestingLevelInfo.skip.length - 1]) {
289 | stack.push(Command.endSkipContent);
290 | nestingLevelInfo.skip.pop();
291 | }
292 |
293 | if (isHelperTag(tag))
294 | helperClose();
295 | else if (isTagBinded(tag))
296 | binderClose();
297 | else
298 | writeCommand(Command.elementClose, tag);
299 | }
300 | } else {
301 | // open tag case
302 | writeAndCloseOpenState();
303 | state.tag = tag;
304 | state.attributes = {};
305 | }
306 | break;
307 | case Mode.Attr: // push attribute in state
308 | state.attributes[command.name] = command.data;
309 | break;
310 | case Mode.Text: // write text
311 | tag = state.tag;
312 | writeAndCloseOpenState();
313 | if (tag === _options.evaluate.name) { // write code
314 | stack.push(formatText(command.data));
315 | } else {
316 | writeText(command.data);
317 | }
318 | break;
319 | case Mode.Comment: // write comments only in debug mode
320 | if (_options.debug)
321 | stack.push('\n// ' + command.data.replace(_options.BREAK_LINE, ' ') + '\n');
322 | break;
323 | }
324 | };
325 |
326 | Builder.prototype.done = function () {
327 | return wrapper(stack, unwrapStaticArrays(staticArraysHolder));
328 | };
329 |
330 | module.exports = Builder;
--------------------------------------------------------------------------------
/source/itemplate.js:
--------------------------------------------------------------------------------
1 | var _options = require('./options');
2 | var prepare = require("./prepare");
3 | var Parser = require("./parser");
4 | var Builder = require("./builder");
5 |
6 | var wrapper = require("./wrapper").createWrapper();
7 | var builder = new Builder(wrapper);
8 | var parser = new Parser(builder);
9 |
10 | var helpers = {};
11 |
12 | var itemplate = {
13 | compile: function (string, library, scopedHelpers, rootKeys) {
14 | builder.reset();
15 | builder.set(
16 | Object.keys(helpers),
17 | scopedHelpers ? Object.keys(scopedHelpers) : [],
18 | rootKeys
19 | );
20 | wrapper.set(library, helpers, null, string);
21 | return parser.parseComplete(prepare(string));
22 | },
23 | options: function (options) {
24 | // mix options
25 | for (var key in options) {
26 | if (options.hasOwnProperty(key))
27 | _options[key] = options[key];
28 | }
29 | },
30 | registerHelper: function (name, fn) {
31 | helpers[name] = fn;
32 | },
33 | unregisterHelper: function (name) {
34 | delete helpers[name];
35 | }
36 | };
37 |
38 | Object.defineProperty(itemplate, 'helpers', {
39 | get: function () {
40 | return helpers;
41 | },
42 | set: function () {
43 | }
44 | });
45 |
46 | module.exports = itemplate;
--------------------------------------------------------------------------------
/source/mode.js:
--------------------------------------------------------------------------------
1 | var Mode = {
2 | Text: 'text',
3 | Tag: 'tag',
4 | Attr: 'attr',
5 | CData: 'cdata',
6 | Doctype: 'doctype',
7 | Comment: 'comment'
8 | };
9 |
10 | module.exports = Mode;
--------------------------------------------------------------------------------
/source/options.js:
--------------------------------------------------------------------------------
1 | var _options = {
2 | BREAK_LINE: /(\r\n|\n|\r)\s{0,}/gm,
3 | // prepare options
4 | template: {
5 | evaluate: /<%([\s\S]+?)%>/g,
6 | interpolate: /<%=([\s\S]+?)%>/g,
7 | escape: /<%-([\s\S]+?)%>/g
8 | },
9 | order: ['interpolate', 'escape', 'evaluate'],
10 | evaluate: {
11 | name: 'script',
12 | open: ''
14 | },
15 | accessory: {
16 | open: '{%',
17 | close: '%}'
18 | },
19 | escape: /(&|<|>|")/g,
20 | MAP: {
21 | '&': '&',
22 | '<': '<',
23 | '>': '>',
24 | '"': '"'
25 | },
26 | // build options
27 | emptyString: true,
28 | skipAttr: 'skip',
29 | staticKey: 'key',
30 | staticArray: 'static-array',
31 | nonStaticAttributes: ['id', 'name', 'ref'],
32 | binderPre: '::',
33 | helperPre: 'i-',
34 | parameterName: 'data',
35 | parentParameterName: 'parent',
36 | renderContentFnName: 'content',
37 | // tags parse rules
38 | textSaveTags: ['pre', 'code'],
39 | voidRequireTags: ['input', 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'keygen', 'link', 'meta',
40 | 'param', 'source', 'track', 'wbr'],
41 | debug: false
42 | };
43 |
44 | module.exports = _options;
--------------------------------------------------------------------------------
/source/parser.js:
--------------------------------------------------------------------------------
1 | var Mode = require('./mode');
2 |
3 | function Parser(builder) {
4 | this._builder = builder;
5 | this.reset();
6 | }
7 |
8 | //**Public**//
9 | Parser.prototype.reset = function () {
10 | this._state = {
11 | mode: Mode.Text,
12 | pos: 0,
13 | data: null,
14 | pendingText: null,
15 | pendingWrite: null,
16 | lastTag: null,
17 | isScript: false,
18 | needData: false,
19 | output: [],
20 | done: false
21 | };
22 | this._builder.reset();
23 | };
24 |
25 | Parser.prototype.parseChunk = function (chunk) {
26 | this._state.needData = false;
27 | this._state.data = (this._state.data !== null) ? this._state.data.substr(this.pos) + chunk : chunk;
28 | while (this._state.pos < this._state.data.length && !this._state.needData) {
29 | this._parse(this._state);
30 | }
31 | };
32 |
33 | Parser.prototype.parseComplete = function (data) {
34 | this.reset();
35 | this.parseChunk(data);
36 | return this.done();
37 | };
38 |
39 | Parser.prototype.done = function () {
40 | this._state.done = true;
41 | this._parse(this._state);
42 | this._flushWrite();
43 | return this._builder.done();
44 | };
45 |
46 | //**Private**//
47 | Parser.prototype._parse = function () {
48 | switch (this._state.mode) {
49 | case Mode.Text:
50 | return this._parseText(this._state);
51 | case Mode.Tag:
52 | return this._parseTag(this._state);
53 | case Mode.Attr:
54 | return this._parseAttr(this._state);
55 | case Mode.CData:
56 | return this._parseCData(this._state);
57 | case Mode.Doctype:
58 | return this._parseDoctype(this._state);
59 | case Mode.Comment:
60 | return this._parseComment(this._state);
61 | }
62 | };
63 |
64 | Parser.prototype._writePending = function (node) {
65 | if (!this._state.pendingWrite) {
66 | this._state.pendingWrite = [];
67 | }
68 | this._state.pendingWrite.push(node);
69 | };
70 |
71 | Parser.prototype._flushWrite = function () {
72 | if (this._state.pendingWrite) {
73 | for (var i = 0, len = this._state.pendingWrite.length; i < len; i++) {
74 | var node = this._state.pendingWrite[i];
75 | this._builder.write(node);
76 | }
77 | this._state.pendingWrite = null;
78 | }
79 | };
80 |
81 | Parser.prototype._write = function (node) {
82 | this._flushWrite();
83 | this._builder.write(node);
84 | };
85 |
86 | Parser._re_parseText_scriptClose = /<\s*\/\s*script/ig;
87 | Parser.prototype._parseText = function () {
88 | var state = this._state;
89 | var foundPos;
90 | if (state.isScript) {
91 | Parser._re_parseText_scriptClose.lastIndex = state.pos;
92 | foundPos = Parser._re_parseText_scriptClose.exec(state.data);
93 | foundPos = (foundPos) ? foundPos.index : -1;
94 | } else {
95 | foundPos = state.data.indexOf('<', state.pos);
96 | }
97 | var text = (foundPos === -1) ? state.data.substring(state.pos, state.data.length) : state.data.substring(state.pos, foundPos);
98 | if (foundPos < 0 && state.done) {
99 | foundPos = state.data.length;
100 | }
101 | if (foundPos < 0) {
102 | if (state.isScript) {
103 | state.needData = true;
104 | return;
105 | }
106 | if (!state.pendingText) {
107 | state.pendingText = [];
108 | }
109 | state.pendingText.push(state.data.substring(state.pos, state.data.length));
110 | state.pos = state.data.length;
111 | } else {
112 | if (state.pendingText) {
113 | state.pendingText.push(state.data.substring(state.pos, foundPos));
114 | text = state.pendingText.join('');
115 | state.pendingText = null;
116 | } else {
117 | text = state.data.substring(state.pos, foundPos);
118 | }
119 | if (text !== '') {
120 | this._write({type: Mode.Text, data: text});
121 | }
122 | state.pos = foundPos + 1;
123 | state.mode = Mode.Tag;
124 | }
125 | };
126 |
127 | Parser.re_parseTag = /\s*(\/?)\s*([^\s>\/]+)(\s*)\??(>?)/g;
128 | Parser.prototype._parseTag = function () {
129 | var state = this._state;
130 | Parser.re_parseTag.lastIndex = state.pos;
131 | var match = Parser.re_parseTag.exec(state.data);
132 |
133 | if (match) {
134 | if (!match[1] && match[2].substr(0, 3) === '!--') {
135 | state.mode = Mode.Comment;
136 | state.pos += 3;
137 | return;
138 | }
139 | if (!match[1] && match[2].substr(0, 8) === '![CDATA[') {
140 | state.mode = Mode.CData;
141 | state.pos += 8;
142 | return;
143 | }
144 | if (!match[1] && match[2].substr(0, 8) === '!DOCTYPE') {
145 | state.mode = Mode.Doctype;
146 | state.pos += 8;
147 | return;
148 | }
149 | if (!state.done && (state.pos + match[0].length) === state.data.length) {
150 | //We're at the and of the data, might be incomplete
151 | state.needData = true;
152 | return;
153 | }
154 | var raw;
155 | if (match[4] === '>') {
156 | state.mode = Mode.Text;
157 | raw = match[0].substr(0, match[0].length - 1);
158 | } else {
159 | state.mode = Mode.Attr;
160 | raw = match[0];
161 | }
162 | state.pos += match[0].length;
163 | var tag = {type: Mode.Tag, name: match[1] + match[2], raw: raw, position: Parser.re_parseTag.lastIndex };
164 | if (state.mode === Mode.Attr) {
165 | state.lastTag = tag;
166 | }
167 | if (tag.name.toLowerCase() === 'script') {
168 | state.isScript = true;
169 | } else if (tag.name.toLowerCase() === '/script') {
170 | state.isScript = false;
171 | }
172 | if (state.mode === Mode.Attr) {
173 | this._writePending(tag);
174 | } else {
175 | this._write(tag);
176 | }
177 | } else {
178 | state.needData = true;
179 | }
180 | };
181 |
182 | Parser.re_parseAttr_findName = /\s*([^=<>\s'"\/]+)\s*/g;
183 | Parser.prototype._parseAttr_findName = function () {
184 | // todo: parse {{ checked ? 'checked' : '' }} in input
185 | Parser.re_parseAttr_findName.lastIndex = this._state.pos;
186 | var match = Parser.re_parseAttr_findName.exec(this._state.data);
187 | if (!match) {
188 | return null;
189 | }
190 | if (this._state.pos + match[0].length !== Parser.re_parseAttr_findName.lastIndex) {
191 | return null;
192 | }
193 | return {
194 | match: match[0],
195 | name: match[1]
196 | };
197 | };
198 | Parser.re_parseAttr_findValue = /\s*=\s*(?:'([^']*)'|"([^"]*)"|([^'"\s\/>]+))\s*/g;
199 | Parser.re_parseAttr_findValue_last = /\s*=\s*['"]?(.*)$/g;
200 | Parser.prototype._parseAttr_findValue = function () {
201 | var state = this._state;
202 | Parser.re_parseAttr_findValue.lastIndex = state.pos;
203 | var match = Parser.re_parseAttr_findValue.exec(state.data);
204 | if (!match) {
205 | if (!state.done) {
206 | return null;
207 | }
208 | Parser.re_parseAttr_findValue_last.lastIndex = state.pos;
209 | match = Parser.re_parseAttr_findValue_last.exec(state.data);
210 | if (!match) {
211 | return null;
212 | }
213 | return {
214 | match: match[0],
215 | value: (match[1] !== '') ? match[1] : null
216 | };
217 | }
218 | if (state.pos + match[0].length !== Parser.re_parseAttr_findValue.lastIndex) {
219 | return null;
220 | }
221 | return {
222 | match: match[0],
223 | value: match[1] || match[2] || match[3]
224 | };
225 | };
226 | Parser.re_parseAttr_splitValue = /\s*=\s*['"]?/g;
227 | Parser.re_parseAttr_selfClose = /(\s*\/\s*)(>?)/g;
228 | Parser.prototype._parseAttr = function () {
229 | var state = this._state;
230 | var name_data = this._parseAttr_findName(state);
231 | if (!name_data || name_data.name === '?') {
232 | Parser.re_parseAttr_selfClose.lastIndex = state.pos;
233 | var matchTrailingSlash = Parser.re_parseAttr_selfClose.exec(state.data);
234 | if (matchTrailingSlash && matchTrailingSlash.index === state.pos) {
235 | if (!state.done && !matchTrailingSlash[2] && state.pos + matchTrailingSlash[0].length === state.data.length) {
236 | state.needData = true;
237 | return;
238 | }
239 | state.lastTag.raw += matchTrailingSlash[1];
240 | this._write({type: Mode.Tag, name: '/' + state.lastTag.name, raw: null});
241 | state.pos += matchTrailingSlash[1].length;
242 | }
243 | var foundPos = state.data.indexOf('>', state.pos);
244 | if (foundPos < 0) {
245 | if (state.done) {
246 | state.lastTag.raw += state.data.substr(state.pos);
247 | state.pos = state.data.length;
248 | return;
249 | }
250 | state.needData = true;
251 | } else {
252 | // state.lastTag = null;
253 | state.pos = foundPos + 1;
254 | state.mode = Mode.Text;
255 | }
256 | return;
257 | }
258 | if (!state.done && state.pos + name_data.match.length === state.data.length) {
259 | state.needData = true;
260 | return null;
261 | }
262 | state.pos += name_data.match.length;
263 | var value_data = this._parseAttr_findValue(state);
264 | if (value_data) {
265 | if (!state.done && state.pos + value_data.match.length === state.data.length) {
266 | state.needData = true;
267 | state.pos -= name_data.match.length;
268 | return;
269 | }
270 | state.pos += value_data.match.length;
271 | } else {
272 | if (state.data.indexOf(' ', state.pos - 1)) {
273 | value_data = {
274 | match: '',
275 | value: null
276 | };
277 |
278 | } else {
279 | Parser.re_parseAttr_splitValue.lastIndex = state.pos;
280 | if (Parser.re_parseAttr_splitValue.exec(state.data)) {
281 | state.needData = true;
282 | state.pos -= name_data.match.length;
283 | return;
284 | }
285 | value_data = {
286 | match: '',
287 | value: null
288 | };
289 | }
290 | }
291 | state.lastTag.raw += name_data.match + value_data.match;
292 |
293 | this._writePending({type: Mode.Attr, name: name_data.name, data: value_data.value});
294 | };
295 |
296 | Parser.re_parseCData_findEnding = /\]{1,2}$/;
297 | Parser.prototype._parseCData = function () {
298 | var state = this._state;
299 | var foundPos = state.data.indexOf(']]>', state.pos);
300 | if (foundPos < 0 && state.done) {
301 | foundPos = state.data.length;
302 | }
303 | if (foundPos < 0) {
304 | Parser.re_parseCData_findEnding.lastIndex = state.pos;
305 | var matchPartialCDataEnd = Parser.re_parseCData_findEnding.exec(state.data);
306 | if (matchPartialCDataEnd) {
307 | state.needData = true;
308 | return;
309 | }
310 | if (!state.pendingText) {
311 | state.pendingText = [];
312 | }
313 | state.pendingText.push(state.data.substr(state.pos, state.data.length));
314 | state.pos = state.data.length;
315 | state.needData = true;
316 | } else {
317 | var text;
318 | if (state.pendingText) {
319 | state.pendingText.push(state.data.substring(state.pos, foundPos));
320 | text = state.pendingText.join('');
321 | state.pendingText = null;
322 | } else {
323 | text = state.data.substring(state.pos, foundPos);
324 | }
325 | this._write({type: Mode.CData, data: text});
326 | state.mode = Mode.Text;
327 | state.pos = foundPos + 3;
328 | }
329 | };
330 |
331 | Parser.prototype._parseDoctype = function () {
332 | var state = this._state;
333 | var foundPos = state.data.indexOf('>', state.pos);
334 | if (foundPos < 0 && state.done) {
335 | foundPos = state.data.length;
336 | }
337 | if (foundPos < 0) {
338 | Parser.re_parseCData_findEnding.lastIndex = state.pos;
339 | if (!state.pendingText) {
340 | state.pendingText = [];
341 | }
342 | state.pendingText.push(state.data.substr(state.pos, state.data.length));
343 | state.pos = state.data.length;
344 | state.needData = true;
345 | } else {
346 | var text;
347 | if (state.pendingText) {
348 | state.pendingText.push(state.data.substring(state.pos, foundPos));
349 | text = state.pendingText.join('');
350 | state.pendingText = null;
351 | } else {
352 | text = state.data.substring(state.pos, foundPos);
353 | }
354 | this._write({type: Mode.Doctype, data: text});
355 | state.mode = Mode.Text;
356 | state.pos = foundPos + 1;
357 | }
358 | };
359 |
360 | Parser.re_parseComment_findEnding = /\-{1,2}$/;
361 | Parser.prototype._parseComment = function () {
362 | var state = this._state;
363 | var foundPos = state.data.indexOf('-->', state.pos);
364 | if (foundPos < 0 && state.done) {
365 | foundPos = state.data.length;
366 | }
367 | if (foundPos < 0) {
368 | Parser.re_parseComment_findEnding.lastIndex = state.pos;
369 | var matchPartialCommentEnd = Parser.re_parseComment_findEnding.exec(state.data);
370 | if (matchPartialCommentEnd) {
371 | state.needData = true;
372 | return;
373 | }
374 | if (!state.pendingText) {
375 | state.pendingText = [];
376 | }
377 | state.pendingText.push(state.data.substr(state.pos, state.data.length));
378 | state.pos = state.data.length;
379 | state.needData = true;
380 | } else {
381 | var text;
382 | if (state.pendingText) {
383 | state.pendingText.push(state.data.substring(state.pos, foundPos));
384 | text = state.pendingText.join('');
385 | state.pendingText = null;
386 | } else {
387 | text = state.data.substring(state.pos, foundPos);
388 | }
389 |
390 | this._write({type: Mode.Comment, data: text});
391 | state.mode = Mode.Text;
392 | state.pos = foundPos + 3;
393 | }
394 | };
395 |
396 | module.exports = Parser;
--------------------------------------------------------------------------------
/source/prepare.js:
--------------------------------------------------------------------------------
1 | var _options = require('./options');
2 |
3 | function replacer(match, p1) {
4 | return _options.accessory.open + p1 + _options.accessory.close;
5 | }
6 |
7 | var methods = {
8 | evaluate: function (string) {
9 | return string.replace(_options.template.evaluate, function (match, p1) {
10 | return _options.evaluate.open + p1.replace(_options.BREAK_LINE, ' ').trim() + _options.evaluate.close;
11 | });
12 | },
13 | interpolate: function (string) {
14 | return string.replace(_options.template.interpolate, replacer);
15 | },
16 | escape: function (string) {
17 | return string.replace(_options.template.escape, replacer);
18 | }
19 | };
20 |
21 | function prepare(string) {
22 | var result = string;
23 | for (var i = 0; i < _options.order.length; i++) {
24 | result = methods[_options.order[i]](result);
25 | }
26 | return result;
27 | }
28 |
29 | module.exports = prepare;
--------------------------------------------------------------------------------
/source/wrapper.js:
--------------------------------------------------------------------------------
1 | var _options = require('./options');
2 |
3 | var Command = { // incremental DOM commands
4 | helpers: '_h',
5 | binder: '_b',
6 | elementOpen: '_o("',
7 | elementClose: '_c("',
8 | elementVoid: '_v("',
9 | saveRef: function (name, command) {
10 | return '_r[' + name + '] = ' + command;
11 | },
12 | text: '_t(',
13 | close: ');\n',
14 | startSkipContent: function (flag) {
15 | // compile static values
16 | flag = (flag === '"false"') ? false : flag;
17 | flag = (flag === '"true"') ? true : flag;
18 |
19 | return 'if(' + flag + '){_l.skip();}else{';
20 | },
21 | endSkipContent: '}'
22 | };
23 |
24 | function createWrapper() {
25 | var _library, _helpers, _fnName, _template;
26 | var glue = '';
27 | var eol = '\n';
28 |
29 | function wrapFn(body) {
30 | var returnValue = eol + ' return _r;';
31 |
32 | var prepareError = 'var TE=function(m,n,o){this.original=o;this.name=n;(o)?this.stack=this.original.stack:' +
33 | 'this.stack=null;this.message=o.message+m;};var CE=function(){};CE.prototype=Error.prototype;' +
34 | 'TE.prototype=new CE();TE.prototype.constructor=TE;';
35 |
36 | if (_options.debug) {
37 | return 'try {'
38 | + body +
39 | '} catch (err) {'
40 | + prepareError +
41 | 'throw new TE(' + JSON.stringify(_template) + ', err.name, err);' +
42 | '}'
43 | + returnValue;
44 | }
45 | return body + returnValue;
46 | }
47 |
48 | function wrapper(stack, holder) {
49 | var resultFn;
50 | var variables = [
51 | 'var _o = _l.elementOpen;',
52 | 'var _c = _l.elementClose;',
53 | 'var _v = _l.elementVoid;',
54 | 'var _t = _l.text;',
55 | 'var _r = {};',
56 | '_b = _b || function(fn, data, content){ return fn(data, content); };'
57 | ].join(eol) + eol;
58 |
59 | for (var key in holder) { // collect static arrays for function
60 | if (holder.hasOwnProperty(key))
61 | variables += 'var ' + key + '=[' + holder[key] + '];';
62 | }
63 | var body = variables + wrapFn(stack.join(glue));
64 |
65 | if (_library) {
66 | body = 'return function(' + _options.parameterName + ', ' + _options.renderContentFnName + ', _b){' + body + '};';
67 | resultFn = (new Function('_l', '_h', body))(_library, _helpers);
68 | } else {
69 | resultFn = new Function(_options.parameterName, '_l', '_h', _options.renderContentFnName, '_b', body);
70 | }
71 | return resultFn;
72 | }
73 |
74 | wrapper.set = function (library, helpers, fnName, template) {
75 | _library = library;
76 | _helpers = helpers;
77 | _fnName = fnName;
78 | _template = template;
79 | };
80 |
81 | return wrapper;
82 | }
83 |
84 | module.exports = {
85 | createWrapper: createWrapper,
86 | Command: Command
87 | };
--------------------------------------------------------------------------------
/test/README.md:
--------------------------------------------------------------------------------
1 | You should have global installed `mocha`:
2 |
3 | ```bash
4 | node install mocha -g
--------------------------------------------------------------------------------
/test/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Tests
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |