User: lukeed / Web Site: https://github.com/lukeed
User: billy / Web Site: https://github.com/billy
`);
134 | },
135 | });
136 |
--------------------------------------------------------------------------------
/bench/readme.md:
--------------------------------------------------------------------------------
1 | # Benchmarks
2 |
3 | > All benchmarks run using Node v14.15.3
4 |
5 | ## Load Time
6 |
7 | Measures how long it takes to `require` a library.
8 |
9 | ```
10 | pug: 127.647ms
11 | ejs: 1.444ms
12 | handlebars: 23.627ms
13 | yeahjs: 0.979ms
14 | dot: 1.363ms
15 | art-template: 8.847ms
16 | tempura: 0.593ms
17 | ```
18 |
19 | ## Compile Time
20 |
21 | How quickly a template is parsed and generated into a `function` equivalent.
22 |
23 | Results display the average of 5 runs, in milliseconds.
24 |
25 | > **Important:** All candidates use a different template syntax.
26 |
27 | ```
28 | Benchmark (Compile)
29 | ~> pug 7.20474ms
30 | ~> handlebars 0.02952ms
31 | ~> ejs 0.28859ms
32 | ~> yeahjs 0.12206ms
33 | ~> dot 0.27055ms
34 | ~> art-template 0.76982ms
35 | ~> tempura 0.19813ms
36 | ```
37 |
38 | ## Render - Raw Values
39 |
40 | Measures how quickly the generated functions render an output string.
41 |
42 | > **Note:** All candidates **do not escape** template values - hence "raw" values.
43 |
44 | All candidates produce the **same** output value, which is used as a validation step.
45 |
46 | ```
47 | Validation (Render)
48 | ✔ pug
49 | ✔ handlebars
50 | ✔ ejs
51 | ✔ yeahjs
52 | ✔ dot
53 | ✔ art-template
54 | ✔ tempura
55 |
56 | Benchmark (Render)
57 | pug x 34,847 ops/sec ±2.79% (93 runs sampled)
58 | handlebars x 6,700 ops/sec ±1.41% (92 runs sampled)
59 | ejs x 802 ops/sec ±0.54% (94 runs sampled)
60 | yeahjs x 31,508 ops/sec ±0.30% (97 runs sampled)
61 | dot x 40,704 ops/sec ±3.08% (93 runs sampled)
62 | art-template x 39,839 ops/sec ±0.86% (90 runs sampled)
63 | tempura x 44,656 ops/sec ±0.42% (92 runs sampled)
64 | ```
65 |
66 | ## Render – Escaped Values
67 |
68 | Measures how quickly the generated functions render an output string. All template **values are escaped**, unlike the [Raw Values benchmark]($render-raw-values) above.
69 |
70 | > **Important:** All candidates use a different `escape()` sequence.
71 |
72 | ```
73 | Benchmark (Render)
74 | pug x 2,800 ops/sec ±0.31% (95 runs sampled)
75 | handlebars x 733 ops/sec ±0.34% (94 runs sampled)
76 | ejs x 376 ops/sec ±0.17% (91 runs sampled)
77 | yeahjs x 873 ops/sec ±0.30% (95 runs sampled)
78 | dot x 707 ops/sec ±0.15% (96 runs sampled)
79 | art-template x 2,707 ops/sec ±0.12% (96 runs sampled)
80 | tempura x 2,922 ops/sec ±0.31% (96 runs sampled)
81 | ```
82 |
--------------------------------------------------------------------------------
/bench/util.js:
--------------------------------------------------------------------------------
1 | const { Suite } = require('benchmark');
2 |
3 | exports.runner = function (name, contenders, options={}) {
4 | if (options.assert) {
5 | console.log('\nValidation (%s)', name);
6 | Object.keys(contenders).forEach(name => {
7 | try {
8 | options.assert(contenders[name]);
9 | console.log(' ✔', name);
10 | } catch (err) {
11 | console.log(' ✘', name, `(FAILED @ "${err.message}")`);
12 | }
13 | });
14 | }
15 |
16 | let value;
17 | if (options.setup) {
18 | value = options.setup();
19 | }
20 |
21 | console.log('\nBenchmark (%s)', name);
22 | const bench = new Suite().on('cycle', e => {
23 | console.log(' ' + e.target);
24 | });
25 |
26 | Object.keys(contenders).forEach(name => {
27 | bench.add(name + ' '.repeat(18 - name.length), () => {
28 | contenders[name](value);
29 | })
30 | });
31 |
32 | bench.run();
33 | }
34 |
--------------------------------------------------------------------------------
/bin/index.js:
--------------------------------------------------------------------------------
1 | const { join } = require('path');
2 | const fs = require('fs/promises');
3 |
4 | (async function () {
5 | const src = join(__dirname, '../src');
6 |
7 | let [$utils, $index] = await Promise.all([
8 | fs.readFile(join(src, '$utils.js'), 'utf8'),
9 | fs.readFile(join(src, '$index.js'), 'utf8'),
10 | ]);
11 |
12 | $utils = $utils.replace('export function', 'function').replace(/\$/g, '\0');
13 | $index = $index.replace(/^import.*(?:\n)/m, $utils).replace(/\0/g, '$$');
14 |
15 | console.log('~> creating "src/index.js" placeholder');
16 | await fs.writeFile(join(src, 'index.js'), $index);
17 | })().catch(err => {
18 | console.error('Oops', err.stack);
19 | process.exitCode = 1;
20 | });
21 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # API
2 |
3 | > **Note:** You may also refer to the [TypeScript definitions](/src/index.d.ts).
4 |
5 | ## Methods
6 |
7 |
8 | ### tempura.esc(value)
9 | Returns: `string`
10 |
11 | #### value
12 | Type: `string` or `unknown`
13 |
14 | The value to be HTML-escaped. The following special characters are escaped: `"`, `&`, and `<`.
15 |
16 | > **Note:** Any non-`string` values are coerced to a strings!
17 | > * `null` and `undefined` become `""`
18 | > * `{ foo: 123 }` becomes `"[object Object]"`
19 | > * `[1, 2, 3]` becomes `"1,2,3"`
20 | > * `123` becomes `"123"`
21 |
22 |
23 | ### tempura.compile(input, options?)
24 | Returns: `Compiler`
25 |
26 | Convert a template into a `function` that can be executed.
27 |
28 | When called with the appropriate, template-specific arguments, the `Compiler` function will return a `string` or a `Promise`, resulting in the desired template output.
29 |
30 | ```js
31 | let render = tempura.compile(`
32 | {{#expect age}}
33 | {{#if age < 100}}
34 | You're not 100+ yet
35 | {{#else}}
36 | What's the secret?
37 | {{/if}}
38 | `);
39 |
40 | console.log(typeof render);
41 | //=> "function"
42 |
43 | render({ age: 31 });
44 | //=> "You're not 100+ yet"
45 |
46 | render({ age: 102 });
47 | //=> "What's the secret?"
48 | ```
49 |
50 |
51 | #### input
52 | Type: `string`
53 |
54 | The template to be converted.
55 |
56 | #### options
57 |
58 | All common [Options](#options-2) are supported, in addition to:
59 |
60 | #### options.escape
61 | Type: `(value: any) => string`
62 | Default: `typeof tempura.esc`
63 |
64 | The escape function to use for `{{{ raw }}}` values.
65 |
66 | When unspecified, [`tempura.esc`](#tempuraescvalue) is used by default;
67 |
68 |
69 | ### tempura.transform(input, options?)
70 | Returns: `string`
71 |
72 | Transforms the `input` source template into a string-representation of the equivalent JavaScript function.
73 |
74 | Unlike [`tempura.compile`](#tempuracompileinput-options), this generates a `string` instead of a `function`, which makes this method suitable for bundler transformations and/or replacing code at build-time.
75 |
76 | ```js
77 | let template = `
78 | {{#expect age}}
79 | {{#if age < 100}}
80 | You're not 100+ yet
81 | {{#else}}
82 | What's the secret?
83 | {{/if}}
84 | `;
85 |
86 | // produces ESM format by default
87 | let esm = tempura.transform(template);
88 | console.log(typeof esm); //=> "string"
89 | console.log(esm);
90 | //=> 'import{esc as $$1}from"tempura";export default function($$3,$$2){var{age}=$$3,x=``;x+=``;if(age < 100){x+=`You\'re not 100+ yet"`;}else{x+=`What\'s the secret?`;}return x}'
91 | ```
92 |
93 | #### input
94 | Type: `string`
95 |
96 | The template to be converted.
97 |
98 | #### options
99 |
100 | All common [Options](#options-2) are supported, in addition to:
101 |
102 | #### options.format
103 | Type: `"esm"` or `"cjs"`
104 | Default: `"esm"`
105 |
106 | Modify the generated output to be compliant with the CommonJS or ES Module format.
107 |
108 | > **Note:** Most bundlers and/or upstream consumers can understand (and prefer) the ESM format.
109 |
110 |
111 | ## Options
112 |
113 | ### options.async
114 | Type: `boolean`
115 | Default: `false`
116 |
117 | When true, `tempura.compile` produces an `AsyncFunction` and `tempura.compile` generates an `async` function. Also will include the `await` keyword when a [Custom Block](/docs/blocks.md) is invoked.
118 |
119 |
120 | ### options.props
121 | Type: `string[]`
122 |
123 | The _names_ of variables that will be provided to a view.
124 |
125 | Declaring `props` names means that they don't have to appear within any `{{#expect ...}}` declarations – and vice versa. In other words, `options.props` is a programmatic way to define (or skip) `{{#expect}}` blocks.
126 |
127 | It is recommended that you include global and/or shared variables within `options.props`, which saves you the trouble of writing the same `{{#expect foo, bar}}` statements over and over. This way, each template can `{{#expect}}` any variables that are unique to it.
128 |
129 | > **Note:** Variable names are deduped. For example, defining `{{#expect foo}}` _and_ `options.props: ['foo']` will not have any adverse/duplicative effects.
130 |
131 |
132 | ### options.loose
133 | Type: `boolean`
134 | Default: `false`
135 |
136 | By default, any template variables must be known ahead of time – either through [`options.props`](/docs/api.md#optionsprops) or through [`#expect`](/docs/syntax.md#expect) declarations. However, when enabled, `options.loose` relaxes this constraint.
137 |
138 | > **Note:** Enabling `options.loose` makes for a more Handlebars-like experience.
139 |
140 | With this option activated, removing the `#expect` declaration from the example below will produce the same output:
141 |
142 | ```diff
143 | --{{#expect name}}
144 |
Hello, {{ name }}!
145 | ```
146 |
147 |
148 | ### options.blocks
149 | Type: `Record`
150 |
151 | Any [custom directives](/docs/blocks.md) that should be made available within the template.
152 |
153 | > **Important:** An error will be thrown while parsing if a custom directive was found but not defined.
154 |
155 | All key names are converted into the directive names. Keys must start with a `A-Z`, `a-z`, or `_` and may include any number of `A-Z`, `a-z`, `_`, or `0-9` characters.
156 |
157 | For example, in order to define and use the `{{#foo}}` and `{{#hello123}}` directives within a template, an `options.blocks` object with `foo` and `hello123` keys must be defined:
158 |
159 | ```js
160 | /**
161 | * @type {tempura.Options}
162 | */
163 | let options = {
164 | async: true,
165 | blocks: {
166 | foo(args) {
167 | return `foo got ~${args.value}~`;
168 | },
169 | async hello123(args) {
170 | return `
"
183 | ```
184 |
--------------------------------------------------------------------------------
/docs/blocks.md:
--------------------------------------------------------------------------------
1 | # Custom Blocks
2 |
3 | You can include and use your own template helpers!
4 |
5 | In order to do so, custom blocks must be defined via [`options.blocks`](/docs/api.md#optionsblocks) during calls to `tempura.compile` and/or `tempura.transform`.
6 |
7 | > **Important:** A parsing error will be thrown when a template references a custom block (eg, `{{#foo}}`) but _has not defined_ an `options.blocks.foo` function.
8 |
9 |
10 | ## Definition
11 |
12 | All custom blocks are defined as stateless functions. They will receive an `args` object (see [Arguments](#arguments)) and are expected to return a string.
13 |
14 | For example, let's have a custom `script` block that's responsible for producing a valid `` HTML tag. It expects to receive a `src` and may optionally receive a `type` and `defer` value:
15 |
16 | ```js
17 | let options = {
18 | blocks: {
19 | script(args) {
20 | let { src, defer, type } = args;
21 |
22 | let output = `';
26 |
27 | return output;
28 | }
29 | }
30 | });
31 | ```
32 |
33 | It can then be used anywhere within a template:
34 |
35 | ```hbs
36 | {{! custom "script" block }}
37 | {{#script src="main.js" defer=true }}
38 | ```
39 |
40 | You may define `async` custom blocks, too. However, you must remember to enable [`options.async`](/docs/api.md#optionsasync)!
41 |
42 | ```js
43 | import { send } from 'httpie';
44 |
45 | let options = {
46 | blocks: {
47 | async nasdaq(args) {
48 | let { symbol } = args;
49 | let output = `
${symbol}
`;
50 |
51 | // DEMO: Send a GET request to some Stock Ticker API
52 | let res = await send('GET', `https://.../symbols/${symbol}`);
53 | output += `
${res.data.price}
`;
54 |
55 | return output;
56 | }
57 | }
58 | });
59 | ```
60 |
61 | It can then be used anywhere within a template:
62 |
63 | ```hbs
64 |
69 | ```
70 |
71 |
72 | ## Arguments
73 |
74 | Your template code may pass arguments to your custom blocks.
75 |
76 | Arguments are whitespace delimited and every key must be followed by an `=` and some value. For example:
77 |
78 | ```hbs
79 | {{#person name="Luke" age=31 }}
80 | ```
81 |
82 | Tempura will parse your blocks' arguments into an object before calling your block definition:
83 |
84 | ```js
85 | let render = tempura.compile('...', {
86 | blocks: {
87 | person(args) {
88 | let { name, age } = args;
89 | return `${name} is ${age} years old.`;
90 | }
91 | }
92 | });
93 | ```
94 |
95 | Argument values may be any valid JavaScript data type and may also reference other variables:
96 |
97 | ```hbs
98 | {{#expect list}}
99 |
100 | {{#var count = list.length}}
101 |
102 | {{#if count > 0}}
103 | {{#table
104 | items=list total=count
105 | max=25 sticky=true }}
106 | {{/if}}
107 | ```
108 |
109 | > **Note:** You can write Arrays and objects inline, but it can get messy!
110 |
111 |
112 | ## Invoking Blocks from Blocks
113 |
114 | Sometimes your custom block(s) may need to reference other custom blocks. For example, let's assume a `#foo` block wants to print a `#bar` block under some condition. In other for this to work, both the `foo` and `bar` blocks need to be defined.
115 |
116 | The naiive approach is to hoist helper function(s) and reference them in both block definintions:
117 |
118 | ```js
119 | function helper(value) {
120 | return `${value}`;
121 | }
122 |
123 | let blocks = {
124 | foo(args) {
125 | let output = '';
126 | if (args.other) output += helper(args.other);
127 | return output + '';
128 | },
129 | bar(args) {
130 | return helper(args.name);
131 | }
132 | };
133 |
134 | let render = tempura.compile('{{#foo other=123}} – {{#bar name="Alice"}}', { blocks });
135 | render(); //=> "123 – Alice"
136 | ```
137 |
138 | While this _does_ work and is a totally valid approach, tempura allows you to skip the helper functions & reference the other custom blocks directly. The above example can be rewritten as:
139 |
140 | ```js
141 | let options = {
142 | blocks: {
143 | // NOTE: `blocks` parameter === `options.blocks` object
144 | foo(args, blocks) {
145 | let output = '';
146 | if (args.other) {
147 | // Call the `bar` method directly
148 | output += blocks.bar({ name: args.other });
149 | }
150 | return output + '';
151 | },
152 | // NOTE: `blocks` parameter === `options.blocks` object
153 | bar(args, blocks) {
154 | return `${args.name}`;
155 | }
156 | }
157 | };
158 |
159 | let render = tempura.compile('{{#foo other=123}} – {{#bar name="Alice"}}', options);
160 | render(); //=> "123 – Alice"
161 | ```
162 |
163 | > **Important:** Notice that when the `foo` definition invoked `blocks.bar`, it had to construct a `{ name }` object to accommodate the `bar` definition's expected arguments.
164 |
165 |
166 | ## Recursive Blocks
167 |
168 | As shown in the previous section, block definitions can directly reference one another. However, blocks can also invoke _themselves_ recursively.
169 |
170 | > **Important** You must include your own exit condition(s) in order to avoid an infinite loop / `Maximum call stack` error!
171 |
172 | In this example, a `#loop` directive should print its current value until `0`:
173 |
174 | ```js
175 | let blocks = {
176 | loop(args, blocks) {
177 | let value = args.value;
178 | let output = String(value);
179 | if (value--) {
180 | output += " ~> " + blocks.loop({ value }, blocks);
181 | }
182 | return output;
183 | }
184 | }
185 |
186 | let render = tempura.compile('{{#loop value=3 }}', { blocks });
187 | render(); //=> "3 ~> 2 ~> 1 ~> 0"
188 | ```
189 |
190 |
191 | ## Compiler Blocks
192 |
193 | If you looked through the [TypeScript definitions](/src/index.d.ts), you may have noticed that custom blocks and the output from `tempura.compile` share the same `Compiler` interface. In other words, they both produce or expect a function with the `(data, blocks?) => Promise | string` signaure.
194 |
195 | This means that you can actually _use and compose_ `tempura.compile` functions within your `options.blocks` definitions!
196 |
197 | Let's rebuild the same example from [Invoking Blocks from Blocks](#invoking-blocks-from-blocks), but using `#foo` and `#bar` statements only:
198 |
199 | ```js
200 | // Declare `blocks` variable upfront, for reference.
201 | // This is needed so that `foo` and can see `bar` block exists.
202 | let blocks = {
203 | bar: tempura.compile(`
204 | {{#expect name}}
205 | {{ name }}
206 | `),
207 | };
208 |
209 | blocks.foo = tempura.compile(`
210 | {{#expect other}}
211 |
212 | {{#if other}}
213 | {{#bar name=other }}
214 | {{/if}}
215 |
216 | `, { blocks });
217 |
218 | let render = tempura.compile('{{#foo other=123}} – {{#bar name="Alice"}}', { blocks });
219 | render(); //=> "123 – Alice"
220 | ```
221 |
222 |
223 | ## Recursive Compiler Blocks
224 |
225 | As with normal [recursive blocks](#recursive-blocks), your custom block definition may still reference itself when using `tempura.compile` to produce a `Compiler`.
226 |
227 | The "trick" is to define an `options.blocks` object early so that the same object can be passed into the definition's `tempura.compile` call. Additionally, any directives must already exist as keys in this empty object.
228 |
229 | For example, the `loop` directive (from above) must be defined like so:
230 |
231 | ```js
232 | let blocks = {
233 | // PLACEHOLDER
234 | loop: null,
235 | };
236 |
237 | // Define `loop`, with `blocks` reference
238 | blocks.loop = tempura.compile(`
239 | {{#expect value }}
240 |
241 | {{ value }}
242 | {{#if value-- }}
243 | ~> {{#loop value=value }}
244 | {{/if}}
245 | `, { blocks });
246 |
247 | let render = tempura.compile('{{#loop value=3 }}', { blocks });
248 | render(); //=> "3 ~> 2 ~> 1 ~> 0"
249 | ```
250 |
251 | This may seem a little odd at first, but this needs to happen because when the `blocks.loop` definition is parsed, it needs to see a `loop` key inside the `options.blocks` object. During generation, the block's _key_ only needs to exist. The functional definition only needs to be set once the top-level `render` is invoked.
252 |
--------------------------------------------------------------------------------
/docs/syntax.md:
--------------------------------------------------------------------------------
1 | # Syntax
2 |
3 | > A quick cheatsheet of the tempura template syntax.
4 |
5 | The tempura template syntax is _very similar_ to the [Handlebars](https://handlebarsjs.com/guide/) or [Mustache](https://mustache.github.io/#demo) template syntax. In fact, they're more similar than they are different!
6 |
7 | > **General Notice**
8 | > Throughout this document, you'll notice that most examples include HTML tags or _produce_ HTML output. This is done only because HTML is a common target. Tempura templates ***do not*** need to use or produce HTML content – it only cares about its own template syntax!
9 |
10 | ## Overview
11 |
12 | Templates are (hopefully!) an easier way to write and maintain view logic. Nearly all template engines, including `tempura`, parse your template files and generate a function (per template) that includes all your view logic and desired output.
13 |
14 | Each of these functions is valid JavaScript and is waiting to accept an `input` object. Together, the input data and the render function produce the expected output.
15 |
16 | ## Expressions
17 |
18 | Values can be printed by wrapping content with the `{{` and `}}` characters. These can appear anywhere within your template, and typically reference a variable name, although they could wrap _any value_.
19 |
20 | When the template is evaluated, these expressions are replaced. For example:
21 |
22 | ```hbs
23 | {{#expect name}}
24 |
37 | ```
38 |
39 | Expression values can reference any defined variables. In this example, `name` was a simple string, but it could have been an object, an Array, or anything else!
40 |
41 | For example, this template:
42 |
43 | ```hbs
44 |
59 | ```
60 |
61 | ### Escaped Values
62 |
63 | By default, all expressions are HTML-escaped. For example, if the value contains the `"`, `&`, or `<` characters, they'd be encoded into the `"`, `&`, and `<` sequences, respectively.
64 |
65 | > **Note:** The HTML-escape function can be customized via the [`escape`](/docs/api.md#optionsescape) option.
66 |
67 | It's generally recommended to use HTML-escaping unless you're certain that the expression's value does not contain any HTML characters; eg, it's a number or you just constructed it via [`#var`](#var).
68 |
69 | ### Raw Values
70 |
71 | To disable HTML-escaping for a value, you must use the triple-curly syntax; for example `{{{ value }}}`.
72 |
73 | When the triple-curly is used, the _raw value_ is printed as is.
74 |
75 | ```hbs
76 | {{! input }}
77 | escaped: {{ 'a & b " c < d' }}
78 | raw: {{{ 'a & b " c < d' }}}
79 | ```
80 |
81 | ```html
82 |
83 | escaped: a & b " c < d
84 | raw: a & b " c < d
85 | ```
86 |
87 |
88 | ## Comments
89 |
90 | It's generally recommended to include comments within your templates, especially as their complexity grows!
91 |
92 | Template comments will never appear in the rendered output. However, HTML comments are kept.
93 |
94 | ```hbs
95 |
96 | {{! template comments are removed }}
97 | {{!
98 | template comments
99 | can also be multi-line
100 | ...and are still removed
101 | !}}
102 |
hello world
103 | ```
104 |
105 | ## Helpers
106 |
107 | The tempura syntax comes with few (but powerful) template helpers!
108 |
109 | Unlike value expressions, template helpers – or "blocks" – always use double-curly tags. This is because they're _not_ values, so there's no HTML-escape vs raw dilemma. The shorter, double-curly tag is used as a convenience.
110 |
111 | > **Note:** You may define your own helpers! See [Custom Blocks](/docs/blocks.md) for more detail.
112 |
113 | ### `#expect`
114 |
115 | A template needs to declare what variables it _expects_ to receive. In several UI frameworks, this is generally referred to as defining your "props". At the parser level, this prepares the `Compiler` so that it won't throw any `Uncaught ReferenceError`s during execution.
116 |
117 | > **Note:** You should hoist common variables via [`options.props`](/docs/api.md#optionsprops) to avoid repetitive `#expect` declarations.
118 |
119 | You may declare multiple variables within the same `#expect` block. You may also have multiple `#expect` blocks in the same file:
120 |
121 | ```hbs
122 | {{#expect name}}
123 | {{#expect foo, bar}}
124 | {{#expect
125 | hello,
126 | title, todos }}
127 | ```
128 |
129 | In this snippet, the rest of the template file may safely use the `name`, `foo`, `bar`, `hello`, `title`, and `todos` variables. If any of these variables had been referenced _without_ being declared (either thru `#expect` or `options.props`), then a `ReferenceError` error would have been thrown at runtime.
130 |
131 | This is because tempura prefers to be strict by default. If, however, you would prefer to relax this condition, you may enable the [`loose`](/docs/api.md#optionsloose) option during any `tempura.compile` or `tempura.transform` calls.
132 |
133 |
134 | ### `#var`
135 |
136 | You may define new variables within your template and reference them throughout the file. Normal JavaScript scoping rules apply.
137 |
138 | These inline variables can evaluate any JavaScript and can reference other template variables.
139 |
140 | Only one variable can be defined per `#var` block. You may have any number of `#var` blocks in your template:
141 |
142 | ```hbs
143 | {{#expect todos}}
144 | {{#var numTotal = todos.length}}
145 | {{#var numDone = todos.filter(x => x.done).length}}
146 | {{#var numActive = numTotal = numDone}}
147 |
148 |
You have {{{ numActive }}} item(s) remaining
149 |
You have {{{ numTotal }}} item(s) in total
150 | ```
151 |
152 | ### `#if`, `#elif`, `#else`
153 |
154 | Your control flow helpers!
155 |
156 | Just like JavaScript, use of `#elif` (short for `else if`) and `#else` are optional, but must always be associated with and follow an `#if` opener. Parenthesis around the `#if` and `#elif` conditions are optional:
157 |
158 | All `#if` blocks must be terminated by an `/if` block.
159 |
160 | ```hbs
161 | {{#expect age, isActive}}
162 |
163 | {{#if isActive}}
164 |
183 | {{/if}}
184 | ```
185 |
186 | ### `#each`
187 |
188 | The `#each` block loops over an Array, passing through each value and index to the template expression(s) within the block.
189 |
190 | All `#each` blocks must be terminated by an `/each` block.
191 |
192 | ```hbs
193 | {{#expect items}}
194 |
195 | {{! only use the value }}
196 | {{#each items as item}}
197 |