├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── continous-integration.yaml ├── .gitignore ├── .jshintrc ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── webpack-es5 │ ├── .gitignore │ ├── README.md │ ├── lib │ │ ├── dummy.idomizer │ │ └── dummy.js │ ├── package.json │ └── webpack.config.js └── webpack-es6 │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ ├── dummy.idomizer │ └── dummy.js │ └── webpack.config.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── Options.ts ├── StringDictionary.ts ├── idomizer.ts └── plugins │ ├── babel-idomizer.ts │ ├── idomizer-loader.ts │ ├── idomizerify.ts │ └── utils.ts ├── test ├── idomizer.spec.ts └── plugins │ ├── babel-idomizer.spec.js │ ├── dummy.es6 │ └── idomizerify.spec.js ├── tsconfig.json ├── webpack.common.js ├── webpack.dev.js └── webpack.prd.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 4 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [*.json] 23 | indent_size = 2 24 | 25 | [*.yaml] 26 | indent_size = 2 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: thibault.morin 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/continous-integration.yaml: -------------------------------------------------------------------------------- 1 | name: Continous Integration 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: '12.x' 14 | registry-url: 'https://registry.npmjs.org' 15 | scope: '@tmorin' 16 | # build 17 | - name: Install dependencies 18 | run: npm ci 19 | - name: Lint sources 20 | run: npm run lint 21 | - name: Build sources 22 | run: npm run build 23 | - name: Test library 24 | run: npm run test:lib 25 | env: 26 | SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} 27 | SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} 28 | - name: Test plugins 29 | run: npm run test:plugins 30 | - name: Build documentation 31 | run: npm run docs:build 32 | # publication 33 | - name: Publish package 34 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 35 | run: npm publish --tag latest 36 | env: 37 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 38 | - name: Publish documentation 39 | if: ${{ startsWith(github.ref, 'refs/tags/') }} 40 | uses: peaceiris/actions-gh-pages@v3 41 | with: 42 | github_token: ${{ secrets.GITHUB_TOKEN }} 43 | publish_dir: ./typedoc 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.tmp 3 | /dist 4 | /lib 5 | /node_modules 6 | /typedoc 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "evil": true 4 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.* 2 | /typedoc 3 | /test 4 | /npm-debug.log 5 | karma.* 6 | webpack.* 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.0.2](https://github.com/tmorin/idomizer/compare/v1.0.1...v1.0.2) (2019-10-17) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * upkg now loads the UMD module ([d3a4049](https://github.com/tmorin/idomizer/commit/d3a40491cf97153393f95971311ed4a00064ee39)) 11 | 12 | 13 | ## [1.0.1](https://github.com/tmorin/idomizer/compare/v1.0.0...v1.0.1) (2018-11-22) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * correct filename about TypeScript types ([3a63cef](https://github.com/tmorin/idomizer/commit/3a63cef)) 19 | 20 | 21 | 22 | 23 | # [1.0.0](https://github.com/tmorin/idomizer/compare/v0.10.2...v1.0.0) (2018-11-22) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Thibault Morin 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 | # idomizer 2 | 3 | [![Continous Integration](https://github.com/tmorin/idomizer/actions/workflows/continous-integration.yaml/badge.svg)](https://github.com/tmorin/idomizer/actions/workflows/continous-integration.yaml) 4 | 5 | `idomizer` is an HTML template compiler providing an [incremental-dom] render factory. 6 | `idomizer` can be used at compile time (front end projects) or runtime time(back end projects). 7 | 8 | Versions and compatibilities: 9 | 10 | - idomizer <= 0.5 -> _incremental-dom_ 0.4 and below. 11 | - idomizer >= 0.6 -> _incremental-dom_ 0.5 and above. 12 | - idomizer >= 1.0.0 -> _incremental-dom_ 0.6 and above. 13 | 14 | ## Installation 15 | 16 | ```bash 17 | $ npm install idomizer 18 | ``` 19 | 20 | ```html 21 | 22 | 23 | 28 | ``` 29 | 30 | ### Babel 31 | 32 | A babel's plugin is available to compile an idomizer template into an [incremental-dom] render factory. 33 | 34 | See the [babel's plugins](https://babeljs.io/docs/en/plugins#syntax-plugins) page to get more information about plugins in babel. 35 | 36 | ```javascript 37 | { 38 | plugins: ['idomizer/lib/plugins/babel-idomizer.js'] 39 | } 40 | ``` 41 | 42 | Presently the plugin only support [ES6 Template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) tagged with _idomizer_. 43 | 44 | For instance, 45 | ```javascript 46 | const template = idomizer`

Hello

`; 47 | ``` 48 | will be compiled into: 49 | ```javascript 50 | var template = function (_i, _h) { 51 | var _elementOpen = _i.elementOpen, 52 | _elementClose = _i.elementClose, 53 | _elementVoid = _i.elementVoid, 54 | _text = _i.text, 55 | _skip = _i.skip; 56 | return function (_data_) { 57 | var helpers = _h || {}, 58 | data = _data_ || {}; 59 | _elementOpen('h1', null, null, 'class', data.h1Class); 60 | _text('Hello'); 61 | _elementClose('h1'); 62 | }; 63 | }; 64 | ``` 65 | 66 | Be aware the template can not contain expressions like ``${anExpression}``. 67 | 68 | ### Webpack 69 | 70 | A webpack's loader is available to compile an idomizer file into an [incremental-dom] render factory. 71 | 72 | See [module.rules](https://webpack.js.org/configuration/module/#module-rules) to get more information about loaders in webpack. 73 | 74 | ``` 75 | module.loaders: [ 76 | {test: /\.idomizer$/, loader: 'idomizer/lib/plugins/idomizer-loader'} 77 | ]; 78 | ``` 79 | 80 | ### Browserify 81 | 82 | A browserify's transform module is available to compile an idomizer file into an [incremental-dom] render factory. 83 | 84 | See [transforms](https://github.com/substack/browserify-handbook#transforms) to get more information about the transform system in browserify. 85 | 86 | ```shell 87 | browserify -t idomizer/lib/plugins/idomizerify main.js > bundle.js 88 | ``` 89 | 90 | ```javascript 91 | const browserify = require('browserify'); 92 | const idomizerify = require('idomizer/lib/plugins/idomizerify'); 93 | const bundle = browserify(); 94 | bundle.transform({ extension: 'html' }, idomizerify); 95 | ``` 96 | 97 | ## API 98 | 99 | ``idomizer.compile`` transforms an HTML template into a factory method. 100 | 101 | ```javascript 102 | // idomizer.compile('

Hello

') will return: 103 | function template(_i, _h) { 104 | var _elementOpen = _i.elementOpen, 105 | _elementClose = _i.elementClose, 106 | _elementVoid = _i.elementVoid, 107 | _text = _i.text, 108 | _skip = _i.skip; 109 | return function (_data_) { 110 | var helpers = _h || {}, 111 | data = _data_ || {}; 112 | _elementOpen('h1', null, ['class', 'main'], null); 113 | _text('Hello'); 114 | _elementClose('h1'); 115 | }; 116 | } 117 | ``` 118 | 119 | The factory method requires the _incremental-dom_ library and an optional map of helpers. 120 | The factory returns the _incremental-dom_'s render method. 121 | 122 | ## Syntax 123 | 124 | ### Static attributes 125 | 126 | From 127 | ```javascript 128 | idomizer.compile(`

Hello

`)(IncrementalDOM); 129 | ``` 130 | To 131 | ```javascript 132 | function template(_i, _h) { 133 | var _elementOpen = _i.elementOpen, 134 | _elementClose = _i.elementClose, 135 | _elementVoid = _i.elementVoid, 136 | _text = _i.text, 137 | _skip = _i.skip; 138 | return function (_data_) { 139 | var helpers = _h || {}, 140 | data = _data_ || {}; 141 | _elementOpen('h1', null, ['class', 'main'], null); 142 | _text('Hello'); 143 | _elementClose('h1'); 144 | }; 145 | } 146 | ``` 147 | 148 | ### Dynamic attributes 149 | 150 | From 151 | ```javascript 152 | idomizer.compile(`

Hello

`)(IncrementalDOM) 153 | ``` 154 | To 155 | ```javascript 156 | function template(_i, _h) { 157 | var _elementOpen = _i.elementOpen, 158 | _elementClose = _i.elementClose, 159 | _elementVoid = _i.elementVoid, 160 | _text = _i.text, 161 | _skip = _i.skip; 162 | return function (_data_) { 163 | var helpers = _h || {}, 164 | data = _data_ || {}; 165 | _elementOpen('h1', null, null, 'class', (data.h1Class)); 166 | _text('Hello'); 167 | _elementClose('h1'); 168 | }; 169 | } 170 | ``` 171 | 172 | ### Self closing elements 173 | 174 | From 175 | ```javascript 176 | idomizer.compile(``)(IncrementalDOM) 177 | ``` 178 | To 179 | ```javascript 180 | function template(_i, _h) { 181 | var _elementOpen = _i.elementOpen, 182 | _elementClose = _i.elementClose, 183 | _elementVoid = _i.elementVoid, 184 | _text = _i.text, 185 | _skip = _i.skip; 186 | return function (_data_) { 187 | var helpers = _h || {}, 188 | data = _data_ || {}; 189 | _elementVoid('input', null, ['type', 'text'], 'value', (data.value)); 190 | }; 191 | } 192 | ``` 193 | 194 | ### Dynamic text nodes 195 | 196 | From 197 | ```javascript 198 | idomizer.compile(``)(IncrementalDOM) 199 | // or 200 | idomizer.compile(`{{ data.value }}`)(IncrementalDOM) 201 | ``` 202 | To 203 | ```javascript 204 | function template(_i, _h) { 205 | var _elementOpen = _i.elementOpen, 206 | _elementClose = _i.elementClose, 207 | _elementVoid = _i.elementVoid, 208 | _text = _i.text, 209 | _skip = _i.skip; 210 | return function (_data_) { 211 | var helpers = _h || {}, 212 | data = _data_ || {}; 213 | _elementOpen('strong', null, null, null); 214 | _text(data.value); 215 | _elementClose('strong'); 216 | }; 217 | } 218 | ``` 219 | 220 | ### Condition with the tags _if_, _else-if_ and _else_ 221 | 222 | From 223 | ```javascript 224 | idomizer.compile(` 225 | 226 | YES! 227 | 228 | MAY BE! 229 | 230 | NO! 231 | 232 | `)(IncrementalDOM); 233 | ``` 234 | To 235 | ```javascript 236 | function template(_i, _h) { 237 | var _elementOpen = _i.elementOpen, 238 | _elementClose = _i.elementClose, 239 | _elementVoid = _i.elementVoid, 240 | _text = _i.text, 241 | _skip = _i.skip; 242 | return function (_data_) { 243 | var helpers = _h || {}, 244 | data = _data_ || {}; 245 | if (data.yes) { 246 | _text('YES!'); 247 | } else if (data.yes !== false) { 248 | _text('MAY BE!'); 249 | } else { 250 | _text('NO!'); 251 | } 252 | }; 253 | } 254 | ``` 255 | 256 | ### Iteration with the tag _each_ 257 | 258 | From 259 | ```javascript 260 | idomizer.compile(` 261 | 262 | 263 | - 264 | 265 | 266 | `)(IncrementalDOM); 267 | ``` 268 | To 269 | ```javascript 270 | function template(_i, _h) { 271 | var _elementOpen = _i.elementOpen, 272 | _elementClose = _i.elementClose, 273 | _elementVoid = _i.elementVoid, 274 | _text = _i.text, 275 | _skip = _i.skip; 276 | return function (_data_) { 277 | var helpers = _h || {}, 278 | data = _data_ || {}; 279 | (data.items || []).forEach(function (item, index) { 280 | _elementOpen('strong', (index), null, null); 281 | _text(index); 282 | _text('-'); 283 | _text(item); 284 | _elementClose('strong'); 285 | }); 286 | }; 287 | } 288 | ``` 289 | 290 | ### Iteration with inline javascript 291 | 292 | From 293 | ```javascript 294 | idomizer.compile(` 295 | [[ data.items.forEach(function (item, i) { ]] 296 | 297 | - 298 | 299 | [[ }); ]] 300 | `)(IncrementalDOM); 301 | ``` 302 | To 303 | ```javascript 304 | function template(_i, _h) { 305 | var _elementOpen = _i.elementOpen, 306 | _elementClose = _i.elementClose, 307 | _elementVoid = _i.elementVoid, 308 | _text = _i.text, 309 | _skip = _i.skip; 310 | return function (_data_) { 311 | var helpers = _h || {}, 312 | data = _data_ || {}; 313 | data.items.forEach(function (item, i) { 314 | _elementOpen('strong', (i), null, null); 315 | _text(i); 316 | _text('-'); 317 | _text(item); 318 | _elementClose('strong'); 319 | }); 320 | }; 321 | } 322 | ``` 323 | 324 | ### Custom tags 325 | 326 | From 327 | ```javascript 328 | idomizer.compile(`strong textstrong text`, { 329 | tags: { 330 | 'x-test': { 331 | onopentag(name, attrs, key, statics, varArgs, options) { 332 | return `t('${name} element');`; 333 | } 334 | } 335 | } 336 | })(IncrementalDOM); 337 | ``` 338 | To 339 | ```javascript 340 | function template(_i, _h) { 341 | var _elementOpen = _i.elementOpen, 342 | _elementClose = _i.elementClose, 343 | _elementVoid = _i.elementVoid, 344 | _text = _i.text, 345 | _skip = _i.skip; 346 | return function (_data_) { 347 | var helpers = _h || {}, 348 | data = _data_ || {}; 349 | _elementOpen('strong', null, null, null); 350 | _text('strong text'); 351 | _elementClose('strong'); 352 | _text('x-test element'); 353 | _elementOpen('strong', null, null, null); 354 | _text('strong text'); 355 | _elementClose('strong'); 356 | }; 357 | } 358 | ``` 359 | 360 | ### Custom helpers 361 | 362 | From 363 | ```javascript 364 | const subRender = compile(`helper content`)(IncrementalDOM); 365 | idomizer.compile(` 366 | strong text 367 | 368 | strong text 369 | `)(IncrementalDOM, {subRender}); 370 | ``` 371 | To 372 | ```javascript 373 | function template(_i, _h) { 374 | var _elementOpen = _i.elementOpen, 375 | _elementClose = _i.elementClose, 376 | _elementVoid = _i.elementVoid, 377 | _text = _i.text, 378 | _skip = _i.skip; 379 | return function (_data_) { 380 | var helpers = _h || {}, 381 | data = _data_ || {}; 382 | _elementOpen('strong', null, null, null); 383 | _text('strong text'); 384 | _elementClose('strong'); 385 | helpers.subRender(data); 386 | _elementOpen('strong', null, null, null); 387 | _text('strong text'); 388 | _elementClose('strong'); 389 | }; 390 | } 391 | ``` 392 | 393 | ### Custom elements 394 | 395 | For [incremental-dom], custom elements are regular HTML elements. 396 | So, if a custom element generates a sub-tree (i.e. a light DOM) outside of a ShadowDOM node, 397 | it will be overridden during the execution of the function `patch()`. 398 | To control this default behavior, [incremental-dom] provides the function `skip()` saying: 399 | _don't touch the inner light DOM of the just opened node!_ 400 | 401 | By default, idomizier detects the custom elements and force the call of the function `skip()` to protect their light DOM nodes. 402 | Custom elements are detected according to the following rules: 403 | 404 | - from the name, because of the `-` character 405 | - from the attribute `ìs` 406 | 407 | Obviously, this behavior can be deactivated: 408 | 409 | - globally (for a whole HTML template) 410 | ```javascript 411 | const render = compile(`

will part of the light DOM

`, {skipCustomElements : false}) 412 | ``` 413 | - locally (an HTML element), `` 414 | ```javascript 415 | const render = compile(`

will part of the light DOM

`) 416 | ``` 417 | 418 | [incremental-dom]: https://google.github.io/incremental-dom 419 | -------------------------------------------------------------------------------- /examples/webpack-es5/.gitignore: -------------------------------------------------------------------------------- 1 | /npm-debug.log 2 | /node_modules 3 | /dist 4 | -------------------------------------------------------------------------------- /examples/webpack-es5/README.md: -------------------------------------------------------------------------------- 1 | # webpack-es5 2 | 3 | ```shell 4 | $ npm install 5 | ``` 6 | 7 | ```shell 8 | $ npm start 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/webpack-es5/lib/dummy.idomizer: -------------------------------------------------------------------------------- 1 |

Hello

-------------------------------------------------------------------------------- /examples/webpack-es5/lib/dummy.js: -------------------------------------------------------------------------------- 1 | var render = require('./dummy.idomizer'); 2 | 3 | if (typeof console != 'undefined') { 4 | console.log(render.toString()); 5 | } 6 | -------------------------------------------------------------------------------- /examples/webpack-es5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-es5", 3 | "private": true, 4 | "scripts": { 5 | "start": "webpack" 6 | }, 7 | "devDependencies": { 8 | "webpack": "^1.13.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/webpack-es5/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './lib/dummy.js', 3 | module: { 4 | loaders: [ 5 | {test: /\.idomizer?$/, loader: '../../../lib/plugins/idomizer-loader.js'} 6 | ] 7 | }, 8 | output: { 9 | path: './dist', 10 | filename: 'dummy.js' 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /examples/webpack-es6/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "env": { 6 | "test": { 7 | "plugins": [ 8 | "__coverage__" 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/webpack-es6/.gitignore: -------------------------------------------------------------------------------- 1 | /npm-debug.log 2 | /node_modules 3 | /lib 4 | -------------------------------------------------------------------------------- /examples/webpack-es6/README.md: -------------------------------------------------------------------------------- 1 | # webpack-es6 2 | 3 | ```shell 4 | $ npm install 5 | ``` 6 | 7 | ```shell 8 | $ npm start 9 | ``` 10 | -------------------------------------------------------------------------------- /examples/webpack-es6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-es6", 3 | "private": true, 4 | "scripts": { 5 | "start": "webpack" 6 | }, 7 | "devDependencies": { 8 | "babel-core": "^6.10.4", 9 | "babel-loader": "^6.0.0", 10 | "babel-preset-es2015": "^6.0.0", 11 | "webpack": "^1.13.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/webpack-es6/src/dummy.idomizer: -------------------------------------------------------------------------------- 1 |

Hello

-------------------------------------------------------------------------------- /examples/webpack-es6/src/dummy.js: -------------------------------------------------------------------------------- 1 | import render from './dummy.idomizer'; 2 | 3 | if (typeof console != 'undefined') { 4 | console.log(render.toString()); 5 | } 6 | -------------------------------------------------------------------------------- /examples/webpack-es6/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/dummy.js', 3 | module: { 4 | loaders: [ 5 | {test: /\.idomizer?$/, loader: '../../../lib/plugins/idomizer-loader.js'}, 6 | {test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel'} 7 | ] 8 | }, 9 | output: { 10 | path: './lib', 11 | filename: 'dummy.js' 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpackDev = require('./webpack.dev'); 2 | 3 | if (!process.env.CHROME_BIN) { 4 | process.env.CHROME_BIN = require('puppeteer').executablePath(); 5 | } 6 | 7 | module.exports = (config) => { 8 | config.set({ 9 | frameworks: ['mocha', 'webpack'], 10 | 11 | reporters: ['progress', 'junit'], 12 | 13 | files: [ 14 | {pattern: 'test/*.spec.ts', watched: false} 15 | ], 16 | 17 | preprocessors: { 18 | 'test/*.spec.ts': ['webpack'] 19 | }, 20 | 21 | webpack: { 22 | module: webpackDev.module, 23 | resolve: webpackDev.resolve, 24 | mode: webpackDev.mode, 25 | devtool: webpackDev.devtool 26 | }, 27 | 28 | webpackMiddleware: { 29 | stats: 'errors-only' 30 | }, 31 | 32 | client: { 33 | mocha: { 34 | reporter: 'html' 35 | } 36 | }, 37 | 38 | junitReporter: { 39 | outputDir: '.tmp/junit' 40 | } 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "idomizer", 3 | "version": "1.0.2", 4 | "description": "An HTML template compiler providing an incremental-dom render factory.", 5 | "keywords": [ 6 | "template", 7 | "incremental", 8 | "dom", 9 | "incremental-dom", 10 | "virtual", 11 | "virtual-dom", 12 | "browserify", 13 | "webpack", 14 | "systemjs", 15 | "jspm", 16 | "loader", 17 | "webpack-loader", 18 | "module", 19 | "plugin", 20 | "babel-plugin", 21 | "systemjs-plugin", 22 | "transform", 23 | "browserify-transform" 24 | ], 25 | "homepage": "https://tmorin.github.io/idomizer", 26 | "bugs": { 27 | "url": "https://github.com/tmorin/idomizer/issues" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/tmorin/idomizer.git" 32 | }, 33 | "license": "MIT", 34 | "author": { 35 | "name": "Thibault Morin", 36 | "url": "https://tmorin.github.io" 37 | }, 38 | "main": "lib/idomizer.js", 39 | "types": "lib/idomizer.d.ts", 40 | "unpkg": "dist/idomizer.min.js", 41 | "scripts": { 42 | "alpha": "npm run build && standard-version -a --skip.changelog --skip.tag --prerelease alpha", 43 | "alpha:publish": "git push --all && npm publish --tag canary", 44 | "build": "npm run build:clean && npm run build:lib && npm run build:umd && npm run build:umd:min", 45 | "build:clean": "rimraf dist lib *.tgz", 46 | "build:lib": "tsc", 47 | "build:umd": "webpack --config webpack.dev.js", 48 | "build:umd:min": "webpack --config webpack.prd.js", 49 | "docs:build": "npm run docs:clean && typedoc", 50 | "docs:clean": "rimraf typedoc", 51 | "docs:publish": "npm run docs:build && cd typedoc && git init && git commit --allow-empty -m 'update typedoc' && git checkout -b gh-pages && git add --all && git commit -am 'update typedoc, [skip ci]' && git push git@github.com:tmorin/idomizer gh-pages --force && cd .. && npm run docs:clean", 52 | "lint": "jshint src test", 53 | "release": "npm run build && standard-version-a", 54 | "release:publish": "git push --follow-tags", 55 | "test": "npm run test:plugins && npm run test:lib", 56 | "test:lib": "karma start --single-run --no-auto-watch --browsers FirefoxHeadless,ChromeHeadless", 57 | "test:lib:local": "karma start --single-run --no-auto-watch --browsers Firefox", 58 | "test:lib:watch": "karma start --no-single-run --auto-watch --browsers Firefox", 59 | "test:plugins": "mocha --require @babel/register test/plugins/*.spec.js" 60 | }, 61 | "standard-version": { 62 | "scripts": { 63 | "prerelease": "npm run build" 64 | } 65 | }, 66 | "dependencies": { 67 | "browserify-transform-tools": "^1.5.0", 68 | "core-js": "^3.14.0", 69 | "htmlparser2": "^6.1.0", 70 | "loader-utils": "^2.0.0" 71 | }, 72 | "devDependencies": { 73 | "@babel/cli": "^7.14.5", 74 | "@babel/core": "^7.14.6", 75 | "@babel/plugin-transform-runtime": "^7.14.5", 76 | "@babel/preset-env": "^7.14.5", 77 | "@babel/preset-typescript": "^7.14.5", 78 | "@babel/register": "^7.14.5", 79 | "@types/chai": "^4.2.18", 80 | "@types/mocha": "^8.2.2", 81 | "@types/node": "^14.17.3", 82 | "babel-loader": "^8.2.2", 83 | "browserify": "^17.0.0", 84 | "chai": "^4.3.4", 85 | "incremental-dom": "^0.7.0", 86 | "jshint": "^2.13.0", 87 | "json-loader": "^0.5.0", 88 | "karma": "^6.3.4", 89 | "karma-chrome-launcher": "^3.1.0", 90 | "karma-cli": "^2.0.0", 91 | "karma-firefox-launcher": "^2.1.1", 92 | "karma-junit-reporter": "^2.0.1", 93 | "karma-mocha": "^2.0.1", 94 | "karma-webpack": "^5.0.0", 95 | "mocha": "^9.0.1", 96 | "puppeteer": "^10.0.0", 97 | "rimraf": "^3.0.2", 98 | "standard-version": "^9.3.0", 99 | "ts-node": "^10.0.0", 100 | "typedoc": "^0.21.0", 101 | "typescript": "^4.3.4", 102 | "webpack": "^5.39.1", 103 | "webpack-cli": "^4.7.2", 104 | "webpack-merge": "^5.8.0" 105 | }, 106 | "babel": { 107 | "presets": [ 108 | [ 109 | "@babel/preset-env", 110 | { 111 | "useBuiltIns": "usage", 112 | "corejs": { 113 | "version": 3, 114 | "proposals": true 115 | } 116 | } 117 | ], 118 | "@babel/preset-typescript" 119 | ] 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Options.ts: -------------------------------------------------------------------------------- 1 | import {StringDictionary} from './StringDictionary'; 2 | 3 | /** 4 | * The purpose of a tag handler is to convert tags to JavaScript instructions. 5 | */ 6 | export interface TagHandler { 7 | onopentag( 8 | name: string, 9 | attrs: StringDictionary, 10 | key: string, 11 | statics: StringDictionary, 12 | varArgs: StringDictionary, 13 | options: Options 14 | ): string 15 | 16 | onclosetag?( 17 | name: string, 18 | options: Options 19 | ): string 20 | } 21 | 22 | /** 23 | * Dictionary of tag handlers. 24 | */ 25 | export interface TagHandlers { 26 | [k: string]: TagHandler 27 | } 28 | 29 | /** 30 | * The default tag handlers. 31 | */ 32 | export interface DefaultTagHandlers extends TagHandlers { 33 | /** 34 | * @example 35 | * ```javascript 36 | * idomizer.compile(''); 37 | * ``` 38 | */ 39 | 'tpl-logger': TagHandler 40 | 41 | /** 42 | * @example 43 | * ```javascript 44 | * idomizer.compile(` 45 | * \\n 46 | *
  • 47 | * 48 | *
  • 49 | *
    50 | * `); 51 | * ``` 52 | */ 53 | 'tpl-each': TagHandler 54 | 55 | /** 56 | * @example 57 | * ```javascript 58 | * idomizer.compile(` 59 | * 60 | *

    1 value

    61 | * 62 | *

    some values

    63 | * 64 | *

    no values to display

    65 | *
    66 | * `); 67 | * ``` 68 | */ 69 | 'tpl-if': TagHandler 70 | 71 | /** 72 | * @example 73 | * ```javascript 74 | * idomizer.compile(` 75 | * 76 | *

    1 value

    77 | * 78 | *

    some values

    79 | *
    80 | * `); 81 | * ``` 82 | */ 83 | 'tpl-else-if': TagHandler 84 | 85 | /** 86 | * @example 87 | * ```javascript 88 | * idomizer.compile(` 89 | * 90 | *

    1 value

    91 | * 92 | *

    no values to display

    93 | *
    94 | * `); 95 | * ``` 96 | */ 97 | 'tpl-else': TagHandler 98 | 99 | /** 100 | * @example 101 | * ```javascript 102 | * idomizer.compile(`;`); 103 | * ``` 104 | */ 105 | 'tpl-text': TagHandler 106 | 107 | /** 108 | * @example 109 | * ```javascript 110 | * let anotherRenderFunction = // antoher IncrementalDOM render function 111 | * idomizer.compile(` 112 | * 113 | * `)(IncrementalDOM, { 114 | * anotherRender: anotherRenderFunction 115 | * }); 116 | * ``` 117 | */ 118 | 'tpl-call': TagHandler 119 | } 120 | 121 | /** 122 | * The idomizer options. 123 | */ 124 | export interface Options { 125 | /** 126 | * Append a end of line character ('\\n') after each statements. 127 | */ 128 | pretty?: boolean 129 | /** 130 | * Discovered static attributes will be handled as dynamic attributes. 131 | */ 132 | ignoreStaticAttributes?: boolean 133 | /** 134 | * Regular expression to inject interpolated values. 135 | */ 136 | interpolation?: RegExp 137 | /** 138 | * Regular expression to inject JavaScript code. 139 | */ 140 | expression?: RegExp 141 | /** 142 | * The name of the IncrementalDOM's key. Should be used when dealing with loops. 143 | */ 144 | attributeKey?: string 145 | /** 146 | * The flag to skip the process eventual children. 147 | * @example 148 | * ``` 149 | *

    150 | * ``` 151 | */ 152 | attributeSkip?: string 153 | /** 154 | * If true exceptions raised during interpolation will be skipped and an empty string wil be used as result value.
    By default true. 155 | */ 156 | skipExceptions?: boolean 157 | /** 158 | * If true element name having `-` or having an attribute `is` will be skipped. 159 | * By default `true`. 160 | */ 161 | skipCustomElements?: boolean 162 | /** 163 | * The name of the variable exposing the data. 164 | */ 165 | varDataName?: string 166 | /** 167 | * The name of the variable exposing the helpers. 168 | */ 169 | varHelpersName?: string 170 | /** 171 | * The list of self closing elements. (http://www.w3.org/TR/html5/syntax.html#void-elements) 172 | */ 173 | selfClosingElements?: string[] 174 | /** 175 | * The built in and custom tags. 176 | */ 177 | tags?: TagHandlers 178 | } 179 | -------------------------------------------------------------------------------- /src/StringDictionary.ts: -------------------------------------------------------------------------------- 1 | export interface StringDictionary { 2 | [type: string]: string 3 | } 4 | -------------------------------------------------------------------------------- /src/idomizer.ts: -------------------------------------------------------------------------------- 1 | import {Parser} from 'htmlparser2'; 2 | import {Options} from './Options'; 3 | import {DefaultTagHandlers} from './Options'; 4 | import {StringDictionary} from './StringDictionary'; 5 | import {TagHandler} from './Options'; 6 | 7 | function assign(...args) { 8 | return args.reduce(function (target, source) { 9 | return Object.keys(Object(source)).reduce((target, key) => { 10 | target[key] = source[key]; 11 | return target; 12 | }, target); 13 | }); 14 | } 15 | 16 | /** 17 | * The default implementation of the default tag handlers. 18 | */ 19 | const DEFAULT_TAGS_HANDLERS: DefaultTagHandlers = { 20 | 'tpl-logger': { 21 | onopentag(name, attrs, key, statics, varArgs) { 22 | let level = statics.level || varArgs.level || 'log', 23 | content = statics.content || varArgs.content || ''; 24 | return `console.${level}(${content});`; 25 | } 26 | }, 27 | 'tpl-each': { 28 | onopentag(name, attrs, key, statics, varArgs) { 29 | let itemsName = statics.items || varArgs.items || `items`, 30 | itemName = statics.item || varArgs.item || `item`, 31 | indexName = statics.index || varArgs.index || `index`; 32 | return `(${itemsName} || []).forEach(function (${itemName}, ${indexName}) {`; 33 | }, 34 | onclosetag() { 35 | return `});`; 36 | } 37 | }, 38 | 'tpl-if': { 39 | onopentag(name, attrs, key, statics, varArgs) { 40 | let expression = statics.expression || varArgs.expression || 'false'; 41 | return `if (${expression}) {`; 42 | }, 43 | onclosetag() { 44 | return `}`; 45 | } 46 | }, 47 | 'tpl-else-if': { 48 | onopentag(name, attrs, key, statics, varArgs) { 49 | let expression = statics.expression || varArgs.expression || 'false'; 50 | return ` } else if (${expression}) { `; 51 | } 52 | }, 53 | 'tpl-else': { 54 | onopentag() { 55 | return ` } else { `; 56 | } 57 | }, 58 | 'tpl-text': { 59 | onopentag(name, attrs, key, statics, varArgs, options) { 60 | return inlineInterpolationEvaluator.inject(statics.value || varArgs.value, options); 61 | } 62 | }, 63 | 'tpl-call': { 64 | onopentag(name, attrs, key, statics, varArgs, options) { 65 | let helperName = statics.name || varArgs.name; 66 | return `${options.varHelpersName}.${helperName}(${options.varDataName});`; 67 | } 68 | } 69 | }; 70 | 71 | /** 72 | * The default options. 73 | */ 74 | const DEFAULT_OPTIONS: Options = { 75 | pretty: false, 76 | ignoreStaticAttributes: false, 77 | interpolation: /{{([\s\S]+?)}}/gm, 78 | expression: /\[\[([\s\S]+?)]]/gm, 79 | attributeKey: 'tpl-key', 80 | attributeSkip: 'tpl-skip', 81 | skipExceptions: true, 82 | skipCustomElements: true, 83 | varDataName: 'data', 84 | varHelpersName: 'helpers', 85 | selfClosingElements: ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'], 86 | tags: DEFAULT_TAGS_HANDLERS 87 | }; 88 | 89 | function stringify(value = ''): string { 90 | return value.replace(/'/gim, '\\\'').replace(/\n/gi, '\\n'); 91 | } 92 | 93 | function isSelfClosing(name = '', options = DEFAULT_OPTIONS): boolean { 94 | return options.selfClosingElements.indexOf(name) > -1; 95 | } 96 | 97 | function getFunctionName(name = '', options = DEFAULT_OPTIONS): string { 98 | return isSelfClosing(name, options) ? '_elementVoid' : '_elementOpen'; 99 | } 100 | 101 | function append(body = '', line = '', options = DEFAULT_OPTIONS): string { 102 | if (line) { 103 | return body + (options.pretty ? '\n' : '') + line; 104 | } 105 | return body; 106 | } 107 | 108 | function createSafeJsBlock(value: string) { 109 | return `(function () { try { return ${value} } catch(e) { return '' } })()`; 110 | } 111 | 112 | /** 113 | * Configuration to transform an expression into a compliant JavaScript fragment. 114 | */ 115 | interface Evaluator { 116 | /** 117 | * Appender between statements. 118 | */ 119 | appender: string 120 | /** 121 | * To convert a text statements. 122 | * @param value the value 123 | */ 124 | toText?: (value: string, options: Options) => string 125 | /** 126 | * To convert a js statements. 127 | * @param value the value 128 | * @param options the options 129 | */ 130 | inject: (value: string, options: Options) => string 131 | } 132 | 133 | const attributeEvaluator: Evaluator = { 134 | appender: ' + ', 135 | toText: (value) => `'${stringify(value)}'`, 136 | inject: (value, options: Options) => options.skipExceptions ? createSafeJsBlock(value.trim()) : `(${value.trim()})` 137 | }; 138 | 139 | const inlineInterpolationEvaluator: Evaluator = { 140 | appender: ' ', 141 | inject: (value, options) => options.skipExceptions ? `_text(${createSafeJsBlock(value.trim())});` : `_text(${value.trim()});` 142 | }; 143 | 144 | const inlineExpressionEvaluator: Evaluator = { 145 | appender: ' ', 146 | inject: value => `${value}` 147 | }; 148 | 149 | function evaluate(value: string, evaluator: Evaluator, regex: RegExp, options: Options): string { 150 | let js = []; 151 | let result; 152 | let lastIndex = 0; 153 | while ((result = regex.exec(value)) !== null) { 154 | let full = result[0]; 155 | let group = result[1]; 156 | let index = result.index; 157 | let before = value.substring(lastIndex, index); 158 | if (before) { 159 | js.push(evaluator.toText(before, options)); 160 | } 161 | if (group.trim()) { 162 | js.push(evaluator.inject(group, options)); 163 | } 164 | lastIndex = index + full.length; 165 | } 166 | let after = value.substring(lastIndex, value.length); 167 | if (after) { 168 | js.push(evaluator.toText(after, options)); 169 | } 170 | return js.join(evaluator.appender); 171 | } 172 | 173 | function wrapExpressions(value: string, options: Options): string { 174 | return value.replace(options.interpolation, '').replace(options.expression, ''); 175 | } 176 | 177 | function unwrapExpressions(value: string): string { 178 | return value.replace(//gim, ''); 179 | } 180 | 181 | function checkSkipAttribute(attrs: StringDictionary = {}, options = DEFAULT_OPTIONS): boolean { 182 | return attrs.hasOwnProperty(options.attributeSkip) && attrs[options.attributeSkip] !== 'deactivated'; 183 | } 184 | 185 | function checkIsAttribute(attrs: StringDictionary = {}, options = DEFAULT_OPTIONS): boolean { 186 | return options.skipCustomElements && attrs.hasOwnProperty('is') && attrs[options.attributeSkip] !== 'deactivated'; 187 | } 188 | 189 | function parseAttributes(attrs: StringDictionary = {}, options = DEFAULT_OPTIONS) { 190 | const skip: boolean = checkSkipAttribute(attrs, options) || checkIsAttribute(attrs, options); 191 | 192 | const statics: StringDictionary = {}; 193 | const varArgs: StringDictionary = {}; 194 | Object.keys(attrs) 195 | .filter(key => [options.attributeSkip].indexOf(key) < 0) 196 | .forEach(function (key) { 197 | let value = unwrapExpressions(attrs[key]); 198 | if (value.search(options.interpolation) > -1 || options.ignoreStaticAttributes) { 199 | varArgs[key] = evaluate(value, attributeEvaluator, options.interpolation, options); 200 | } else { 201 | statics[key] = value; 202 | } 203 | }); 204 | 205 | const key = statics[options.attributeKey] || varArgs[options.attributeKey]; 206 | delete statics[options.attributeKey]; 207 | delete varArgs[options.attributeKey]; 208 | 209 | return {statics, varArgs, key, skip}; 210 | } 211 | 212 | function varArgsToJs(varArgs = {}): string { 213 | let keys = Object.keys(varArgs); 214 | return keys.length > 0 ? (keys.map(key => `'${key}', ${varArgs[key]}`).join(', ')) : 'null'; 215 | } 216 | 217 | function staticsToJs(statics = {}): string { 218 | let keys = Object.keys(statics); 219 | return keys.length > 0 ? `[${keys.map(key => `'${key}', '${stringify(statics[key])}'`).join(', ')}]` : 'null'; 220 | } 221 | 222 | function checkCustomElement(name = '', attrs: StringDictionary = {}, options = DEFAULT_OPTIONS): boolean { 223 | return options.skipCustomElements && attrs[options.attributeSkip] !== 'deactivated' && name.indexOf('-') > -1; 224 | } 225 | 226 | /** 227 | * Compile the given HTML template into a function factory. 228 | * 229 | * If the incrementalDOM argument is provided, this function will return a render function. 230 | * The render function is used with IncrementalDOM.patch. 231 | * 232 | * If the incrementalDOM argument is not provided, this function will return a factory function. 233 | * The factory function requires the IncrementalDOM library as argument and return the render function.. 234 | * 235 | * Basically, when the template is compiled at build time, the IncrementalDOM should not be given. 236 | * When the template is compiled at runtime, the IncrementalDOM should be given. 237 | * 238 | * @param html the template 239 | * @param options the options 240 | * @returns the function factory 241 | */ 242 | export function compile(html = '', options: Options = DEFAULT_OPTIONS): Function { 243 | options = assign({}, DEFAULT_OPTIONS, options, { 244 | tags: assign({}, DEFAULT_TAGS_HANDLERS, options.tags) 245 | }); 246 | let fnBody = ''; 247 | let parser = new Parser({ 248 | onopentag(name, attrs) { 249 | const {statics, varArgs, key, skip} = parseAttributes(attrs, options); 250 | if (options.tags[name]) { 251 | const tagHandler: TagHandler = options.tags[name]; 252 | if (typeof tagHandler.onopentag === 'function') { 253 | fnBody = append( 254 | fnBody, 255 | tagHandler.onopentag(name, attrs, key, statics, varArgs, options), 256 | options 257 | ); 258 | } 259 | } else { 260 | const fn = getFunctionName(name, options); 261 | fnBody = append( 262 | fnBody, 263 | `${fn}('${name}', ${key ? `${key}` : 'null'}, ${staticsToJs(statics)}, ${varArgsToJs(varArgs)});`, 264 | options 265 | ); 266 | if (skip || checkCustomElement(name, attrs, options)) { 267 | fnBody = append( 268 | fnBody, 269 | `_skip();`, 270 | options 271 | ); 272 | } 273 | } 274 | }, 275 | onclosetag(name) { 276 | if (options.tags[name]) { 277 | const tagHandler: TagHandler = options.tags[name]; 278 | if (typeof tagHandler.onclosetag === 'function') { 279 | fnBody = append( 280 | fnBody, 281 | tagHandler.onclosetag(name, options), 282 | options 283 | ); 284 | } 285 | } else if (!isSelfClosing(name, options)) { 286 | fnBody = append( 287 | fnBody, 288 | `_elementClose('${name}');`, 289 | options 290 | ); 291 | } 292 | }, 293 | ontext(text) { 294 | if (text.search(options.expression) > -1) { 295 | fnBody = append( 296 | fnBody, 297 | `${evaluate(text, inlineExpressionEvaluator, options.expression, options)}`, 298 | options 299 | ); 300 | } else if (text.search(options.interpolation) > -1) { 301 | fnBody = append( 302 | fnBody, 303 | `${evaluate(text, inlineInterpolationEvaluator, options.interpolation, options)}`, 304 | options 305 | ); 306 | } else { 307 | fnBody = append( 308 | fnBody, 309 | `_text('${stringify(text)}');`, 310 | options 311 | ); 312 | } 313 | } 314 | }, { 315 | xmlMode: false, 316 | decodeEntities: true, 317 | lowerCaseTags: false, 318 | lowerCaseAttributeNames: false, 319 | recognizeSelfClosing: true, 320 | recognizeCDATA: true 321 | }); 322 | 323 | // wrap inline expression with a CDATA tag to allow inline javascript 324 | parser.parseComplete(wrapExpressions(html, options)); 325 | 326 | let fnWrapper = ` 327 | var _elementOpen = _i.elementOpen, 328 | _elementClose = _i.elementClose, 329 | _elementVoid = _i.elementVoid, 330 | _text = _i.text, 331 | _skip = _i.skip; 332 | return function (_data_) { 333 | var ${options.varHelpersName || 'helpers'} = _h, 334 | ${options.varDataName || 'data'} = _data_; 335 | ${fnBody} 336 | }; 337 | `; 338 | 339 | // @ts-ignore 340 | return new Function(['_i', '_h'], fnWrapper); 341 | } 342 | -------------------------------------------------------------------------------- /src/plugins/babel-idomizer.ts: -------------------------------------------------------------------------------- 1 | import {toStringFunction} from './utils'; 2 | 3 | /** 4 | * @ignore 5 | */ 6 | export default function ({types: t}) { 7 | return { 8 | visitor: { 9 | TaggedTemplateExpression(path, state) { 10 | if (path.node.tag.name === 'idomizer' && path.node.quasi.quasis.length === 1) { 11 | let factory = toStringFunction(path.node.quasi.quasis[0].value.cooked, state.opts); 12 | path.replaceWithSourceString(factory); 13 | } 14 | } 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/plugins/idomizer-loader.ts: -------------------------------------------------------------------------------- 1 | import loaderUtils from 'loader-utils'; 2 | 3 | const utils = require('./utils'); 4 | 5 | /** 6 | * @ignore 7 | */ 8 | module.exports = function (source) { 9 | const options = loaderUtils.getOptions(this); 10 | return 'module.exports = ' + utils.toStringFunction(source || '', options || {}); 11 | }; 12 | 13 | -------------------------------------------------------------------------------- /src/plugins/idomizerify.ts: -------------------------------------------------------------------------------- 1 | import {makeStringTransform} from 'browserify-transform-tools'; 2 | import {toStringFunction} from './utils'; 3 | 4 | const options = { 5 | includeExtensions: ['.idomizer'] 6 | }; 7 | 8 | /** 9 | * @ignore 10 | */ 11 | export default makeStringTransform('idomizerify', options, (content, transformOptions, done) => { 12 | done(null, 'module.exports = ' + toStringFunction(content, transformOptions.config)); 13 | }); 14 | -------------------------------------------------------------------------------- /src/plugins/utils.ts: -------------------------------------------------------------------------------- 1 | var idomizer = require('../idomizer'); 2 | 3 | /** 4 | * @ignore 5 | */ 6 | export function toStringFunction(html, options) { 7 | return idomizer.compile(html, options).toString().replace('function anonymous', 'function'); 8 | } 9 | -------------------------------------------------------------------------------- /test/idomizer.spec.ts: -------------------------------------------------------------------------------- 1 | import {compile} from '../src/idomizer'; 2 | import * as IncrementalDOM from 'incremental-dom'; 3 | import {expect} from 'chai'; 4 | 5 | describe('idomizer', () => { 6 | let sandbox; 7 | 8 | beforeEach(function () { 9 | sandbox = document.body.appendChild(document.createElement('div')); 10 | }); 11 | 12 | it('should render a simple h1 with a static attribute', () => { 13 | const render = compile(`

    Hello

    `)(IncrementalDOM); 14 | IncrementalDOM.patch(sandbox, render); 15 | expect(sandbox.innerHTML).to.eq('

    Hello

    '); 16 | }); 17 | 18 | it('should render a simple h1 with a dynamic attributes', () => { 19 | const render = compile(`

    Hello

    `)(IncrementalDOM); 20 | IncrementalDOM.patch(sandbox, render, {h1Class: 'main'}); 21 | expect(sandbox.innerHTML).to.eq('

    Hello

    '); 22 | 23 | IncrementalDOM.patch(sandbox, render, {h1Class: 'child'}); 24 | expect(sandbox.innerHTML).to.eq('

    Hello

    '); 25 | }); 26 | 27 | it('should render a simple input with a dynamic attribute', () => { 28 | const render = compile(``, {skipExceptions: false})(IncrementalDOM); 29 | expect(render.toString()).to.match(/'input', null, \['type', 'text'\], 'value', \(data.value\)/); 30 | 31 | IncrementalDOM.patch(sandbox, render, {value: 'value'}); 32 | expect(sandbox.innerHTML).to.eq(''); 33 | 34 | IncrementalDOM.patch(sandbox, render, {value: 'value bis'}); 35 | expect(sandbox.innerHTML).to.eq(''); 36 | }); 37 | 38 | it('should render a text from the tpl-text element', () => { 39 | const render = compile(``)(IncrementalDOM); 40 | IncrementalDOM.patch(sandbox, render, {value: 'value'}); 41 | expect(sandbox.innerHTML).to.eq('value'); 42 | }); 43 | 44 | it('should iterate over items with tpl-each element', () => { 45 | const render = compile(` 46 | - 47 | `)(IncrementalDOM); 48 | IncrementalDOM.patch(sandbox, render, {items: ['item0', 'item1']}); 49 | expect(sandbox.innerHTML.trim()).to.eq('0-item01-item1'); 50 | 51 | IncrementalDOM.patch(sandbox, render, {items: ['item0', 'item1', 'item2']}); 52 | expect(sandbox.innerHTML.trim()).to.eq('0-item01-item12-item2'); 53 | }); 54 | 55 | it('should iterate over items with an inline statement', () => { 56 | const render = compile(` 57 | [[ data.items.forEach(function (item, index) { ]]-[[ }); ]] 58 | `)(IncrementalDOM); 59 | IncrementalDOM.patch(sandbox, render, {items: ['item0', 'item1']}); 60 | expect(sandbox.innerHTML.trim()).to.eq('0-item01-item1'); 61 | 62 | IncrementalDOM.patch(sandbox, render, {items: ['item0', 'item1', 'item2']}); 63 | expect(sandbox.innerHTML.trim()).to.eq('0-item01-item12-item2'); 64 | }); 65 | 66 | it('should handle conditional statements with tpl-if, tpl-else-if and tpl-else-if elements', () => { 67 | const render = compile(` 68 | 69 |

    1 item

    70 | 71 |

    items

    72 |
    73 | 74 |

    no items

    75 |
    76 | `)(IncrementalDOM); 77 | IncrementalDOM.patch(sandbox, render, {items: ['item0']}); 78 | expect(sandbox.innerHTML.trim()).to.contain('

    1 item

    '); 79 | 80 | IncrementalDOM.patch(sandbox, render, {items: ['item0', 'item2']}); 81 | expect(sandbox.innerHTML.trim()).to.contain('

    items

    '); 82 | 83 | IncrementalDOM.patch(sandbox, render, {items: []}); 84 | expect(sandbox.innerHTML.trim()).to.contain('

    no items

    '); 85 | }); 86 | 87 | it('should handle conditional statements with inline statements', () => { 88 | const render = compile(` 89 | [[ if (data.items.length > 0 && data.items.length < 2) { ]] 90 |

    1 item

    91 | [[ } else if (data.items.length > 1) { ]] 92 |

    items

    93 | [[ } else { ]] 94 |

    no items

    95 | [[ } ]] 96 | `)(IncrementalDOM); 97 | IncrementalDOM.patch(sandbox, render, {items: ['item0']}); 98 | expect(sandbox.innerHTML.trim()).to.contain('

    1 item

    '); 99 | 100 | IncrementalDOM.patch(sandbox, render, {items: ['item0', 'item2']}); 101 | expect(sandbox.innerHTML.trim()).to.contain('

    items

    '); 102 | 103 | IncrementalDOM.patch(sandbox, render, {items: []}); 104 | expect(sandbox.innerHTML.trim()).to.contain('

    no items

    '); 105 | }); 106 | 107 | it('should use custom elements', () => { 108 | const render = compile(`strong textstrong text`, { 109 | tags: { 110 | 'x-test': { 111 | onopentag(name, attrs, key, statics, varArgs, options) { 112 | return `_text('${name} element');`; 113 | } 114 | } 115 | } 116 | })(IncrementalDOM); 117 | IncrementalDOM.patch(sandbox, render, {items: ['item0', 'item1']}); 118 | expect(sandbox.innerHTML.trim()).to.eq('strong textx-test elementstrong text'); 119 | }); 120 | 121 | it('should call helpers', () => { 122 | const subRender = compile(`helper content`)(IncrementalDOM); 123 | const render = compile(`strong textstrong text`)(IncrementalDOM, {subRender}); 124 | IncrementalDOM.patch(sandbox, render, {items: ['item0', 'item1']}); 125 | expect(sandbox.innerHTML.trim()).to.eq('strong texthelper contentstrong text'); 126 | }); 127 | 128 | it('should skip content node', () => { 129 | const render1 = compile(`strong text

    skipped content

    strong text`)(IncrementalDOM); 130 | const render2 = compile(`strong text bis

    strong text bis`)(IncrementalDOM); 131 | IncrementalDOM.patch(sandbox, render1); 132 | expect(sandbox.innerHTML.trim()).to.eq('strong text

    skipped content

    strong text', 'render1'); 133 | 134 | IncrementalDOM.patch(sandbox, render2); 135 | expect(sandbox.innerHTML.trim()).to.eq('strong text bis

    skipped content

    strong text bis', 'render2'); 136 | }); 137 | 138 | it('should skip content node of custom element', () => { 139 | const render = compile(`strong text bisstrong text bis`)(IncrementalDOM); 140 | sandbox.innerHTML = `strong textskipped contentstrong text`; 141 | IncrementalDOM.patch(sandbox, render); 142 | expect(sandbox.innerHTML.trim()).to.eq('strong text bisskipped contentstrong text bis', 'render'); 143 | }); 144 | 145 | it('should not skip content node of custom element - locally', () => { 146 | const render = compile(`strong text biscontentstrong text bis`)(IncrementalDOM); 147 | sandbox.innerHTML = `strong textskipped contentstrong text`; 148 | IncrementalDOM.patch(sandbox, render); 149 | expect(sandbox.innerHTML.trim()).to.eq('strong text biscontentstrong text bis', 'render'); 150 | }); 151 | 152 | it('should not skip content node of custom element - globally', () => { 153 | const render = compile(`strong text biscontentstrong text bis`, {skipCustomElements: false})(IncrementalDOM); 154 | sandbox.innerHTML = `strong textskipped contentstrong text`; 155 | IncrementalDOM.patch(sandbox, render); 156 | expect(sandbox.innerHTML.trim()).to.eq('strong text biscontentstrong text bis', 'render'); 157 | }); 158 | 159 | it('should skip content node of custom element having is attribute', () => { 160 | const render = compile(`strong text bis

    strong text bis`)(IncrementalDOM); 161 | sandbox.innerHTML = `strong text

    skipped content

    strong text`; 162 | IncrementalDOM.patch(sandbox, render); 163 | expect(sandbox.innerHTML.trim()).to.eq('strong text bis

    skipped content

    strong text bis', 'render'); 164 | }); 165 | 166 | it('should not skip content node of custom element having is attribute - locally', () => { 167 | const render = compile(`strong text bis

    content

    strong text bis`)(IncrementalDOM); 168 | sandbox.innerHTML = `strong text

    skipped content

    strong text`; 169 | IncrementalDOM.patch(sandbox, render); 170 | expect(sandbox.innerHTML.trim()).to.eq('strong text bis

    content

    strong text bis', 'render'); 171 | }); 172 | 173 | it('should not skip content node of custom element having is attribute - globally', () => { 174 | const render = compile(`strong text bis

    content

    strong text bis`, {skipCustomElements: false})(IncrementalDOM); 175 | sandbox.innerHTML = `strong text

    skipped content

    strong text`; 176 | IncrementalDOM.patch(sandbox, render); 177 | expect(sandbox.innerHTML.trim()).to.eq('strong text bis

    content

    strong text bis', 'render'); 178 | }); 179 | 180 | it('should ignore static attributes', () => { 181 | const render = compile(` 182 |

    Hello

    183 | `, {ignoreStaticAttributes: true, skipExceptions: false})(IncrementalDOM); 184 | expect(render.toString()).to.match(/'class', 'foo ' \+ \(data.h1Class\) \+ ' bar', 'id', 'anId'/); 185 | 186 | IncrementalDOM.patch(sandbox, render, {h1Class: 'main'}); 187 | expect(sandbox.innerHTML.trim()).to.eq('

    Hello

    '); 188 | 189 | IncrementalDOM.patch(sandbox, render, {h1Class: 'child'}); 190 | expect(sandbox.innerHTML.trim()).to.eq('

    Hello

    '); 191 | }); 192 | 193 | it('should interpolate text node', () => { 194 | const render1 = compile(` 195 | [[ if (data.v1 > 0) { ]]YES[[ } ]]

    t {{ data.txtNode1 }} t {{ data.txtNode2 }} {{ }}

    196 | `)(IncrementalDOM); 197 | IncrementalDOM.patch(sandbox, render1, { 198 | v1: 1, 199 | txtNode1: 'value1', 200 | txtNode2: 'value2', 201 | att1: 'a1', 202 | att2: 'a2' 203 | }); 204 | expect(sandbox.innerHTML.trim()).to.eq('YES

    t value1 t value2

    ', 'render1'); 205 | }); 206 | 207 | it('should ingore interpolation exception', () => { 208 | const render1 = compile(` 209 |

    t {{ foo.bar }} t {{ data.txtNode2 }}

    210 | `, {pretty: true, skipExceptions: true})(IncrementalDOM); 211 | IncrementalDOM.patch(sandbox, render1, { 212 | v1: 1, 213 | txtNode1: 'value1', 214 | txtNode2: 'value2', 215 | att1: 'a1', 216 | att2: 'a2' 217 | }); 218 | expect(sandbox.innerHTML.trim()).to.eq('

    t t value2

    ', 'render1'); 219 | }); 220 | 221 | }); 222 | -------------------------------------------------------------------------------- /test/plugins/babel-idomizer.spec.js: -------------------------------------------------------------------------------- 1 | const babelIdomizer = require('../../lib/plugins/babel-idomizer'); 2 | const {expect} = require('chai'); 3 | const babel = require('@babel/core'); 4 | 5 | describe('babel-idomizer', () => { 6 | 7 | it('should convert an idomizer file into a string function', (done) => { 8 | let options = { 9 | plugins: [[babelIdomizer, {skipExceptions: false}]] 10 | }; 11 | babel.transformFile('test/plugins/dummy.es6', options, (err, result) => { 12 | if (err) { 13 | return done(err); 14 | } 15 | expect(result.code).to.contain(`_elementOpen('h1', null, null, 'class', data.h1Class);`); 16 | expect(result.code).to.contain(`_text('\\n Hello\\n ');`); 17 | expect(result.code).to.contain(`_elementClose('h1');`); 18 | done(); 19 | }); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /test/plugins/dummy.es6: -------------------------------------------------------------------------------- 1 | let template = idomizer` 2 |

    3 | Hello 4 |

    5 | `; 6 | -------------------------------------------------------------------------------- /test/plugins/idomizerify.spec.js: -------------------------------------------------------------------------------- 1 | import {join} from 'path'; 2 | import idomizerify from '../../lib/plugins/idomizerify'; 3 | import {runTransform} from 'browserify-transform-tools'; 4 | import {expect} from 'chai'; 5 | 6 | describe('idomizerify', () => { 7 | 8 | it('should convert an idomizer file into a string function', (done) => { 9 | const dummyJsFile = join(__dirname, 'plugins/dummy.idomizer'); 10 | const content = `

    Hello

    `; 11 | runTransform(idomizerify, dummyJsFile, {content, config: {skipExceptions: false}}, (err, result) => { 12 | if (err) { 13 | return done(err); 14 | } 15 | expect(result).to.contain(`_elementOpen('h1', null, null, 'class', (data.h1Class));`); 16 | expect(result).to.contain(`_text('Hello');`); 17 | expect(result).to.contain(`_elementClose('h1');`); 18 | done(); 19 | }); 20 | }); 21 | 22 | }); 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "declaration": true, 5 | "module": "commonjs", 6 | "outDir": "lib", 7 | "lib": [ 8 | "es2018" 9 | ], 10 | "types": [ 11 | "node" 12 | ] 13 | }, 14 | "include": [ 15 | "src" 16 | ], 17 | "typedocOptions": { 18 | "entryPoints": [ 19 | "./src/idomizer.ts" 20 | ], 21 | "readme": "none", 22 | "out": "./typedoc" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: path.resolve(__dirname, 'src/idomizer.ts'), 5 | module: { 6 | rules: [ 7 | {test: /\.(ts|js)$/, exclude: /node_modules/, loader: 'babel-loader'}, 8 | {test: /\.idomizer$/, loader: 'idomizer/lib/plugins/idomizer-loader'} 9 | ] 10 | }, 11 | resolve: { 12 | extensions: ['.ts', '.js', '.json'] 13 | }, 14 | output: { 15 | library: 'idomizer', 16 | libraryTarget: 'umd', 17 | path: path.resolve(__dirname) 18 | } 19 | }; 20 | 21 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const {merge} = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'development', 6 | devtool: 'inline-source-map', 7 | output: { 8 | filename: 'dist/idomizer.js' 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /webpack.prd.js: -------------------------------------------------------------------------------- 1 | const {merge} = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'production', 6 | output: { 7 | filename: 'dist/idomizer.min.js' 8 | } 9 | }); 10 | --------------------------------------------------------------------------------