import { suite } from 'uvu'; import * as assert from 'uvu/assert'; import { gen } from '../src/$utils'; const API = suite('API'); API('should be a function', () => { assert.type(gen, 'function'); }); API('should return a string', () => { assert.type(gen(''), 'string'); }); API('should throw if no input', () => { assert.throws(gen); }); API.run(); // --- const values = suite('{{ values }}'); values('{{ value }}', () => { assert.is( gen('{{ value }}'), 'var x=`${$$1(value)}`;return x' ); assert.is( gen('{{value }}'), 'var x=`${$$1(value)}`;return x' ); assert.is( gen('{{ value}}'), 'var x=`${$$1(value)}`;return x' ); assert.is( gen('{{value}}'), 'var x=`${$$1(value)}`;return x' ); }); values('{{ foo.bar }}', () => { assert.is( gen('{{ foo.bar }}'), 'var x=`${$$1(foo.bar)}`;return x' ); }); values('{{ foo["bar"] }}', () => { assert.is( gen('{{ foo["bar"] }}'), 'var x=`${$$1(foo["bar"])}`;return x' ); }); values('<h1>{{ foo.bar }} ...</h1>', () => { assert.is( gen('<h1>{{ foo.bar }} <span>howdy</span></h1>'), 'var x=`<h1>${$$1(foo.bar)} <span>howdy</span></h1>`;return x' ); }); values.run(); // --- const raws = suite('{{{ raw }}}'); raws('{{ value }}', () => { assert.is( gen('{{{ value }}}'), 'var x=`${value}`;return x' ); assert.is( gen('{{{value }}}'), 'var x=`${value}`;return x' ); assert.is( gen('{{{ value}}}'), 'var x=`${value}`;return x' ); assert.is( gen('{{{value}}}'), 'var x=`${value}`;return x' ); }); raws('{{{ foo.bar }}}', () => { assert.is( gen('{{{ foo.bar }}}'), 'var x=`${foo.bar}`;return x' ); }); raws('{{{ foo["bar"] }}}', () => { assert.is( gen('{{{ foo["bar"] }}}'), 'var x=`${foo["bar"]}`;return x' ); }); raws('<h1>{{{ foo.bar }}} ...</h1>', () => { assert.is( gen('<h1>{{{ foo.bar }}} <span>howdy</span></h1>'), 'var x=`<h1>${foo.bar} <span>howdy</span></h1>`;return x' ); }); raws.run(); // --- const expect = suite('#expect'); expect('{{#expect foo,bar}}', () => { assert.is( gen('{{#expect foo,bar}}'), 'var{foo,bar}=$$3,x="";return x' ); assert.is( gen('{{#expect foo , bar}}'), 'var{foo,bar}=$$3,x="";return x' ); assert.is( gen('{{#expect\n\tfoo ,bar}}'), 'var{foo,bar}=$$3,x="";return x' ); }); expect('{{#expect foobar}}', () => { assert.is( gen('{{#expect foobar}}'), 'var{foobar}=$$3,x="";return x' ); assert.is( gen('{{#expect \n foobar\n}}'), 'var{foobar}=$$3,x="";return x' ); }); expect.run(); // --- const control = suite('#if'); control('{{#if isActive}}...{{/if}}', () => { assert.is( gen('{{#if isActive}}<p>yes</p>{{/if}}'), 'var x="";if(isActive){x+=`<p>yes</p>`;}return x' ); }); control('{{#if foo.bar}}...{{#else}}...{{/if}}', () => { assert.is( gen('{{#if foo.bar}}<p>yes</p>{{#else}}<p>no {{ way }}</p>{{/if}}'), 'var x="";if(foo.bar){x+=`<p>yes</p>`;}else{x+=`<p>no ${$$1(way)}</p>`;}return x' ); }); control('{{#if foo == 0}}...{{#else}}...{{/if}}', () => { assert.is( gen('{{#if foo == 0}}<p>zero</p>{{#else}}<p>not zero</p>{{/if}}'), 'var x="";if(foo == 0){x+=`<p>zero</p>`;}else{x+=`<p>not zero</p>`;}return x' ); }); control('{{#if isActive}}...{{#elif isMuted}}...{{#else}}...{{/if}}', () => { assert.is( gen('{{#if isActive}}<p>active</p>{{#elif isMuted}}<p>muted</p>{{#else}}<p>inactive</p>{{/if}}'), 'var x="";if(isActive){x+=`<p>active</p>`;}else if(isMuted){x+=`<p>muted</p>`;}else{x+=`<p>inactive</p>`;}return x' ); }); control('{{#if isActive}}...{{#elif isMuted}}...{{/if}}', () => { assert.is( gen('{{#if isActive }}<p>active</p>{{#elif isMuted}}<p>muted</p>{{/if}}'), 'var x="";if(isActive){x+=`<p>active</p>`;}else if(isMuted){x+=`<p>muted</p>`;}return x' ); }); control.run(); // --- const vars = suite('#vars'); vars('{{#var foo = "world" }}', () => { assert.is( gen('{{#var foo = "world"}}<p>hello {{ foo }}</p>'), 'var x="";var foo="world";x+=`<p>hello ${$$1(foo)}</p>`;return x' ); assert.is( gen('{{#var foo = "world";}}<p>hello {{ foo }}</p>'), 'var x="";var foo="world";x+=`<p>hello ${$$1(foo)}</p>`;return x' ); }); vars('{{#var foo = 1+2 }}', () => { assert.is( gen('{{#var foo = 1+2}}<p>hello {{ foo }}</p>'), 'var x="";var foo=1+2;x+=`<p>hello ${$$1(foo)}</p>`;return x' ); assert.is( gen('{{#var foo = 1+2;}}<p>hello {{ foo }}</p>'), 'var x="";var foo=1+2;x+=`<p>hello ${$$1(foo)}</p>`;return x' ); }); vars('{{#var foo = {...} }}', () => { assert.is( gen('{{#var name = { first: "luke" } }}<p>hello {{ name.first }}</p>'), 'var x="";var name={ first: "luke" };x+=`<p>hello ${$$1(name.first)}</p>`;return x' ); assert.is( gen('{{#var name = { first:"luke" }; }}<p>hello {{ name.first }}</p>'), 'var x="";var name={ first:"luke" };x+=`<p>hello ${$$1(name.first)}</p>`;return x' ); }); vars('{{#var foo = [...] }}', () => { assert.is( gen('{{#var name = ["luke"] }}<p>hello {{ name[0] }}</p>'), 'var x="";var name=["luke"];x+=`<p>hello ${$$1(name[0])}</p>`;return x' ); assert.is( gen('{{#var name = ["luke"]; }}<p>hello {{ name[0] }}</p>'), 'var x="";var name=["luke"];x+=`<p>hello ${$$1(name[0])}</p>`;return x' ); assert.is( gen('{{#var name = ["luke"]; }}<p>hello {{{ name[0] }}}</p>'), 'var x="";var name=["luke"];x+=`<p>hello ${name[0]}</p>`;return x' ); }); vars('{{#var foo = truthy(bar) }}', () => { assert.is( gen('{{#var foo = truthy(bar)}}{{#if foo != 0}}<p>yes</p>{{/if}}'), 'var x="";var foo=truthy(bar);if(foo != 0){x+=`<p>yes</p>`;}return x' ); assert.is( gen('{{#var foo = truthy(bar); }}{{#if foo != 0}}<p>yes</p>{{ /if }}'), 'var x="";var foo=truthy(bar);if(foo != 0){x+=`<p>yes</p>`;}return x' ); }); vars.run(); // --- const comments = suite('!comments'); comments('{{! hello }}', () => { assert.is( gen('{{! hello }}'), 'var x="";return x' ); assert.is( gen('{{!hello}}'), 'var x="";return x' ); }); comments('{{! "hello world" }}', () => { assert.is( gen('{{! "hello world" }}'), 'var x="";return x' ); assert.is( gen('{{!"hello world"}}'), 'var x="";return x' ); }); comments.run(); // --- const each = suite('#each'); each('{{#each items}}...{{/each}}', () => { assert.is( gen('{{#each items}}<p>hello</p>{{/each}}'), 'var x="";for(var i=0,$$a=items;i<$$a.length;i++){x+=`<p>hello</p>`;}return x' ); }); each('{{#each items as item}}...{{/each}}', () => { assert.is( gen('{{#each items as item}}<p>hello {{item.name}}</p>{{/each}}'), 'var x="";for(var i=0,item,$$a=items;i<$$a.length;i++){item=$$a[i];x+=`<p>hello ${$$1(item.name)}</p>`;}return x' ); assert.is( gen('{{#each items as (item) }}<p>hello {{item.name}}</p>{{/each}}'), 'var x="";for(var i=0,item,$$a=items;i<$$a.length;i++){item=$$a[i];x+=`<p>hello ${$$1(item.name)}</p>`;}return x' ); assert.is( gen('{{#each items as (item) }}<p>hello {{{item.name}}}</p>{{/each}}'), 'var x="";for(var i=0,item,$$a=items;i<$$a.length;i++){item=$$a[i];x+=`<p>hello ${item.name}</p>`;}return x' ); }); each('{{#each items as (item,idx)}}...{{/each}}', () => { assert.is( gen('<ul>{{#each items as (item,idx)}}<li>hello {{item.name}} (#{{ idx }})</li>{{/each}}</ul>'), 'var x=`<ul>`;for(var idx=0,item,$$a=items;idx<$$a.length;idx++){item=$$a[idx];x+=`<li>hello ${$$1(item.name)} (#${$$1(idx)})</li>`;}x+=`</ul>`;return x' ); assert.is( gen('<ul>{{#each items as (item, idx) }}<li>hello {{item.name}} (#{{ idx }})</li>{{/each}}</ul>'), 'var x=`<ul>`;for(var idx=0,item,$$a=items;idx<$$a.length;idx++){item=$$a[idx];x+=`<li>hello ${$$1(item.name)} (#${$$1(idx)})</li>`;}x+=`</ul>`;return x' ); assert.is( gen('<ul>{{#each items as (item, idx) }}<li>hello {{item.name}} (#{{{ idx }}})</li>{{/each}}</ul>'), 'var x=`<ul>`;for(var idx=0,item,$$a=items;idx<$$a.length;idx++){item=$$a[idx];x+=`<li>hello ${$$1(item.name)} (#${idx})</li>`;}x+=`</ul>`;return x' ); }); each('{{#each items as item, idx}}...{{/each}}', () => { assert.is( gen('<ul>{{#each items as item,idx}}<li>hello {{item.name}} (#{{ idx }})</li>{{/each}}</ul>'), 'var x=`<ul>`;for(var idx=0,item,$$a=items;idx<$$a.length;idx++){item=$$a[idx];x+=`<li>hello ${$$1(item.name)} (#${$$1(idx)})</li>`;}x+=`</ul>`;return x' ); assert.is( gen('<ul>{{#each items as item, idx }}<li>hello {{item.name}} (#{{ idx }})</li>{{/each}}</ul>'), 'var x=`<ul>`;for(var idx=0,item,$$a=items;idx<$$a.length;idx++){item=$$a[idx];x+=`<li>hello ${$$1(item.name)} (#${$$1(idx)})</li>`;}x+=`</ul>`;return x' ); assert.is( gen('<ul>{{#each items as item, idx }}<li>hello {{{item.name}}} (#{{{ idx }}})</li>{{/each}}</ul>'), 'var x=`<ul>`;for(var idx=0,item,$$a=items;idx<$$a.length;idx++){item=$$a[idx];x+=`<li>hello ${item.name} (#${idx})</li>`;}x+=`</ul>`;return x' ); }); each.run(); // --- const loose = suite('options.loose'); loose('should not declare unknown vars by default', () => { let output = gen('{{ hello }}'); assert.is(output, 'var x=`${$$1(hello)}`;return x'); }); loose('should prepare surprise vars', () => { let foo = gen('{{ hello }}', { loose: true }); assert.is(foo, 'var{hello}=$$3,x=`${$$1(hello)}`;return x'); let bar = gen('{{{ hello+world }}}', { loose: true }); assert.is(bar, 'var{hello,world}=$$3,x=`${hello+world}`;return x'); let baz = gen('{{{ hello / world }}}', { loose: true }); assert.is(baz, 'var{hello,world}=$$3,x=`${hello / world}`;return x'); }); loose('should ignore non-identifiers', () => { let foo = gen('{{{ "123" }}}', { loose: true }); assert.is(foo, 'var x=`${"123"}`;return x'); let bar = gen('{{{ 123 }}}', { loose: true }); assert.is(bar, 'var x=`${123}`;return x'); let baz = gen('{{{ hello == 123 }}}', { loose: true }); assert.is(baz, 'var{hello}=$$3,x=`${hello == 123}`;return x'); }); loose.run(); // --- const blocks = suite('options.blocks'); blocks('should throw error on unknown block', () => { try { gen('{{#hello}}'); assert.unreachable('should have thrown'); } catch (err) { assert.instance(err, Error); assert.is(err.message, 'Unknown "hello" block'); } }); blocks('should allow custom directives', () => { let tmpl = '{{#include x="name" src=true }}'; let output = gen(tmpl, { blocks: { include() { assert.unreachable('do not run function on parse'); } } }); assert.type(output, 'string'); assert.is(output, 'var x="";x+=`${$$2.include({x:"name",src:true},$$2)}`;return x'); }); blocks('should allow functional replacement', () => { let output = gen(`{{#hello "ignored"}}`, { blocks: { hello() { assert.unreachable('do not call functions during parse'); } } }); assert.is(output, 'var x="";x+=`${$$2.hello({},$$2)}`;return x'); }); blocks('should allow {{{ raw }}} function callers', () => { let output = gen(`{{{#hello foo="bar"}}}`, { blocks: { hello() { assert.unreachable('do not call functions during parse'); } } }); assert.is(output, 'var x="";x+=`${$$2.hello({foo:"bar"},$$2)}`;return x'); }); blocks('should parse arguments for function callers', () => { let output = gen(`{{#hello foo="foo" arr=["a b c", 123] bar='bar' o = { foo, bar } hi= howdy}}`, { blocks: { hello() { // } } }); let args = `{foo:"foo",arr:["a b c", 123],bar:'bar',o:{ foo, bar },hi:howdy}`; assert.is(output, 'var x="";x+=`${$$2.hello(' + args + ',$$2)}`;return x'); }); blocks('arguments parsing : strings', () => { let output = gen(`{{#hello foo="foo" bar = 'bar' baz= \`baz\` hello ="foo 'bar' baz" }}`, { blocks: { hello() { // } } }); let args = `{foo:"foo",bar:'bar',baz:\`baz\`,hello:"foo 'bar' baz"}`; assert.is(output, 'var x="";x+=`${$$2.hello(' + args + ',$$2)}`;return x'); }); blocks('arguments parsing : booleans', () => { let output = gen(`{{#hello foo=true bar = true baz= false hello =false }}`, { blocks: { hello() { // } } }); let args = `{foo:true,bar:true,baz:false,hello:false}`; assert.is(output, 'var x="";x+=`${$$2.hello(' + args + ',$$2)}`;return x'); }); blocks('arguments parsing : arrays', () => { let output = gen(`{{#hello foo=[1,2,3] bar = [1, 2, 3] baz= ['foo','baz'] }}`, { blocks: { hello() { // } } }); let args = `{foo:[1,2,3],bar:[1, 2, 3],baz:['foo','baz']}`; assert.is(output, 'var x="";x+=`${$$2.hello(' + args + ',$$2)}`;return x'); }); blocks('arguments parsing : objects', () => { let output = gen(`{{#hello foo={a,b} bar = { x, y:123 } }}`, { blocks: { hello() { // } } }); let args = `{foo:{a,b},bar:{ x, y:123 }}`; assert.is(output, 'var x="";x+=`${$$2.hello(' + args + ',$$2)}`;return x'); }); blocks('should still throw on unknown block', () => { try { gen('{{#var foo = 123}}{{#bar}}{{#howdy}}{{{ foo }}}', { blocks: { bar: () => 'bar' } }); assert.unreachable(); } catch (err) { assert.instance(err, Error); assert.is(err.message, 'Unknown "howdy" block'); } }); blocks('should allow multi-line arguments', () => { let blocks = { script: 1 }; let output = gen( `{{#script type="module" src="foobar.mjs" async=true }}`, { blocks } ); let expects = 'var x="";x+=`${$$2.script({type:"module",src:"foobar.mjs",async:true},$$2)}`;return x'; assert.is(output, expects); output = gen( `{{#script type="module" src="foobar.mjs" async=true }}`, { blocks } ); assert.is(output, expects); }); blocks.run(); // --- const props = suite('options.props'); props('should take the place of `#expect` decls', () => { let foo = gen('{{ url }}', { props: ['url'] }); assert.is(foo, 'var{url}=$$3,x=`${$$1(url)}`;return x'); let bar = gen('{{{ a + b }}}', { props: ['a', 'b'] }); assert.is(bar, 'var{a,b}=$$3,x=`${a + b}`;return x'); }); props('should dedupe with `#expect` repeats', () => { let foo = gen('{{#expect foo}}{{{ foo }}}', { props: ['foo'] }); assert.is(foo, 'var{foo}=$$3,x="";x+=`${foo}`;return x'); }); props('should work with `loose` enabled', () => { let foo = gen('{{{ foo }}}', { props: ['bar'], loose: true }); assert.is(foo, 'var{bar,foo}=$$3,x=`${foo}`;return x'); }); props.run(); // --- const stack = suite('stack'); stack('should throw on incorrect block order :: if->each', () => { try { gen(` {{#expect items}} {{#if items.length > 0}} {{#each items as item}} <p>{{{ item.name }}}</p> {{/if}} `); assert.unreachable(); } catch (err) { assert.instance(err, Error); assert.is(err.message, `Expected to close "each" block; closed "if" instead`); } }); stack('should throw on incorrect block order :: each->if', () => { try { gen(` {{#each items as item}} {{#if items.length > 0}} <p>{{{ item.name }}}</p> {{/each}} `); assert.unreachable(); } catch (err) { assert.instance(err, Error); assert.is(err.message, `Expected to close "if" block; closed "each" instead`); } }); stack('unterminated #if block', () => { try { gen(` {{#if items.length > 0}} <p>{{{ item.name }}}</p> `); assert.unreachable(); } catch (err) { assert.instance(err, Error); assert.is(err.message, `Unterminated "if" block`); } }); stack('unterminated #if->#elif block', () => { try { gen(` {{#if items.length === 1}} <p>{{{ item.name }}}</p> {{#elif items.length === 2}} <p>has two items</p> `); assert.unreachable(); } catch (err) { assert.instance(err, Error); assert.is(err.message, `Unterminated "if" block`); } }); stack('unterminated #each block', () => { try { gen(` {{#each items as item}} <p>{{{ item.name }}}</p> `); assert.unreachable(); } catch (err) { assert.instance(err, Error); assert.is(err.message, `Unterminated "each" block`); } }); stack.run(); // --- const async = suite('async'); async('should attach `await` keyword to custom blocks', () => { let output = gen(`{{#foo x="hello" }} {{#bar y="world" }}`, { async: true, blocks: { foo() { // return }, async bar() { // } } }); let expects = 'var x="";' expects += 'x+=`${await $$2.foo({x:"hello"},$$2)} `;'; expects += 'x+=`${await $$2.bar({y:"world"},$$2)}`;return x'; assert.is(output, expects); }); async.run();