├── .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 | [](https://travis-ci.com/luwes/hyperstache)
4 | 
5 | [](https://codecov.io/gh/luwes/hyperstache)
6 | [](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 |
61 | {{#each kids}}
62 | - {{name}} is {{age}}
63 | {{/kids}}
64 |
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 |
125 | {{#each kids}}
126 | - {{name}} is {{age}}
127 | {{/kids}}
128 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
{{subject}}
317 | {{~#if @first}},{{/if~}} 318 | {{~#if @last}}last one{{/if~}} 319 | {{{body}}} 320 |