├── .editorconfig ├── .eslintrc.yml ├── .gitignore ├── .gitmodules ├── .prettierrc.yml ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── babel.config.js ├── mini └── package.json ├── package.json ├── packages └── babel-plugin-hyperstache │ ├── README.md │ ├── package.json │ ├── src │ └── index.js │ ├── test │ └── babel.js │ └── yarn.lock ├── rollup.config.js ├── runtime-mini └── package.json ├── runtime └── package.json ├── src ├── build.js ├── constants.js ├── helpers.js ├── index.js ├── runtime.js └── utils.js ├── test ├── htm.js ├── hyperstache.js ├── mustache.js └── test.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es6: true 4 | node: true 5 | 6 | extends: 7 | - eslint:recommended 8 | 9 | parserOptions: 10 | ecmaVersion: 9 11 | sourceType: module 12 | 13 | rules: 14 | no-prototype-builtins: off 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | *.sublime-project 4 | yarn-error.log 5 | .nyc_output/ 6 | coverage 7 | *.lcov 8 | 9 | dist/ 10 | module/ 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test/mustache"] 2 | path = test/mustache 3 | url = https://github.com/mustache/spec.git 4 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | # .prettierrc or .prettierrc.yml 2 | tabWidth: 2 3 | singleQuote: true 4 | semi: true 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - "10" 7 | 8 | cache: 9 | yarn: true 10 | directories: 11 | - node_modules 12 | 13 | # As --depth implies --single-branch, removing this flag means that all branches will be checked out 14 | git: 15 | depth: false 16 | 17 | # Make chrome browser available for testing 18 | before_install: 19 | - curl -o- -L https://yarnpkg.com/install.sh | bash 20 | - export PATH="$HOME/.yarn/bin:$PATH" 21 | 22 | services: 23 | - xvfb 24 | 25 | install: 26 | - yarn 27 | 28 | addons: 29 | chrome: stable 30 | sauce_connect: true 31 | 32 | jobs: 33 | include: 34 | - stage: tests 35 | name: "Unit tests" 36 | script: yarn test 37 | 38 | after_success: 39 | - yarn coverage 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## 0.5.1 - 2019-10-29 6 | 7 | ### Changed 8 | 9 | - Golfed down some bytes. 10 | - Added `hyperstache/runtime-mini` for Babel plugin. 11 | 12 | ## 0.5.0 - 2019-10-29 13 | 14 | ### Added 15 | 16 | - Made most of Mustache spec pass [#10](https://github.com/luwes/hyperstache/pull/10) 17 | 18 | ## 0.4.0 - 2019-10-25 19 | 20 | ### Added 21 | 22 | - Added babel plugin [#9](https://github.com/luwes/hyperstache/pull/9) 23 | 24 | ## 0.3.0 - 2019-10-22 25 | 26 | ### Added 27 | 28 | - Added hash params [#8](https://github.com/luwes/hyperstache/pull/8) 29 | - Added mini version without built-in helpers 30 | https://unpkg.com/hyperstache@0.3.0/dist/mini.js 31 | 32 | ## 0.2.0 - 2019-10-21 33 | 34 | ### Added 35 | 36 | - Implemented parsing of comments [#6](https://github.com/luwes/hyperstache/pull/6) 37 | 38 | ## 0.1.0 - 2019-10-20 39 | 40 | ### Added 41 | 42 | - Add built-in helpers (if, unless, each, with) with else chaining [#7](https://github.com/luwes/hyperstache/pull/7) 43 | 44 | ## 0.0.1 - 2019-10-17 45 | 46 | ### Added 47 | 48 | - First release 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyperstache 2 | 3 | [![Build Status](https://img.shields.io/travis/com/luwes/hyperstache/master.svg?style=flat-square&label=Travis+CI)](https://travis-ci.com/luwes/hyperstache) 4 | ![Badge size](https://img.badgesize.io/https://unpkg.com/hyperstache/dist/hyperstache.min.js?compression=gzip&label=gzip&style=flat-square) 5 | [![codecov](https://img.shields.io/codecov/c/github/luwes/hyperstache.svg?style=flat-square)](https://codecov.io/gh/luwes/hyperstache) 6 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 7 | 8 | Logic-less templates to template literals transformer. 9 | Hyperstache includes a full parser and runtime. 10 | It uses no `eval` and minimal regex for the best performance. 11 | It's largely compatible with [Handlebars](https://github.com/wycats/handlebars.js/) and [Mustache](https://github.com/janl/mustache.js/) templates. 12 | 13 | **npm**: `npm install hyperstache --save` 14 | **cdn**: https://unpkg.com/hyperstache 15 | **module**: https://unpkg.com/hyperstache?module 16 | 17 | ## Why? 18 | 19 | The goal is to make projects invested in Handlebars templates adopt a tagged templates only solution easily or add an additional layer of logic-less templates on top of any tagged template library. 20 | 21 | - [Sinuous](https://github.com/luwes/sinuous/) ([CodeSandbox](https://codesandbox.io/s/hyperstache-sinuous-5j4u9)) 22 | - [htm](https://github.com/developit/htm) ([CodeSandbox](https://codesandbox.io/s/hyperstache-htm-ju83x)) 23 | - [Lighterhtml](https://github.com/WebReflection/lighterhtml) ([CodeSandbox](https://codesandbox.io/s/hyperstache-lighterhtml-qnesy)) 24 | - [lit-html](https://github.com/Polymer/lit-html) ([CodeSandbox](https://codesandbox.io/s/hyperstache-lithtml-xip2v)) 25 | 26 | ## `hyperstache` by the numbers: 27 | 28 | 🚙 **2.07kB** when used directly in the browser 29 | 30 | 🏍 **1.74kB** `hyperstache/mini` version ~~(built-in helpers)~~ 31 | 32 | 🏎 **1.07kB** if compiled using [babel-plugin-hyperstache](./packages/babel-plugin-hyperstache) 33 | 34 | ## Features 35 | 36 | - [x] variables `{{escaped}}`, `{{{unescaped}}}` 37 | - [x] variables dot notation `{{obj.prop}}` 38 | - [x] helpers `{{loud lastname}}` 39 | - [x] helpers literal arguments: `numbers`, `strings`, `true`, `false`, `null` and `undefined` 40 | - [x] basic block helpers `{{#bold}}` 41 | - [x] built-in helpers: `if`, `unless`, `each`, `with` 42 | - [x] helper hash arguments 43 | - [x] comments `{{!comment}}`, `{{!-- comment with }} --}}` 44 | - [x] whitespace control `{{~ trimStart }}` 45 | - [ ] helper block parameters 46 | - [ ] subexpressions 47 | - [ ] partials `{{>partial}}` 48 | 49 | ## Usage ([CodeSandbox](https://codesandbox.io/s/boring-breeze-y3od0)) 50 | 51 | ```js 52 | import { compile } from "hyperstache"; 53 | 54 | const o = (...args) => args; 55 | const template = compile.bind(o)` 56 |

57 | Hello, my name is {{name}}. 58 | I am from {{hometown}}. I have {{kids.length}} kids: 59 |

60 | 65 | `; 66 | 67 | const data = { 68 | name: "Alan", 69 | hometown: "Somewhere, TX", 70 | kids: [{ name: "Jimmy", age: "12" }, { name: "Sally", age: "4" }] 71 | }; 72 | console.log(template(data)); 73 | 74 | /** => 75 | [ 76 | [ 77 | "

↵ Hello, my name is ", 78 | ". ↵ I am from ", 79 | ". I have ", 80 | " kids:↵

" 82 | ], 83 | "Alan", 84 | "Somewhere, TX", 85 | 2, 86 | [ 87 | ["", "", ""], 88 | [ 89 | ["
  • ", " is ", "
  • "], 90 | "Jimmy", 91 | "12" 92 | ], 93 | [ 94 | ["
  • ", " is ", "
  • "], 95 | "Sally", 96 | "4" 97 | ] 98 | ] 99 | ] 100 | */ 101 | ``` 102 | 103 | ## API 104 | 105 | `compile(statics, ...exprs)` 106 | 107 | `registerHelper(name, fn)` 108 | 109 | `escapeExpression(str)` 110 | 111 | `new SafeString(htmlStr)` 112 | 113 | ## Real world ([CodeSandbox](https://codesandbox.io/s/damp-wave-5j4u9)) 114 | 115 | ```js 116 | import { html } from "sinuous"; 117 | import { compile } from "hyperstache"; 118 | 119 | const template = compile.bind(html)` 120 |

    121 | Hello, my name is {{name}}. 122 | I am from {{hometown}}. I have {{kids.length}} kids: 123 |

    124 | 129 | `; 130 | 131 | const data = { 132 | name: "Alan", 133 | hometown: "Somewhere, TX", 134 | kids: [{ name: "Jimmy", age: "12" }, { name: "Sally", age: "4" }] 135 | }; 136 | const dom = template(data); 137 | document.body.append(dom); 138 | ``` 139 | 140 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | presets: [ 4 | [ 5 | '@babel/preset-env', 6 | { 7 | modules: false, 8 | loose: true, 9 | targets: { 10 | browsers: ['ie >= 11'] 11 | } 12 | } 13 | ] 14 | ], 15 | plugins: [ 16 | ['@babel/plugin-transform-object-assign'], 17 | ['@babel/plugin-proposal-object-rest-spread', { 'loose': true }] 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /mini/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperstache-mini", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "Hyperstache mini, no built-in helpers.", 6 | "module": "../module/mini.js", 7 | "main": "../dist/mini.js" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperstache", 3 | "version": "0.5.2", 4 | "description": "Handlebars to template literals transformer.", 5 | "main": "dist/hyperstache.js", 6 | "module": "module/hyperstache.js", 7 | "scripts": { 8 | "build": "rollup -c --silent", 9 | "watch": "rollup -wc --silent", 10 | "test": "nyc --reporter=lcov --reporter=text tape -r esm test/test.js | tap-spec", 11 | "test:watch": "chokidar '**/(src|test)/**/*.js' -c 'yarn test' --initial --silent", 12 | "coverage": "codecov" 13 | }, 14 | "files": [ 15 | "module", 16 | "dist", 17 | "src", 18 | "mini", 19 | "runtime", 20 | "runtime-mini" 21 | ], 22 | "keywords": [ 23 | "dom", 24 | "handlebars", 25 | "mustache", 26 | "templates", 27 | "templateliterals" 28 | ], 29 | "author": "Wesley Luyten (https://wesleyluyten.com)", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/luwes/hyperstache/issues" 33 | }, 34 | "homepage": "https://github.com/luwes/hyperstache#readme", 35 | "devDependencies": { 36 | "@babel/core": "^7.9.0", 37 | "@babel/plugin-proposal-object-rest-spread": "^7.9.5", 38 | "@babel/plugin-transform-object-assign": "^7.8.3", 39 | "@babel/preset-env": "^7.9.5", 40 | "chokidar-cli": "^2.1.0", 41 | "codecov": "^3.6.5", 42 | "eslint": "^6.8.0", 43 | "eslint-plugin-jsdoc": "^24.0.0", 44 | "esm": "^3.2.25", 45 | "faucet": "^0.0.1", 46 | "htm": "^3.0.4", 47 | "nyc": "^15.0.1", 48 | "prettier": "^2.0.5", 49 | "rollup": "^2.7.2", 50 | "rollup-plugin-babel": "^4.4.0", 51 | "rollup-plugin-node-builtins": "^2.1.2", 52 | "rollup-plugin-node-resolve": "^5.2.0", 53 | "rollup-plugin-replace": "^2.2.0", 54 | "rollup-plugin-size": "^0.2.2", 55 | "rollup-plugin-terser": "^5.3.0", 56 | "tap-spec": "^5.0.0", 57 | "tape": "^5.0.0", 58 | "underscore": "^1.10.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/babel-plugin-hyperstache/README.md: -------------------------------------------------------------------------------- 1 | # `babel-plugin-hyperstache` 2 | 3 | A Babel plugin to pre-compile handlebars. 4 | 5 | ## Usage 6 | 7 | Basic usage: 8 | 9 | ```js 10 | [ 11 | ["hyperstache", { 12 | "tag": "hbs", 13 | "tagOut": "html", 14 | "runtime": "hyperstache/runtime" 15 | }] 16 | ] 17 | ``` 18 | 19 | ```js 20 | // input: 21 | hbs`
    {{fruit}}
    `({ fruit: 'Apple' }); 22 | 23 | // output: 24 | const { template } = require("hyperstache/runtime"); 25 | 26 | template((hys,ctx,data) => html`
    ${ 27 | hys.escape(hys.expr("fruit",ctx,{data})) 28 | }
    `)({ fruit: 'Apple' }); 29 | ``` 30 | 31 | ## options 32 | 33 | ### `tag=hbs` 34 | 35 | By default, `babel-plugin-hyperstache` will process all Tagged Templates with a tag function named `hbs`. To use a different name, use the `tag` option in your Babel configuration: 36 | 37 | ```js 38 | {"plugins":[ 39 | ["babel-plugin-hyperstache", { 40 | "tag": "myCustomHbsFunction" 41 | }] 42 | ]} 43 | ``` 44 | 45 | ### `tagOut=html` 46 | 47 | The output tag given to Tagged Templates for further processing. 48 | 49 | ```js 50 | {"plugins":[ 51 | ["babel-plugin-hyperstache", { 52 | "tagOut": "myCustomHtmlFunction" 53 | }] 54 | ]} 55 | ``` 56 | -------------------------------------------------------------------------------- /packages/babel-plugin-hyperstache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-hyperstache", 3 | "version": "0.0.1", 4 | "description": "Babel plugin to pre-compile handlebars.", 5 | "main": "dist/babel-plugin-hyperstache.js", 6 | "module": "module/babel-plugin-hyperstache.js", 7 | "files": [ 8 | "module", 9 | "dist", 10 | "src" 11 | ], 12 | "author": "Wesley Luyten (https://wesleyluyten.com)", 13 | "license": "MIT", 14 | "dependencies": { 15 | "hyperstache": "^0.3.0" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.6.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/babel-plugin-hyperstache/src/index.js: -------------------------------------------------------------------------------- 1 | import { build, EXPR_VAR, CHILD_RECURSE } from '../../../src/build.js'; 2 | import { unwrap, parseLiteral, log } from '../../../src/utils.js'; 3 | 4 | const defaults = { 5 | tag: 'hbs', 6 | tagOut: 'html', 7 | runtime: 'hyperstache/runtime' 8 | } 9 | 10 | /** 11 | * @param {Babel} babel 12 | * @param {object} options 13 | * @param {string} [options.tag=hbs] The tagged template "tag" function name to process. 14 | * @param {string} [options.tagOut=html] The tagged template "tag" function name to output. 15 | */ 16 | export default function hysBabelPlugin({ types: t }, options = {}) { 17 | options = { ...defaults, ...options }; 18 | 19 | function TaggedTemplateExpression(path) { 20 | const tag = path.node.tag.name; 21 | if (tag === options.tag) { 22 | const stats = path.node.quasi.quasis.map(e => e.value.raw); 23 | const fields = [0, ...path.node.quasi.expressions]; 24 | 25 | const built = build(stats); 26 | // log('BUILT', built); 27 | 28 | const node = evaluate(built, fields, true); 29 | 30 | // const { template } = require("hyperstache/runtime"); 31 | const runtimeTpl = t.variableDeclaration('const', [ 32 | t.variableDeclarator( 33 | t.objectPattern([ 34 | t.objectProperty( 35 | t.identifier('template'), 36 | t.identifier('template'), 37 | false, 38 | true 39 | ) 40 | ]), 41 | t.callExpression(t.identifier('require'), [ 42 | t.stringLiteral(options.runtime) 43 | ]) 44 | ) 45 | ]); 46 | 47 | path.replaceWithMultiple([runtimeTpl, t.expressionStatement(node)]); 48 | } 49 | } 50 | 51 | const evaluate = (built, fields, root) => { 52 | const statics = []; 53 | const exprs = []; 54 | // log('BUILT', built); 55 | for (let i = 1; i < built.length; i++) { 56 | const field = built[i]; 57 | // log('FIELD', field); 58 | const type = built[++i]; 59 | 60 | if (typeof field === 'number') { 61 | exprs.push(fields[field]); 62 | } else if (type >= EXPR_VAR) { 63 | const params = built[++i]; 64 | const hash = built[++i]; 65 | 66 | let expr = transform(field); 67 | // log('EXPR', expr); 68 | 69 | if (t.isIdentifier(expr)) { 70 | const args = [t.stringLiteral(field), t.identifier('ctx')]; 71 | 72 | const properties = []; 73 | if (params.length > 0) { 74 | properties.push( 75 | t.objectProperty( 76 | t.identifier('params'), 77 | t.arrayExpression(params.map(transformParams)) 78 | ) 79 | ); 80 | } 81 | 82 | if (Object.keys(hash).length > 0) { 83 | properties.push( 84 | t.objectProperty( 85 | t.identifier('hash'), 86 | t.objectExpression( 87 | Object.keys(hash).map(key => { 88 | return t.objectProperty( 89 | t.stringLiteral(key), 90 | transform(hash[key]) 91 | ); 92 | }) 93 | ) 94 | ) 95 | ); 96 | } 97 | 98 | properties.push( 99 | t.objectProperty( 100 | t.identifier('data'), 101 | t.identifier('data'), 102 | false, // computed 103 | true // shorthand 104 | ) 105 | ); 106 | 107 | properties.push( 108 | t.objectProperty( 109 | t.identifier('depths'), 110 | t.identifier('depths'), 111 | false, // computed 112 | true // shorthand 113 | ) 114 | ); 115 | 116 | args.push(t.objectExpression(properties)); 117 | expr = t.callExpression(dottedIdentifier('hys.expr'), args); 118 | } 119 | 120 | if ( 121 | type === EXPR_VAR && 122 | (t.isCallExpression(expr) || t.isStringLiteral(expr)) 123 | ) { 124 | expr = t.callExpression(dottedIdentifier('hys.escape'), [expr]); 125 | } 126 | // log('EXPR', expr); 127 | exprs.push(expr); 128 | } else if (type === CHILD_RECURSE) { 129 | /** 130 | * field = [ 131 | * [parent], 132 | * [[parent], if, 5, ['@first'], { hash: param }, 'body', 3], // if block 133 | * [[parent], if, 5, ['@last'], {}, 'body', 3, 'End', 1] // else block 134 | * ] 135 | */ 136 | const fnName = field[1][1]; 137 | const params = field[1][3]; 138 | const hash = field[1][4]; 139 | const inverse = field[2]; 140 | const inverted = field[3]; 141 | 142 | const args = [t.stringLiteral(fnName), t.identifier('ctx')]; 143 | 144 | const properties = [ 145 | t.objectProperty( 146 | t.identifier('fn'), 147 | evaluate([0].concat(field[1].slice(5)), fields) 148 | ) 149 | ]; 150 | 151 | if (inverse.length > 1) { 152 | properties.push( 153 | t.objectProperty(t.identifier('inverse'), evaluate(inverse, fields)) 154 | ); 155 | } 156 | 157 | if (params.length > 0) { 158 | properties.push( 159 | t.objectProperty( 160 | t.identifier('params'), 161 | t.arrayExpression(params.map(transformParams)) 162 | ) 163 | ); 164 | } 165 | 166 | if (Object.keys(hash).length > 0) { 167 | properties.push( 168 | t.objectProperty( 169 | t.identifier('hash'), 170 | t.objectExpression( 171 | Object.keys(hash).map(key => { 172 | return t.objectProperty( 173 | t.stringLiteral(key), 174 | transformParams(hash[key]) 175 | ); 176 | }) 177 | ) 178 | ) 179 | ); 180 | } 181 | 182 | if (inverted) { 183 | properties.push( 184 | t.objectProperty( 185 | t.identifier('inverted'), 186 | t.booleanLiteral(inverted) 187 | ) 188 | ); 189 | } 190 | 191 | properties.push( 192 | t.objectProperty( 193 | t.identifier('data'), 194 | t.identifier('data'), 195 | false, // computed 196 | true // shorthand 197 | ) 198 | ); 199 | 200 | properties.push( 201 | t.objectProperty( 202 | t.identifier('depths'), 203 | t.identifier('depths'), 204 | false, // computed 205 | true // shorthand 206 | ) 207 | ); 208 | 209 | args.push(t.objectExpression(properties)); 210 | let expr = t.callExpression(dottedIdentifier('hys.block'), args); 211 | // log('EXPR', expr); 212 | exprs.push(expr); 213 | } else { 214 | // code === CHILD_APPEND 215 | statics.push( 216 | t.templateElement({ 217 | raw: field, 218 | cooked: field 219 | }) 220 | ); 221 | } 222 | } 223 | 224 | // log('EXPRS', exprs); 225 | const params = []; 226 | params.push(t.identifier('ctx')); 227 | params.push(t.identifier('data')); 228 | params.push(t.identifier('depths')); 229 | if (root) { 230 | params.push(t.identifier('hys')); 231 | } 232 | 233 | const quasi = t.templateLiteral(statics, exprs); 234 | const body = t.taggedTemplateExpression(t.identifier(options.tagOut), quasi); 235 | const node = t.callExpression(t.identifier('template'), [ 236 | t.arrowFunctionExpression(params, body) 237 | ]); 238 | return node; 239 | }; 240 | 241 | function dottedIdentifier(keypath) { 242 | const path = keypath.split('.'); 243 | let out; 244 | for (let i = 0; i < path.length; i++) { 245 | const ident = propertyName(path[i]); 246 | out = i === 0 ? ident : t.memberExpression(out, ident); 247 | } 248 | return out; 249 | } 250 | 251 | function propertyName(key) { 252 | if (t.isValidIdentifier(key)) { 253 | return t.identifier(key); 254 | } 255 | return t.stringLiteral(key); 256 | } 257 | 258 | function transform(value) { 259 | if (value === '') { 260 | return t.stringLiteral(value); 261 | } 262 | 263 | if (typeof value === 'string') { 264 | value = parseLiteral(value); 265 | } 266 | 267 | let str; 268 | switch (typeof value) { 269 | case 'string': 270 | if ((str = unwrap(value, '"')) || (str = unwrap(value, "'"))) { 271 | return t.stringLiteral(str); 272 | } 273 | return t.identifier(value); 274 | case 'number': 275 | return t.numericLiteral(value); 276 | case 'boolean': 277 | return t.booleanLiteral(value); 278 | default: 279 | return t.identifier('' + value); 280 | } 281 | } 282 | 283 | function transformParams(value) { 284 | if (typeof value === 'string') { 285 | value = parseLiteral(value); 286 | } 287 | 288 | switch (typeof value) { 289 | case 'string': 290 | if (unwrap(value, '"') || unwrap(value, "'")) { 291 | return t.stringLiteral(value); 292 | } 293 | return t.stringLiteral(value); 294 | case 'number': 295 | return t.numericLiteral(value); 296 | case 'boolean': 297 | return t.booleanLiteral(value); 298 | default: 299 | return t.identifier('' + value); 300 | } 301 | } 302 | 303 | // The tagged template tag function name we're looking for. 304 | // This is static because it's generally assigned via hyperstache.bind(html), 305 | // which could be imported from elsewhere, making tracking impossible. 306 | return { 307 | name: 'hyperstache', 308 | visitor: { 309 | TaggedTemplateExpression 310 | } 311 | }; 312 | } 313 | -------------------------------------------------------------------------------- /packages/babel-plugin-hyperstache/test/babel.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import test from 'tape'; 4 | import { transform } from '@babel/core'; 5 | import hysBabelPlugin from '../src/index.js'; 6 | 7 | const options = { 8 | babelrc: false, 9 | configFile: false, 10 | sourceType: 'script', 11 | compact: true 12 | }; 13 | 14 | const out = (result) => 'const{template}=require("hyperstache/runtime");' + result; 15 | 16 | test('basic transformation', t => { 17 | t.equal( 18 | transform('hbs`
    ${"hello"}
    `;', { 19 | ...options, 20 | plugins: [hysBabelPlugin] 21 | }).code, 22 | out('template((ctx,data,depths,hys)=>html`
    ${"hello"}
    `);') 23 | ); 24 | t.equal( 25 | transform('hbs`
    {{"hello"}}
    `;', { 26 | ...options, 27 | plugins: [hysBabelPlugin] 28 | }).code, 29 | out('template((ctx,data,depths,hys)=>html`
    ${hys.escape("hello")}
    `);') 30 | ); 31 | t.equal( 32 | transform('hbs`
    {{99}}
    `;', { 33 | ...options, 34 | plugins: [hysBabelPlugin] 35 | }).code, 36 | out('template((ctx,data,depths,hys)=>html`
    ${99}
    `);') 37 | ); 38 | t.equal( 39 | transform('hbs`
    {{fruit}}
    `;', { 40 | ...options, 41 | plugins: [hysBabelPlugin] 42 | }).code, 43 | out('template((ctx,data,depths,hys)=>html`
    ${' + 44 | 'hys.escape(hys.expr("fruit",ctx,{' + 45 | 'data,' + 46 | 'depths' + 47 | '}))' + 48 | '}
    `);') 49 | ); 50 | t.equal( 51 | transform('hbs`
    {{loud "big"}}
    `;', { 52 | ...options, 53 | plugins: [hysBabelPlugin] 54 | }).code, 55 | out('template((ctx,data,depths,hys)=>html`
    ${hys.escape(hys.expr("loud",ctx,{' + 56 | 'params:["\\"big\\""],' + 57 | 'data,' + 58 | 'depths' + 59 | '}))}
    `);') 60 | ); 61 | t.equal( 62 | transform('hbs`
    {{sum a=1 b=1}}
    `;', { 63 | ...options, 64 | plugins: [hysBabelPlugin] 65 | }).code, 66 | out('template((ctx,data,depths,hys)=>html`
    ${hys.escape(hys.expr("sum",ctx,{' + 67 | 'hash:{"a":1,"b":1},' + 68 | 'data,' + 69 | 'depths' + 70 | '}))}
    `);') 71 | ); 72 | t.equal( 73 | transform('hbs`{{#bold}}{{body}}{{/bold}}`;', { 74 | ...options, 75 | plugins: [hysBabelPlugin] 76 | }).code, 77 | out('template((ctx,data,depths,hys)=>html`${hys.block("bold",ctx,{' + 78 | 'fn:template((ctx,data,depths)=>html`${hys.escape(hys.expr("body",ctx,{' + 79 | 'data,' + 80 | 'depths' + 81 | '}))}`),' + 82 | 'data,' + 83 | 'depths' + 84 | '})}`);') 85 | ); 86 | t.equal( 87 | transform('hbs`{{#if true}}99{{else}}11{{/if}}`;', { 88 | ...options, 89 | plugins: [hysBabelPlugin] 90 | }).code, 91 | out('template((ctx,data,depths,hys)=>html`${hys.block("if",ctx,{' + 92 | 'fn:template((ctx,data,depths)=>html`99`),' + 93 | 'inverse:template((ctx,data,depths)=>html`11`),' + 94 | 'params:[true],' + 95 | 'data,' + 96 | 'depths' + 97 | '})}`);') 98 | ); 99 | t.end(); 100 | }); 101 | 102 | 103 | // Run all of the main tests against the Babel plugin: 104 | const mod = fs.readFileSync( 105 | path.resolve(__dirname, '../../../test/hyperstache.js'), 'utf8').replace(/\\0/g, '\0' 106 | ); 107 | 108 | const runtimeModule = '../../../src/runtime.js'; 109 | 110 | const source = mod 111 | .replace("import test from 'tape';", '') 112 | .replace("import htm from 'htm';", "const htm = require('htm');") 113 | .replace( 114 | /^import { compile, (.+?) } from '\.\.\/src\/index\.js';$/mi, 115 | 'const { $1 } = require("' + runtimeModule + '");' 116 | ) 117 | .replace("const hbs = compile.bind(html);", ''); 118 | 119 | const { code } = transform(source, { 120 | ...options, 121 | plugins: [hysBabelPlugin] 122 | }); 123 | 124 | eval( 125 | code.replace(/hyperstache\/runtime/g, runtimeModule) 126 | ); 127 | -------------------------------------------------------------------------------- /packages/babel-plugin-hyperstache/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5": 6 | version "7.5.5" 7 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" 8 | integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== 9 | dependencies: 10 | "@babel/highlight" "^7.0.0" 11 | 12 | "@babel/code-frame@^7.22.13": 13 | version "7.22.13" 14 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" 15 | integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== 16 | dependencies: 17 | "@babel/highlight" "^7.22.13" 18 | chalk "^2.4.2" 19 | 20 | "@babel/core@^7.6.4": 21 | version "7.6.4" 22 | resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.4.tgz#6ebd9fe00925f6c3e177bb726a188b5f578088ff" 23 | integrity sha512-Rm0HGw101GY8FTzpWSyRbki/jzq+/PkNQJ+nSulrdY6gFGOsNseCqD6KHRYe2E+EdzuBdr2pxCp6s4Uk6eJ+XQ== 24 | dependencies: 25 | "@babel/code-frame" "^7.5.5" 26 | "@babel/generator" "^7.6.4" 27 | "@babel/helpers" "^7.6.2" 28 | "@babel/parser" "^7.6.4" 29 | "@babel/template" "^7.6.0" 30 | "@babel/traverse" "^7.6.3" 31 | "@babel/types" "^7.6.3" 32 | convert-source-map "^1.1.0" 33 | debug "^4.1.0" 34 | json5 "^2.1.0" 35 | lodash "^4.17.13" 36 | resolve "^1.3.2" 37 | semver "^5.4.1" 38 | source-map "^0.5.0" 39 | 40 | "@babel/generator@^7.23.0": 41 | version "7.23.0" 42 | resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" 43 | integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== 44 | dependencies: 45 | "@babel/types" "^7.23.0" 46 | "@jridgewell/gen-mapping" "^0.3.2" 47 | "@jridgewell/trace-mapping" "^0.3.17" 48 | jsesc "^2.5.1" 49 | 50 | "@babel/generator@^7.6.4": 51 | version "7.6.4" 52 | resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.4.tgz#a4f8437287bf9671b07f483b76e3bb731bc97671" 53 | integrity sha512-jsBuXkFoZxk0yWLyGI9llT9oiQ2FeTASmRFE32U+aaDTfoE92t78eroO7PTpU/OrYq38hlcDM6vbfLDaOLy+7w== 54 | dependencies: 55 | "@babel/types" "^7.6.3" 56 | jsesc "^2.5.1" 57 | lodash "^4.17.13" 58 | source-map "^0.5.0" 59 | 60 | "@babel/helper-environment-visitor@^7.22.20": 61 | version "7.22.20" 62 | resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" 63 | integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== 64 | 65 | "@babel/helper-function-name@^7.23.0": 66 | version "7.23.0" 67 | resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" 68 | integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== 69 | dependencies: 70 | "@babel/template" "^7.22.15" 71 | "@babel/types" "^7.23.0" 72 | 73 | "@babel/helper-hoist-variables@^7.22.5": 74 | version "7.22.5" 75 | resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" 76 | integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== 77 | dependencies: 78 | "@babel/types" "^7.22.5" 79 | 80 | "@babel/helper-split-export-declaration@^7.22.6": 81 | version "7.22.6" 82 | resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" 83 | integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== 84 | dependencies: 85 | "@babel/types" "^7.22.5" 86 | 87 | "@babel/helper-string-parser@^7.22.5": 88 | version "7.22.5" 89 | resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" 90 | integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== 91 | 92 | "@babel/helper-validator-identifier@^7.22.20": 93 | version "7.22.20" 94 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" 95 | integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== 96 | 97 | "@babel/helpers@^7.6.2": 98 | version "7.6.2" 99 | resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.6.2.tgz#681ffe489ea4dcc55f23ce469e58e59c1c045153" 100 | integrity sha512-3/bAUL8zZxYs1cdX2ilEE0WobqbCmKWr/889lf2SS0PpDcpEIY8pb1CCyz0pEcX3pEb+MCbks1jIokz2xLtGTA== 101 | dependencies: 102 | "@babel/template" "^7.6.0" 103 | "@babel/traverse" "^7.6.2" 104 | "@babel/types" "^7.6.0" 105 | 106 | "@babel/highlight@^7.0.0": 107 | version "7.5.0" 108 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" 109 | integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ== 110 | dependencies: 111 | chalk "^2.0.0" 112 | esutils "^2.0.2" 113 | js-tokens "^4.0.0" 114 | 115 | "@babel/highlight@^7.22.13": 116 | version "7.22.20" 117 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" 118 | integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== 119 | dependencies: 120 | "@babel/helper-validator-identifier" "^7.22.20" 121 | chalk "^2.4.2" 122 | js-tokens "^4.0.0" 123 | 124 | "@babel/parser@^7.22.15", "@babel/parser@^7.23.0": 125 | version "7.23.0" 126 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" 127 | integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== 128 | 129 | "@babel/parser@^7.6.0", "@babel/parser@^7.6.4": 130 | version "7.6.4" 131 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.4.tgz#cb9b36a7482110282d5cb6dd424ec9262b473d81" 132 | integrity sha512-D8RHPW5qd0Vbyo3qb+YjO5nvUVRTXFLQ/FsDxJU2Nqz4uB5EnUN0ZQSEYpvTIbRuttig1XbHWU5oMeQwQSAA+A== 133 | 134 | "@babel/template@^7.22.15": 135 | version "7.22.15" 136 | resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" 137 | integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== 138 | dependencies: 139 | "@babel/code-frame" "^7.22.13" 140 | "@babel/parser" "^7.22.15" 141 | "@babel/types" "^7.22.15" 142 | 143 | "@babel/template@^7.6.0": 144 | version "7.6.0" 145 | resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6" 146 | integrity sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ== 147 | dependencies: 148 | "@babel/code-frame" "^7.0.0" 149 | "@babel/parser" "^7.6.0" 150 | "@babel/types" "^7.6.0" 151 | 152 | "@babel/traverse@^7.6.2", "@babel/traverse@^7.6.3": 153 | version "7.23.2" 154 | resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" 155 | integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== 156 | dependencies: 157 | "@babel/code-frame" "^7.22.13" 158 | "@babel/generator" "^7.23.0" 159 | "@babel/helper-environment-visitor" "^7.22.20" 160 | "@babel/helper-function-name" "^7.23.0" 161 | "@babel/helper-hoist-variables" "^7.22.5" 162 | "@babel/helper-split-export-declaration" "^7.22.6" 163 | "@babel/parser" "^7.23.0" 164 | "@babel/types" "^7.23.0" 165 | debug "^4.1.0" 166 | globals "^11.1.0" 167 | 168 | "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": 169 | version "7.23.0" 170 | resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" 171 | integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== 172 | dependencies: 173 | "@babel/helper-string-parser" "^7.22.5" 174 | "@babel/helper-validator-identifier" "^7.22.20" 175 | to-fast-properties "^2.0.0" 176 | 177 | "@babel/types@^7.6.0", "@babel/types@^7.6.3": 178 | version "7.6.3" 179 | resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.3.tgz#3f07d96f854f98e2fbd45c64b0cb942d11e8ba09" 180 | integrity sha512-CqbcpTxMcpuQTMhjI37ZHVgjBkysg5icREQIEZ0eG1yCNwg3oy+5AaLiOKmjsCj6nqOsa6Hf0ObjRVwokb7srA== 181 | dependencies: 182 | esutils "^2.0.2" 183 | lodash "^4.17.13" 184 | to-fast-properties "^2.0.0" 185 | 186 | "@jridgewell/gen-mapping@^0.3.2": 187 | version "0.3.3" 188 | resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" 189 | integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== 190 | dependencies: 191 | "@jridgewell/set-array" "^1.0.1" 192 | "@jridgewell/sourcemap-codec" "^1.4.10" 193 | "@jridgewell/trace-mapping" "^0.3.9" 194 | 195 | "@jridgewell/resolve-uri@^3.1.0": 196 | version "3.1.1" 197 | resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" 198 | integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== 199 | 200 | "@jridgewell/set-array@^1.0.1": 201 | version "1.1.2" 202 | resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" 203 | integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== 204 | 205 | "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": 206 | version "1.4.15" 207 | resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" 208 | integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== 209 | 210 | "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": 211 | version "0.3.20" 212 | resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" 213 | integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== 214 | dependencies: 215 | "@jridgewell/resolve-uri" "^3.1.0" 216 | "@jridgewell/sourcemap-codec" "^1.4.14" 217 | 218 | ansi-styles@^3.2.1: 219 | version "3.2.1" 220 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 221 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 222 | dependencies: 223 | color-convert "^1.9.0" 224 | 225 | chalk@^2.0.0, chalk@^2.4.2: 226 | version "2.4.2" 227 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" 228 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 229 | dependencies: 230 | ansi-styles "^3.2.1" 231 | escape-string-regexp "^1.0.5" 232 | supports-color "^5.3.0" 233 | 234 | color-convert@^1.9.0: 235 | version "1.9.3" 236 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 237 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 238 | dependencies: 239 | color-name "1.1.3" 240 | 241 | color-name@1.1.3: 242 | version "1.1.3" 243 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 244 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 245 | 246 | convert-source-map@^1.1.0: 247 | version "1.6.0" 248 | resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" 249 | integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== 250 | dependencies: 251 | safe-buffer "~5.1.1" 252 | 253 | debug@^4.1.0: 254 | version "4.3.4" 255 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 256 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 257 | dependencies: 258 | ms "2.1.2" 259 | 260 | escape-string-regexp@^1.0.5: 261 | version "1.0.5" 262 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 263 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 264 | 265 | esutils@^2.0.2: 266 | version "2.0.3" 267 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" 268 | integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== 269 | 270 | globals@^11.1.0: 271 | version "11.12.0" 272 | resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" 273 | integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== 274 | 275 | has-flag@^3.0.0: 276 | version "3.0.0" 277 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 278 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 279 | 280 | hyperstache@^0.3.0: 281 | version "0.3.0" 282 | resolved "https://registry.yarnpkg.com/hyperstache/-/hyperstache-0.3.0.tgz#c97ca5b617f5d456fbd9f06b939147120337e5d7" 283 | integrity sha512-qD9wZmABDGUPvp71rhqhxZ5xhALUHBQi8d9AZ8jmhYesmDxVPQ88OFJu21Oc43uN+/XlPTCOjgSPgoMdhp+/9A== 284 | 285 | js-tokens@^4.0.0: 286 | version "4.0.0" 287 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 288 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 289 | 290 | jsesc@^2.5.1: 291 | version "2.5.2" 292 | resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" 293 | integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== 294 | 295 | json5@^2.1.0: 296 | version "2.1.1" 297 | resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6" 298 | integrity sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ== 299 | dependencies: 300 | minimist "^1.2.0" 301 | 302 | lodash@^4.17.13: 303 | version "4.17.15" 304 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" 305 | integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== 306 | 307 | minimist@^1.2.0: 308 | version "1.2.0" 309 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 310 | integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= 311 | 312 | ms@2.1.2: 313 | version "2.1.2" 314 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 315 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 316 | 317 | path-parse@^1.0.6: 318 | version "1.0.6" 319 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" 320 | integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== 321 | 322 | resolve@^1.3.2: 323 | version "1.12.0" 324 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" 325 | integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== 326 | dependencies: 327 | path-parse "^1.0.6" 328 | 329 | safe-buffer@~5.1.1: 330 | version "5.1.2" 331 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 332 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 333 | 334 | semver@^5.4.1: 335 | version "5.7.2" 336 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" 337 | integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== 338 | 339 | source-map@^0.5.0: 340 | version "0.5.7" 341 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" 342 | integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= 343 | 344 | supports-color@^5.3.0: 345 | version "5.5.0" 346 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 347 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 348 | dependencies: 349 | has-flag "^3.0.0" 350 | 351 | to-fast-properties@^2.0.0: 352 | version "2.0.0" 353 | resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" 354 | integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= 355 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | import nodeResolve from 'rollup-plugin-node-resolve'; 3 | import builtins from 'rollup-plugin-node-builtins'; 4 | import babel from 'rollup-plugin-babel'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | import bundleSize from 'rollup-plugin-size'; 7 | import replace from 'rollup-plugin-replace'; 8 | 9 | const env = process.env.NODE_ENV; 10 | 11 | const terserPlugin = terser({ 12 | sourcemap: true, 13 | warnings: true, 14 | compress: { 15 | passes: 2 16 | }, 17 | mangle: { 18 | properties: { 19 | regex: /^_/ 20 | } 21 | }, 22 | nameCache: { 23 | props: { 24 | cname: 6, 25 | props: { 26 | // $_tag: '__t', 27 | } 28 | } 29 | } 30 | }); 31 | 32 | const config = { 33 | input: 'src/index.js', 34 | output: { 35 | format: env, 36 | name: 'hyperstache', 37 | strict: false, // Remove `use strict;` 38 | interop: false, // Remove `r=r&&r.hasOwnProperty("default")?r.default:r;` 39 | freeze: false, // Remove `Object.freeze()` 40 | esModule: false // Remove `esModule` property 41 | }, 42 | plugins: [ 43 | bundleSize(), 44 | nodeResolve({ 45 | preferBuiltins: true 46 | }), 47 | builtins(), 48 | terserPlugin 49 | ] 50 | }; 51 | 52 | const babelPlugin = babel(); 53 | 54 | export default [ 55 | { 56 | ...config, 57 | output: { 58 | ...config.output, 59 | file: 'module/hyperstache.js', 60 | format: 'es' 61 | } 62 | }, 63 | { 64 | ...config, 65 | output: { 66 | ...config.output, 67 | file: 'dist/hyperstache.js', 68 | format: 'umd' 69 | }, 70 | plugins: [ 71 | ...config.plugins, 72 | babelPlugin 73 | ] 74 | }, 75 | { 76 | ...config, 77 | output: { 78 | ...config.output, 79 | file: 'dist/hyperstache.min.js', 80 | format: 'iife' 81 | }, 82 | plugins: [ 83 | ...config.plugins, 84 | babelPlugin 85 | ] 86 | }, 87 | { 88 | ...config, 89 | output: { 90 | ...config.output, 91 | file: 'module/mini.js', 92 | format: 'es' 93 | }, 94 | plugins: [ 95 | ...config.plugins, 96 | replace({ 97 | delimiters: ['', ''], 98 | 'export const MINI = false;': 'export const MINI = true;' 99 | }) 100 | ] 101 | }, 102 | { 103 | ...config, 104 | output: { 105 | ...config.output, 106 | file: 'dist/mini.js', 107 | format: 'umd' 108 | }, 109 | plugins: [ 110 | ...config.plugins, 111 | replace({ 112 | delimiters: ['', ''], 113 | 'export const MINI = false;': 'export const MINI = true;' 114 | }), 115 | babelPlugin 116 | ] 117 | }, 118 | { 119 | ...config, 120 | output: { 121 | ...config.output, 122 | file: 'dist/mini.min.js', 123 | format: 'iife' 124 | }, 125 | plugins: [ 126 | ...config.plugins, 127 | replace({ 128 | delimiters: ['', ''], 129 | 'export const MINI = false;': 'export const MINI = true;' 130 | }), 131 | babelPlugin 132 | ] 133 | }, 134 | { 135 | ...config, 136 | input: 'src/runtime.js', 137 | output: { 138 | ...config.output, 139 | file: 'module/runtime.js', 140 | format: 'es' 141 | } 142 | }, 143 | { 144 | ...config, 145 | input: 'src/runtime.js', 146 | output: { 147 | ...config.output, 148 | file: 'dist/runtime.js', 149 | format: 'umd' 150 | }, 151 | plugins: [ 152 | ...config.plugins, 153 | babelPlugin 154 | ] 155 | }, 156 | { 157 | ...config, 158 | input: 'src/runtime.js', 159 | output: { 160 | ...config.output, 161 | file: 'dist/runtime.min.js', 162 | format: 'iife' 163 | }, 164 | plugins: [ 165 | ...config.plugins, 166 | babelPlugin 167 | ] 168 | }, 169 | { 170 | ...config, 171 | input: 'src/runtime.js', 172 | output: { 173 | ...config.output, 174 | file: 'module/runtime-mini.js', 175 | format: 'es' 176 | }, 177 | plugins: [ 178 | ...config.plugins, 179 | replace({ 180 | delimiters: ['', ''], 181 | 'export const MINI = false;': 'export const MINI = true;' 182 | }) 183 | ] 184 | }, 185 | { 186 | ...config, 187 | input: 'src/runtime.js', 188 | output: { 189 | ...config.output, 190 | file: 'dist/runtime-mini.js', 191 | format: 'umd' 192 | }, 193 | plugins: [ 194 | ...config.plugins, 195 | replace({ 196 | delimiters: ['', ''], 197 | 'export const MINI = false;': 'export const MINI = true;' 198 | }), 199 | babelPlugin 200 | ] 201 | }, 202 | { 203 | ...config, 204 | input: 'src/runtime.js', 205 | output: { 206 | ...config.output, 207 | file: 'dist/runtime-mini.min.js', 208 | format: 'iife' 209 | }, 210 | plugins: [ 211 | ...config.plugins, 212 | replace({ 213 | delimiters: ['', ''], 214 | 'export const MINI = false;': 'export const MINI = true;' 215 | }), 216 | babelPlugin 217 | ] 218 | }, 219 | { 220 | ...config, 221 | input: 'packages/babel-plugin-hyperstache/src/index.js', 222 | output: { 223 | ...config.output, 224 | file: 'packages/babel-plugin-hyperstache/module/babel-plugin-hyperstache.js', 225 | format: 'es' 226 | } 227 | }, 228 | { 229 | ...config, 230 | input: 'packages/babel-plugin-hyperstache/src/index.js', 231 | output: { 232 | ...config.output, 233 | file: 'packages/babel-plugin-hyperstache/dist/babel-plugin-hyperstache.js', 234 | format: 'umd' 235 | } 236 | }, 237 | ]; 238 | -------------------------------------------------------------------------------- /runtime-mini/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperstache-runtime-mini", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "Hyperstache runtime mini, no built-in helpers.", 6 | "module": "../module/runtime-mini.js", 7 | "main": "../dist/runtime-mini.js" 8 | } 9 | -------------------------------------------------------------------------------- /runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperstache-runtime", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "Hyperstache runtime", 6 | "module": "../module/runtime.js", 7 | "main": "../dist/runtime.js" 8 | } 9 | -------------------------------------------------------------------------------- /src/build.js: -------------------------------------------------------------------------------- 1 | /* Adapted from HTM - Apache License 2.0 - Jason Miller, Joachim Viide */ 2 | 3 | import { expr, block } from './helpers.js'; 4 | import { escapeExpression, parseLiteral, log } from './utils.js'; 5 | 6 | const MODE_TEXT = 0; 7 | const MODE_EXPR_SET = 1; 8 | const MODE_EXPR_APPEND = 2; 9 | 10 | const TEXT = 0; 11 | export const CHILD_RECURSE = 1; 12 | const EXPR_INVERSE = 2; 13 | const EXPR_BLOCK = 3; 14 | export const EXPR_VAR = 4; 15 | const EXPR_RAW = 5; 16 | const EXPR_COMMENT = 6; 17 | 18 | export const build = function(statics) { 19 | let str; 20 | let char; 21 | let mode = MODE_TEXT; 22 | let expr; 23 | let buffer = ''; 24 | let lastBuffer; 25 | let quote; 26 | let current = [0]; 27 | let closeEnd; 28 | let propName; 29 | let line = [current]; // Keeps track of `current` arrays per line. 30 | let lines = line; // Keeps track of all `current` arrays. 31 | let hasTag; // Is there a {{tag}} on the current line? 32 | let nonSpace; // Is there a non-space char on the current line? 33 | let isWhiteSpace; // Is current character a space? 34 | let skipWhiteSpace; 35 | 36 | // log('STATICS', statics); 37 | for (let i = 0; i < statics.length; i++) { 38 | str = statics[i]; 39 | 40 | if (i) { 41 | if (mode === MODE_TEXT) { 42 | commit(); 43 | if (!lastBuffer) { 44 | // Add a split if there is no content before the expression. 45 | commit(''); 46 | } 47 | } 48 | commit(i); 49 | } 50 | 51 | for (let j = 0; j < str.length; j++) { 52 | char = str[j]; 53 | isWhiteSpace = /\s/.test(char); 54 | 55 | if (mode === MODE_TEXT) { 56 | if (char === '{' && str[j + 1] === '{') { 57 | skipWhiteSpace = false; 58 | 59 | commit(); 60 | if (!lastBuffer) { 61 | // Add a split if there is no content before the expression. 62 | commit(''); 63 | } 64 | 65 | hasTag = true; 66 | expr = EXPR_VAR; 67 | mode = MODE_EXPR_SET; 68 | j++; 69 | } else { 70 | if (!isWhiteSpace) { 71 | nonSpace = true; 72 | skipWhiteSpace = false; 73 | } 74 | 75 | if (!skipWhiteSpace || !isWhiteSpace) { 76 | buffer += char; 77 | } 78 | 79 | if (char === '\n') { 80 | stripSpace(); 81 | 82 | // Keep track of more linear arrays for whitespace control. 83 | lines = lines.concat(line); 84 | line = [current]; 85 | } 86 | 87 | expr = undefined; 88 | } 89 | } else if ( 90 | (!closeEnd || buffer === closeEnd) && 91 | char === '}' && 92 | str[j + 1] === '}' && 93 | str[j + 2] !== '}' 94 | ) { 95 | if (expr === EXPR_VAR || expr === EXPR_RAW) { 96 | nonSpace = true; 97 | } 98 | 99 | if (expr === EXPR_COMMENT) { 100 | commit(''); 101 | } else { 102 | commit(); 103 | } 104 | 105 | closeEnd = false; 106 | mode = MODE_TEXT; 107 | j++; 108 | } else if (expr === EXPR_COMMENT) { 109 | // Just keep track of 2 characters. 110 | buffer = str[j - 1] + char; 111 | if (buffer === '--') { 112 | closeEnd = buffer; 113 | } 114 | } else if (quote) { 115 | if (char === quote) { 116 | quote = ''; 117 | } 118 | buffer += char; 119 | } else if (char === '"' || char === "'") { 120 | quote = char; 121 | buffer += char; 122 | } else if (isWhiteSpace) { 123 | if (expr === EXPR_INVERSE) { 124 | // Add `else` chaining. 125 | // e.g. transforms {{else if }} into {{else}}{{#/if }} 126 | str = `{{#/${buffer}${str.substr(j)}`; // {{#/ autoclose 127 | mode = MODE_TEXT; 128 | j = -1; 129 | buffer = ''; 130 | } else { 131 | // Only commit if there is buffer, ignore spaces after `{{`. 132 | if (buffer) { 133 | commit(); 134 | propName = ''; 135 | } 136 | } 137 | } else if (char === '~' && str[j + 1] === '}') { 138 | skipWhiteSpace = true; 139 | } else if (!buffer && char === '~') { 140 | // Remove previous whitespace until a tag or non space character. 141 | eachToken(lines, (type, field, i, curr) => { 142 | if ( 143 | type > TEXT || 144 | (type === TEXT && (curr[i] = field.replace(/\s*$/, ''))) 145 | ) { 146 | return true; 147 | } 148 | }); 149 | } else if ((!buffer && (char === '{' || char === '&')) || char === '}') { 150 | // First `{` after opening expression `{{`. 151 | expr = EXPR_RAW; 152 | nonSpace = true; 153 | } else if (!buffer && char === '!') { 154 | expr = EXPR_COMMENT; 155 | } else if (!buffer && (char === '#' || char === '^')) { 156 | // First `#` after opening expression `{{`. 157 | 158 | // [1] is reserved for `if`, [2] for `else`. 159 | const block = [current]; 160 | current = block[1] = [block]; 161 | line.push(current); 162 | block[2] = [block]; 163 | block[3] = char === '^'; 164 | block[4] = str[j + 1] === '/' && ++j; // autoclose 165 | 166 | expr = EXPR_BLOCK; 167 | mode = MODE_EXPR_SET; 168 | } else if (char === '=') { 169 | propName = buffer; 170 | buffer = ''; 171 | } else if (char === '/') { 172 | if (current[0][4]) { 173 | // autoclose 174 | str = `}}{{/${str.substr(j + 1)}`; 175 | j = -1; 176 | } 177 | 178 | mode = current[0]; 179 | (current = current[0][0]).push(mode, CHILD_RECURSE); 180 | 181 | expr = EXPR_BLOCK; 182 | } else { 183 | buffer += char; 184 | } 185 | } 186 | } 187 | 188 | commit(); 189 | if (!lastBuffer) { 190 | // Add a split if there is no content before the expression. 191 | commit(''); 192 | } 193 | 194 | stripSpace(); 195 | return current; 196 | 197 | function commit(field) { 198 | if (mode === MODE_TEXT && (field != null || buffer)) { 199 | current.push(field != null ? field : buffer, TEXT); 200 | } else if (mode >= MODE_EXPR_SET && (field != null || buffer)) { 201 | if (mode === MODE_EXPR_SET) { 202 | if (buffer === 'else' || buffer === '^') { 203 | current = current[0][2]; 204 | line.push(current); 205 | 206 | expr = EXPR_INVERSE; 207 | mode = MODE_EXPR_SET; 208 | } else { 209 | // [..., (var|fn), EXPR, args, hash, ...] 210 | current.push(field != null ? field : buffer, expr, [], {}); 211 | mode = MODE_EXPR_APPEND; 212 | } 213 | } else { 214 | // mode = MODE_EXPR_APPEND; 215 | current[current.length - 3] = expr; 216 | if (propName) { 217 | current[current.length - 1][propName] = parseLiteral(buffer); 218 | } else { 219 | current[current.length - 2].push(parseLiteral(buffer)); 220 | } 221 | } 222 | } 223 | 224 | lastBuffer = buffer; 225 | buffer = ''; 226 | } 227 | 228 | function stripSpace() { 229 | if (hasTag && !nonSpace) { 230 | buffer = buffer.replace(/\r?\n$/, ''); 231 | 232 | // Remove whitespace until \n or non space characters. 233 | eachToken(line, (type, field, i, curr) => { 234 | if (type === TEXT && (curr[i] = field.replace(/[^\S\n]*$/, ''))) { 235 | return true; 236 | } 237 | }); 238 | } 239 | 240 | nonSpace = false; 241 | hasTag = false; 242 | } 243 | }; 244 | 245 | function eachToken(currents, fn) { 246 | for (let i = currents.length; i--; ) { 247 | for (let j = currents[i].length; j > 2; ) { 248 | if (fn(currents[i][--j], currents[i][--j], j, currents[i])) { 249 | return; 250 | } 251 | } 252 | } 253 | } 254 | 255 | export const evaluate = (h, built, fields, context, data, depths) => { 256 | depths = depths || [context]; 257 | 258 | const statics = []; 259 | const exprs = []; 260 | let value; 261 | 262 | // log('BUILT', built); 263 | for (let i = 1; i < built.length; i++) { 264 | const field = built[i]; 265 | // log('FIELD', field); 266 | const type = built[++i]; 267 | 268 | if (type >= EXPR_VAR) { 269 | value = expr(field, context, { 270 | params: built[++i], 271 | hash: built[++i], 272 | data, 273 | depths 274 | }); 275 | // log('VALUE', value); 276 | 277 | if (value != null) { 278 | if (type === EXPR_VAR) { 279 | value = escapeExpression(value); 280 | } 281 | exprs.push(value); 282 | } else { 283 | // If the context has no value, push an empty string as expression. 284 | exprs.push(''); 285 | } 286 | } else if (type === CHILD_RECURSE) { 287 | /** 288 | * field = [ 289 | * [parent], 290 | * [[parent], if, 5, ['@first'], { hash: param }, 'body', 3], // if block 291 | * [[parent], if, 5, ['@last'], {}, 'body', 3, 'End', 1] // else block 292 | * ] 293 | */ 294 | const makeFun = ifOrElse => (ctx, opts) => { 295 | depths = ctx != depths[0] ? [ctx].concat(depths) : depths; 296 | return evaluate(h, ifOrElse, fields, ctx, opts && opts.data, depths); 297 | }; 298 | 299 | value = block(field[1][1], context, { 300 | fn: makeFun([0].concat(field[1].slice(5))), 301 | inverse: makeFun(field[2]), 302 | params: field[1][3], 303 | hash: field[1][4], 304 | inverted: field[3], 305 | data, 306 | depths 307 | }); 308 | 309 | if (value) { 310 | // log('RESULT', value); 311 | exprs.push(value); 312 | } else { 313 | // If the result is empty, push an empty string as expression. 314 | exprs.push(''); 315 | } 316 | } else if (typeof field === 'number') { 317 | exprs.push(fields[field]); 318 | } else { 319 | // type === TEXT 320 | statics.push(field); 321 | } 322 | } 323 | 324 | const args = [statics].concat(exprs); 325 | // log('ARGS', args); 326 | return h.apply(null, args); 327 | }; 328 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | export const MINI = false; 2 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | /* Adapted code from Handlebars - MIT License - Yehuda Katz */ 2 | import { MINI } from './constants.js'; 3 | import { isEmpty, createFrame, lookup, log } from './utils.js'; 4 | 5 | export const helpers = MINI 6 | ? {} 7 | : { 8 | with: withHelper, 9 | if: ifHelper, 10 | unless: unlessHelper, 11 | each: eachHelper 12 | }; 13 | 14 | export function expr(field, context, options) { 15 | options = options || {}; 16 | (options.data = options.data || {}).root = context; 17 | 18 | return helpers[field] 19 | ? block(field, context, options) 20 | : lookup(context, options)(field); 21 | } 22 | 23 | export function block(field, context, options) { 24 | options = options || {}; 25 | (options.data = options.data || {}).root = context; 26 | options.params = options.params || []; 27 | options.hash = options.hash || {}; 28 | options.inverse = options.inverse || (() => ''); 29 | 30 | let value; 31 | if (helpers[field]) { 32 | value = helpers[field].apply( 33 | context, 34 | options.params.map(lookup(context, options)).concat(options) 35 | ); 36 | } else { 37 | value = lookup(context, options)(field); 38 | 39 | if (options.inverted) { 40 | const tmp = options.fn; 41 | options.fn = options.inverse; 42 | options.inverse = tmp; 43 | } 44 | 45 | options.params = [value]; 46 | 47 | return block( 48 | Array.isArray(value) ? 'each' : typeof value === 'object' ? 'with' : 'if', 49 | context, 50 | options 51 | ); 52 | } 53 | 54 | return value; 55 | } 56 | 57 | export function registerHelper(name, fn) { 58 | helpers[name] = fn; 59 | } 60 | 61 | function withHelper(context, options) { 62 | if (typeof context === 'function') { 63 | context = context.call(this); 64 | } 65 | 66 | if (!isEmpty(context)) { 67 | let data = options.data; 68 | 69 | return options.fn(context, { 70 | data: data 71 | }); 72 | } else { 73 | return options.inverse(this); 74 | } 75 | } 76 | 77 | function ifHelper(conditional, options) { 78 | if (typeof conditional === 'function') { 79 | conditional = conditional.call(this); 80 | } 81 | 82 | if (!conditional || isEmpty(conditional)) { 83 | return options.inverse(this); 84 | } else { 85 | return options.fn(this); 86 | } 87 | } 88 | 89 | function unlessHelper(conditional, options) { 90 | return ifHelper.call(this, conditional, { 91 | fn: options.inverse, 92 | inverse: options.fn, 93 | hash: options.hash 94 | }); 95 | } 96 | 97 | function eachHelper(context, options) { 98 | let i = 0, 99 | ret = [], 100 | data; 101 | 102 | if (typeof context === 'function') { 103 | context = context.call(this); 104 | } 105 | 106 | if (options.data) { 107 | data = createFrame(options.data); 108 | } 109 | 110 | function execIteration(field, index, last) { 111 | if (data) { 112 | data.key = field; 113 | data.index = index; 114 | data.first = index === 0; 115 | data.last = !!last; 116 | } 117 | 118 | ret.push( 119 | options.fn(context[field], { 120 | data: data 121 | }) 122 | ); 123 | } 124 | 125 | if (context && typeof context === 'object') { 126 | if (Array.isArray(context)) { 127 | for (let j = context.length; i < j; i++) { 128 | if (i in context) { 129 | execIteration(i, i, i === context.length - 1); 130 | } 131 | } 132 | } else { 133 | let priorKey; 134 | 135 | for (let key in context) { 136 | if (context.hasOwnProperty(key)) { 137 | // We're running the iterations one step out of sync so we can detect 138 | // the last iteration without have to scan the object twice and create 139 | // an itermediate keys array. 140 | if (priorKey !== undefined) { 141 | execIteration(priorKey, i - 1); 142 | } 143 | priorKey = key; 144 | i++; 145 | } 146 | } 147 | if (priorKey !== undefined) { 148 | execIteration(priorKey, i - 1, true); 149 | } 150 | } 151 | } 152 | 153 | if (i === 0) { 154 | return options.inverse(this); 155 | } 156 | 157 | return ret; 158 | } 159 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { build, evaluate } from './build.js'; 2 | 3 | export { registerHelper, helpers } from './helpers.js'; 4 | export { escapeExpression, SafeString } from './utils.js'; 5 | 6 | export function compile(statics) { 7 | const template = build(statics); 8 | const fields = arguments; 9 | const h = this; 10 | return function(context) { 11 | return evaluate(h, template, fields, context); 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/runtime.js: -------------------------------------------------------------------------------- 1 | import { expr, block } from './helpers.js'; 2 | import { escapeExpression } from './utils.js'; 3 | 4 | export { registerHelper, helpers } from './helpers.js'; 5 | export { SafeString, escapeExpression } from './utils.js'; 6 | 7 | let depths; 8 | 9 | export function template(spec) { 10 | const container = { 11 | escape: escapeExpression, 12 | expr, 13 | block 14 | }; 15 | 16 | function ret(ctx, opts) { 17 | if (depths) { 18 | depths = ctx != depths[0] ? 19 | [ctx].concat(depths) : depths; 20 | } else { 21 | depths = [ctx]; 22 | } 23 | 24 | const result = spec(ctx, opts && opts.data, depths, container); 25 | depths = null; 26 | 27 | return result; 28 | } 29 | 30 | return ret; 31 | } 32 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /* Some code from Handlebars - MIT License - Yehuda Katz */ 2 | import { inspect } from 'util'; 3 | 4 | export function log(label, ...args) { 5 | console.log(label, ...args.map(a => inspect(a, { depth: 10, colors: true }))); 6 | } 7 | 8 | export function createFrame(object) { 9 | let frame = {}; 10 | for (let i in object) frame[i] = object[i]; 11 | frame._parent = object; 12 | return frame; 13 | } 14 | 15 | export function isEmpty(value) { 16 | if (!value && value !== 0) { 17 | return true; 18 | } else if (Array.isArray(value) && value.length === 0) { 19 | return true; 20 | } else { 21 | return false; 22 | } 23 | } 24 | 25 | export function parseLiteral(value) { 26 | if (value === 'true') { 27 | value = true; 28 | } else if (value === 'false') { 29 | value = false; 30 | } else if (value === 'null') { 31 | value = null; 32 | } else if (value === 'undefined') { 33 | value = undefined; 34 | } else if (!isNaN(+value)) { 35 | value = +value; 36 | } 37 | return value; 38 | } 39 | 40 | export function lookup(context, options) { 41 | const data = options.data; 42 | const depths = options.depths; 43 | return name => { 44 | if (typeof name === 'string') { 45 | const unwrapped = unwrap(name, '"') || unwrap(name, "'"); 46 | if (unwrapped) { 47 | return unwrapped; 48 | } else if (name[0] === '@' && (name = name.slice(1)) && name in data) { 49 | return data[name]; 50 | } else { 51 | if (name === '.') { 52 | return context; 53 | } else { 54 | const paths = name.split('.'); 55 | for (let i = 0; i < depths.length; i++) { 56 | if (depths[i] && objectPath([paths[0]], depths[i]) != null) { 57 | return objectPath(paths, depths[i]); 58 | } 59 | } 60 | return; 61 | } 62 | } 63 | } 64 | return name; 65 | }; 66 | } 67 | 68 | export function objectPath(paths, val) { 69 | let idx = 0; 70 | while (idx < paths.length) { 71 | if (val == null) { 72 | return; 73 | } 74 | 75 | const path = 76 | unwrap(paths[idx], '"') || 77 | unwrap(paths[idx], "'") || 78 | unwrap(paths[idx], '[]') || 79 | paths[idx]; 80 | 81 | val = val[path]; 82 | idx++; 83 | } 84 | return val; 85 | } 86 | 87 | export function unwrap(str, adfix) { 88 | return ( 89 | str[0] === adfix[0] && 90 | str[str.length - 1] === adfix[adfix.length - 1] && 91 | str.slice(1, -1) 92 | ); 93 | } 94 | 95 | const escape = { 96 | '&': '&', 97 | '<': '<', 98 | '>': '>', 99 | '"': '"', 100 | "'": ''', 101 | '`': '`', 102 | '=': '=' 103 | }; 104 | 105 | function escapeChar(chr) { 106 | return escape[chr]; 107 | } 108 | 109 | /** 110 | * Escape an expression, aka make HTML characters safe. 111 | * 112 | * This is different from Handlebars because some expressions are better left 113 | * in its original type for further processing. 114 | * 115 | * @param {*} string 116 | * @return {*} 117 | */ 118 | export function escapeExpression(string) { 119 | if (string == null) return ''; 120 | 121 | // don't escape SafeStrings, since they're already safe 122 | if (string && string._toHTML) { 123 | return string + ''; 124 | } 125 | 126 | if (typeof string !== 'string') { 127 | return string; 128 | } 129 | 130 | return string.replace(/[&<>"'`=]/g, escapeChar); 131 | } 132 | 133 | // Build out our basic SafeString type 134 | export function SafeString(string) { 135 | const toString = () => '' + string; 136 | return { 137 | toString, 138 | _toHTML: toString 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /test/htm.js: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import htm from 'htm'; 3 | import { compile } from '../src/index.js'; 4 | 5 | const h = (tag, props, ...children) => ({ tag, props, children }); 6 | const hh = (args) => htm.apply(h, args); 7 | 8 | const hbs = (statics, ...fields) => { 9 | const hyperstache = compile(statics, ...fields); 10 | return hyperstache.bind(hh); 11 | } 12 | 13 | test('single named elements', t => { 14 | t.deepEqual( 15 | hbs`
    `(), 16 | { tag: 'div', props: null, children: [] } 17 | ); 18 | t.deepEqual( 19 | hbs`
    `(), 20 | { tag: 'div', props: null, children: [] } 21 | ); 22 | t.deepEqual( 23 | hbs``(), 24 | { tag: 'span', props: null, children: [] } 25 | ); 26 | t.end(); 27 | }); 28 | 29 | test('multiple root elements', t => { 30 | t.deepEqual( 31 | hbs``(), 32 | [ 33 | { tag: 'a', props: null, children: [] }, 34 | { tag: 'b', props: null, children: [] }, 35 | { tag: 'c', props: null, children: [] } 36 | ] 37 | ); 38 | t.end(); 39 | }); 40 | 41 | test('single boolean prop', t => { 42 | t.deepEqual( 43 | hbs``(), 44 | { tag: 'a', props: { disabled: true }, children: [] } 45 | ); 46 | t.end(); 47 | }); 48 | 49 | test('two boolean props', t => { 50 | t.deepEqual( 51 | hbs``(), 52 | { tag: 'a', props: { invisible: true, disabled: true }, children: [] } 53 | ); 54 | t.end(); 55 | }); 56 | 57 | test('single prop with empty value', t => { 58 | t.deepEqual( 59 | hbs``(), 60 | { tag: 'a', props: { href: '' }, children: [] } 61 | ); 62 | t.end(); 63 | }); 64 | 65 | test('two props with empty values', t => { 66 | t.deepEqual( 67 | hbs``(), 68 | { tag: 'a', props: { href: '', foo: '' }, children: [] } 69 | ); 70 | t.end(); 71 | }); 72 | 73 | test('single prop with empty name', t => { 74 | t.deepEqual( 75 | hbs``(), 76 | { tag: 'a', props: { '': 'foo' }, children: [] } 77 | ); 78 | t.end(); 79 | }); 80 | 81 | test('single prop with static value', t => { 82 | t.deepEqual( 83 | hbs``(), 84 | { tag: 'a', props: { href: '/hello' }, children: [] } 85 | ); 86 | t.end(); 87 | }); 88 | 89 | test('single prop with static value followed by a single boolean prop', t => { 90 | t.deepEqual( 91 | hbs``(), 92 | { tag: 'a', props: { href: '/hello', b: true }, children: [] } 93 | ); 94 | t.end(); 95 | }); 96 | 97 | test('two props with static values', t => { 98 | t.deepEqual( 99 | hbs``(), 100 | { tag: 'a', props: { href: '/hello', target: '_blank' }, children: [] } 101 | ); 102 | t.end(); 103 | }); 104 | 105 | test('slash in the middle of tag name or property name self-closes the element', t => { 106 | t.deepEqual( 107 | hbs``(), 108 | { tag: 'ab', props: null, children: [] } 109 | ); 110 | t.deepEqual( 111 | hbs``(), 112 | { tag: 'abba', props: { pr: true }, children: [] } 113 | ); 114 | t.end(); 115 | }); 116 | 117 | test('slash in a property value does not self-closes the element, unless followed by >', t => { 118 | t.deepEqual(hbs``(), { 119 | tag: 'abba', 120 | props: { prop: 'val/ue' }, 121 | children: [] 122 | }); 123 | t.deepEqual( 124 | hbs``(), 125 | { tag: 'abba', props: { prop: 'value' }, children: [] } 126 | ); 127 | t.deepEqual(hbs``(), { 128 | tag: 'abba', 129 | props: { prop: 'value/' }, 130 | children: [] 131 | }); 132 | t.end(); 133 | }); 134 | 135 | test('closing tag', t => { 136 | t.deepEqual( 137 | hbs``(), 138 | { tag: 'a', props: null, children: [] } 139 | ); 140 | t.deepEqual( 141 | hbs``(), 142 | { tag: 'a', props: { b: true }, children: [] } 143 | ); 144 | t.end(); 145 | }); 146 | 147 | test('auto-closing tag', t => { 148 | t.deepEqual( 149 | hbs``(), 150 | { tag: 'a', props: null, children: [] } 151 | ); 152 | t.end(); 153 | }); 154 | 155 | test('non-element roots', t => { 156 | t.deepEqual(hbs`${1}`(), 1); 157 | t.deepEqual(hbs`foo${1}`(), ['foo', 1]); 158 | t.deepEqual(hbs`foo${1}bar`(), ['foo', 1, 'bar']); 159 | t.end(); 160 | }); 161 | 162 | test('text child', t => { 163 | t.deepEqual( 164 | hbs`foo`(), 165 | { tag: 'a', props: null, children: ['foo'] } 166 | ); 167 | t.deepEqual( 168 | hbs`foo bar`(), 169 | { tag: 'a', props: null, children: ['foo bar'] } 170 | ); 171 | t.deepEqual( 172 | hbs`foo "`(), 173 | {tag: 'a',props: null,children: ['foo "', { tag: 'b', props: null, children: [] }] 174 | } 175 | ); 176 | t.end(); 177 | }); 178 | 179 | test('element child', t => { 180 | t.deepEqual( 181 | hbs``(), 182 | h('a', null, h('b', null)) 183 | ); 184 | t.end(); 185 | }); 186 | 187 | test('multiple element children', t => { 188 | t.deepEqual( 189 | hbs``(), 190 | h('a', null, h('b', null), h('c', null)) 191 | ); 192 | t.deepEqual( 193 | hbs``(), 194 | h('a', { x: true }, h('b', { y: true }), h('c', { z: true })) 195 | ); 196 | t.deepEqual( 197 | hbs``(), 198 | h('a', { x: '1' }, h('b', { y: '2' }), h('c', { z: '3' })) 199 | ); 200 | t.end(); 201 | }); 202 | 203 | test('mixed typed children', t => { 204 | t.deepEqual( 205 | hbs`foo`(), 206 | h('a', null, 'foo', h('b', null)) 207 | ); 208 | t.deepEqual( 209 | hbs`bar`(), 210 | h('a', null, h('b', null), 'bar') 211 | ); 212 | t.deepEqual( 213 | hbs`beforeafter`(), 214 | h('a', null, 'before', h('b', null), 'after') 215 | ); 216 | t.deepEqual( 217 | hbs`beforeafter`(), 218 | h('a', null, 'before', h('b', { x: '1' }), 'after') 219 | ); 220 | t.end(); 221 | }); 222 | 223 | test('hyphens (-) are allowed in attribute names', t => { 224 | t.deepEqual( 225 | hbs``(), 226 | h('a', { 'b-c': true }) 227 | ); 228 | t.end(); 229 | }); 230 | 231 | test('NUL characters are allowed in attribute values', t => { 232 | t.deepEqual( 233 | hbs``(), 234 | h('a', { b: '\0' }) 235 | ); 236 | t.end(); 237 | }); 238 | 239 | test('NUL characters are allowed in text', t => { 240 | t.deepEqual( 241 | hbs`\0`(), 242 | h('a', null, '\0') 243 | ); 244 | t.end(); 245 | }); 246 | 247 | test('ignore html comments', t => { 248 | t.deepEqual( 249 | hbs``(), 250 | h('a', null) 251 | ); 252 | t.deepEqual( 253 | hbs``(), 255 | h('a', null) 256 | ); 257 | t.deepEqual( 258 | hbs` Hello, world `(), 259 | h('a', null) 260 | ); 261 | t.end(); 262 | }); 263 | -------------------------------------------------------------------------------- /test/hyperstache.js: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import htm from 'htm'; 3 | import { compile, registerHelper } from '../src/index.js'; 4 | 5 | const h = (tag, props, ...children) => ({ tag, props, children }); 6 | const html = htm.bind(h); 7 | const hbs = compile.bind(html); 8 | 9 | registerHelper('loud', (str) => str.toUpperCase()); 10 | registerHelper('sum', (a, b) => a + b); 11 | registerHelper('noop', function(options) { 12 | return options.fn(this) 13 | }); 14 | registerHelper('bold', function(options) { 15 | return hbs`${options.fn(this)}`(); 16 | }); 17 | registerHelper('mul', options => options.hash.a * options.hash.b); 18 | registerHelper('pass', options => options.hash.value); 19 | 20 | test('simple expressions', t => { 21 | t.deepEqual( 22 | hbs`
    {{mustache}}
    `({ mustache: 'Hyper&' }), 23 | { tag: 'div', props: null, children: ['Hyper&'] } 24 | ); 25 | t.deepEqual( 26 | hbs`
    {{mustache}}{{snor}}
    `({ mustache: 'Hyper&', snor: 9 }), 27 | { tag: 'div', props: null, children: ['Hyper&', 9] } 28 | ); 29 | t.deepEqual( 30 | hbs`
    {{mustache}} {{snor}}
    `({ mustache: 'Hyper', snor: 'Handle it' }), 31 | { tag: 'div', props: null, children: ['Hyper', ' ', 'Handle it'] } 32 | ); 33 | t.deepEqual( 34 | hbs`
    {{mustache}} {{nooper}}
    `({ mustache: 'Hyper' }), 35 | { tag: 'div', props: null, children: ['Hyper', ' ', ''] } 36 | ); 37 | t.deepEqual( 38 | hbs`
    ${99}{{mustache}}
    `({ mustache: 'Hyper' }), 39 | { tag: 'div', props: null, children: [99, 'Hyper'] } 40 | ); 41 | t.deepEqual( 42 | hbs`
    {{mustache}} ${99}
    `({ mustache: 'Hyper' }), 43 | { tag: 'div', props: null, children: ['Hyper', ' ', 99] } 44 | ); 45 | t.deepEqual( 46 | hbs`
    ${99} {{mustache}}${99}
    `({ mustache: 'Hyper' }), 47 | { tag: 'div', props: null, children: [' ', 99, ' ', 'Hyper', 99] } 48 | ); 49 | t.end(); 50 | }); 51 | 52 | test('raw expressions', t => { 53 | t.deepEqual( 54 | hbs`
    {{{mustache}}}
    `({ mustache: 'Hyper&son' }), 55 | { tag: 'div', props: null, children: ['Hyper&son'] } 56 | ); 57 | t.end(); 58 | }); 59 | 60 | test('nested input objects', t => { 61 | t.deepEqual( 62 | hbs`
    {{person.mustache}}
    `({ person: { mustache: 'brown' } }), 63 | { tag: 'div', props: null, children: ['brown'] } 64 | ); 65 | t.deepEqual( 66 | hbs`
    {{ articles.[2].[#comments] }}
    `({ 67 | articles: [{}, {}, { '#comments': 5 }] 68 | }), 69 | { tag: 'div', props: null, children: [5] } 70 | ); 71 | t.end(); 72 | }); 73 | 74 | test('simple helpers', t => { 75 | t.equal(hbs`{{loud "big"}}`(), 'BIG'); 76 | t.deepEqual( 77 | hbs`
    {{loud mustache}}
    `({ mustache: 'Hyper' }), 78 | { tag: 'div', props: null, children: ['HYPER'] } 79 | ); 80 | t.deepEqual(hbs` {{sum 1 1}}`(), [' ', 2]); 81 | t.end(); 82 | }); 83 | 84 | test('simple block helpers', t => { 85 | t.equal(hbs`{{#noop}}hello{{/noop}}`(), 'hello'); 86 | t.deepEqual( 87 | hbs`{{#bold}}{{body}}{{/bold}}`({ body: 'hyper' }), 88 | { tag: 'b', props: null, children: ['hyper'] } 89 | ); 90 | t.deepEqual( 91 | hbs`
    92 | {{#bold}} 93 | {{~body}} 94 | {{/bold~}} 95 |
    `({ body: 'hyper' }), 96 | h('div', null, h('b', null, 'hyper')) 97 | ); 98 | t.deepEqual( 99 | hbs`
    100 | {{#bold~}} 101 | {{body}} 102 | {{/bold~}} 103 |
    `({ body: 'hyper' }), 104 | h('div', null, h('b', null, h('span', null, 'hyper'))) 105 | ); 106 | t.end(); 107 | }); 108 | 109 | test('block helpers with args', t => { 110 | t.deepEqual( 111 | hbs` 112 | {{#with story~}} 113 |
    {{{intro}}}
    114 |
    {{{body}}}
    115 | {{/with~}} 116 | `({ story: { intro: 'Hello', body: 'World' } }), 117 | [ 118 | { tag: 'div', props: { class: 'intro' }, children: ['Hello'] }, 119 | { tag: 'div', props: { class: 'body' }, children: ['World'] } 120 | ] 121 | ); 122 | t.end(); 123 | }); 124 | 125 | test('if/else/unless without chaining', t => { 126 | t.deepEqual(hbs`{{#if truthy}}Hello{{/if}}`({ truthy: 1 }), 'Hello'); 127 | 128 | t.deepEqual(hbs`{{#if false}}Hello{{else}}Bye{{/if}}`(), 'Bye'); 129 | 130 | t.deepEqual(hbs` 131 | {{#unless license~}} 132 | WARNING: This entry does not have a license! 133 | {{/unless~}} 134 | `({ license: false }), 'WARNING: This entry does not have a license!'); 135 | 136 | t.deepEqual(hbs` 137 | {{#if false}} 138 | Hello 139 | {{else}} 140 | {{#if true}}Bye{{/if}} 141 | {{/if~}} 142 | `(), 'Bye'); 143 | 144 | t.deepEqual(hbs` 145 | {{#if false}} 146 | Hello 147 | {{else}} 148 | {{#if false}} 149 | Hello again 150 | {{else~}} 151 | Bye 152 | {{/if}} 153 | {{/if~}} 154 | `(), 'Bye'); 155 | 156 | t.end(); 157 | }); 158 | 159 | test('if/else with chaining', t => { 160 | // no chained variant 161 | t.deepEqual(hbs` 162 | {{#if false}} 163 | Hello 1 164 | {{else}} 165 | {{#if true~}} 166 | Hello 2 167 | {{else}} 168 | {{#if false}} 169 | Hello 3 170 | {{else}} 171 | Bye 172 | {{/if}} 173 | {{/if}} 174 | {{/if~}} 175 | `(), 'Hello 2'); 176 | 177 | t.deepEqual(hbs` 178 | {{#if false}} 179 | Hello 180 | {{else if true~}} 181 | Bye 182 | {{/if~}} 183 | `({ truthy: 1 }), 'Bye'); 184 | 185 | t.deepEqual(hbs` 186 | {{#if false}} 187 | Hello 188 | {{else if false}} 189 | Hey again 190 | {{else if true}} 191 | {{#if truthy~}} 192 | Bye 193 | {{/if}} 194 | {{/if~}} 195 | 99 196 | `({ truthy: 1 }), ['Bye', '99']); 197 | 198 | t.deepEqual(hbs` 199 | {{#if false}} 200 | Hello 201 | {{else if false}} 202 | Hello 2 203 | {{else if false}} 204 | Hello 3 205 | {{else if false}} 206 | Hello 4 207 | {{else if true~}} 208 | Hello 5 209 | {{else}} 210 | Bye 211 | {{/if~}} 212 | `(), 'Hello 5'); 213 | 214 | t.deepEqual(hbs` 215 | {{#if false}} 216 | Hello 217 | {{else if false}} 218 | Hello 2 219 | {{else if false}} 220 | Hello 3 221 | {{else if false}} 222 | Hello 4 223 | {{else if false}} 224 | Hello 5 225 | {{else~}} 226 | Bye 227 | {{/if~}} 228 | `(), 'Bye'); 229 | 230 | t.deepEqual(hbs` 231 | {{#if false}} 232 | Hello 233 | {{else if true~}} 234 | Hello 2 235 | {{else if true}} 236 | Hello 3 237 | {{else if true}} 238 | Hello 4 239 | {{else if true}} 240 | Hello 5 241 | {{else}} 242 | Bye 243 | {{/if~}} 244 | `(), 'Hello 2'); 245 | 246 | t.deepEqual(hbs` 247 | {{#if false}} 248 | 99 licenses 249 | {{else unless license~}} 250 | WARNING: This entry does not have a license! 251 | {{/unless~}} 252 | `({ license: false }), 'WARNING: This entry does not have a license!'); 253 | 254 | t.end(); 255 | }); 256 | 257 | test('template comments', t => { 258 | t.deepEqual( 259 | hbs`
    {{! This comment will not show up in the output}}
    `(), 260 | { tag: 'div', props: null, children: [''] } 261 | ); 262 | t.deepEqual( 263 | hbs`
    {{!-- This comment may contain mustaches like }} --}}
    `(), 264 | { tag: 'div', props: null, children: [''] } 265 | ); 266 | t.deepEqual( 267 | hbs` 268 |
    269 | {{!-- This comment may contain mustaches like }} --}} 270 | {{~fruit~}} 271 |
    272 | `({ fruit: 'Banana' }), 273 | { tag: 'div', props: null, children: ['', 'Banana'] } 274 | ); 275 | t.end(); 276 | }); 277 | 278 | test('@data variables', t => { 279 | const ctx = {}; 280 | t.equals( 281 | hbs`
    {{@root}}
    `(ctx).children[0], 282 | ctx 283 | ); 284 | t.end(); 285 | }); 286 | 287 | test('hash params', t => { 288 | t.deepEqual(hbs` {{mul a=8 b=20}}`(), [' ', 160]); 289 | t.deepEqual(hbs`{{pass value=33}}`(), 33); 290 | t.deepEqual(hbs`{{pass value=true}}`(), true); 291 | t.deepEqual(hbs`{{pass value=false}}`(), false); 292 | t.deepEqual(hbs`{{pass value=null}}`(), ''); 293 | t.deepEqual(hbs`{{pass value=undefined}}`(), ''); 294 | t.end(); 295 | }); 296 | 297 | test('each over array', t => { 298 | t.deepEqual(hbs` 299 | {{#each @root}} 300 | {{~@root}} 301 | {{~/each~}} 302 | `([ 303 | 'Item 1', 304 | 'Item 2', 305 | 'Item 3' 306 | ]), 307 | [ 308 | 'Item 1', 309 | 'Item 2', 310 | 'Item 3' 311 | ]); 312 | 313 | t.deepEqual(hbs` 314 | {{#each comments~}} 315 |
    316 |

    {{subject}}

    317 | {{~#if @first}},{{/if~}} 318 | {{~#if @last}}last one{{/if~}} 319 | {{{body}}} 320 |
    321 | {{/each~}} 322 | `({ comments: [ 323 | { subject: 'Hello', body: hbs`

    World

    `() }, 324 | { subject: 'Handle', body: hbs`

    Bars

    `() }, 325 | { subject: 'You', body: hbs`

    will pass!

    `() } 326 | ] }), 327 | [ 328 | { tag: 'div', props: { class: 'comment0' }, children: [ 329 | { tag: 'h2', props: null, children: ['Hello'] }, 330 | ',', 331 | '', 332 | { tag: 'p', props: null, children: ['World'] } 333 | ] }, 334 | { tag: 'div', props: { class: 'comment1' }, children: [ 335 | { tag: 'h2', props: null, children: ['Handle'] }, 336 | '', 337 | '', 338 | { tag: 'p', props: null, children: ['Bars'] } 339 | ] }, 340 | { tag: 'div', props: { class: 'comment2' }, children: [ 341 | { tag: 'h2', props: null, children: ['You'] }, 342 | '', 343 | 'last one', 344 | { tag: 'p', props: null, children: ['will pass!'] } 345 | ] } 346 | ] 347 | ); 348 | 349 | t.deepEqual(hbs` 350 | {{#each comments}} 351 |
    352 |

    {{subject}}

    353 | {{{body}}} 354 |
    355 | {{else~}} 356 | no dice 357 | {{/each~}} 358 | `({ comments: [] }), 'no dice' 359 | ); 360 | 361 | t.end(); 362 | }); 363 | 364 | test('each over object', t => { 365 | t.deepEqual(hbs` 366 |
      {{#each list}}
    • {{name}}
    • {{else}}no dice{{/each}}
    367 | `({ list: { 368 | item1: { name: 'John' }, 369 | item2: { name: 'Frank' } 370 | } }), { tag: 'ul', props: null, children: [ [ 371 | { tag: 'li', props: null, children: [ 'John' ] }, 372 | { tag: 'li', props: null, children: [ 'Frank' ] } ] ] } 373 | ); 374 | t.end(); 375 | }); 376 | 377 | test('Deeply Nested Contexts', t => { 378 | t.deepEqual(hbs` 379 | {{~#a}} 380 | {{one}} 381 | {{#b}} 382 | {{one}}{{two}}{{one}} 383 | {{#c}} 384 | {{one}}{{two}}{{three}}{{two}}{{one}} 385 | {{#d}} 386 | {{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}} 387 | {{#e}} 388 | {{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}} 389 | {{/e}} 390 | {{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}} 391 | {{/d}} 392 | {{one}}{{two}}{{three}}{{two}}{{one}} 393 | {{/c}} 394 | {{one}}{{two}}{{one}} 395 | {{/b}} 396 | {{one}} 397 | {{/a~}} 398 | `({ 399 | a: { one: 1 }, 400 | b: { two: 2 }, 401 | c: { three: 3 }, 402 | d: { four: 4 }, 403 | e: { five: 5 } 404 | }), [ 1, 405 | [ 1, 2, 1, 406 | [ 1, 2, 3, 2, 1, 407 | [ 1, 2, 3, 4, 3, 2, 1, 408 | [ 1, 2, 3, 4, 5, 4, 3, 2, 1 ], 409 | 1, 2, 3, 4, 3, 2, 1 410 | ], 1, 2, 3, 2, 1 411 | ], 1, 2, 1 412 | ], 1 413 | ]); 414 | t.end(); 415 | }); 416 | 417 | test('Falsey sections should have their contents rendered.', (t) => { 418 | t.deepEqual( 419 | hbs`{{^boolean}}This should be rendered.{{/boolean}}`({ 420 | boolean: false 421 | }), 422 | 'This should be rendered.' 423 | ); 424 | t.end(); 425 | }); 426 | -------------------------------------------------------------------------------- /test/mustache.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import _ from 'underscore'; 3 | 4 | import tape from 'tape'; 5 | import { compile } from '../src/index.js'; 6 | 7 | const stitch = (strings, ...values) => 8 | strings.map((str, i) => `${str}${ 9 | (Array.isArray(values[i]) ? values[i].join('') : values[i]) || '' 10 | }`).join(''); 11 | 12 | const hbs = compile.bind(stitch); 13 | 14 | var specDir = __dirname + '/mustache/specs/'; 15 | var specs = _.filter(fs.readdirSync(specDir), function(name) { 16 | return /.*\.json$/.test(name); 17 | }); 18 | 19 | _.each(specs, function(name) { 20 | var spec = require(specDir + name); 21 | _.each(spec.tests, function(test) { 22 | // Our lambda implementation knowingly deviates from the optional Mustace lambda spec 23 | // We also do not support alternative delimeters 24 | if ( 25 | // Hyperstache has helpers instead 26 | name === '~lambdas.json' || 27 | // Hyperstache doesn't support partials 28 | name === 'partials.json' || 29 | // name === 'inverted.json' || 30 | // name === 'interpolation.json' || 31 | // name === 'comments.json' || 32 | // name === 'sections.json' || 33 | 34 | /\{\{=/.test(test.template) || 35 | _.any(test.partials, function(partial) { 36 | return /\{\{=/.test(partial); 37 | }) 38 | ) { 39 | tape.skip(name + ' - ' + test.name); 40 | return; 41 | } 42 | 43 | // if (test.name !== 'Deeply Nested Contexts') return; 44 | 45 | var data = _.clone(test.data); 46 | if (data.lambda) { 47 | // Blergh 48 | /* eslint-disable no-eval */ 49 | data.lambda = eval('(' + data.lambda.js + ')'); 50 | /* eslint-enable no-eval */ 51 | } 52 | 53 | tape(name + ' - ' + test.name, function(t) { 54 | t.equal( 55 | hbs.call(0, [test.template])(data), 56 | test.expected, 57 | test.desc + ' "' + test.template + '"' 58 | ); 59 | t.end(); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | // import './htm.js'; 2 | import './hyperstache.js'; 3 | import '../packages/babel-plugin-hyperstache/test/babel.js'; 4 | import './mustache.js'; 5 | --------------------------------------------------------------------------------