├── .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 |
13 | 14 | 15 | 31 | 32 | 43 | 44 | 64 | 65 | 66 | 69 | 72 | 73 | 74 | 86 | 87 | 88 | 123 | 124 | 125 | 132 | 133 | 136 | 137 | 138 | 152 | 153 | 154 | 158 | 159 | 160 | 163 | 164 | 165 | 170 | 171 | 172 | 179 | 180 | 181 | 188 | 189 | 205 | 206 | 207 | 212 | 213 | 218 | 219 | 220 | 227 | 228 | 235 | 236 | 237 | 242 | 243 | 244 | 261 | 262 | 267 | 268 | 276 | 277 | 292 | 293 | 307 | 308 | 324 | 325 | 342 | 343 |
344 | 345 | 346 |
347 | 348 | 349 | 354 | 355 | 363 | 364 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | describe("iTemplate Tests", function () { 2 | var container; 3 | 4 | beforeEach(function () { 5 | container = document.createElement('div'); 6 | document.body.appendChild(container); 7 | }); 8 | 9 | afterEach(function () { 10 | document.body.removeChild(container); 11 | }); 12 | 13 | it("0.0.1.1: plain html (main function type)", function () { 14 | var templateFn = itemplate.compile(document.querySelector('#test-0_0_1').textContent, IncrementalDOM); 15 | var innerHTML = '
Title
  • John Smith*
  • ' + 16 | '
  • Mark Smith

* Olympic gold medalist

'; 17 | 18 | IncrementalDOM.patch(container, templateFn); 19 | 20 | expect(container.innerHTML).to.equal(innerHTML); 21 | }); 22 | 23 | it("0.0.1.2: plain html (second function type)", function () { 24 | var templateFn = itemplate.compile(document.querySelector('#test-0_0_1').textContent); 25 | var innerHTML = '
Title
  • John Smith*
  • ' + 26 | '
  • Mark Smith

* Olympic gold medalist

'; 27 | 28 | IncrementalDOM.patch(container, function (data) { 29 | return templateFn.call(null, data, IncrementalDOM); 30 | }); 31 | 32 | expect(container.innerHTML).to.equal(innerHTML); 33 | }); 34 | 35 | it("0.0.2.1: plain html - attributes (main function type)", function () { 36 | var templateFn = itemplate.compile(document.querySelector('#test-0_0_2').textContent, IncrementalDOM); 37 | var innerHTML = '
some text
' + 38 | '
some text
' + 39 | '
some text
'; 40 | 41 | IncrementalDOM.patch(container, templateFn); 42 | 43 | expect(container.innerHTML).to.equal(innerHTML); 44 | }); 45 | 46 | it("0.0.2.2: plain html - attributes (second function type)", function () { 47 | var templateFn = itemplate.compile(document.querySelector('#test-0_0_2').textContent); 48 | var innerHTML = '
some text
' + 49 | '
some text
' + 50 | '
some text
'; 51 | 52 | IncrementalDOM.patch(container, function (data) { 53 | return templateFn.call(null, data, IncrementalDOM); 54 | }); 55 | 56 | expect(container.innerHTML).to.equal(innerHTML); 57 | }); 58 | 59 | it("0.0.3.1: plain html - custom tags (main function type)", function () { 60 | var templateFn = itemplate.compile(document.querySelector('#test-0_0_3').textContent, IncrementalDOM); 61 | var innerHTML = '
Some text
Other text
' + 62 | '
some codes "<" and "&" \'.
my code' + 63 | 'My custom tag ;-)
'; 64 | 65 | IncrementalDOM.patch(container, templateFn); 66 | 67 | expect(container.innerHTML).to.equal(innerHTML); 68 | }); 69 | 70 | it("0.0.3.2: plain html - custom tags (second function type)", function () { 71 | var templateFn = itemplate.compile(document.querySelector('#test-0_0_3').textContent); 72 | var innerHTML = '
Some text
Other text
' + 73 | '
some codes "<" and "&" \'.
my code' + 74 | 'My custom tag ;-)
'; 75 | 76 | IncrementalDOM.patch(container, function (data) { 77 | return templateFn.call(null, data, IncrementalDOM); 78 | }); 79 | 80 | expect(container.innerHTML).to.equal(innerHTML); 81 | }); 82 | 83 | it("0.1.1: open inputs", function () { 84 | var templateFn = itemplate.compile(document.querySelector('#test-0_1_1').textContent, IncrementalDOM); 85 | IncrementalDOM.patch(container, templateFn); 86 | 87 | var input = container.querySelector('input'); 88 | expect(input).to.not.equal(null); 89 | expect(input.hasAttribute('readonly')).to.equal(true); 90 | expect(input.getAttribute('type')).to.equal('text'); 91 | expect(input.getAttribute('name')).to.equal('text-2'); 92 | expect(input.getAttribute('placeholder')).to.equal('some name'); 93 | }); 94 | 95 | it("0.1.2: close inputs", function () { 96 | var templateFn = itemplate.compile(document.querySelector('#test-0_1_2').textContent, IncrementalDOM); 97 | IncrementalDOM.patch(container, templateFn); 98 | 99 | var input = container.querySelector('input'); 100 | expect(input).to.not.equal(null); 101 | expect(input.hasAttribute('readonly')).to.equal(true); 102 | expect(input.getAttribute('type')).to.equal('text'); 103 | expect(input.getAttribute('name')).to.equal('text-2'); 104 | expect(input.getAttribute('placeholder')).to.equal('some name'); 105 | }); 106 | 107 | 108 | it("0.2.1: inner expressions <%= %>, <%- %>", function () { 109 | var templateFn = itemplate.compile(document.querySelector('#test-0_2_1').textContent, IncrementalDOM); 110 | var data = { 111 | isTrue: true, 112 | name: 'name', 113 | lastName: 'lastName', 114 | listTitle: 'listTitle' 115 | }; 116 | IncrementalDOM.patch(container, templateFn, data); 117 | 118 | var span = container.querySelector('span.first'); 119 | expect(span).to.not.equal(null); 120 | expect(span.getAttribute('title')).to.equal('name lastName'); 121 | 122 | expect(container.querySelector('._1').textContent).to.equal('listTitle'); 123 | expect(container.querySelector('._2').textContent).to.equal('testlistTitle'); 124 | expect(container.querySelector('._3').textContent).to.equal('listTitletest'); 125 | expect(container.querySelector('._4').textContent).to.equal('testlistTitletest'); 126 | expect(container.querySelector('._5').textContent).to.equal('testlistTitletestlistTitle'); 127 | }); 128 | 129 | it("0.2.2: embedded js", function () { 130 | var templateFn = itemplate.compile(document.querySelector('#test-0_2_2').textContent, IncrementalDOM); 131 | var data = { 132 | listTitle: "Olympic Volleyball Players", 133 | listItems: [ 134 | { 135 | // name: "Misty May-Treanor", 136 | hasOlympicGold: true 137 | }, 138 | { 139 | name: "Kerri Walsh Jennings", 140 | hasOlympicGold: true 141 | }, 142 | { 143 | name: "Jennifer Kessy", 144 | hasOlympicGold: false 145 | }, 146 | { 147 | name: "April Ross", 148 | hasOlympicGold: false 149 | } 150 | ] 151 | }; 152 | var innerHTML = '
  • *
  • Kerri Walsh Jennings*
  • ' + 153 | '
  • Jennifer Kessy
  • April Ross
' + 154 | '

* Olympic gold medalist

  • *
  • ' + 155 | '
  • Kerri Walsh Jennings*
  • Jennifer Kessy*
  • ' + 156 | '
  • April Ross*
'; 157 | IncrementalDOM.patch(container, templateFn, data); 158 | 159 | expect(container.innerHTML).to.equal(innerHTML); 160 | }); 161 | 162 | it("0.2.3: decode accessory", function () { 163 | var templateFn = itemplate.compile(document.querySelector('#test-0_2_3').textContent, IncrementalDOM); 164 | var data = { 165 | listTitle: 'listTitle' 166 | }; 167 | IncrementalDOM.patch(container, templateFn, data); 168 | 169 | expect(container.querySelector('._1').getAttribute('attr')).to.equal('listTitle'); 170 | expect(container.querySelector('._2').getAttribute('attr')).to.equal('testlistTitle'); 171 | expect(container.querySelector('._3').getAttribute('attr')).to.equal('listTitletest'); 172 | expect(container.querySelector('._4').getAttribute('attr')).to.equal('testlistTitletest'); 173 | expect(container.querySelector('._5').getAttribute('attr')).to.equal('testlistTitletestlistTitle'); 174 | }); 175 | 176 | it("0.2.4: 'if decode accessory", function () { 177 | var templateFn = itemplate.compile(document.querySelector('#test-0_2_4').textContent, IncrementalDOM); 178 | var data = { 179 | listTitle: 'listTitle' 180 | }; 181 | IncrementalDOM.patch(container, templateFn, data); 182 | 183 | expect(container.querySelector('._1').getAttribute('attr')).to.equal('a'); 184 | }); 185 | 186 | 187 | it("0.3.1: static keys", function () { 188 | var templateFn = itemplate.compile(document.querySelector('#test-0_3_1').textContent, IncrementalDOM); 189 | var data = { 190 | array: [ 191 | {id: 0, value: 0}, 192 | {id: 1, value: 1}, 193 | {id: 2, value: 2} 194 | ] 195 | }; 196 | 197 | // render 198 | IncrementalDOM.patch(container, templateFn, data); 199 | 200 | // get pointer to static & non static elements 201 | var staticLI = container.querySelectorAll('ul li'); 202 | var nonStaticLI = container.querySelectorAll('ul:nth-child(2) li'); 203 | 204 | // insert first element to data array 205 | data.array.unshift({id: 3, value: 3}); 206 | 207 | // rerender 208 | IncrementalDOM.patch(container, templateFn, data); 209 | 210 | expect(container.querySelectorAll('ul li')[0]).to.not.equal(staticLI[0]); 211 | expect(container.querySelectorAll('ul li')[1]).to.equal(staticLI[0]); 212 | 213 | expect(container.querySelectorAll('ul:nth-child(2) li')[0]).to.equal(nonStaticLI[0]); 214 | }); 215 | 216 | it("0.3.2: static arrays", function () { 217 | var templateFn = itemplate.compile(document.querySelector('#test-0_3_2').textContent, IncrementalDOM); 218 | IncrementalDOM.patch(container, templateFn); 219 | 220 | var firstEl = container.querySelector('#id_1'); 221 | var secondEl = container.querySelector('#id_2'); 222 | 223 | expect(firstEl.style.border).to.equal(secondEl.style.border); 224 | expect(firstEl.className).to.equal(secondEl.className); 225 | }); 226 | 227 | it("0.3.3: helpers", function () { 228 | itemplate.registerHelper('my-input', function (attr) { 229 | IncrementalDOM.elementVoid('input', null, null, 'class', attr.class); 230 | }); 231 | 232 | 233 | var templateFn = itemplate.compile(document.querySelector('#test-0_3_3').textContent, IncrementalDOM); 234 | IncrementalDOM.patch(container, templateFn); 235 | 236 | var input = container.querySelector('.my-class'); 237 | expect(input).to.not.equal(null); 238 | }); 239 | 240 | it("0.3.4: wrapped helpers", function () { 241 | itemplate.registerHelper('my-div', function (attr, content) { 242 | IncrementalDOM.elementOpen('div', null, null, 'class', attr.class); 243 | content(); 244 | IncrementalDOM.elementClose('div'); 245 | }); 246 | 247 | var innerHTML = '
'; 248 | var templateFn = itemplate.compile(document.querySelector('#test-0_3_4').textContent, IncrementalDOM); 249 | IncrementalDOM.patch(container, templateFn); 250 | 251 | expect(container.innerHTML).to.equal(innerHTML); 252 | }); 253 | 254 | it("0.3.5: wrapped helpers", function () { 255 | itemplate.registerHelper('my-section', function (attr, render) { 256 | IncrementalDOM.elementOpen('section'); 257 | render(); 258 | IncrementalDOM.elementClose('section'); 259 | }); 260 | 261 | var innerHTML = '
'; 262 | var templateFn = itemplate.compile(document.querySelector('#test-0_3_5').textContent, IncrementalDOM); 263 | IncrementalDOM.patch(container, templateFn); 264 | 265 | expect(container.innerHTML).to.equal(innerHTML); 266 | }); 267 | 268 | it("0.3.6.1: helpers context (main function type)", function () { 269 | var templateFn = itemplate.compile(document.querySelector('#test-0_3_6').textContent, IncrementalDOM); 270 | var context = { 271 | div: 'div', 272 | input: 'input' 273 | }; 274 | var innerHTML = '
'; 275 | 276 | IncrementalDOM.patch(container, templateFn.bind(context)); 277 | 278 | expect(container.innerHTML).to.equal(innerHTML); 279 | }); 280 | 281 | it("0.3.6.2: helpers context (second function type)", function () { 282 | var templateFn = itemplate.compile(document.querySelector('#test-0_3_6').textContent); 283 | var context = { 284 | div: 'div', 285 | input: 'input' 286 | }; 287 | var innerHTML = '
'; 288 | 289 | IncrementalDOM.patch(container, function (data) { 290 | templateFn.call(context, data, IncrementalDOM, itemplate.helpers); 291 | }); 292 | 293 | expect(container.innerHTML).to.equal(innerHTML); 294 | }); 295 | 296 | 297 | it("0.3.7.1: several wrappers (main function type)", function () { 298 | var templateFn = itemplate.compile(document.querySelector('#test-0_3_7').textContent, IncrementalDOM); 299 | var context = { 300 | div: 'div', 301 | input: 'input' 302 | }; 303 | var innerHTML = '
' + 304 | '
test
test class
' + 305 | '
'; 306 | 307 | IncrementalDOM.patch(container, templateFn.bind(context)); 308 | 309 | expect(container.innerHTML).to.equal(innerHTML); 310 | }); 311 | 312 | it("0.3.7.2: several wrappers (second function type)", function () { 313 | var templateFn = itemplate.compile(document.querySelector('#test-0_3_7').textContent); 314 | var context = { 315 | div: 'div', 316 | input: 'input' 317 | }; 318 | var innerHTML = '
' + 319 | '
test
test class
' + 320 | '
'; 321 | 322 | IncrementalDOM.patch(container, function (data) { 323 | templateFn.call(context, data, IncrementalDOM, itemplate.helpers); 324 | }); 325 | 326 | expect(container.innerHTML).to.equal(innerHTML); 327 | }); 328 | 329 | it("0.3.8.1: data from parent wrapper (main function type)", function () { 330 | // You can compile helper templates only by the first type of functions 331 | var helperTemplate = itemplate.compile(document.querySelector('#template-1').textContent, IncrementalDOM); 332 | itemplate.registerHelper('parent-div', helperTemplate); 333 | 334 | var innerHTML = '
'; 335 | var templateFn = itemplate.compile(document.querySelector('#test-0_3_8').textContent, IncrementalDOM); 336 | IncrementalDOM.patch(container, templateFn); 337 | 338 | expect(container.innerHTML).to.equal(innerHTML); 339 | }); 340 | 341 | it("0.3.8.2: data from parent wrapper (second function type)", function () { 342 | // You can compile helper templates only by the first type of functions 343 | var helperTemplate = itemplate.compile(document.querySelector('#template-1').textContent, IncrementalDOM); 344 | itemplate.registerHelper('parent-div', helperTemplate); 345 | 346 | var innerHTML = '
'; 347 | var templateFn = itemplate.compile(document.querySelector('#test-0_3_8').textContent); 348 | 349 | IncrementalDOM.patch(container, function (data) { 350 | templateFn(data, IncrementalDOM, itemplate.helpers); 351 | }); 352 | 353 | expect(container.innerHTML).to.equal(innerHTML); 354 | }); 355 | 356 | it("0.4.1.1: static refs (main function type)", function () { 357 | var templateFn = itemplate.compile(document.querySelector('#test-0_4_1').textContent, IncrementalDOM); 358 | var refs; 359 | 360 | IncrementalDOM.patch(container, function (data) { 361 | refs = templateFn(data); 362 | }); 363 | 364 | expect(container.querySelector('section')).to.equal(refs.section); 365 | expect(container.querySelector('div')).to.equal(refs.div); 366 | expect(container.querySelector('input')).to.equal(refs.input); 367 | }); 368 | 369 | it("0.4.1.2: static refs (second function type)", function () { 370 | var templateFn = itemplate.compile(document.querySelector('#test-0_4_1').textContent); 371 | var refs; 372 | 373 | IncrementalDOM.patch(container, function (data) { 374 | refs = templateFn(data, IncrementalDOM); 375 | }); 376 | 377 | expect(container.querySelector('section')).to.equal(refs.section); 378 | expect(container.querySelector('div')).to.equal(refs.div); 379 | expect(container.querySelector('input')).to.equal(refs.input); 380 | }); 381 | 382 | it("0.4.2: dynamic refs", function () { 383 | var templateFn = itemplate.compile(document.querySelector('#test-0_4_2').textContent); 384 | var refs; 385 | 386 | IncrementalDOM.patch(container, function () { 387 | refs = templateFn(null, IncrementalDOM); 388 | }); 389 | 390 | expect(container.querySelector('section')).to.equal(refs.section_1); 391 | expect(container.querySelector('div')).to.equal(refs.myDiv); 392 | expect(container.querySelector('input')).to.equal(refs.myInput); 393 | }); 394 | 395 | it("0.5.1: html escape", function () { 396 | var templateFn = itemplate.compile(document.querySelector('#test-0_5_1').textContent); 397 | var text = ''; 399 | 400 | IncrementalDOM.patch(container, function () { 401 | templateFn(null, IncrementalDOM); 402 | }); 403 | 404 | expect(container.textContent).to.equal(text); 405 | }); 406 | 407 | it("0.6.1: skip syntax's", function () { 408 | var templateFn = itemplate.compile(document.querySelector('#test-0_6_1').textContent); 409 | var innerHTML = '
A
'; 410 | 411 | IncrementalDOM.patch(container, function () { 412 | templateFn(null, IncrementalDOM); 413 | }); 414 | 415 | expect(container.innerHTML).to.equal(innerHTML); 416 | }); 417 | 418 | it("0.6.2: skip functionality 'false'", function () { 419 | var templateFn = itemplate.compile(document.querySelector('#test-0_6_2').textContent, IncrementalDOM); 420 | var innerHTML = '
AAA
'; 421 | 422 | IncrementalDOM.patch(container, templateFn, {skip: false}); 423 | 424 | expect(container.innerHTML).to.equal(innerHTML); 425 | }); 426 | 427 | it("0.6.3: skip functionality 'true'", function () { 428 | var templateFn = itemplate.compile(document.querySelector('#test-0_6_2').textContent, IncrementalDOM); 429 | var innerHTML = '
'; 430 | 431 | IncrementalDOM.patch(container, templateFn, {skip: true}); 432 | 433 | expect(container.innerHTML).to.equal(innerHTML); 434 | }); 435 | 436 | it("0.6.4: nested skip functionality", function () { 437 | var templateFn = itemplate.compile(document.querySelector('#test-0_6_4').textContent, IncrementalDOM); 438 | var innerHTML = '
BBB
AAA
'; 439 | var afterModification = '
CCC
AAA
'; 440 | 441 | IncrementalDOM.patch(container, templateFn, {skipRoot: false, skipChild: false}); 442 | expect(container.innerHTML).to.equal(innerHTML); 443 | 444 | container.querySelector('span').innerHTML = 'CCC'; 445 | 446 | IncrementalDOM.patch(container, templateFn, {skipRoot: true, skipChild: false}); 447 | expect(container.innerHTML).to.equal(afterModification); 448 | }); 449 | 450 | it("0.7.1: bind function to tag", function () { 451 | var templateFn = itemplate.compile(document.querySelector('#test-0_7_1').textContent, IncrementalDOM); 452 | var innerHTML = '
  • item:0
  • item:1
'; 453 | 454 | IncrementalDOM.patch(container, templateFn); 455 | 456 | expect(container.innerHTML).to.equal(innerHTML); 457 | }); 458 | 459 | it("0.7.2: bind function to tag - context", function () { 460 | var templateFn = itemplate.compile(document.querySelector('#test-0_7_2').textContent, IncrementalDOM); 461 | var context = { 462 | name: 'my-name', 463 | clazz: 'my-class' 464 | }; 465 | var innerHTML = '
my-name
'; 466 | 467 | IncrementalDOM.patch(container, templateFn.bind(context)); 468 | 469 | expect(container.innerHTML).to.equal(innerHTML); 470 | }); 471 | 472 | it("0.7.3: custom binding to tag", function () { 473 | var templateFn = itemplate.compile(document.querySelector('#test-0_7_3').textContent); 474 | var innerHTML = '
amount:0
'; 475 | 476 | function binder(fn, data, content) { 477 | var obj = new fn(data); 478 | return obj.foo(content); 479 | } 480 | 481 | IncrementalDOM.patch(container, function (data) { 482 | templateFn(data, IncrementalDOM, itemplate.helpers, null, binder); 483 | }); 484 | 485 | expect(container.innerHTML).to.equal(innerHTML); 486 | }); 487 | 488 | it("0.7.4: custom binding to tag - context", function () { 489 | var context = {amount: 0}; 490 | var templateFn = itemplate.compile(document.querySelector('#test-0_7_4').textContent).bind(context); 491 | var innerHTML = '
amount:0amount from context:0
'; 492 | 493 | function binder(fn, data, content) { 494 | var obj = new fn(data); 495 | return obj.foo(content); 496 | } 497 | 498 | IncrementalDOM.patch(container, function (data) { 499 | templateFn(data, IncrementalDOM, itemplate.helpers, null, binder); 500 | }); 501 | 502 | expect(container.innerHTML).to.equal(innerHTML); 503 | }); 504 | 505 | }); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Path = require('path'); 4 | 5 | function getSourcePath(url) { 6 | return Path.join(__dirname, 'source', url); 7 | } 8 | 9 | module.exports = { 10 | entry: getSourcePath('itemplate.js'), 11 | output: { 12 | libraryTarget: "umd", 13 | library: "itemplate", 14 | path: Path.join(__dirname, 'bin'), 15 | filename: "itemplate.js" 16 | }, node: { 17 | __filename: true 18 | } 19 | }; -------------------------------------------------------------------------------- /webpack.config.release.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Path = require('path'); 4 | var webpack = require("webpack"); 5 | 6 | function getSourcePath(url) { 7 | return Path.join(__dirname, 'source', url); 8 | } 9 | 10 | module.exports = { 11 | entry: getSourcePath('itemplate.js'), 12 | output: { 13 | libraryTarget: "umd", 14 | library: "itemplate", 15 | path: Path.join(__dirname, 'bin'), 16 | filename: "itemplate.min.js" 17 | }, 18 | node: { 19 | __filename: true 20 | }, 21 | plugins: [ 22 | new webpack.optimize.UglifyJsPlugin({ output: {comments: false} }) 23 | ] 24 | }; --------------------------------------------------------------------------------