');
+ assert.strictEqual(br.closeTag, null);
+ }
+
if (assertNodeType(div, 'ElementNode')) {
+ locEqual(div, 4, 6, 6, 12, 'div element');
+
+ locEqual(div, 4, 6, 6, 12, 'div element');
+ locEqual(div.path, 4, 7, 4, 10);
+ locEqual(div.path.head, 4, 7, 4, 10);
+ locEqual(div.openTag, 4, 6, 4, 11, '');
+ locEqual(div.closeTag, 6, 6, 6, 12, '
');
+
let [, hr] = div.children;
- locEqual(hr, 5, 8, 5, 14, 'hr element');
+ if (assertNodeType(hr, 'ElementNode')) {
+ locEqual(hr, 5, 8, 5, 14, 'hr element');
+ locEqual(hr.path, 5, 9, 5, 11);
+ locEqual(hr.path.head, 5, 9, 5, 11);
+ locEqual(hr.openTag, 5, 8, 5, 14, ' ');
+ assert.strictEqual(hr.closeTag, null);
+ }
}
}
});
-test('html elements with paths', () => {
+test('various html element paths', () => {
let ast = parse(`
-
-
-
-
+
+
+
+
+ <@Foo />
+ <@Foo.bar.baz />
+ <:foo />
`);
- let [, foo] = ast.body;
- locEqual(foo, 2, 4, 5, 10, 'Foo element');
- if (assertNodeType(foo, 'ElementNode')) {
- locEqual(foo.startTag, 2, 4, 2, 18, 'Foo start tag');
- locEqual(foo.nameNode, 2, 5, 2, 8, 'Foo name node');
- locEqual(foo.endTag, 5, 4, 5, 10, 'Foo end tag');
- let [, barSelfClosed] = foo.children;
- if (assertNodeType(barSelfClosed, 'ElementNode')) {
- locEqual(barSelfClosed.parts[0], 3, 7, 3, 10, 'bar.x.y bar part');
- locEqual(barSelfClosed.parts[1], 3, 11, 3, 12, 'bar.x.y x part');
- locEqual(barSelfClosed.parts[2], 3, 13, 3, 14, 'bar.x.y y part');
- }
+ let [, Foo, , FooDotBar, , This, , ThisDotFoo, , AtFoo, , AtFooDotBar, , NamedBlock] = ast.body;
+
+ if (assertNodeType(Foo, 'ElementNode')) {
+ locEqual(Foo.path, 2, 5, 2, 8);
+ locEqual(Foo.path.head, 2, 5, 2, 8);
+ }
+
+ if (assertNodeType(FooDotBar, 'ElementNode')) {
+ locEqual(FooDotBar.path, 3, 5, 3, 16);
+ locEqual(FooDotBar.path.head, 3, 5, 3, 8);
+ }
+
+ if (assertNodeType(This, 'ElementNode')) {
+ locEqual(This.path, 4, 5, 4, 9);
+ locEqual(This.path.head, 4, 5, 4, 9);
+ }
+
+ if (assertNodeType(ThisDotFoo, 'ElementNode')) {
+ locEqual(ThisDotFoo.path, 5, 5, 5, 17);
+ locEqual(ThisDotFoo.path.head, 5, 5, 5, 9);
+ }
+
+ if (assertNodeType(AtFoo, 'ElementNode')) {
+ locEqual(AtFoo.path, 6, 5, 6, 9);
+ locEqual(AtFoo.path.head, 6, 5, 6, 9);
+ }
+
+ if (assertNodeType(AtFooDotBar, 'ElementNode')) {
+ locEqual(AtFooDotBar.path, 7, 5, 7, 17);
+ locEqual(AtFooDotBar.path.head, 7, 5, 7, 9);
+ }
+
+ if (assertNodeType(NamedBlock, 'ElementNode')) {
+ locEqual(NamedBlock.path, 8, 5, 8, 9);
+ locEqual(NamedBlock.path.head, 8, 5, 8, 9);
}
});
@@ -224,6 +285,241 @@ test('mustache + newline + element ', () => {
locEqual(p, 3, 4, 3, 14, 'p element');
});
+test('block with block params', () => {
+ let ast = parse(`
+ {{#foo as |bar bat baz|}}
+ {{bar}} {{bat}} {{baz}}
+ {{/foo}}
+ `);
+
+ let statement = ast.body[1];
+ if (assertNodeType(statement, 'BlockStatement')) {
+ let block = statement.program;
+
+ if (assertNodeType(block.params[0], 'VarHead')) {
+ locEqual(block.params[0], 2, 15, 2, 18, 'bar');
+ }
+
+ if (assertNodeType(block.params[1], 'VarHead')) {
+ locEqual(block.params[1], 2, 19, 2, 22, 'bat');
+ }
+
+ if (assertNodeType(block.params[2], 'VarHead')) {
+ locEqual(block.params[2], 2, 23, 2, 26, 'baz');
+ }
+ }
+});
+
+test('block with block params edge case: multiline', () => {
+ let ast = parse(`
+ {{#foo as
+|bar bat
+ b
+a
+ z|}}
+ {{bar}} {{bat}} {{baz}}
+ {{/foo}}
+ `);
+
+ let statement = ast.body[1];
+ if (assertNodeType(statement, 'BlockStatement')) {
+ let block = statement.program;
+
+ if (assertNodeType(block.params[0], 'VarHead')) {
+ locEqual(block.params[0], 3, 1, 3, 4, 'bar');
+ }
+
+ if (assertNodeType(block.params[1], 'VarHead')) {
+ locEqual(block.params[1], 3, 5, 3, 8, 'bat');
+ }
+
+ if (assertNodeType(block.params[2], 'VarHead')) {
+ locEqual(block.params[2], 4, 6, 4, 7, 'b');
+ }
+
+ if (assertNodeType(block.params[3], 'VarHead')) {
+ locEqual(block.params[3], 5, 0, 5, 1, 'a');
+ }
+
+ if (assertNodeType(block.params[4], 'VarHead')) {
+ locEqual(block.params[4], 6, 6, 6, 7, 'z');
+ }
+ }
+});
+
+test('block with block params edge case: block-params like params', () => {
+ let ast = parse(`
+ {{#foo "as |bar bat baz|" as |bar bat baz|}}
+ {{bar}} {{bat}} {{baz}}
+ {{/foo}}
+ `);
+
+ let statement = ast.body[1];
+ if (assertNodeType(statement, 'BlockStatement')) {
+ let block = statement.program;
+
+ if (assertNodeType(block.params[0], 'VarHead')) {
+ locEqual(block.params[0], 2, 34, 2, 37, 'bar');
+ }
+
+ if (assertNodeType(block.params[1], 'VarHead')) {
+ locEqual(block.params[1], 2, 38, 2, 41, 'bat');
+ }
+
+ if (assertNodeType(block.params[2], 'VarHead')) {
+ locEqual(block.params[2], 2, 42, 2, 45, 'baz');
+ }
+ }
+});
+
+test('block with block params edge case: block-params like content', () => {
+ let ast = parse(`
+ {{#foo as |bar bat baz|}}as |bar bat baz|{{/foo}}
+ `);
+
+ let statement = ast.body[1];
+ if (assertNodeType(statement, 'BlockStatement')) {
+ let block = statement.program;
+
+ if (assertNodeType(block.params[0], 'VarHead')) {
+ locEqual(block.params[0], 2, 15, 2, 18, 'bar');
+ }
+
+ if (assertNodeType(block.params[1], 'VarHead')) {
+ locEqual(block.params[1], 2, 19, 2, 22, 'bat');
+ }
+
+ if (assertNodeType(block.params[2], 'VarHead')) {
+ locEqual(block.params[2], 2, 23, 2, 26, 'baz');
+ }
+ }
+});
+
+test('element with block params', () => {
+ let ast = parse(`
+
+ {{bar}} {{bat}} {{baz}}
+
+ `);
+
+ let element = ast.body[1];
+ if (assertNodeType(element, 'ElementNode')) {
+ if (assertNodeType(element.params[0], 'VarHead')) {
+ locEqual(element.params[0], 2, 13, 2, 16, 'bar');
+ }
+
+ if (assertNodeType(element.params[1], 'VarHead')) {
+ locEqual(element.params[1], 2, 17, 2, 20, 'bat');
+ }
+
+ if (assertNodeType(element.params[2], 'VarHead')) {
+ locEqual(element.params[2], 2, 21, 2, 24, 'baz');
+ }
+ }
+});
+
+test('element with block params edge case: multiline', () => {
+ let ast = parse(`
+
+ {{bar}} {{bat}} {{baz}}
+
+ `);
+
+ let element = ast.body[1];
+ if (assertNodeType(element, 'ElementNode')) {
+ if (assertNodeType(element.params[0], 'VarHead')) {
+ locEqual(element.params[0], 3, 1, 3, 4, 'bar');
+ }
+
+ if (assertNodeType(element.params[1], 'VarHead')) {
+ locEqual(element.params[1], 3, 5, 3, 8, 'bat');
+ }
+
+ if (assertNodeType(element.params[2], 'VarHead')) {
+ locEqual(element.params[2], 4, 6, 4, 7, 'b');
+ }
+
+ if (assertNodeType(element.params[3], 'VarHead')) {
+ locEqual(element.params[3], 5, 0, 5, 1, 'a');
+ }
+
+ if (assertNodeType(element.params[4], 'VarHead')) {
+ locEqual(element.params[4], 6, 6, 6, 7, 'z');
+ }
+ }
+});
+
+test('elment with block params edge case: block-params like attribute names', () => {
+ let ast = parse(`
+
+ {{bar}} {{bat}} {{baz}}
+
+ `);
+
+ let element = ast.body[1];
+ if (assertNodeType(element, 'ElementNode')) {
+ if (assertNodeType(element.params[0], 'VarHead')) {
+ locEqual(element.params[0], 2, 30, 2, 33, 'bar');
+ }
+
+ if (assertNodeType(element.params[1], 'VarHead')) {
+ locEqual(element.params[1], 2, 34, 2, 37, 'bat');
+ }
+
+ if (assertNodeType(element.params[2], 'VarHead')) {
+ locEqual(element.params[2], 2, 38, 2, 41, 'baz');
+ }
+ }
+});
+
+test('elment with block params edge case: block-params like attribute values', () => {
+ let ast = parse(`
+
+ {{bar}} {{bat}} {{baz}}
+
+ `);
+
+ let element = ast.body[1];
+ if (assertNodeType(element, 'ElementNode')) {
+ if (assertNodeType(element.params[0], 'VarHead')) {
+ locEqual(element.params[0], 2, 36, 2, 39, 'bar');
+ }
+
+ if (assertNodeType(element.params[1], 'VarHead')) {
+ locEqual(element.params[1], 2, 40, 2, 43, 'bat');
+ }
+
+ if (assertNodeType(element.params[2], 'VarHead')) {
+ locEqual(element.params[2], 2, 44, 2, 47, 'baz');
+ }
+ }
+});
+
+test('element with block params edge case: block-params like content', () => {
+ let ast = parse(`
+ as |bar bat baz|
+ `);
+
+ let element = ast.body[1];
+ if (assertNodeType(element, 'ElementNode')) {
+ if (assertNodeType(element.params[0], 'VarHead')) {
+ locEqual(element.params[0], 2, 13, 2, 16, 'bar');
+ }
+
+ if (assertNodeType(element.params[1], 'VarHead')) {
+ locEqual(element.params[1], 2, 17, 2, 20, 'bat');
+ }
+
+ if (assertNodeType(element.params[2], 'VarHead')) {
+ locEqual(element.params[2], 2, 21, 2, 24, 'baz');
+ }
+ }
+});
+
test('blocks with nested html elements', () => {
let ast = parse(`
{{#foo-bar}}Foo
{{/foo-bar}} Hi!
@@ -337,32 +633,6 @@ data-barf="herpy"
}
});
-test('element block params', () => {
- let ast = parse(` `);
-
- let [Foo] = ast.body;
- if (assertNodeType(Foo, 'ElementNode')) {
- let [ab, cd, efg] = guardArray({ blockParamNodes: Foo.blockParamNodes }, { min: 3 });
- locEqual(ab, 1, 9, 1, 11);
- locEqual(cd, 1, 12, 1, 14);
- locEqual(efg, 1, 15, 1, 18);
- }
-});
-
-test('mustache block params', () => {
- let ast = parse(`{{#Foo as |ab cd efg|}}{{/Foo}}`);
- `
-{{#Foo as |ab cd efg|}}{{/Foo}}`;
-
- let [Foo] = ast.body;
- if (assertNodeType(Foo, 'BlockStatement')) {
- let [ab, cd, efg] = guardArray({ blockParamNodes: Foo.program.blockParamNodes }, { min: 3 });
- locEqual(ab, 1, 11, 1, 13);
- locEqual(cd, 1, 14, 1, 16);
- locEqual(efg, 1, 17, 1, 20);
- }
-});
-
test('element dynamic attribute', () => {
let ast = parse(` `);
diff --git a/packages/@glimmer/syntax/test/parser-node-test.ts b/packages/@glimmer/syntax/test/parser-node-test.ts
index f600fb873c..dc8d3e150e 100644
--- a/packages/@glimmer/syntax/test/parser-node-test.ts
+++ b/packages/@glimmer/syntax/test/parser-node-test.ts
@@ -5,23 +5,39 @@ import { syntaxErrorFor } from '@glimmer-workspace/test-utils';
import { astEqual } from './support';
-const { test, skip } = QUnit;
+const { test } = QUnit;
QUnit.module('[glimmer-syntax] Parser - AST');
test('a simple piece of content', () => {
let t = 'some content';
- astEqual(t, b.program([b.text('some content')]));
+ astEqual(t, b.template([b.text('some content')]));
});
test('self-closed element', () => {
let t = ' ';
- astEqual(t, b.program([element('g/')]));
+ astEqual(t, b.template([element('g/')]));
+});
+
+test('various html element paths', () => {
+ const cases = [
+ [` `, b.fullPath(b.var('Foo'))],
+ [` `, b.fullPath(b.var('Foo'), ['bar', 'baz'])],
+ [` `, b.fullPath(b.this())],
+ [` `, b.fullPath(b.this(), ['foo', 'bar'])],
+ [`<@Foo />`, b.fullPath(b.at('@Foo'))],
+ [`<@Foo.bar.baz />`, b.fullPath(b.at('@Foo'), ['bar', 'baz'])],
+ [`<:foo />`, b.fullPath(b.var(':foo'))],
+ ] satisfies Array<[string, ASTv1.PathExpression]>;
+
+ for (const [t, path] of cases) {
+ astEqual(t, b.template([b.element({ path, selfClosing: true })]));
+ }
});
test('elements can have empty attributes', () => {
let t = ' ';
- astEqual(t, b.program([element('img', ['attrs', ['id', '']])]));
+ astEqual(t, b.template([element('img', ['attrs', ['id', '']])]));
});
test('disallowed quote in element space is rejected', (assert) => {
@@ -46,17 +62,17 @@ test('disallowed equals sign in element space is rejected', (assert) => {
test('svg content', () => {
let t = ' ';
- astEqual(t, b.program([element('svg')]));
+ astEqual(t, b.template([element('svg')]));
});
test('html content with html content inline', () => {
let t = '';
- astEqual(t, b.program([element('div', ['body', element('p')])]));
+ astEqual(t, b.template([element('div', ['body', element('p')])]));
});
test('html content with svg content inline', () => {
let t = '
';
- astEqual(t, b.program([element('div', ['body', element('svg')])]));
+ astEqual(t, b.template([element('div', ['body', element('svg')])]));
});
let integrationPoints = ['foreignObject', 'desc'];
@@ -65,7 +81,7 @@ function buildIntegrationPointTest(integrationPoint: string) {
let t = '<' + integrationPoint + '>
' + integrationPoint + '> ';
astEqual(
t,
- b.program([element('svg', ['body', element(integrationPoint, ['body', element('div')])])])
+ b.template([element('svg', ['body', element(integrationPoint, ['body', element('div')])])])
);
};
}
@@ -81,7 +97,7 @@ test('svg title with html content', () => {
let t = '
';
astEqual(
t,
- b.program([element('svg', ['body', element('title', ['body', b.text('
')])])])
+ b.template([element('svg', ['body', element('title', ['body', b.text('
')])])])
);
});
@@ -89,7 +105,7 @@ test('a piece of content with HTML', () => {
let t = 'some content
done';
astEqual(
t,
- b.program([b.text('some '), element('div', ['body', b.text('content')]), b.text(' done')])
+ b.template([b.text('some '), element('div', ['body', b.text('content')]), b.text(' done')])
);
});
@@ -97,7 +113,7 @@ test('a piece of Handlebars with HTML', () => {
let t = 'some {{content}}
done';
astEqual(
t,
- b.program([
+ b.template([
b.text('some '),
element('div', ['body', b.mustache(b.path('content'))]),
b.text(' done'),
@@ -109,7 +125,7 @@ test('Handlebars embedded in an attribute (quoted)', () => {
let t = 'some content
done';
astEqual(
t,
- b.program([
+ b.template([
b.text('some '),
element(
'div',
@@ -125,7 +141,7 @@ test('Handlebars embedded in an attribute (unquoted)', () => {
let t = 'some content
done';
astEqual(
t,
- b.program([
+ b.template([
b.text('some '),
element('div', ['attrs', ['class', b.mustache(b.path('foo'))]], ['body', b.text('content')]),
b.text(' done'),
@@ -137,14 +153,14 @@ test('Handlebars embedded in an attribute of a self-closing tag (unqouted)', ()
let t = ' ';
let el = element('input/', ['attrs', ['value', b.mustache(b.path('foo'))]]);
- astEqual(t, b.program([el]));
+ astEqual(t, b.template([el]));
});
test('Handlebars embedded in an attribute (sexprs)', () => {
let t = 'some content
done';
astEqual(
t,
- b.program([
+ b.template([
b.text('some '),
element(
'div',
@@ -166,7 +182,7 @@ test('Handlebars embedded in an attribute with other content surrounding it', ()
let t = 'some content done';
astEqual(
t,
- b.program([
+ b.template([
b.text('some '),
element(
'a',
@@ -185,7 +201,7 @@ test('A more complete embedding example', () => {
" {{more 'embed'}}";
astEqual(
t,
- b.program([
+ b.template([
b.mustache(b.path('embed')),
b.text(' '),
b.mustache(b.path('some'), [b.string('content')]),
@@ -219,7 +235,7 @@ test('Simple embedded block helpers', () => {
let t = '{{#if foo}}{{content}}
{{/if}}';
astEqual(
t,
- b.program([
+ b.template([
b.block(
b.path('if'),
[b.path('foo')],
@@ -230,31 +246,12 @@ test('Simple embedded block helpers', () => {
);
});
-test('block params', (assert) => {
- let t = ' {{#Foo as |bar baz qux|}}{{/Foo}}';
- let element = b.element('Foo', {
- blockParams: ['bar', 'baz', 'qux'],
- });
- let mustache = b.block(b.path('Foo'), [], b.hash(), b.blockItself([], ['bar', 'baz', 'qux']));
- astEqual(t, b.program([element, mustache]));
- assert.strictEqual(element.blockParamNodes.length, 3);
- assert.strictEqual(mustache.program.blockParamNodes.length, 3);
- assert.deepEqual(
- element.blockParamNodes.map((b) => b.value),
- ['bar', 'baz', 'qux']
- );
- assert.deepEqual(
- mustache.program.blockParamNodes.map((b) => b.value),
- ['bar', 'baz', 'qux']
- );
-});
-
test('Involved block helper', () => {
let t =
'hi
content {{#testing shouldRender}}Appears!
{{/testing}} more content here';
astEqual(
t,
- b.program([
+ b.template([
element('p', ['body', b.text('hi')]),
b.text(' content '),
b.block(
@@ -270,11 +267,169 @@ test('Involved block helper', () => {
);
});
+test('block with block params', () => {
+ let t = `{{#foo as |bar bat baz|}}{{bar}} {{bat}} {{baz}}{{/foo}}`;
+
+ astEqual(
+ t,
+ b.template([
+ b.block(
+ b.path('foo'),
+ null,
+ null,
+ b.blockItself(
+ [b.mustache('bar'), b.text(' '), b.mustache('bat'), b.text(' '), b.mustache('baz')],
+ ['bar', 'bat', 'baz']
+ )
+ ),
+ ])
+ );
+});
+
+test('block with block params edge case: multiline', () => {
+ let t = `{{#foo as
+|bar bat
+ b
+a
+ z|}}{{bar}} {{bat}} {{baz}}{{/foo}}`;
+
+ astEqual(
+ t,
+ b.template([
+ b.block(
+ b.path('foo'),
+ null,
+ null,
+ b.blockItself(
+ [b.mustache('bar'), b.text(' '), b.mustache('bat'), b.text(' '), b.mustache('baz')],
+ ['bar', 'bat', 'b', 'a', 'z']
+ )
+ ),
+ ])
+ );
+});
+
+test('block with block params edge case: block-params like params', () => {
+ let t = `{{#foo "as |a b c|" as |bar bat baz|}}{{bar}} {{bat}} {{baz}}{{/foo}}`;
+
+ astEqual(
+ t,
+ b.template([
+ b.block(
+ b.path('foo'),
+ [b.string('as |a b c|')],
+ null,
+ b.blockItself(
+ [b.mustache('bar'), b.text(' '), b.mustache('bat'), b.text(' '), b.mustache('baz')],
+ ['bar', 'bat', 'baz']
+ )
+ ),
+ ])
+ );
+});
+
+test('block with block params edge case: block-params like content', () => {
+ let t = `{{#foo as |bar bat baz|}}as |a b c|{{/foo}}`;
+
+ astEqual(
+ t,
+ b.template([
+ b.block(
+ b.path('foo'),
+ null,
+ null,
+ b.blockItself([b.text('as |a b c|')], ['bar', 'bat', 'baz'])
+ ),
+ ])
+ );
+});
+
+test('element with block params', () => {
+ let t = `{{bar}} {{bat}} {{baz}} `;
+
+ astEqual(
+ t,
+ b.template([
+ element(
+ 'Foo',
+ ['as', b.var('bar'), b.var('bat'), b.var('baz')],
+ ['body', b.mustache('bar'), b.text(' '), b.mustache('bat'), b.text(' '), b.mustache('baz')]
+ ),
+ ])
+ );
+});
+
+test('element with block params edge case: multiline', () => {
+ let t = `{{bar}} {{bat}} {{baz}} `;
+
+ astEqual(
+ t,
+ b.template([
+ element(
+ 'Foo',
+ ['as', b.var('bar'), b.var('bat'), b.var('b'), b.var('a'), b.var('z')],
+ ['body', b.mustache('bar'), b.text(' '), b.mustache('bat'), b.text(' '), b.mustache('baz')]
+ ),
+ ])
+ );
+});
+
+test('element with block params edge case: block-params like attribute names', () => {
+ let t = `as |a b c| `;
+
+ astEqual(
+ t,
+ b.template([
+ element(
+ 'Foo',
+ ['attrs', ['as', 'a'], ['async', 'b']],
+ ['as', b.var('bar'), b.var('bat'), b.var('baz')],
+ ['body', b.text('as |a b c|')]
+ ),
+ ])
+ );
+});
+
+test('element with block params edge case: block-params like attribute values', () => {
+ let t = `{{bar}} {{bat}} {{baz}} `;
+
+ astEqual(
+ t,
+ b.template([
+ element(
+ 'Foo',
+ ['attrs', ['foo', 'as |a b c|']],
+ ['as', b.var('bar'), b.var('bat'), b.var('baz')],
+ ['body', b.mustache('bar'), b.text(' '), b.mustache('bat'), b.text(' '), b.mustache('baz')]
+ ),
+ ])
+ );
+});
+
+test('element with block params edge case: block-params like content', () => {
+ let t = `as |a b c| `;
+
+ astEqual(
+ t,
+ b.template([
+ element(
+ 'Foo',
+ ['as', b.var('bar'), b.var('bat'), b.var('baz')],
+ ['body', b.text('as |a b c|')]
+ ),
+ ])
+ );
+});
+
test('Element modifiers', () => {
let t = "Some content
";
astEqual(
t,
- b.program([
+ b.template([
element(
'p',
['attrs', ['class', 'bar']],
@@ -285,47 +440,36 @@ test('Element modifiers', () => {
);
});
-test('Element paths', (assert) => {
- let t = " ";
- const elem = element('bar.x.y', ['attrs', ['class', 'bar']]);
- astEqual(t, b.program([elem]));
- assert.strictEqual(elem.parts.length, 3);
- assert.deepEqual(
- elem.parts.map((p) => p.value),
- ['bar', 'x', 'y']
- );
-});
-
test('Tokenizer: MustacheStatement encountered in beforeAttributeName state', () => {
let t = ' ';
- astEqual(t, b.program([element('input', ['modifiers', 'bar'])]));
+ astEqual(t, b.template([element('input', ['modifiers', 'bar'])]));
});
test('Tokenizer: MustacheStatement encountered in attributeName state', () => {
let t = ' ';
- astEqual(t, b.program([element('input', ['attrs', ['foo', '']], ['modifiers', ['bar']])]));
+ astEqual(t, b.template([element('input', ['attrs', ['foo', '']], ['modifiers', ['bar']])]));
});
test('Tokenizer: MustacheStatement encountered in afterAttributeName state', () => {
let t = ' ';
- astEqual(t, b.program([element('input', ['attrs', ['foo', '']], ['modifiers', 'bar'])]));
+ astEqual(t, b.template([element('input', ['attrs', ['foo', '']], ['modifiers', 'bar'])]));
});
test('Tokenizer: MustacheStatement encountered in afterAttributeValue state', () => {
let t = ' ';
- astEqual(t, b.program([element('input', ['attrs', ['foo', '1']], ['modifiers', ['bar']])]));
+ astEqual(t, b.template([element('input', ['attrs', ['foo', '1']], ['modifiers', ['bar']])]));
});
test('Tokenizer: MustacheStatement encountered in afterAttributeValueQuoted state', () => {
let t = " ";
- astEqual(t, b.program([element('input', ['attrs', ['foo', '1']], ['modifiers', 'bar'])]));
+ astEqual(t, b.template([element('input', ['attrs', ['foo', '1']], ['modifiers', 'bar'])]));
});
test('Stripping - mustaches', () => {
let t = 'foo {{~content}} bar';
astEqual(
t,
- b.program([
+ b.template([
b.text('foo'),
b.mustache(b.path('content'), undefined, undefined, undefined, undefined, {
open: true,
@@ -338,7 +482,7 @@ test('Stripping - mustaches', () => {
t = 'foo {{content~}} bar';
astEqual(
t,
- b.program([
+ b.template([
b.text('foo '),
b.mustache(b.path('content'), undefined, undefined, undefined, undefined, {
open: false,
@@ -353,7 +497,7 @@ test('Stripping - blocks', () => {
let t = 'foo {{~#wat}}{{/wat}} bar';
astEqual(
t,
- b.program([
+ b.template([
b.text('foo'),
b.block(b.path('wat'), [], b.hash(), b.blockItself(), undefined, undefined, {
open: true,
@@ -366,7 +510,7 @@ test('Stripping - blocks', () => {
t = 'foo {{#wat}}{{/wat~}} bar';
astEqual(
t,
- b.program([
+ b.template([
b.text('foo '),
b.block(
b.path('wat'),
@@ -388,7 +532,7 @@ test('Stripping - programs', () => {
let t = '{{#wat~}} foo {{else}}{{/wat}}';
astEqual(
t,
- b.program([
+ b.template([
b.block(
b.path('wat'),
[],
@@ -404,7 +548,7 @@ test('Stripping - programs', () => {
t = '{{#wat}} foo {{~else}}{{/wat}}';
astEqual(
t,
- b.program([
+ b.template([
b.block(
b.path('wat'),
[],
@@ -421,7 +565,7 @@ test('Stripping - programs', () => {
t = '{{#wat}}{{else~}} foo {{/wat}}';
astEqual(
t,
- b.program([
+ b.template([
b.block(
b.path('wat'),
[],
@@ -438,7 +582,7 @@ test('Stripping - programs', () => {
t = '{{#wat}}{{else}} foo {{~/wat}}';
astEqual(
t,
- b.program([
+ b.template([
b.block(
b.path('wat'),
[],
@@ -459,7 +603,7 @@ test('Stripping - removes unnecessary text nodes', () => {
astEqual(
t,
- b.program([
+ b.template([
b.block(
b.path('each'),
[],
@@ -480,7 +624,7 @@ test('Whitespace control - linebreaks after blocks removed by default', () => {
astEqual(
t,
- b.program([
+ b.template([
b.block(
b.path('each'),
[],
@@ -497,7 +641,7 @@ test('Whitespace control - preserve all whitespace if config is set', () => {
astEqual(
t,
- b.program([
+ b.template([
b.block(
b.path('each'),
[],
@@ -514,38 +658,63 @@ test('Whitespace control - preserve all whitespace if config is set', () => {
});
// TODO: Make these throw an error.
-skip('Awkward mustache in unquoted attribute value', () => {
- let t = '
';
- astEqual(
- t,
- b.program([element('div', ['attrs', ['class', b.concat([b.text('a'), b.mustache('foo')])]])])
+test('Awkward mustache in unquoted attribute value', (assert) => {
+ assert.throws(
+ () => {
+ parse('
', {
+ meta: { moduleName: 'test-module' },
+ });
+ },
+ syntaxErrorFor(
+ `An unquoted attribute value must be a string or a mustache, preceded by whitespace or a '=' character, and followed by whitespace, a '>' character, or '/>'`,
+ 'class=a{{foo}}',
+ 'test-module',
+ 1,
+ 5
+ )
);
- t = '
';
- astEqual(
- t,
- b.program([
- element('div', ['attrs', ['class', b.concat([b.text('a'), b.mustache('foo'), b.text('b')])]]),
- ])
+ assert.throws(
+ () => {
+ parse('
', {
+ meta: { moduleName: 'test-module' },
+ });
+ },
+ syntaxErrorFor(
+ `An unquoted attribute value must be a string or a mustache, preceded by whitespace or a '=' character, and followed by whitespace, a '>' character, or '/>'`,
+ 'class=a{{foo}}b',
+ 'test-module',
+ 1,
+ 5
+ )
);
- t = '
';
- astEqual(
- t,
- b.program([element('div', ['attrs', ['class', b.concat([b.mustache('foo'), b.text('b')])]])])
+ assert.throws(
+ () => {
+ parse('
', {
+ meta: { moduleName: 'test-module' },
+ });
+ },
+ syntaxErrorFor(
+ `An unquoted attribute value must be a string or a mustache, preceded by whitespace or a '=' character, and followed by whitespace, a '>' character, or '/>'`,
+ 'class={{foo}}b',
+ 'test-module',
+ 1,
+ 5
+ )
);
});
test('an HTML comment', () => {
let t = 'before after';
- astEqual(t, b.program([b.text('before '), b.comment(' some comment '), b.text(' after')]));
+ astEqual(t, b.template([b.text('before '), b.comment(' some comment '), b.text(' after')]));
});
test('a Handlebars comment inside an HTML comment', () => {
let t = 'before after';
astEqual(
t,
- b.program([
+ b.template([
b.text('before '),
b.comment(' some {{! nested thing }} comment '),
b.text(' after'),
@@ -557,7 +726,7 @@ test('a Handlebars comment', () => {
let t = 'before {{! some comment }} after';
astEqual(
t,
- b.program([b.text('before '), b.mustacheComment(' some comment '), b.text(' after')])
+ b.template([b.text('before '), b.mustacheComment(' some comment '), b.text(' after')])
);
});
@@ -565,7 +734,7 @@ test('a Handlebars comment in proper element space', () => {
let t = 'before
after';
astEqual(
t,
- b.program([
+ b.template([
b.text('before '),
element(
'div',
@@ -581,7 +750,7 @@ test('a Handlebars comment after a valueless attribute', () => {
let t = ' ';
astEqual(
t,
- b.program([
+ b.template([
element('input', ['attrs', ['foo', '']], ['comments', b.mustacheComment(' comment ')]),
])
);
@@ -637,25 +806,25 @@ test('a Handlebars comment in invalid element space', (assert) => {
test('allow {{null}} to be passed as helper name', () => {
let ast = parse('{{null}}');
- astEqual(ast, b.program([b.mustache(b.null())]));
+ astEqual(ast, b.template([b.mustache(b.null())]));
});
test('allow {{null}} to be passed as a param', () => {
let ast = parse('{{foo null}}');
- astEqual(ast, b.program([b.mustache(b.path('foo'), [b.null()])]));
+ astEqual(ast, b.template([b.mustache(b.path('foo'), [b.null()])]));
});
test('allow {{undefined}} to be passed as helper name', () => {
let ast = parse('{{undefined}}');
- astEqual(ast, b.program([b.mustache(b.undefined())]));
+ astEqual(ast, b.template([b.mustache(b.undefined())]));
});
test('allow {{undefined}} to be passed as a param', () => {
let ast = parse('{{foo undefined}}');
- astEqual(ast, b.program([b.mustache(b.path('foo'), [b.undefined()])]));
+ astEqual(ast, b.template([b.mustache(b.path('foo'), [b.undefined()])]));
});
test('Handlebars partial should error', (assert) => {
@@ -725,7 +894,7 @@ test('disallowed mustaches in the tagName space', (assert) => {
test('mustache immediately followed by self closing tag does not error', () => {
let ast = parse('');
let el = element('FooBar/', ['attrs', ['data-foo', b.mustache('blah')]]);
- astEqual(ast, b.program([el]));
+ astEqual(ast, b.template([el]));
});
QUnit.dump.maxDepth = 100;
@@ -749,10 +918,10 @@ test('named blocks', () => {
element(
':body',
['body', element('div', ['body', b.mustache('contents')])],
- ['as', 'contents']
+ ['as', b.var('contents')]
),
]);
- astEqual(ast, b.program([el]));
+ astEqual(ast, b.template([el]));
});
test('path expression with "dangling dot" throws error', (assert) => {
@@ -860,8 +1029,8 @@ export type ElementParts =
| ['attrs', ...AttrSexp[]]
| ['modifiers', ...ModifierSexp[]]
| ['body', ...ASTv1.Statement[]]
- | ['comments', ...ElementComment[]]
- | ['as', ...string[]]
+ | ['comments', ...ASTv1.MustacheCommentStatement[]]
+ | ['as', ...ASTv1.VarHead[]]
| ['loc', ASTv1.SourceLocation];
export type PathSexp = string | ['path', string, LocSexp?];
@@ -876,8 +1045,6 @@ export type AttrSexp = [string, ASTv1.AttrNode['value'] | string, LocSexp?];
export type LocSexp = ['loc', ASTv1.SourceLocation];
-export type ElementComment = ASTv1.MustacheCommentStatement | ASTv1.SourceLocation | string;
-
export type SexpValue =
| string
| ASTv1.Expression[]
@@ -886,16 +1053,9 @@ export type SexpValue =
| PathSexp
| undefined;
-export interface BuildElementOptions {
- attrs?: ASTv1.AttrNode[];
- modifiers?: ASTv1.ElementModifierStatement[];
- children?: ASTv1.Statement[];
- comments?: ElementComment[];
- blockParams?: string[];
- loc?: ASTv1.SourceLocation;
-}
-
-export type TagDescriptor = string | { name: string; selfClosing: boolean };
+export type BuildElementParams = Parameters;
+export type TagDescriptor = BuildElementParams[0];
+export type BuildElementOptions = NonNullable;
export function element(tag: TagDescriptor, ...options: ElementParts[]): ASTv1.ElementNode {
let normalized: BuildElementOptions;
@@ -905,48 +1065,7 @@ export function element(tag: TagDescriptor, ...options: ElementParts[]): ASTv1.E
normalized = options || {};
}
- let { attrs, blockParams, modifiers, comments, children, loc } = normalized;
-
- // this is used for backwards compat, prior to `selfClosing` being part of the ElementNode AST
- let selfClosing = false;
- if (typeof tag === 'object') {
- selfClosing = tag.selfClosing;
- tag = tag.name;
- } else {
- if (tag.slice(-1) === '/') {
- tag = tag.slice(0, -1);
- selfClosing = true;
- }
- }
-
- return {
- type: 'ElementNode',
- tag: tag,
- nameNode: {
- type: 'ElementNameNode',
- value: tag,
- } as ASTv1.ElementNameNode,
- startTag: {
- type: 'ElementStartNode',
- value: tag,
- } as ASTv1.ElementStartNode,
- endTag: {
- type: 'ElementEndNode',
- value: selfClosing ? '' : tag,
- } as ASTv1.ElementEndNode,
- parts: tag
- .split('.')
- .map((t) => ({ type: 'ElementPartNode', value: t }) as ASTv1.ElementPartNode),
- selfClosing: selfClosing,
- attributes: attrs || [],
- blockParams: blockParams || [],
- blockParamNodes:
- blockParams?.map((b) => ({ type: 'BlockParam', value: b }) as ASTv1.BlockParam) || [],
- modifiers: modifiers || [],
- comments: (comments as ASTv1.MustacheCommentStatement[]) || [],
- children: children || [],
- loc: b.loc(loc || null),
- };
+ return b.element(tag, normalized);
}
export function normalizeElementParts(...args: ElementParts[]): BuildElementOptions {
@@ -1037,13 +1156,7 @@ export function normalizeModifier(sexp: ModifierSexp): ASTv1.ElementModifierStat
loc = next[1];
}
- return {
- type: 'ElementModifierStatement',
- path,
- params: params || [],
- hash: hash || b.hash([]),
- loc: b.loc(loc || null),
- };
+ return b.elementModifier(path as ASTv1.CallableExpression, params, hash, b.loc(loc || null));
}
export function normalizeHead(path: PathSexp): ASTv1.Expression {
diff --git a/packages/@glimmer/syntax/test/plugin-node-test.ts b/packages/@glimmer/syntax/test/plugin-node-test.ts
index 0e9e7cdc5f..8d725f157f 100644
--- a/packages/@glimmer/syntax/test/plugin-node-test.ts
+++ b/packages/@glimmer/syntax/test/plugin-node-test.ts
@@ -13,8 +13,8 @@ test('function based AST plugins can be provided to the compiler', (assert) => {
() => ({
name: 'plugin-a',
visitor: {
- Program() {
- assert.step('Program');
+ Template() {
+ assert.step('Template');
assert.ok(true, 'transform was called!');
},
},
@@ -23,7 +23,7 @@ test('function based AST plugins can be provided to the compiler', (assert) => {
},
});
- assert.verifySteps(['Program']);
+ assert.verifySteps(['Template']);
});
test('plugins are provided the syntax package', (assert) => {
@@ -43,6 +43,32 @@ test('plugins are provided the syntax package', (assert) => {
assert.verifySteps(['syntax']);
});
+test('deprecated program visitor', (assert) => {
+ let plugin = () => {
+ return {
+ name: 'plugin',
+ visitor: {
+ // eslint-disable-next-line deprecation/deprecation
+ Program(node: AST.Program) {
+ assert.step(node.type);
+ },
+
+ BlockStatement(node: AST.BlockStatement) {
+ assert.step(node.type);
+ },
+ },
+ };
+ };
+
+ preprocess('Hello {{#inner}}world{{/inner}}', {
+ plugins: {
+ ast: [plugin],
+ },
+ });
+
+ assert.verifySteps(['Template', 'BlockStatement', 'Block']);
+});
+
test('can support the legacy AST transform API via ASTPlugin', (assert) => {
function ensurePlugin(FunctionOrPlugin: any): ASTPluginBuilder {
if (FunctionOrPlugin.prototype && FunctionOrPlugin.prototype.transform) {
@@ -51,7 +77,7 @@ test('can support the legacy AST transform API via ASTPlugin', (assert) => {
name: 'plugin-a',
visitor: {
- Program(node: AST.Program) {
+ Template(node: AST.Template) {
let plugin = new FunctionOrPlugin(env);
plugin.syntax = env.syntax;
@@ -69,9 +95,9 @@ test('can support the legacy AST transform API via ASTPlugin', (assert) => {
class Plugin {
declare syntax: Syntax;
- transform(program: AST.Program): AST.Program {
+ transform(template: AST.Template): AST.Template {
assert.ok(true, 'transform was called!');
- return program;
+ return template;
}
}
@@ -82,18 +108,18 @@ test('can support the legacy AST transform API via ASTPlugin', (assert) => {
});
});
-const FIRST_PLUGIN = new WeakMap();
-const SECOND_PLUGIN = new WeakMap();
-const THIRD_PLUGIN = new WeakMap();
+const FIRST_PLUGIN = new WeakMap();
+const SECOND_PLUGIN = new WeakMap();
+const THIRD_PLUGIN = new WeakMap();
test('AST plugins can be chained', (assert) => {
let first = () => {
return {
name: 'first',
visitor: {
- Program(program: AST.Program | AST.Template | AST.Block) {
- assert.step('Program first');
- FIRST_PLUGIN.set(program, true);
+ Template(template: AST.Template) {
+ assert.step('Template first');
+ FIRST_PLUGIN.set(template, true);
},
},
};
@@ -103,8 +129,8 @@ test('AST plugins can be chained', (assert) => {
return {
name: 'second',
visitor: {
- Program(node: AST.Program | AST.Block | AST.Template) {
- assert.step('Program second');
+ Template(node: AST.Template) {
+ assert.step('Template second');
assert.true(FIRST_PLUGIN.get(node), 'AST from first plugin is passed to second');
SECOND_PLUGIN.set(node, true);
@@ -117,8 +143,8 @@ test('AST plugins can be chained', (assert) => {
return {
name: 'third',
visitor: {
- Program(node: AST.Program | AST.Block | AST.Template) {
- assert.step('Program third');
+ Template(node: AST.Template) {
+ assert.step('Template third');
assert.true(SECOND_PLUGIN.get(node), 'AST from second plugin is passed to third');
THIRD_PLUGIN.set(node, true);
@@ -135,7 +161,7 @@ test('AST plugins can be chained', (assert) => {
assert.true(THIRD_PLUGIN.get(ast), 'return value from last AST transform is used');
- assert.verifySteps(['Program first', 'Program second', 'Program third']);
+ assert.verifySteps(['Template first', 'Template second', 'Template third']);
});
test('AST plugins can access meta from environment', (assert) => {
@@ -143,8 +169,8 @@ test('AST plugins can access meta from environment', (assert) => {
return {
name: 'exposedMetaTemplateData',
visitor: {
- Program() {
- assert.step('Program');
+ Template() {
+ assert.step('Template');
const { meta } = env;
const { moduleName } = expectPresent(
meta as { moduleName: 'string' },
@@ -170,5 +196,5 @@ test('AST plugins can access meta from environment', (assert) => {
},
});
- assert.verifySteps(['Program']);
+ assert.verifySteps(['Template']);
});
diff --git a/packages/@glimmer/syntax/test/support.ts b/packages/@glimmer/syntax/test/support.ts
index 234e39e740..8dbe6d0c86 100644
--- a/packages/@glimmer/syntax/test/support.ts
+++ b/packages/@glimmer/syntax/test/support.ts
@@ -6,15 +6,17 @@ function normalizeNode(obj: AST.Node | Array): AST.Node | Array(obj: T): T {
if (obj && typeof obj === 'object') {
if (Array.isArray(obj)) {
return obj.map(normalizeValue) as T;
} else {
return fromEntries(
- entries(obj).flatMap(([key, value]) =>
- key === 'loc' ? [] : [[key, normalizeValue(value)]]
- )
+ entries(obj).flatMap(([key, value]) => (isLoc(key) ? [] : [[key, normalizeValue(value)]]))
) as T;
}
} else {
diff --git a/packages/@glimmer/syntax/test/traversal/manipulating-node-test.ts b/packages/@glimmer/syntax/test/traversal/manipulating-node-test.ts
index e22df27f47..8fe5eaa425 100644
--- a/packages/@glimmer/syntax/test/traversal/manipulating-node-test.ts
+++ b/packages/@glimmer/syntax/test/traversal/manipulating-node-test.ts
@@ -23,7 +23,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating');
traverse(ast, {
MustacheStatement: {
[eventName]: (node: AST.MustacheStatement) => {
- if (node.path.type === 'PathExpression' && node.path.parts[0] === 'z') {
+ if (node.path.type === 'PathExpression' && node.path.original === 'z') {
return null;
}
return;
@@ -45,7 +45,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating');
traverse(ast, {
MustacheStatement: {
[eventName](node: AST.MustacheStatement) {
- if (node.path.type === 'PathExpression' && node.path.parts[0] === 'z') {
+ if (node.path.type === 'PathExpression' && node.path.original === 'z') {
return [];
}
return;
@@ -63,7 +63,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating');
traverse(ast, {
MustacheStatement: {
[eventName](node: AST.MustacheStatement) {
- if (node.path.type === 'PathExpression' && node.path.parts[0] === 'z') {
+ if (node.path.type === 'PathExpression' && node.path.original === 'z') {
return b.mustache('a');
}
return;
@@ -82,7 +82,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating');
traverse(ast, {
MustacheStatement: {
[eventName](node: AST.MustacheStatement) {
- if (node.path.type === 'PathExpression' && node.path.parts[0] === 'z') {
+ if (node.path.type === 'PathExpression' && node.path.original === 'z') {
return [b.mustache('a')];
}
return;
@@ -106,7 +106,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating');
traverse(ast, {
MustacheStatement: {
[eventName](node: AST.MustacheStatement) {
- if (node.path.type === 'PathExpression' && node.path.parts[0] === 'z') {
+ if (node.path.type === 'PathExpression' && node.path.original === 'z') {
return [b.mustache('a'), b.mustache('b'), b.mustache('c')];
}
return;
@@ -125,7 +125,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating');
traverse(ast, {
MustacheStatement: {
[eventName](node: AST.MustacheStatement) {
- if (node.path.type === 'PathExpression' && node.path.parts[0] === 'y') {
+ if (node.path.type === 'PathExpression' && node.path.original === 'y') {
return null;
}
return;
@@ -142,7 +142,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating');
traverse(ast, {
MustacheStatement: {
[eventName](node: AST.MustacheStatement) {
- if (node.path.type === 'PathExpression' && node.path.parts[0] === 'y') {
+ if (node.path.type === 'PathExpression' && node.path.original === 'y') {
return [];
}
return;
@@ -159,7 +159,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating');
traverse(ast, {
MustacheStatement: {
[eventName](node: AST.MustacheStatement) {
- if (node.path.type === 'PathExpression' && node.path.parts[0] === 'y') {
+ if (node.path.type === 'PathExpression' && node.path.original === 'y') {
return b.mustache('a');
}
return;
@@ -178,7 +178,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating');
traverse(ast, {
MustacheStatement: {
[eventName](node: AST.MustacheStatement) {
- if (node.path.type === 'PathExpression' && node.path.parts[0] === 'y') {
+ if (node.path.type === 'PathExpression' && node.path.original === 'y') {
return [b.mustache('a')];
}
return;
@@ -198,7 +198,7 @@ QUnit.module('[glimmer-syntax] Traversal - manipulating');
traverse(ast, {
MustacheStatement: {
[eventName](node: AST.MustacheStatement) {
- if (node.path.type === 'PathExpression' && node.path.parts[0] === 'y') {
+ if (node.path.type === 'PathExpression' && node.path.original === 'y') {
return [b.mustache('a'), b.mustache('b'), b.mustache('c')];
}
return;
@@ -218,7 +218,7 @@ QUnit.test('Inside of a block', () => {
traverse(ast, {
MustacheStatement(node) {
- if (node.path.type === 'PathExpression' && node.path.parts[0] === 'y') {
+ if (node.path.type === 'PathExpression' && node.path.original === 'y') {
return [b.mustache('a'), b.mustache('b'), b.mustache('c')];
}
return;