diff --git a/.gitignore b/.gitignore index 7ea645bc14..529ce4972e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ node_modules/ **/node_modules/ **/.turbo +**/*.tsbuildinfo tmp/ /ts-dist/ **/ts-dist diff --git a/packages/@glimmer-workspace/integration-tests/test/syntax/general-errors-test.ts b/packages/@glimmer-workspace/integration-tests/test/syntax/general-errors-test.ts index 0c9ce0892a..d350fa210c 100644 --- a/packages/@glimmer-workspace/integration-tests/test/syntax/general-errors-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/syntax/general-errors-test.ts @@ -51,6 +51,22 @@ class SyntaxErrors extends RenderTest { ); } + @test + 'Block params in HTML syntax - requires a space between as and pipes'() { + this.assert.throws( + () => { + preprocess('foo', { meta: { moduleName: 'test-module' } }); + }, + syntaxErrorFor( + 'Invalid block parameters syntax: expecting at least one space character between "as" and "|"', + 'as|', + 'test-module', + 1, + 7 + ) + ); + } + @test 'Block params in HTML syntax - Throws exception if given zero parameters'() { this.assert.throws( @@ -58,11 +74,11 @@ class SyntaxErrors extends RenderTest { preprocess('foo', { meta: { moduleName: 'test-module' } }); }, syntaxErrorFor( - 'Cannot use zero block parameters', - 'foo', + 'Invalid block parameters syntax: empty parameters list, expecting at least one identifier', + 'as ||', 'test-module', 1, - 0 + 7 ) ); @@ -71,11 +87,108 @@ class SyntaxErrors extends RenderTest { preprocess('foo', { meta: { moduleName: 'test-module' } }); }, syntaxErrorFor( - 'Cannot use zero block parameters', - 'foo', + 'Invalid block parameters syntax: empty parameters list, expecting at least one identifier', + 'as | |', 'test-module', 1, - 0 + 7 + ) + ); + } + + @test + 'Block params in HTML syntax - invalid mustaches in block params list'() { + this.assert.throws( + () => { + preprocess('foo', { meta: { moduleName: 'test-module' } }); + }, + syntaxErrorFor( + 'Invalid block parameters syntax: mustaches cannot be used inside parameters list', + '{{foo}}', + 'test-module', + 1, + 11 + ) + ); + + this.assert.throws( + () => { + preprocess('foo', { meta: { moduleName: 'test-module' } }); + }, + syntaxErrorFor( + 'Invalid block parameters syntax: mustaches cannot be used inside parameters list', + '{{bar}}', + 'test-module', + 1, + 14 + ) + ); + + this.assert.throws( + () => { + preprocess('foo', { meta: { moduleName: 'test-module' } }); + }, + syntaxErrorFor( + 'Invalid block parameters syntax: mustaches cannot be used inside parameters list', + '{{bar}}', + 'test-module', + 1, + 15 + ) + ); + + this.assert.throws( + () => { + preprocess('foo', { meta: { moduleName: 'test-module' } }); + }, + syntaxErrorFor( + 'Invalid block parameters syntax: modifiers cannot follow parameters list', + '{{bar}}', + 'test-module', + 1, + 16 + ) + ); + } + + @test + 'Block params in HTML syntax - EOF in block params list'() { + this.assert.throws( + () => { + preprocess('" or "/>" after parameters list', + 'as |', + 'test-module', + 1, + 7 + ) + ); + + this.assert.throws( + () => { + preprocess('" or "/>" after parameters list', + 'as |foo', + 'test-module', + 1, + 7 + ) + ); + + this.assert.throws( + () => { + preprocess('" or "/>" after parameters list', + 'as |foo|', + 'test-module', + 1, + 7 ) ); } @@ -87,24 +200,26 @@ class SyntaxErrors extends RenderTest { preprocess('{{x}},{{y}}', { meta: { moduleName: 'test-module' } }); }, syntaxErrorFor( - "Invalid block parameters syntax, 'as |x y'", - '{{x}},{{y}}', + 'Invalid block parameters syntax: expecting "|" but the tag was closed prematurely', + 'as |x y>', 'test-module', 1, - 0 + 7 ) ); this.assert.throws( () => { - preprocess('{{x}},{{y}}', { meta: { moduleName: 'test-module' } }); + preprocess('{{x}},{{y}}', { + meta: { moduleName: 'test-module' }, + }); }, syntaxErrorFor( - "Invalid block parameters syntax, 'as |x| y'", - '{{x}},{{y}}', + 'Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list', + 'wat', 'test-module', 1, - 0 + 14 ) ); @@ -113,11 +228,11 @@ class SyntaxErrors extends RenderTest { preprocess('{{x}},{{y}}', { meta: { moduleName: 'test-module' } }); }, syntaxErrorFor( - "Invalid block parameters syntax, 'as |x| y|'", - '{{x}},{{y}}', + 'Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list', + 'y|', 'test-module', 1, - 0 + 14 ) ); } @@ -129,11 +244,11 @@ class SyntaxErrors extends RenderTest { preprocess('', { meta: { moduleName: 'test-module' } }); }, syntaxErrorFor( - "Invalid identifier for block parameters, 'foo.bar'", - '', + 'Invalid block parameters syntax: invalid identifier name `foo.bar`', + 'foo.bar', 'test-module', 1, - 0 + 13 ) ); @@ -141,7 +256,13 @@ class SyntaxErrors extends RenderTest { () => { preprocess('', { meta: { moduleName: 'test-module' } }); }, - syntaxErrorFor('" is not a valid character within attribute names', '', 'test-module', 1, 17) + syntaxErrorFor( + 'Invalid block parameters syntax: invalid identifier name `"foo"`', + '"foo"', + 'test-module', + 1, + 13 + ) ); this.assert.throws( @@ -149,11 +270,11 @@ class SyntaxErrors extends RenderTest { preprocess('', { meta: { moduleName: 'test-module' } }); }, syntaxErrorFor( - "Invalid identifier for block parameters, 'foo[bar]'", - '', + 'Invalid block parameters syntax: invalid identifier name `foo[bar]`', + 'foo[bar]', 'test-module', 1, - 0 + 11 ) ); } @@ -165,11 +286,11 @@ class SyntaxErrors extends RenderTest { preprocess('', { meta: { moduleName: 'test-module' } }); }, syntaxErrorFor( - 'Block parameters must be preceded by the `as` keyword, detected block parameters without `as`', - '', + 'Invalid block parameters syntax: block parameters must be preceded by the `as` keyword', + '|x|', 'test-module', 1, - 0 + 7 ) ); @@ -180,11 +301,11 @@ class SyntaxErrors extends RenderTest { }); }, syntaxErrorFor( - 'Block parameters must be preceded by the `as` keyword, detected block parameters without `as`', - '<:baz |x|>', + 'Invalid block parameters syntax: block parameters must be preceded by the `as` keyword', + '|x|', 'test-module', 1, - 7 + 13 ) ); } diff --git a/packages/@glimmer-workspace/tsconfig.tsbuildinfo b/packages/@glimmer-workspace/tsconfig.tsbuildinfo deleted file mode 100644 index b6e52e5a05..0000000000 --- a/packages/@glimmer-workspace/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"program":{"fileNames":["../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.webworker.importscripts.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.scripthost.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.esnext.intl.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/lib.es2020.full.d.ts","../../node_modules/.pnpm/rollup@3.21.5/node_modules/rollup/dist/rollup.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/assert.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/assert/strict.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/globals.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/async_hooks.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/buffer.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/child_process.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/cluster.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/console.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/constants.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/crypto.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/dgram.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/diagnostics_channel.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/dns.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/dns/promises.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/domain.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/dom-events.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/events.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/fs.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/fs/promises.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/http.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/http2.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/https.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/inspector.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/module.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/net.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/os.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/path.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/perf_hooks.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/process.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/punycode.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/querystring.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/readline.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/readline/promises.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/repl.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/stream.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/stream/promises.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/stream/consumers.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/stream/web.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/string_decoder.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/test.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/timers.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/timers/promises.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/tls.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/trace_events.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/tty.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/url.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/util.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/v8.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/vm.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/wasi.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/worker_threads.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/zlib.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/globals.global.d.ts","../../node_modules/.pnpm/@types+node@20.1.0/node_modules/@types/node/index.d.ts","../../node_modules/.pnpm/esbuild@0.17.18/node_modules/esbuild/lib/main.d.ts","../../node_modules/.pnpm/source-map-js@1.0.2/node_modules/source-map-js/source-map.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/comment.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/at-rule.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/rule.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/container.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/declaration.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/previous-map.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/input.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/css-syntax-error.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/warning.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/document.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/root.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/lazy-result.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/no-work-result.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/processor.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/result.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/node.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/list.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/postcss.d.ts","../../node_modules/.pnpm/postcss@8.4.23/node_modules/postcss/lib/postcss.d.mts","../../node_modules/.pnpm/vite@4.3.3_@types+node@20.1.0/node_modules/vite/dist/node/index.d.ts","../../node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib/typescript.d.ts","./@glimmer/build/lib/config.d.ts","./@glimmer/build/lib/import-meta.d.ts","./@glimmer/build/index.d.ts","./@glimmer/build/plugins.d.ts","./@glimmer/build/dist/index.d.ts","./@glimmer/build/lib/inline.d.ts","./@glimmer/build/lib/replace.d.ts","./@glimmer/vm-babel-plugins/dist/index.d.cts","./@glimmer/vm-babel-plugins/dist/index.d.ts","../@types/qunit/index.d.ts","../../node_modules/.pnpm/@babel+types@7.21.5/node_modules/@babel/types/lib/index.d.ts","../../node_modules/.pnpm/@types+babel__generator@7.6.4/node_modules/@types/babel__generator/index.d.ts","../../node_modules/.pnpm/@babel+parser@7.21.8/node_modules/@babel/parser/typings/babel-parser.d.ts","../../node_modules/.pnpm/@types+babel__template@7.4.1/node_modules/@types/babel__template/index.d.ts","../../node_modules/.pnpm/@types+babel__traverse@7.18.5/node_modules/@types/babel__traverse/index.d.ts","../../node_modules/.pnpm/@types+babel__core@7.20.0/node_modules/@types/babel__core/index.d.ts","../../node_modules/.pnpm/@types+babel-plugin-macros@3.1.0/node_modules/@types/babel-plugin-macros/index.d.ts","../../node_modules/.pnpm/@types+prettier@2.7.2/node_modules/@types/prettier/index.d.ts","../../node_modules/.pnpm/@types+preval.macro@3.0.0/node_modules/@types/preval.macro/index.d.ts","../../node_modules/.pnpm/devtools-protocol@0.0.1120988/node_modules/devtools-protocol/types/protocol.d.ts","../../node_modules/.pnpm/devtools-protocol@0.0.1120988/node_modules/devtools-protocol/types/protocol-mapping.d.ts","../../node_modules/.pnpm/puppeteer@20.1.2_typescript@5.0.4/node_modules/puppeteer/lib/types.d.ts","../../node_modules/.pnpm/vite@4.3.5_@types+node@20.1.0/node_modules/vite/dist/node/index.d.ts"],"fileInfos":[{"version":"6a6b471e7e43e15ef6f8fe617a22ce4ecb0e34efa6c3dfcfe7cebd392bcca9d2","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"dc48272d7c333ccf58034c0026162576b7d50ea0e69c3b9292f803fc20720fd5","impliedFormat":1},{"version":"27147504487dc1159369da4f4da8a26406364624fa9bc3db632f7d94a5bae2c3","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"fcd3ecc9f764f06f4d5c467677f4f117f6abf49dee6716283aa204ff1162498b","affectsGlobalScope":true,"impliedFormat":1},{"version":"9a60b92bca4c1257db03b349d58e63e4868cfc0d1c8d0ba60c2dbc63f4e6c9f6","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5c5565225fce2ede835725a92a28ece149f83542aa4866cfb10290bff7b8996","affectsGlobalScope":true,"impliedFormat":1},{"version":"7d2dbc2a0250400af0809b0ad5f84686e84c73526de931f84560e483eb16b03c","affectsGlobalScope":true,"impliedFormat":1},{"version":"f296963760430fb65b4e5d91f0ed770a91c6e77455bacf8fa23a1501654ede0e","affectsGlobalScope":true,"impliedFormat":1},{"version":"5114a95689b63f96b957e00216bc04baf9e1a1782aa4d8ee7e5e9acbf768e301","affectsGlobalScope":true,"impliedFormat":1},{"version":"4443e68b35f3332f753eacc66a04ac1d2053b8b035a0e0ac1d455392b5e243b3","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab22100fdd0d24cfc2cc59d0a00fc8cf449830d9c4030dc54390a46bd562e929","affectsGlobalScope":true,"impliedFormat":1},{"version":"f7bd636ae3a4623c503359ada74510c4005df5b36de7f23e1db8a5c543fd176b","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"0c20f4d2358eb679e4ae8a4432bdd96c857a2960fd6800b21ec4008ec59d60ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"36ae84ccc0633f7c0787bc6108386c8b773e95d3b052d9464a99cd9b8795fbec","affectsGlobalScope":true,"impliedFormat":1},{"version":"82d0d8e269b9eeac02c3bd1c9e884e85d483fcb2cd168bccd6bc54df663da031","affectsGlobalScope":true,"impliedFormat":1},{"version":"b8deab98702588840be73d67f02412a2d45a417a3c097b2e96f7f3a42ac483d1","affectsGlobalScope":true,"impliedFormat":1},{"version":"4738f2420687fd85629c9efb470793bb753709c2379e5f85bc1815d875ceadcd","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"376d554d042fb409cb55b5cbaf0b2b4b7e669619493c5d18d5fa8bd67273f82a","affectsGlobalScope":true,"impliedFormat":1},{"version":"9fc46429fbe091ac5ad2608c657201eb68b6f1b8341bd6d670047d32ed0a88fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"61c37c1de663cf4171e1192466e52c7a382afa58da01b1dc75058f032ddf0839","affectsGlobalScope":true,"impliedFormat":1},{"version":"c4138a3dd7cd6cf1f363ca0f905554e8d81b45844feea17786cdf1626cb8ea06","affectsGlobalScope":true,"impliedFormat":1},{"version":"6ff3e2452b055d8f0ec026511c6582b55d935675af67cdb67dd1dc671e8065df","affectsGlobalScope":true,"impliedFormat":1},{"version":"03de17b810f426a2f47396b0b99b53a82c1b60e9cba7a7edda47f9bb077882f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"8184c6ddf48f0c98429326b428478ecc6143c27f79b79e85740f17e6feb090f1","affectsGlobalScope":true,"impliedFormat":1},{"version":"261c4d2cf86ac5a89ad3fb3fafed74cbb6f2f7c1d139b0540933df567d64a6ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"6af1425e9973f4924fca986636ac19a0cf9909a7e0d9d3009c349e6244e957b6","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"15a630d6817718a2ddd7088c4f83e4673fde19fa992d2eae2cf51132a302a5d3","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7e9f95a7387e3f66be0ed6db43600c49cec33a3900437ce2fd350d9b7cb16f2","affectsGlobalScope":true,"impliedFormat":1},{"version":"01e0ee7e1f661acedb08b51f8a9b7d7f959e9cdb6441360f06522cc3aea1bf2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac17a97f816d53d9dd79b0d235e1c0ed54a8cc6a0677e9a3d61efb480b2a3e4e","affectsGlobalScope":true,"impliedFormat":1},{"version":"bf14a426dbbf1022d11bd08d6b8e709a2e9d246f0c6c1032f3b2edb9a902adbe","affectsGlobalScope":true,"impliedFormat":1},{"version":"ec0104fee478075cb5171e5f4e3f23add8e02d845ae0165bfa3f1099241fa2aa","affectsGlobalScope":true,"impliedFormat":1},{"version":"2b72d528b2e2fe3c57889ca7baef5e13a56c957b946906d03767c642f386bbc3","affectsGlobalScope":true,"impliedFormat":1},{"version":"9cc66b0513ad41cb5f5372cca86ef83a0d37d1c1017580b7dace3ea5661836df","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"307c8b7ebbd7f23a92b73a4c6c0a697beca05b06b036c23a34553e5fe65e4fdc","affectsGlobalScope":true,"impliedFormat":1},{"version":"189c0703923150aa30673fa3de411346d727cc44a11c75d05d7cf9ef095daa22","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"322cc0ca9c311414642c0d7ef3b57beedbac198ca074e3e109a4be4c366dcb81","impliedFormat":1},{"version":"092d6ef5196e5c6487bcf68c86b8ce950cc6538e39d3d530ed05bc2b6d262562","impliedFormat":1},{"version":"66a6e7b6e32eea099615d627028ed0d681679e31237df03e68e1b912e3e61da0","impliedFormat":1},{"version":"a69c09dbea52352f479d3e7ac949fde3d17b195abe90b045d619f747b38d6d1a","impliedFormat":1},{"version":"f749812878fecfa53cfc13b36e5d35086fb6377983a9df44175da83ccc23af1f","affectsGlobalScope":true,"impliedFormat":1},{"version":"fd536fe411f46ae62c4729e342a3494adbdd54335e41039c238170c9781da9ff","impliedFormat":1},{"version":"772ff00e189d93488d1ca6f0f4dfba77bd090a99ed31e19a14bc16fad4078e48","affectsGlobalScope":true,"impliedFormat":1},{"version":"609fe91e93a4932286a0906c2afbd78f7d6fe5050d6ed5866c03f1c6da8df579","impliedFormat":1},{"version":"ac0c7cb0a4c1bec60be2c0460b3bda1e674eaf2c98312d7b6f16a0bb58718e2d","impliedFormat":1},{"version":"7e2181a6fc140b4525d5a45c204477c37fa78a635558e88552c68f76a4325403","affectsGlobalScope":true,"impliedFormat":1},{"version":"82408ed3e959ddc60d3e9904481b5a8dc16469928257af22a3f7d1a3bc7fd8c4","impliedFormat":1},{"version":"6a6bfa45d3ed96dea1ad653444fb26ddaa9245d2e2d6ddaab050cb9a2c0936d7","impliedFormat":1},{"version":"276b547eeb8eeeee9a446a3bfa6e07d1c0199269bdcf33813abab1281394a9cb","impliedFormat":1},{"version":"c999f7816955f75b447823e3e4085f699600e2a4a3327907df9af65b0a9c0df6","impliedFormat":1},{"version":"958df89fdcf5555cdfd57a14037e05505048dd29c59e8009bea279b65523b241","impliedFormat":1},{"version":"ffa8324a4c0611f9a50262fb36f097eeabe0fa0d71755aa67156da13cde1b932","impliedFormat":1},{"version":"9b814e0806922056145bedb096f11b73bdce70cc871f3ccfc4ce00b2cba75718","impliedFormat":1},{"version":"6b526a5ec4a401ca7c26cfe6a48e641d8f30af76673bad3b06a1b4504594a960","affectsGlobalScope":true,"impliedFormat":1},{"version":"be3d24f3fd8c85fedd91485b207d180f14ac2602805b52f6a33462ada14c27b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"e58cfbe7db26c2077027662ef6c87a3cf9d721717e77ecaa98c4d8d7911b3299","impliedFormat":1},{"version":"2ad6a251b6ef19fd1f8498f83bb7b265033bd52287e1f6569d09544f18806713","impliedFormat":1},{"version":"bfa08f2c30c475aef1c9451855ba6b2acfdc64f61950a38fae75806d66fb85c2","impliedFormat":1},{"version":"159807eb55a9439f9a675bd493788190a6203b5f36c315f8c3acbfcb875c7072","impliedFormat":1},{"version":"fe31b2b31ac5453fc7b8eef32b62017e55b214ceb884f0b177f442af92e84682","impliedFormat":1},{"version":"dd72576c8ea64d55af46a386068503d3cfcecce84ed7e1cbd4ff4081ba67fafc","impliedFormat":1},{"version":"125af9d85cb9d5e508353f10a8d52f01652d2d48b2cea54789a33e5b4d289c1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"70a7e8a7880d55396285e4b85ff5bdf3af3083176abe07f944967836f2a43188","impliedFormat":1},{"version":"3570df7c6f3a976109f55b596a2d88c2f87e0574cd1502272594ee5c4e56d0ef","impliedFormat":1},{"version":"850e95721334c2aa7697b08782f443ec4286274e5024169d4443933544f359d7","impliedFormat":1},{"version":"74e6cd21f7b5e29fab05060ea24e2b90aa254f16f3f62ccd7055bdb8fc7b2ff5","affectsGlobalScope":true,"impliedFormat":1},{"version":"5761c90b0cabdd6bd1f5fb1c3bf942088fdd39e18ed35dbe39b0c34bc733bf13","affectsGlobalScope":true,"impliedFormat":1},{"version":"1eb6c5569d41e6021832d0a8a71f45fecbc13d03ad7d306da485999148b64955","impliedFormat":1},{"version":"c05ef0ecf06540ad3039515c10d3b27f9380639ced40f4093fd073e1b5ff21d9","impliedFormat":1},{"version":"c774096c5935712de41c763e3c08a04b7a788a85fb07a0c2df23fb5ace3bc610","impliedFormat":1},{"version":"2b847f2aba41dcd69677684d5d300c08aea4e306c305fd39bf548cef19f03cfe","impliedFormat":1},{"version":"f4c0cbc93eb51fd61e07a86111739770dd524f6297bd83266ff004aec553e085","impliedFormat":1},{"version":"9a134dbb29f0af914d90b23f609b39019d66ed53db7d492ab6b04c67114559da","impliedFormat":1},{"version":"1b952304137851e45bc009785de89ada562d9376177c97e37702e39e60c2f1ff","impliedFormat":1},{"version":"785e5be57d4f20f290a20e7b0c6263f6c57fd6e51283050756cef07d6d651c68","impliedFormat":1},{"version":"44b8b584a338b190a59f4f6929d072431950c7bd92ec2694821c11bce180c8a5","impliedFormat":1},{"version":"7b3781fbdfddbee8dba55ccee5aa74a7c8d6701ade11d49ab7d8cb1fcefe669e","impliedFormat":1},{"version":"c4aab2ec3a249f2a4caa9cbdb099752a80daf999b79d85aa3504cdfd6e559476","impliedFormat":1},{"version":"2cff47e15621f3055af1df1547426f833c9d0a571750c4f0659836f45c50fe0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"ad08154d9602429522cac965a715fde27d421d69b24756c5d291877dda75353e","impliedFormat":1},{"version":"c764a6cf523d13f2304a23216cd1084e28c041eebabd8aa9b2a9d99866c668c0","impliedFormat":1},{"version":"1272a5c2bd05961adc473e905332b7a422b00485c10b41c752f7fcf6835e3436","impliedFormat":1},{"version":"30ef92bf8135ce36ba1231fe41715276f2a40be72a478ddeb862bc16672e8680","impliedFormat":1},{"version":"4ace0a30a70fe5963442d75ea6e69f525671ae76f6e57ab7556c44839b4237e8","affectsGlobalScope":true,"impliedFormat":1},{"version":"a6f03dbf03c001fb3ac1c9bea6dde049dfff27ef8886cc4a886374aacf2e997d","affectsGlobalScope":true,"impliedFormat":1},{"version":"66bfb3de947abf4b117ee849c245425dbe494d6903e28f9ded566e91c9d05d77","impliedFormat":1},{"version":"c28d4f58131b93d60e087b86148d4e0c9d9b5c49c23ff1a9d1a9594fdedd5d08","impliedFormat":1},{"version":"c6b5d7f259544c91024ecf2b17138574a3f6ff2476468fafd7f957d2b68d6d98","impliedFormat":1},{"version":"1ec27c4b695590464113276d174f873e260e468ef226b7dc18f9193875fa559d","affectsGlobalScope":true,"impliedFormat":1},{"version":"33da4ee2ab9fdd9ef8b3fc526d871ce02ae8c825283f5695e7cad507c087b97c","impliedFormat":1},{"version":"ab9b9a36e5284fd8d3bf2f7d5fcbc60052f25f27e4d20954782099282c60d23e","affectsGlobalScope":true,"impliedFormat":1},{"version":"71709584ed5ed7d236dc225441ec4634ffc6c718853e04b9c27b9ea121459044","impliedFormat":1},{"version":"99d951629f7096dcd79adbaa83a85e3be57613005533bd23029b3aba4ce9383e","impliedFormat":1},{"version":"858d0d831826c6eb563df02f7db71c90e26deadd0938652096bea3cc14899700","impliedFormat":1},{"version":"ae128dd11edfae24d9dfc6bc60390e488cec3792e4dd80115e422b5c2e1524cf","impliedFormat":1},{"version":"cef246ba183aff9bdb98831690ffcd1603655ea23ce38252e3db3ce3e06d262c","impliedFormat":1},{"version":"9d110b91e4c091ceee9d360ec425737580fd0d531bdb2d4f77e203c520d4da4c","impliedFormat":1},{"version":"96826ee13c04e92150ae68c6d89bf6ab7970abeca6e1a41cc59c5dcacefb4526","impliedFormat":1},{"version":"cb81640e895111bdf71201045f0f6664663cb0120e05a03f91e978b6a4ea2dd7","impliedFormat":1},{"version":"0d598aeda14bcdaa823b05a043eb7c18045c0a17fc3594d2387a11c09a5ff64b","impliedFormat":1},{"version":"be0db7e6060868cbabb02b3400fbaad4471d0815ce49d1b0f1e027296017b710","impliedFormat":1},{"version":"dbdec414a2de11b47a0bc8e2a0a93397235878ca7e6c02b62570e0a72faa878e","impliedFormat":1},{"version":"d9f79888a9ecee73c3cb33ae3eaaacb973dc96d0537599300d3dcb50ed1a1cf3","impliedFormat":1},{"version":"7ad14170a7f1f59dbe90ca3747699396f4a320921c7c560aa53f05022221df13","impliedFormat":1},{"version":"096c2a810f50f8a8b5bbb336ee32476d7882ffd93b2ee50ec5d4c1a9d5c209cf","impliedFormat":1},{"version":"6be9369a41da87241c164021631f20fe4706daf9f82a33c754974c6f4eade3cd","impliedFormat":1},{"version":"2eb26714666406e7c6c3ad3482da148781845b50a354a351d28bd5dba1a3f88a","impliedFormat":1},{"version":"022a6fbd269120763350030e66333f8c084a328ce7d6f6414d9cd49d834cbc77","impliedFormat":1},{"version":"4f85fea72dfeb24be05b650d680a21bda8324430bbfb437af9452630c9c740c1","impliedFormat":1},{"version":"179bfe3e835098360fdfa7f1c139c01b83c00410dccac214b3c71f16d949044b","impliedFormat":1},{"version":"18af117b1a77567a980c6ab6611be835e809300af6b511a2093208dc14ad2f7c","impliedFormat":1},{"version":"59345c3c2297677cb323f5f70df756776ae8eec2c97e6254e0739f711ab39131","impliedFormat":1},{"version":"77926a706478940016e826b162f95f8e4077b1ad3184b2592dc03bd8b33e0384","impliedFormat":99},{"version":"745dc480fe54b1c4f0207d9919020656218dfdab72b99ac5ef3b94666f1b8696","impliedFormat":99},{"version":"21c0786dddf52537611499340166278507eb9784628d321c2cb6acc696cba0f6","impliedFormat":1},{"version":"f57556e0e2930e617d5d9c441e40d3f32290a02c62ab1f1360ec4db52abdbf3a","impliedFormat":99},{"version":"b0e48ebeadb1bb0b4800ba7d4bcfb82135b3aeb088f7b20fe14d5ee9604b8656","impliedFormat":99},{"version":"8660efeb8b1f66e01a1a544d840fe8b58841c449fcdbb429687f20aee03509b9","impliedFormat":99},{"version":"4b6db46b7acd3e3e7c02288e2abbf62535814d776933bfc0764a46cb4e116951","impliedFormat":99},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":99},{"version":"0387301ca9e16c7b2b3e728f29e749b5415324b3700039a50758069a984d11a2","impliedFormat":99},{"version":"a6192dc1f77521c47fd57ffdb6ad5c6a920ab79641f0f61444720ac6d52227d6","impliedFormat":99},{"version":"649edc362664fa7aef2a923e62dd7a92d477cb486bdaff4577f33277259e9684","impliedFormat":1},{"version":"3835f8d92ad0699690cf572ad0da8aa3bfa5cc1c66fbd2609c52a02b9da828dc","impliedFormat":1},{"version":"314c601bedf7ae241feb4b274e519b9dd3f63fe22f7950c8a54f3dd81fda1ccf","affectsGlobalScope":true,"impliedFormat":1},{"version":"a20fc1fcd9cd7c2b79d5f00d14802c1d58c3848e09ee4f84b350591af88b7636","impliedFormat":1},{"version":"cc957354aa3c94c9961ebf46282cfde1e81d107fc5785a61f62c67f1dd3ac2eb","impliedFormat":1},{"version":"b4f76b34637d79cefad486127115fed843762c69512d7101b7096e1293699679","impliedFormat":1},{"version":"93de1c6dab503f053efe8d304cb522bb3a89feab8c98f307a674a4fae04773e9","impliedFormat":1},{"version":"dae3d1adc67ac3dbd1cd471889301339ec439837b5df565982345be20c8fca9a","impliedFormat":1},{"version":"b6ddf3a46ccfa4441d8be84d2e9bf3087573c48804196faedbd4a25b60631beb","impliedFormat":1},{"version":"7b7df5ade976f0049e32a75339dd1c0632da161c5168dda539b93d0f1c1911a3","impliedFormat":1},{"version":"bc88e4049153bc4dddb4503ed7e624eb141edfa9064b3659d6c86e900fe9e621","impliedFormat":1},{"version":"eee3f58de215609efb8e04b332b1df37519f204ee3fffaf1fa4bf0cd100eff18","impliedFormat":1},{"version":"757c84e91e2abbeec53b7884ebd7b524d32f1e93ceaf206f7ba4123bb57f3a11","impliedFormat":1},{"version":"2d03c4fb7c546057a09d18fc7cb0f09798978eb59ac7dde72cddf948d935fd91","impliedFormat":1},{"version":"9740fbc194086bf11aaee58266e503a5f79436d9490cbae2b93bc974aae4d320","impliedFormat":1}],"root":[[127,135]],"options":{"composite":true,"exactOptionalPropertyTypes":true,"module":199,"noImplicitOverride":true,"noPropertyAccessFromIndexSignature":true,"noUncheckedIndexedAccess":true,"skipLibCheck":true,"strict":true,"suppressImplicitAnyIndexErrors":false,"target":7,"useDefineForClassFields":false},"fileIdsList":[[96,137],[96],[96,142],[96,137,138,139,140,141],[96,137,139],[50,96],[53,96],[54,59,87,96],[55,66,67,74,84,95,96],[55,56,66,74,96],[57,96],[58,59,67,75,96],[59,84,92,96],[60,62,66,74,96],[61,96],[62,63,96],[66,96],[64,66,96],[66,67,68,84,95,96],[66,67,68,81,84,87,96],[96,100],[62,66,69,74,84,95,96],[66,67,69,70,74,84,92,95,96],[69,71,84,92,95,96],[50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102],[66,72,96],[73,95,96],[62,66,74,84,96],[75,96],[76,96],[53,77,96],[78,94,96,100],[79,96],[80,96],[66,81,82,96],[81,83,96,98],[54,66,84,85,86,87,96],[54,84,86,96],[84,85,96],[87,96],[88,96],[84,96],[66,90,91,96],[90,91,96],[59,74,84,92,96],[93,96],[74,94,96],[54,69,80,95,96],[59,96],[84,96,97],[96,98],[96,99],[54,59,66,68,77,84,95,96,98,100],[84,96,101],[96,146],[96,109],[96,109,121],[96,106,107,108,110,121],[96,112],[96,109,116,120,123],[96,111,123],[96,114,116,119,120,123],[96,114,116,117,119,120,123],[96,106,107,108,109,110,112,113,114,115,116,120,123],[96,123],[96,105,106,107,108,109,110,112,113,114,115,116,117,119,120,121,122],[96,105,123],[96,116,117,118,120,123],[96,119,123],[96,109,115,120,123],[96,113,121],[55,84,96,103,146,147],[96,105],[49,66,67,69,71,74,84,92,95,96,101,103,104,124],[96,127,128],[49,96,125,126],[49,96],[49,96,126,149]],"referencedMap":[[139,1],[137,2],[143,3],[142,4],[138,1],[140,5],[141,1],[50,6],[51,6],[53,7],[54,8],[55,9],[56,10],[57,11],[58,12],[59,13],[60,14],[61,15],[62,16],[63,16],[65,17],[64,18],[66,17],[67,19],[68,20],[52,21],[102,2],[69,22],[70,23],[71,24],[103,25],[72,26],[73,27],[74,28],[75,29],[76,30],[77,31],[78,32],[79,33],[80,34],[81,35],[82,35],[83,36],[84,37],[86,38],[85,39],[87,40],[88,41],[89,42],[90,43],[91,44],[92,45],[93,46],[94,47],[95,48],[96,49],[97,50],[98,51],[99,52],[100,53],[101,54],[144,2],[145,2],[147,55],[146,2],[104,2],[107,56],[106,57],[109,58],[113,59],[110,57],[115,60],[112,61],[117,62],[122,2],[118,63],[121,64],[124,65],[123,66],[111,67],[119,68],[120,69],[116,70],[108,56],[114,71],[148,72],[49,2],[105,73],[46,2],[47,2],[8,2],[9,2],[13,2],[12,2],[2,2],[14,2],[15,2],[16,2],[17,2],[18,2],[19,2],[20,2],[21,2],[3,2],[4,2],[25,2],[22,2],[23,2],[24,2],[26,2],[27,2],[28,2],[5,2],[29,2],[30,2],[31,2],[32,2],[6,2],[36,2],[33,2],[34,2],[35,2],[37,2],[7,2],[38,2],[48,2],[43,2],[44,2],[39,2],[40,2],[41,2],[42,2],[1,2],[45,2],[11,2],[10,2],[126,2],[125,74],[136,2],[131,2],[129,75],[127,76],[128,77],[132,77],[133,77],[130,2],[134,2],[135,2]],"exportedModulesMap":[[139,1],[137,2],[143,3],[142,4],[138,1],[140,5],[141,1],[50,6],[51,6],[53,7],[54,8],[55,9],[56,10],[57,11],[58,12],[59,13],[60,14],[61,15],[62,16],[63,16],[65,17],[64,18],[66,17],[67,19],[68,20],[52,21],[102,2],[69,22],[70,23],[71,24],[103,25],[72,26],[73,27],[74,28],[75,29],[76,30],[77,31],[78,32],[79,33],[80,34],[81,35],[82,35],[83,36],[84,37],[86,38],[85,39],[87,40],[88,41],[89,42],[90,43],[91,44],[92,45],[93,46],[94,47],[95,48],[96,49],[97,50],[98,51],[99,52],[100,53],[101,54],[144,2],[145,2],[147,55],[146,2],[104,2],[107,56],[106,57],[109,58],[113,59],[110,57],[115,60],[112,61],[117,62],[122,2],[118,63],[121,64],[124,65],[123,66],[111,67],[119,68],[120,69],[116,70],[108,56],[114,71],[148,72],[49,2],[105,73],[46,2],[47,2],[8,2],[9,2],[13,2],[12,2],[2,2],[14,2],[15,2],[16,2],[17,2],[18,2],[19,2],[20,2],[21,2],[3,2],[4,2],[25,2],[22,2],[23,2],[24,2],[26,2],[27,2],[28,2],[5,2],[29,2],[30,2],[31,2],[32,2],[6,2],[36,2],[33,2],[34,2],[35,2],[37,2],[7,2],[38,2],[48,2],[43,2],[44,2],[39,2],[40,2],[41,2],[42,2],[1,2],[45,2],[11,2],[10,2],[126,2],[125,74],[136,2],[131,2],[129,75],[127,78],[128,77],[132,77],[133,77],[130,2],[134,2],[135,2]],"semanticDiagnosticsPerFile":[139,137,143,142,138,140,141,50,51,53,54,55,56,57,58,59,60,61,62,63,65,64,66,67,68,52,102,69,70,71,103,72,73,74,75,76,77,78,79,80,81,82,83,84,86,85,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,144,145,147,146,104,107,106,109,113,110,115,112,117,122,118,121,124,123,111,119,120,116,108,114,148,49,105,46,47,8,9,13,12,2,14,15,16,17,18,19,20,21,3,4,25,22,23,24,26,27,28,5,29,30,31,32,6,36,33,34,35,37,7,38,48,43,44,39,40,41,42,1,45,11,10,126,125,136,131,129,127,128,132,133,130,134,135],"latestChangedDtsFile":"../../ts-dist/build/@glimmer/vm-babel-plugins/test/fixtures/explicit-true/output.d.ts"},"version":"5.0.4"} \ No newline at end of file diff --git a/packages/@glimmer/syntax/lib/generation/printer.ts b/packages/@glimmer/syntax/lib/generation/printer.ts index 535edad7ee..c8327485b0 100644 --- a/packages/@glimmer/syntax/lib/generation/printer.ts +++ b/packages/@glimmer/syntax/lib/generation/printer.ts @@ -96,7 +96,6 @@ export default class Printer { switch (node.type) { case 'MustacheStatement': case 'BlockStatement': - case 'PartialStatement': case 'MustacheCommentStatement': case 'CommentStatement': case 'TextNode': @@ -113,8 +112,6 @@ export default class Printer { case 'PathExpression': case 'SubExpression': return this.Expression(node); - case 'Program': - return this.Block(node); case 'ConcatStatement': // should have an AttrNode parent return this.ConcatStatement(node); @@ -163,8 +160,6 @@ export default class Printer { return this.MustacheStatement(statement); case 'BlockStatement': return this.BlockStatement(statement); - case 'PartialStatement': - return this.PartialStatement(statement); case 'MustacheCommentStatement': return this.MustacheCommentStatement(statement); case 'CommentStatement': @@ -174,15 +169,20 @@ export default class Printer { case 'ElementNode': return this.ElementNode(statement); case 'Block': - case 'Template': return this.Block(statement); + case 'Template': + return this.Template(statement); case 'AttrNode': // should have element return this.AttrNode(statement); } } - Block(block: ASTv1.Block | ASTv1.Program | ASTv1.Template): void { + Template(template: ASTv1.Template): void { + this.TopLevelStatements(template.body); + } + + Block(block: ASTv1.Block): void { /* When processing a template like: @@ -320,7 +320,7 @@ export default class Printer { return; } - this.buffer += mustache.escaped ? '{{' : '{{{'; + this.buffer += mustache.trusting ? '{{{' : '{{'; if (mustache.strip.open) { this.buffer += '~'; @@ -334,7 +334,7 @@ export default class Printer { this.buffer += '~'; } - this.buffer += mustache.escaped ? '}}' : '}}}'; + this.buffer += mustache.trusting ? '}}}' : '}}'; } BlockStatement(block: ASTv1.BlockStatement): void { @@ -385,18 +385,6 @@ export default class Printer { this.buffer += ` as |${blockParams.join(' ')}|`; } - PartialStatement(partial: ASTv1.PartialStatement): void { - if (this.handledByOverride(partial)) { - return; - } - - this.buffer += '{{>'; - this.Expression(partial.name); - this.Params(partial.params); - this.Hash(partial.hash); - this.buffer += '}}'; - } - ConcatStatement(concat: ASTv1.ConcatStatement): void { if (this.handledByOverride(concat)) { return; diff --git a/packages/@glimmer/syntax/lib/parser.ts b/packages/@glimmer/syntax/lib/parser.ts index a73410ad5b..449b25e4c9 100644 --- a/packages/@glimmer/syntax/lib/parser.ts +++ b/packages/@glimmer/syntax/lib/parser.ts @@ -11,19 +11,28 @@ import type * as ASTv1 from './v1/api'; import type * as HBS from './v1/handlebars-ast'; export type ParserNodeBuilder = Omit & { - loc: src.SourceOffset; + start: src.SourceOffset; }; -export interface Tag { - readonly type: T; +export interface StartTag { + readonly type: 'StartTag'; name: string; + nameStart: Nullable; + nameEnd: Nullable; readonly attributes: ASTv1.AttrNode[]; readonly modifiers: ASTv1.ElementModifierStatement[]; readonly comments: ASTv1.MustacheCommentStatement[]; + readonly params: ASTv1.VarHead[]; selfClosing: boolean; readonly loc: src.SourceSpan; } +export interface EndTag { + readonly type: 'EndTag'; + name: string; + readonly loc: src.SourceSpan; +} + export interface Attribute { name: string; currentPart: ASTv1.TextNode | null; @@ -42,9 +51,9 @@ export abstract class Parser { public currentNode: Nullable< Readonly< | ParserNodeBuilder - | ASTv1.TextNode - | ParserNodeBuilder> - | ParserNodeBuilder> + | ParserNodeBuilder + | ParserNodeBuilder + | ParserNodeBuilder > > = null; public tokenizer: EventedTokenizer; @@ -70,12 +79,14 @@ export abstract class Parser { finish(node: ParserNodeBuilder): T { return assign({}, node, { - loc: node.loc.until(this.offset()), + loc: node.start.until(this.offset()), } as const) as unknown as T; // node.loc = node.loc.withEnd(end); } + abstract parse(node: HBS.Program, locals: string[]): ASTv1.Template; + abstract Program(node: HBS.Program): HBS.Output<'Program'>; abstract MustacheStatement(node: HBS.MustacheStatement): HBS.Output<'MustacheStatement'>; abstract Decorator(node: HBS.Decorator): HBS.Output<'Decorator'>; @@ -119,19 +130,19 @@ export abstract class Parser { return expect(this.currentAttribute, 'expected attribute'); } - get currentTag(): ParserNodeBuilder> { + get currentTag(): ParserNodeBuilder | ParserNodeBuilder { let node = this.currentNode; assert(node && (node.type === 'StartTag' || node.type === 'EndTag'), 'expected tag'); return node; } - get currentStartTag(): ParserNodeBuilder> { + get currentStartTag(): ParserNodeBuilder { let node = this.currentNode; assert(node && node.type === 'StartTag', 'expected start tag'); return node; } - get currentEndTag(): ParserNodeBuilder> { + get currentEndTag(): ParserNodeBuilder { let node = this.currentNode; assert(node && node.type === 'EndTag', 'expected end tag'); return node; @@ -143,18 +154,12 @@ export abstract class Parser { return node; } - get currentData(): ASTv1.TextNode { + get currentData(): ParserNodeBuilder { let node = this.currentNode; assert(node && node.type === 'TextNode', 'expected a text node'); return node; } - acceptTemplate(node: HBS.Program): ASTv1.Template { - return this[node.type as 'Program'](node) as ASTv1.Template; - } - - acceptNode(node: HBS.Program): ASTv1.Block | ASTv1.Template; - acceptNode(node: HBS.Node): U; acceptNode(node: HBS.Node): HBS.Output { return (this[node.type as T] as (node: HBS.Node) => HBS.Output)(node); } diff --git a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts index c11b06a1b0..6f7a620bb6 100644 --- a/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts +++ b/packages/@glimmer/syntax/lib/parser/handlebars-node-visitors.ts @@ -1,71 +1,104 @@ import type { Nullable, Recast } from '@glimmer/interfaces'; import type { TokenizerState } from 'simple-html-tokenizer'; -import { getLast, isPresentArray, unwrap } from '@glimmer/util'; +import { assert, getLast, isPresentArray, unwrap } from '@glimmer/util'; -import type { ParserNodeBuilder, Tag } from '../parser'; +import type { ParserNodeBuilder, StartTag } from '../parser'; +import type { SourceOffset, SourceSpan } from '../source/span'; import type * as ASTv1 from '../v1/api'; import type * as HBS from '../v1/handlebars-ast'; import { Parser } from '../parser'; import { NON_EXISTENT_LOCATION } from '../source/location'; import { generateSyntaxError } from '../syntax-error'; -import { appendChild, isHBSLiteral, parseProgramBlockParamsLocs, printLiteral } from '../utils'; -import { PathExpressionImplV1 } from '../v1/legacy-interop'; +import { appendChild, isHBSLiteral, printLiteral } from '../utils'; import b from '../v1/parser-builders'; -const BEFORE_ATTRIBUTE_NAME = 'beforeAttributeName' as TokenizerState; -const ATTRIBUTE_VALUE_UNQUOTED = 'attributeValueUnquoted' as TokenizerState; +const BEFORE_ATTRIBUTE_NAME = 'beforeAttributeName' as TokenizerState.beforeAttributeName; +const ATTRIBUTE_VALUE_UNQUOTED = 'attributeValueUnquoted' as TokenizerState.attributeValueUnquoted; + +export interface PendingError { + mustache(span: SourceSpan): never; + eof(offset: SourceOffset): never; +} export abstract class HandlebarsNodeVisitors extends Parser { + // Because we interleave the HTML and HBS parsing, sometimes the HTML + // tokenizer can run out of tokens when we switch into {{...}} or reached + // EOF. There are positions where neither of these are expected, and it would + // like to generate an error, but there is no span to attach the error to. + // This allows the HTML tokenization to stash an error message and the next + // mustache visitor will attach the message to the appropriate span and throw + // the error. + protected pendingError: Nullable = null; + abstract override appendToCommentData(s: string): void; abstract override beginAttributeValue(quoted: boolean): void; abstract override finishAttributeValue(): void; - private get isTopLevel() { - return this.elementStack.length === 0; + parse(program: HBS.Program, blockParams: string[]): ASTv1.Template { + let node = b.template({ + body: [], + blockParams, + loc: this.source.spanFor(program.loc), + }); + + let template = this.parseProgram(node, program); + + // TODO: we really need to verify that the tokenizer is in an acceptable + // state when we are "done" parsing. For example, right now, `(node: T, program: HBS.Program): T { + if (program.body.length === 0) { + return node; } - for (i = 0; i < l; i++) { - this.acceptNode(unwrap(program.body[i])); + let poppedNode; + + try { + this.elementStack.push(node); + + for (let child of program.body) { + this.acceptNode(child); + } + } finally { + poppedNode = this.elementStack.pop(); } // Ensure that that the element stack is balanced properly. - const poppedNode = this.elementStack.pop(); - if (poppedNode !== node) { - const elementNode = poppedNode as ASTv1.ElementNode; - - throw generateSyntaxError(`Unclosed element \`${elementNode.tag}\``, elementNode.loc); + if (node !== poppedNode) { + if (poppedNode?.type === 'ElementNode') { + throw generateSyntaxError(`Unclosed element \`${poppedNode.tag}\``, poppedNode.loc); + } else { + // If the stack is not balanced, then it is likely our own bug, because + // any unclosed Handlebars blocks should already been caught by now + assert(poppedNode !== undefined, '[BUG] empty parser elementStack'); + assert(false, `[BUG] mismatched parser elementStack node: ${node.type}`); + } } return node; @@ -85,6 +118,65 @@ export abstract class HandlebarsNodeVisitors extends Parser { } const { path, params, hash } = acceptCallNodes(this, block); + const loc = this.source.spanFor(block.loc); + + // Backfill block params loc for the default block + let blockParams: ASTv1.VarHead[] = []; + + if (block.program.blockParams?.length) { + // Start from right after the hash + let span = hash.loc.collapse('end'); + + // Extend till the beginning of the block + if (block.program.loc) { + span = span.withEnd(this.source.spanFor(block.program.loc).getStart()); + } else if (block.program.body[0]) { + span = span.withEnd(this.source.spanFor(block.program.body[0].loc).getStart()); + } else { + // ...or if all else fail, use the end of the block statement + // this can only happen if the block statement is empty anyway + span = span.withEnd(loc.getEnd()); + } + + // Now we have a span for something like this: + // + // {{#foo bar baz=bat as |wow wat|}} + // ~~~~~~~~~~~~~~~ + // + // Or, if we are unlucky: + // + // {{#foo bar baz=bat as |wow wat|}}{{/foo}} + // ~~~~~~~~~~~~~~~~~~~~~~~ + // + // Either way, within this span, there should be exactly two pipes + // fencing our block params, neatly whitespace separated and with + // legal identifiers only + const content = span.asString(); + let skipStart = content.indexOf('|') + 1; + const limit = content.indexOf('|', skipStart); + + for (const name of block.program.blockParams) { + let nameStart: number; + let loc: SourceSpan; + + if (skipStart >= limit) { + nameStart = -1; + } else { + nameStart = content.indexOf(name, skipStart); + } + + if (nameStart === -1 || nameStart + name.length > limit) { + skipStart = limit; + loc = this.source.spanFor(NON_EXISTENT_LOCATION); + } else { + skipStart = nameStart; + loc = span.sliceStartChars({ skipStart, chars: name.length }); + skipStart += name.length; + } + + blockParams.push(b.var({ name, loc })); + } + } // These are bugs in Handlebars upstream if (!block.program.loc) { @@ -95,8 +187,8 @@ export abstract class HandlebarsNodeVisitors extends Parser { block.inverse.loc = NON_EXISTENT_LOCATION; } - const program = this.Program(block.program); - const inverse = block.inverse ? this.Program(block.inverse) : null; + const program = this.Program(block.program, blockParams); + const inverse = block.inverse ? this.Program(block.inverse, []) : null; const node = b.block({ path, @@ -109,7 +201,6 @@ export abstract class HandlebarsNodeVisitors extends Parser { inverseStrip: block.inverseStrip, closeStrip: block.closeStrip, }); - parseProgramBlockParamsLocs(this.source, node); const parentProgram = this.currentElement(); @@ -117,6 +208,8 @@ export abstract class HandlebarsNodeVisitors extends Parser { } MustacheStatement(rawMustache: HBS.MustacheStatement): ASTv1.MustacheStatement | void { + this.pendingError?.mustache(this.source.spanFor(rawMustache.loc)); + const { tokenizer } = this; if (tokenizer.state === 'comment') { @@ -129,9 +222,9 @@ export abstract class HandlebarsNodeVisitors extends Parser { if (isHBSLiteral(rawMustache.path)) { mustache = b.mustache({ - path: this.acceptNode(rawMustache.path), + path: this.acceptNode<(typeof rawMustache.path)['type']>(rawMustache.path), params: [], - hash: b.hash([], this.source.spanFor(rawMustache.path.loc).collapse('end')), + hash: b.hash({ pairs: [], loc: this.source.spanFor(rawMustache.path.loc).collapse('end') }), trusting: !escaped, loc: this.source.spanFor(loc), strip, @@ -231,7 +324,7 @@ export abstract class HandlebarsNodeVisitors extends Parser { } const { value, loc } = rawComment; - const comment = b.mustacheComment(value, this.source.spanFor(loc)); + const comment = b.mustacheComment({ value, loc: this.source.spanFor(loc) }); switch (tokenizer.state) { case 'beforeAttributeName': @@ -338,13 +431,12 @@ export abstract class HandlebarsNodeVisitors extends Parser { let pathHead: ASTv1.PathHead; if (thisHead) { - pathHead = { - type: 'ThisHead', - loc: { + pathHead = b.this({ + loc: this.source.spanFor({ start: path.loc.start, end: { line: path.loc.start.line, column: path.loc.start.column + 4 }, - }, - }; + }), + }); } else if (path.data) { const head = parts.shift(); @@ -355,14 +447,13 @@ export abstract class HandlebarsNodeVisitors extends Parser { ); } - pathHead = { - type: 'AtHead', + pathHead = b.atName({ name: `@${head}`, - loc: { + loc: this.source.spanFor({ start: path.loc.start, end: { line: path.loc.start.line, column: path.loc.start.column + head.length + 1 }, - }, - }; + }), + }); } else { const head = parts.shift(); @@ -373,49 +464,72 @@ export abstract class HandlebarsNodeVisitors extends Parser { ); } - pathHead = { - type: 'VarHead', + pathHead = b.var({ name: head, - loc: { + loc: this.source.spanFor({ start: path.loc.start, end: { line: path.loc.start.line, column: path.loc.start.column + head.length }, - }, - }; + }), + }); } - return new PathExpressionImplV1(path.original, pathHead, parts, this.source.spanFor(path.loc)); + return b.path({ + head: pathHead, + tail: parts, + loc: this.source.spanFor(path.loc), + }); } Hash(hash: HBS.Hash): ASTv1.Hash { const pairs = hash.pairs.map((pair) => b.pair({ key: pair.key, - value: this.acceptNode(pair.value), + value: this.acceptNode(pair.value), loc: this.source.spanFor(pair.loc), }) ); - return b.hash(pairs, this.source.spanFor(hash.loc)); + return b.hash({ pairs, loc: this.source.spanFor(hash.loc) }); } StringLiteral(string: HBS.StringLiteral): ASTv1.StringLiteral { - return b.literal({ type: 'StringLiteral', value: string.value, loc: string.loc }); + return b.literal({ + type: 'StringLiteral', + value: string.value, + loc: this.source.spanFor(string.loc), + }); } BooleanLiteral(boolean: HBS.BooleanLiteral): ASTv1.BooleanLiteral { - return b.literal({ type: 'BooleanLiteral', value: boolean.value, loc: boolean.loc }); + return b.literal({ + type: 'BooleanLiteral', + value: boolean.value, + loc: this.source.spanFor(boolean.loc), + }); } NumberLiteral(number: HBS.NumberLiteral): ASTv1.NumberLiteral { - return b.literal({ type: 'NumberLiteral', value: number.value, loc: number.loc }); + return b.literal({ + type: 'NumberLiteral', + value: number.value, + loc: this.source.spanFor(number.loc), + }); } UndefinedLiteral(undef: HBS.UndefinedLiteral): ASTv1.UndefinedLiteral { - return b.literal({ type: 'UndefinedLiteral', value: undefined, loc: undef.loc }); + return b.literal({ + type: 'UndefinedLiteral', + value: undefined, + loc: this.source.spanFor(undef.loc), + }); } NullLiteral(nul: HBS.NullLiteral): ASTv1.NullLiteral { - return b.literal({ type: 'NullLiteral', value: null, loc: nul.loc }); + return b.literal({ + type: 'NullLiteral', + value: null, + loc: this.source.spanFor(nul.loc), + }); } } @@ -480,40 +594,45 @@ function acceptCallNodes( params: ASTv1.Expression[]; hash: ASTv1.Hash; } { - if (node.path.type.endsWith('Literal')) { - const path = node.path as unknown as - | HBS.StringLiteral - | HBS.UndefinedLiteral - | HBS.NullLiteral - | HBS.NumberLiteral - | HBS.BooleanLiteral; - - let value = ''; - if (path.type === 'BooleanLiteral') { - value = path.original.toString(); - } else if (path.type === 'StringLiteral') { - value = `"${path.original}"`; - } else if (path.type === 'NullLiteral') { - value = 'null'; - } else if (path.type === 'NumberLiteral') { - value = path.value.toString(); - } else { - value = 'undefined'; + let path: ASTv1.PathExpression | ASTv1.SubExpression; + + switch (node.path.type) { + case 'PathExpression': + path = compiler.PathExpression(node.path); + break; + + case 'SubExpression': + path = compiler.SubExpression(node.path); + break; + + case 'StringLiteral': + case 'UndefinedLiteral': + case 'NullLiteral': + case 'NumberLiteral': + case 'BooleanLiteral': { + let value: string; + if (node.path.type === 'BooleanLiteral') { + value = node.path.original.toString(); + } else if (node.path.type === 'StringLiteral') { + value = `"${node.path.original}"`; + } else if (node.path.type === 'NullLiteral') { + value = 'null'; + } else if (node.path.type === 'NumberLiteral') { + value = node.path.value.toString(); + } else { + value = 'undefined'; + } + throw generateSyntaxError( + `${node.path.type} "${ + node.path.type === 'StringLiteral' ? node.path.original : value + }" cannot be called as a sub-expression, replace (${value}) with ${value}`, + compiler.source.spanFor(node.path.loc) + ); } - throw generateSyntaxError( - `${path.type} "${ - path.type === 'StringLiteral' ? path.original : value - }" cannot be called as a sub-expression, replace (${value}) with ${value}`, - compiler.source.spanFor(path.loc) - ); } - const path = - node.path.type === 'PathExpression' - ? compiler.PathExpression(node.path) - : compiler.SubExpression(node.path as unknown as HBS.SubExpression); const params = node.params - ? node.params.map((e) => compiler.acceptNode(e)) + ? node.params.map((e) => compiler.acceptNode(e)) : []; // if there is no hash, position it as a collapsed node immediately after the last param (or the @@ -522,17 +641,16 @@ function acceptCallNodes( const hash = node.hash ? compiler.Hash(node.hash) - : ({ - type: 'Hash', - pairs: [] as ASTv1.HashPair[], + : b.hash({ + pairs: [], loc: compiler.source.spanFor(end).collapse('end'), - } as const); + }); return { path, params, hash }; } function addElementModifier( - element: ParserNodeBuilder>, + element: ParserNodeBuilder, mustache: ASTv1.MustacheStatement ) { const { path, params, hash, loc } = mustache; diff --git a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts index 6b0e39598a..69bc492a16 100644 --- a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts +++ b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts @@ -1,9 +1,18 @@ import type { Nullable } from '@glimmer/interfaces'; -import { assertPresentArray, assign, getFirst, getLast, isPresentArray } from '@glimmer/util'; +import type { TokenizerState } from 'simple-html-tokenizer'; +import { + asPresentArray, + assert, + assertPresentArray, + assign, + getFirst, + getLast, + isPresentArray, +} from '@glimmer/util'; import { parse, parseWithoutProcessing } from '@handlebars/parser'; import { EntityParser } from 'simple-html-tokenizer'; -import type { Tag } from '../parser'; +import type { EndTag, StartTag } from '../parser'; import type { NodeVisitor } from '../traversal/visitor'; import type * as ASTv1 from '../v1/api'; import type * as HBS from '../v1/handlebars-ast'; @@ -14,11 +23,16 @@ import * as src from '../source/api'; import { generateSyntaxError } from '../syntax-error'; import traverse from '../traversal/traverse'; import Walker from '../traversal/walker'; -import { appendChild, parseElementBlockParams, parseElementPartLocs } from '../utils'; +import { appendChild } from '../utils'; import b from '../v1/parser-builders'; import publicBuilder from '../v1/public-builders'; import { HandlebarsNodeVisitors } from './handlebars-node-visitors'; +// vendored from simple-html-tokenizer because it's unexported +function isSpace(char: string): boolean { + return /[\t\n\f ]/u.test(char); +} + export class TokenizerEventHandlers extends HandlebarsNodeVisitors { private tagOpenLine = 0; private tagOpenColumn = 0; @@ -30,7 +44,11 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { // Comment beginComment(): void { - this.currentNode = b.comment('', this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn)); + this.currentNode = { + type: 'CommentStatement', + value: '', + start: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn), + }; } appendToCommentData(char: string): void { @@ -38,16 +56,17 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { } finishComment(): void { - appendChild(this.currentElement(), this.finish(this.currentComment)); + appendChild(this.currentElement(), b.comment(this.finish(this.currentComment))); } // Data beginData(): void { - this.currentNode = b.text({ + this.currentNode = { + type: 'TextNode', chars: '', - loc: this.offset().collapsed(), - }); + start: this.offset(), + }; } appendToData(char: string): void { @@ -55,9 +74,7 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { } finishData(): void { - this.currentData.loc = this.currentData.loc.withEnd(this.offset()); - - appendChild(this.currentElement(), this.currentData); + appendChild(this.currentElement(), b.text(this.finish(this.currentData))); } // Tags - basic @@ -71,11 +88,14 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { this.currentNode = { type: 'StartTag', name: '', + nameStart: null, + nameEnd: null, attributes: [], modifiers: [], comments: [], + params: [], selfClosing: false, - loc: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn), + start: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn), }; } @@ -83,16 +103,12 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { this.currentNode = { type: 'EndTag', name: '', - attributes: [], - modifiers: [], - comments: [], - selfClosing: false, - loc: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn), + start: this.source.offsetFor(this.tagOpenLine, this.tagOpenColumn), }; } finishTag(): void { - let tag = this.finish(this.currentTag); + let tag = this.finish(this.currentTag); if (tag.type === 'StartTag') { this.finishStartTag(); @@ -101,7 +117,7 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { throw generateSyntaxError( 'Invalid named block named detected, you may have created a named block without a name, or you may have began your name with a number. Named blocks must have names that are at least one character long, and begin with a lower case letter', this.source.spanFor({ - start: this.currentTag.loc.toJSON(), + start: this.currentTag.start.toJSON(), end: this.offset().toJSON(), }) ); @@ -116,70 +132,93 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { } finishStartTag(): void { - let { - name, - attributes: attrs, - modifiers, - comments, - selfClosing, - loc, - } = this.finish(this.currentStartTag); + let { name, nameStart, nameEnd } = this.currentStartTag; + + // <> should probably be a syntax error, but s-h-t is currently broken for that case + assert(name !== '', 'tag name cannot be empty'); + assert(nameStart !== null, 'nameStart unexpectedly null'); + assert(nameEnd !== null, 'nameEnd unexpectedly null'); + + let nameLoc = nameStart.until(nameEnd); + let [head, ...tail] = asPresentArray(name.split('.')); + let path = b.path({ + head: b.head({ original: head, loc: nameLoc.sliceStartChars({ chars: head.length }) }), + tail, + loc: nameLoc, + }); + + let { attributes, modifiers, comments, params, selfClosing, loc } = this.finish( + this.currentStartTag + ); let element = b.element({ - tag: name, + path, selfClosing, - attrs, + attributes, modifiers, comments, + params, children: [], - blockParams: [], + openTag: loc, + closeTag: selfClosing ? null : src.SourceSpan.broken(), loc, }); - element.startTag = { - type: 'ElementStartNode', - value: name, - loc: loc, - }; - element.nameNode = { - type: 'ElementNameNode', - value: name, - loc: loc - .withStart(this.source.offsetFor(loc.startPosition.line, loc.startPosition.column + 1)) - .withEnd( - this.source.offsetFor(loc.startPosition.line, loc.startPosition.column + 1 + name.length) - ), - }; - parseElementPartLocs(this.source, element); this.elementStack.push(element); } finishEndTag(isVoid: boolean): void { - let tag = this.finish(this.currentTag); + let { start: closeTagStart } = this.currentTag; + let tag = this.finish(this.currentTag); let element = this.elementStack.pop() as ASTv1.ElementNode; - element.endTag = { - type: 'ElementEndNode', - loc: tag.loc, - value: element.selfClosing ? '' : tag.name, - }; - this.validateEndTag(tag, element, isVoid); let parent = this.currentElement(); + if (isVoid) { + element.closeTag = null; + } else if (element.selfClosing) { + assert(element.closeTag === null, 'element.closeTag unexpectedly present'); + } else { + element.closeTag = closeTagStart.until(this.offset()); + } + element.loc = element.loc.withEnd(this.offset()); - parseElementBlockParams(element); - appendChild(parent, element); + + appendChild(parent, b.element(element)); } markTagAsSelfClosing(): void { - this.currentTag.selfClosing = true; + let tag = this.currentTag; + + if (tag.type === 'StartTag') { + tag.selfClosing = true; + } else { + throw generateSyntaxError( + `Invalid end tag: closing tag must not be self-closing`, + this.source.spanFor({ start: tag.start.toJSON(), end: this.offset().toJSON() }) + ); + } } // Tags - name appendToTagName(char: string): void { - this.currentTag.name += char; + let tag = this.currentTag; + tag.name += char; + + if (tag.type === 'StartTag') { + let offset = this.offset(); + + if (tag.nameStart === null) { + assert(tag.nameEnd === null, 'nameStart and nameEnd must both be null'); + + // Note that the tokenizer already consumed the token here + tag.nameStart = offset.move(-1); + } + + tag.nameEnd = offset; + } } // Tags - attributes @@ -200,6 +239,13 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { appendToAttributeName(char: string): void { this.currentAttr.name += char; + + // The block params parsing code can actually handle peek=non-space just + // fine, but this check was added as an optimization, as there is a little + // bit of setup overhead for the parsing logic just to immediately bail + if (this.currentAttr.name === 'as') { + this.parsePossibleBlockParams(); + } } beginAttributeValue(isQuoted: boolean): void { @@ -243,11 +289,20 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { if (tag.type === 'EndTag') { throw generateSyntaxError( `Invalid end tag: closing tag must not have attributes`, - this.source.spanFor({ start: tag.loc.toJSON(), end: tokenizerPos.toJSON() }) + this.source.spanFor({ start: tag.start.toJSON(), end: tokenizerPos.toJSON() }) ); } let { name, parts, start, isQuoted, isDynamic, valueSpan } = this.currentAttr; + + // Just trying to be helpful with `` rather than letting it through as an attribute + if (name.startsWith('|') && parts.length === 0 && !isQuoted && !isDynamic) { + throw generateSyntaxError( + 'Invalid block parameters syntax: block parameters must be preceded by the `as` keyword', + start.until(start.move(name.length)) + ); + } + let value = this.assembleAttributeValue(parts, isQuoted, isDynamic, start.until(tokenizerPos)); value.loc = valueSpan.withEnd(tokenizerPos); @@ -256,6 +311,248 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { this.currentStartTag.attributes.push(attribute); } + private parsePossibleBlockParams() { + // const enums that we can't use directly + const BEFORE_ATTRIBUTE_NAME = 'beforeAttributeName' as TokenizerState.beforeAttributeName; + const ATTRIBUTE_NAME = 'attributeName' as TokenizerState.attributeName; + const AFTER_ATTRIBUTE_NAME = 'afterAttributeName' as TokenizerState.afterAttributeName; + + // Regex to validate the identifier for block parameters. + // Based on the ID validation regex in Handlebars. + const ID_INVERSE_PATTERN = /[!"#%&'()*+./;<=>@[\\\]^`{|}~]/u; + + type States = { + PossibleAs: { state: 'PossibleAs' }; + BeforeStartPipe: { state: 'BeforeStartPipe' }; + BeforeBlockParamName: { state: 'BeforeBlockParamName' }; + BlockParamName: { + state: 'BlockParamName'; + name: string; + start: src.SourceOffset; + }; + AfterEndPipe: { state: 'AfterEndPipe' }; + Error: { + state: 'Error'; + message: string; + start: src.SourceOffset; + }; + Done: { state: 'Done' }; + }; + + type State = States[keyof States]; + + type Handler = (next: string) => void; + + assert(this.tokenizer.state === ATTRIBUTE_NAME, 'must be in TokenizerState.attributeName'); + + const element = this.currentStartTag; + const as = this.currentAttr; + + let state = { state: 'PossibleAs' } as State; + + const handlers = { + PossibleAs: (next: string) => { + assert(state.state === 'PossibleAs', 'bug in block params parser'); + + if (isSpace(next)) { + // " as ..." + state = { state: 'BeforeStartPipe' }; + this.tokenizer.transitionTo(AFTER_ATTRIBUTE_NAME); + this.tokenizer.consume(); + } else if (next === '|') { + // " as|..." + // Following Handlebars and require a space between "as" and the pipe + throw generateSyntaxError( + `Invalid block parameters syntax: expecting at least one space character between "as" and "|"`, + as.start.until(this.offset().move(1)) + ); + } else { + // " as{{...", " async...", " as=...", " as>...", " as/>..." + // Don't consume, let the normal tokenizer code handle the next steps + state = { state: 'Done' }; + } + }, + + BeforeStartPipe: (next: string) => { + assert(state.state === 'BeforeStartPipe', 'bug in block params parser'); + + if (isSpace(next)) { + this.tokenizer.consume(); + } else if (next === '|') { + state = { state: 'BeforeBlockParamName' }; + this.tokenizer.transitionTo(BEFORE_ATTRIBUTE_NAME); + this.tokenizer.consume(); + } else { + // " as {{...", " as bs...", " as =...", " as ...", " as/>..." + // Don't consume, let the normal tokenizer code handle the next steps + state = { state: 'Done' }; + } + }, + + BeforeBlockParamName: (next: string) => { + assert(state.state === 'BeforeBlockParamName', 'bug in block params parser'); + + if (isSpace(next)) { + this.tokenizer.consume(); + } else if (next === '') { + // The HTML tokenizer ran out of characters, so we are either + // encountering mustache or + state = { state: 'Done' }; + this.pendingError = { + mustache(loc: src.SourceSpan) { + throw generateSyntaxError( + `Invalid block parameters syntax: mustaches cannot be used inside parameters list`, + loc + ); + }, + eof(loc: src.SourceOffset) { + throw generateSyntaxError( + `Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list`, + as.start.until(loc) + ); + }, + }; + } else if (next === '|') { + if (element.params.length === 0) { + // Following Handlebars and treat empty block params a syntax error + throw generateSyntaxError( + `Invalid block parameters syntax: empty parameters list, expecting at least one identifier`, + as.start.until(this.offset().move(1)) + ); + } else { + state = { state: 'AfterEndPipe' }; + } + } else if (next === '>' || next === '/') { + throw generateSyntaxError( + `Invalid block parameters syntax: incomplete parameters list, expecting "|" but the tag was closed prematurely`, + as.start.until(this.offset().move(1)) + ); + } else { + // slurp up anything else into the name, validate later + state = { + state: 'BlockParamName', + name: next, + start: this.offset(), + }; + this.tokenizer.consume(); + } + }, + + BlockParamName: (next: string) => { + assert(state.state === 'BlockParamName', 'bug in block params parser'); + + if (next === '') { + // The HTML tokenizer ran out of characters, so we are either + // encountering mustache or , HBS side will attach the error + // to the next span + state = { state: 'Done' }; + this.pendingError = { + mustache(loc: src.SourceSpan) { + throw generateSyntaxError( + `Invalid block parameters syntax: mustaches cannot be used inside parameters list`, + loc + ); + }, + eof(loc: src.SourceOffset) { + throw generateSyntaxError( + `Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list`, + as.start.until(loc) + ); + }, + }; + } else if (next === '|' || isSpace(next)) { + let loc = state.start.until(this.offset()); + + if (state.name === 'this' || ID_INVERSE_PATTERN.test(state.name)) { + throw generateSyntaxError( + `Invalid block parameters syntax: invalid identifier name \`${state.name}\``, + loc + ); + } + + element.params.push(b.var({ name: state.name, loc })); + + state = next === '|' ? { state: 'AfterEndPipe' } : { state: 'BeforeBlockParamName' }; + this.tokenizer.consume(); + } else if (next === '>' || next === '/') { + throw generateSyntaxError( + `Invalid block parameters syntax: expecting "|" but the tag was closed prematurely`, + as.start.until(this.offset().move(1)) + ); + } else { + // slurp up anything else into the name, validate later + state.name += next; + this.tokenizer.consume(); + } + }, + + AfterEndPipe: (next: string) => { + assert(state.state === 'AfterEndPipe', 'bug in block params parser'); + + if (isSpace(next)) { + this.tokenizer.consume(); + } else if (next === '') { + // The HTML tokenizer ran out of characters, so we are either + // encountering mustache or , HBS side will attach the error + // to the next span + state = { state: 'Done' }; + this.pendingError = { + mustache(loc: src.SourceSpan) { + throw generateSyntaxError( + `Invalid block parameters syntax: modifiers cannot follow parameters list`, + loc + ); + }, + eof(loc: src.SourceOffset) { + throw generateSyntaxError( + `Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list`, + as.start.until(loc) + ); + }, + }; + } else if (next === '>' || next === '/') { + // Don't consume, let the normal tokenizer code handle the next steps + state = { state: 'Done' }; + } else { + // Slurp up the next "token" for the error span + state = { + state: 'Error', + message: + 'Invalid block parameters syntax: expecting the tag to be closed with ">" or "/>" after parameters list', + start: this.offset(), + }; + this.tokenizer.consume(); + } + }, + + Error: (next: string) => { + assert(state.state === 'Error', 'bug in block params parser'); + + if (next === '' || next === '/' || next === '>' || isSpace(next)) { + throw generateSyntaxError(state.message, state.start.until(this.offset())); + } else { + // Slurp up the next "token" for the error span + this.tokenizer.consume(); + } + }, + + Done: () => { + assert(false, 'This should never be called'); + }, + } as const satisfies { + [S in keyof States]: Handler; + }; + + let next: string; + + do { + next = this.tokenizer.peek(); + handlers[state.state](next); + } while (state.state !== 'Done' && next !== ''); + + assert(state.state === 'Done', 'bug in block params parser'); + } + reportSyntaxError(message: string): void { throw generateSyntaxError(message, this.offset().collapsed()); } @@ -277,14 +574,13 @@ export class TokenizerEventHandlers extends HandlebarsNodeVisitors { let first = getFirst(parts); let last = getLast(parts); - return b.concat(parts, this.source.spanFor(first.loc).extend(this.source.spanFor(last.loc))); + return b.concat({ + parts, + loc: this.source.spanFor(first.loc).extend(this.source.spanFor(last.loc)), + }); } - validateEndTag( - tag: Tag<'StartTag' | 'EndTag'>, - element: ASTv1.ElementNode, - selfClosing: boolean - ): void { + validateEndTag(tag: StartTag | EndTag, element: ASTv1.ElementNode, selfClosing: boolean): void { if (voidMap.has(tag.name) && !selfClosing) { // EngTag is also called by StartTag for void and self-closing tags (i.e. // or
, so we need to check for that here. Otherwise, we would @@ -461,21 +757,20 @@ export function preprocess( end: offsets.endPosition, }; - let program = new TokenizerEventHandlers(source, entityParser, mode).acceptTemplate(ast); - - if (options.strictMode) { - program.blockParams = options.locals ?? []; - } + let template = new TokenizerEventHandlers(source, entityParser, mode).parse( + ast, + options.locals ?? [] + ); - if (options && options.plugins && options.plugins.ast) { + if (options?.plugins?.ast) { for (const transform of options.plugins.ast) { let env: ASTPluginEnvironment = assign({}, options, { syntax }, { plugins: undefined }); let pluginResult = transform(env); - traverse(program, pluginResult.visitor); + traverse(template, pluginResult.visitor); } } - return program; + return template; } diff --git a/packages/@glimmer/syntax/lib/source/loc/span.ts b/packages/@glimmer/syntax/lib/source/loc/span.ts index 73c50a0db2..627de87a77 100644 --- a/packages/@glimmer/syntax/lib/source/loc/span.ts +++ b/packages/@glimmer/syntax/lib/source/loc/span.ts @@ -190,7 +190,7 @@ export class SourceSpan implements SourceLocation { /** * Create a new span with the current span's beginning and a new ending. */ - withEnd(this: SourceSpan, other: SourceOffset): SourceSpan { + withEnd(other: SourceOffset): SourceSpan { return span(this.data.getStart(), other.data); } diff --git a/packages/@glimmer/syntax/lib/traversal/traverse.ts b/packages/@glimmer/syntax/lib/traversal/traverse.ts index 81d0fc2128..80a86afc8f 100644 --- a/packages/@glimmer/syntax/lib/traversal/traverse.ts +++ b/packages/@glimmer/syntax/lib/traversal/traverse.ts @@ -67,8 +67,11 @@ function getNodeHandler( visitor: NodeVisitor, nodeType: N['type'] ): NodeTraversal | undefined { - if (nodeType === 'Template' || nodeType === 'Block') { - if (visitor.Program) { + if (visitor.Program) { + if ( + (nodeType === 'Template' && !visitor.Template) || + (nodeType === 'Block' && !visitor.Block) + ) { deprecate( `The 'Program' visitor node is deprecated. Use 'Template' or 'Block' instead (node was '${nodeType}') ` ); diff --git a/packages/@glimmer/syntax/lib/traversal/visitor.ts b/packages/@glimmer/syntax/lib/traversal/visitor.ts index 4e1069a969..3810a14061 100644 --- a/packages/@glimmer/syntax/lib/traversal/visitor.ts +++ b/packages/@glimmer/syntax/lib/traversal/visitor.ts @@ -13,6 +13,11 @@ export type NodeTraversal = FullNodeTraversal | NodeHan export type NodeVisitor = { [P in keyof ASTv1.Nodes]?: NodeTraversal } & { All?: NodeTraversal; + + /** + * @deprecated use Template or Block instead + */ + Program?: NodeTraversal; }; export interface FullKeyTraversal { @@ -27,4 +32,9 @@ export type KeyTraversal> = export type KeysVisitor = { [P in VisitorKey]?: KeyTraversal } & { All?: KeyTraversal>; + + /** + * @deprecated use Template or Block instead + */ + Program?: KeyTraversal; }; diff --git a/packages/@glimmer/syntax/lib/traversal/walker.ts b/packages/@glimmer/syntax/lib/traversal/walker.ts index 7be62d9fb8..232e616e37 100644 --- a/packages/@glimmer/syntax/lib/traversal/walker.ts +++ b/packages/@glimmer/syntax/lib/traversal/walker.ts @@ -33,40 +33,21 @@ export default class Walker { switch (node.type) { case 'Block': case 'Template': - return visitors.Program(this, node as unknown as ASTv1.Program, callback); + walkBody(this, node.body, callback); + return; case 'ElementNode': - return visitors.ElementNode(this, node, callback); + walkBody(this, node.children, callback); + return; case 'BlockStatement': - return visitors.BlockStatement(this, node, callback); + this.visit(node.program, callback); + this.visit(node.inverse || null, callback); + return; default: return; } } } -const visitors = { - Program(walker: Walker, node: ASTv1.Program, callback: NodeCallback) { - walkBody(walker, node.body, callback); - }, - - Template(walker: Walker, node: ASTv1.Template, callback: NodeCallback) { - walkBody(walker, node.body, callback); - }, - - Block(walker: Walker, node: ASTv1.Block, callback: NodeCallback) { - walkBody(walker, node.body, callback); - }, - - ElementNode(walker: Walker, node: ASTv1.ElementNode, callback: NodeCallback) { - walkBody(walker, node.children, callback); - }, - - BlockStatement(walker: Walker, node: ASTv1.BlockStatement, callback: NodeCallback) { - walker.visit(node.program, callback); - walker.visit(node.inverse || null, callback); - }, -} as const; - function walkBody( walker: Walker, body: ASTv1.Statement[], diff --git a/packages/@glimmer/syntax/lib/utils.ts b/packages/@glimmer/syntax/lib/utils.ts index 9aeb3793cf..ec6f6ed06b 100644 --- a/packages/@glimmer/syntax/lib/utils.ts +++ b/packages/@glimmer/syntax/lib/utils.ts @@ -1,148 +1,6 @@ -import type { Nullable } from '@glimmer/interfaces'; -import { expect, unwrap } from '@glimmer/util'; - -import type * as src from './source/api'; import type * as ASTv1 from './v1/api'; import type * as HBS from './v1/handlebars-ast'; -import { generateSyntaxError } from './syntax-error'; - -// Regex to validate the identifier for block parameters. -// Based on the ID validation regex in Handlebars. - -let ID_INVERSE_PATTERN = /[!"#%&'()*+./;<=>@[\\\]^`{|}~]/u; - -// Checks the element's attributes to see if it uses block params. -// If it does, registers the block params with the program and -// removes the corresponding attributes from the element. - -export function parseElementBlockParams(element: ASTv1.ElementNode): void { - let params = parseBlockParams(element); - if (params) { - element.blockParamNodes = params; - element.blockParams = params.map((p) => p.value); - } -} - -export function parseProgramBlockParamsLocs(code: src.Source, block: ASTv1.BlockStatement) { - const blockRange = [block.loc.getStart().offset!, block.loc.getEnd().offset!] as [number, number]; - let part = code.slice(...blockRange); - let start = blockRange[0]; - let idx = part.indexOf('|') + 1; - start += idx; - part = part.slice(idx, -1); - idx = part.indexOf('|'); - part = part.slice(0, idx); - for (const param of block.program.blockParamNodes) { - const regex = new RegExp(`\\b${param.value}\\b`); - const match = regex.exec(part)!; - const range = [start + match.index, 0] as [number, number]; - range[1] = range[0] + param.value.length; - param.loc = code.spanFor({ - start: code.hbsPosFor(range[0])!, - end: code.hbsPosFor(range[1])!, - }); - } -} - -export function parseElementPartLocs(code: src.Source, element: ASTv1.ElementNode) { - const elementRange = [element.loc.getStart().offset!, element.loc.getEnd().offset!] as [ - number, - number, - ]; - let start = elementRange[0]; - let codeSlice = code.slice(...elementRange); - for (const part of element.parts) { - const idx = codeSlice.indexOf(part.value); - const range = [start + idx, 0] as [number, number]; - range[1] = range[0] + part.value.length; - codeSlice = code.slice(range[1], elementRange[1]); - start = range[1]; - part.loc = code.spanFor({ - start: code.hbsPosFor(range[0])!, - end: code.hbsPosFor(range[1])!, - }); - } -} - -function parseBlockParams(element: ASTv1.ElementNode): Nullable { - let l = element.attributes.length; - let attrNames = []; - - for (let i = 0; i < l; i++) { - attrNames.push(unwrap(element.attributes[i]).name); - } - - let asIndex = attrNames.indexOf('as'); - - if ( - asIndex === -1 && - attrNames.length > 0 && - unwrap(attrNames[attrNames.length - 1]).charAt(0) === '|' - ) { - throw generateSyntaxError( - 'Block parameters must be preceded by the `as` keyword, detected block parameters without `as`', - element.loc - ); - } - - if (asIndex !== -1 && l > asIndex && unwrap(attrNames[asIndex + 1]).charAt(0) === '|') { - // Some basic validation, since we're doing the parsing ourselves - let paramsString = attrNames.slice(asIndex).join(' '); - if ( - paramsString.charAt(paramsString.length - 1) !== '|' || - expect(paramsString.match(/\|/gu), `block params must exist here`).length !== 2 - ) { - throw generateSyntaxError( - "Invalid block parameters syntax, '" + paramsString + "'", - element.loc - ); - } - - let params: ASTv1.BlockParam[] = []; - for (let i = asIndex + 1; i < l; i++) { - let param = unwrap(attrNames[i]).replace(/\|/gu, ''); - if (param !== '') { - if (ID_INVERSE_PATTERN.test(param)) { - throw generateSyntaxError( - "Invalid identifier for block parameters, '" + param + "'", - element.loc - ); - } - let loc = element.attributes[i]!.loc; - if (attrNames[i]!.startsWith('|')) { - loc = loc.slice({ skipStart: 1 }); - } - if (attrNames[i]!.endsWith('|')) { - loc = loc.slice({ skipEnd: 1 }); - } - - // fix hbs parser bug, the range contains the whitespace between attributes... - if (loc.endPosition.column - loc.startPosition.column > param.length) { - loc = loc.slice({ - skipEnd: loc.endPosition.column - loc.startPosition.column - param.length, - }); - } - - params.push({ - type: 'BlockParam', - value: param, - loc, - }); - } - } - - if (params.length === 0) { - throw generateSyntaxError('Cannot use zero block parameters', element.loc); - } - - element.attributes = element.attributes.slice(0, asIndex); - return params; - } - - return null; -} - export function childrenFor( node: ASTv1.Block | ASTv1.Template | ASTv1.ElementNode ): ASTv1.TopLevelStatement[] { diff --git a/packages/@glimmer/syntax/lib/v1/handlebars-ast.ts b/packages/@glimmer/syntax/lib/v1/handlebars-ast.ts index 3099cb3e4e..371503f7b9 100644 --- a/packages/@glimmer/syntax/lib/v1/handlebars-ast.ts +++ b/packages/@glimmer/syntax/lib/v1/handlebars-ast.ts @@ -13,7 +13,7 @@ export interface CommonNode { } export interface NodeMap { - Program: { input: Program; output: ASTv1.Template | ASTv1.Block }; + Program: { input: Program; output: ASTv1.Block }; MustacheStatement: { input: MustacheStatement; output: ASTv1.MustacheStatement | void }; Decorator: { input: Decorator; output: never }; BlockStatement: { input: BlockStatement; output: ASTv1.BlockStatement | void }; @@ -50,7 +50,7 @@ export interface Position { export interface Program extends CommonNode { type: 'Program'; body: Statement[]; - blockParams: string[]; + blockParams?: string[]; chained?: boolean; } @@ -86,9 +86,9 @@ export interface CommonBlock extends CommonNode { hash: Hash; program: Program; inverse: Program; - openStrip: StripFlags; - inverseStrip: StripFlags; - closeStrip: StripFlags; + openStrip?: StripFlags; + inverseStrip?: StripFlags; + closeStrip?: StripFlags; } export interface BlockStatement extends CommonBlock { diff --git a/packages/@glimmer/syntax/lib/v1/legacy-interop.ts b/packages/@glimmer/syntax/lib/v1/legacy-interop.ts index 686b22e9a1..7687664615 100644 --- a/packages/@glimmer/syntax/lib/v1/legacy-interop.ts +++ b/packages/@glimmer/syntax/lib/v1/legacy-interop.ts @@ -1,63 +1,120 @@ -import { asPresentArray, assertPresentArray, getFirst } from '@glimmer/util'; +import type { PresentArray } from '@glimmer/interfaces/index'; +import { asPresentArray, deprecate } from '@glimmer/util'; -import type { SourceSpan } from '../source/span'; -import type { PathExpression, PathHead } from './nodes-v1'; +import type * as ASTv1 from './nodes-v1'; import b from './public-builders'; -export class PathExpressionImplV1 implements PathExpression { - type = 'PathExpression' as const; - public parts: string[]; - public this = false; - public data = false; - - constructor( - public original: string, - head: PathHead, - tail: string[], - public loc: SourceSpan - ) { - let parts = tail.slice(); - - if (head.type === 'ThisHead') { - this.this = true; - } else if (head.type === 'AtHead') { - this.data = true; - parts.unshift(head.name.slice(1)); - } else { - parts.unshift(head.name); - } - - this.parts = parts; - } - - // Cache for the head value. - _head?: PathHead = undefined; - - get head(): PathHead { - if (this._head) { - return this._head; - } - - let firstPart: string; - - if (this.this) { - firstPart = 'this'; - } else if (this.data) { - firstPart = `@${getFirst(asPresentArray(this.parts))}`; - } else { - assertPresentArray(this.parts); - firstPart = getFirst(this.parts); - } - - let firstPartLoc = this.loc.collapse('start').sliceStartChars({ - chars: firstPart.length, - }).loc; - - return (this._head = b.head(firstPart, firstPartLoc)); - } - - get tail(): string[] { - return this.this ? this.parts : this.parts.slice(1); - } +export type MustacheStatementParams = Omit; + +export function buildLegacyMustache({ + path, + params, + hash, + trusting, + strip, + loc, +}: MustacheStatementParams): ASTv1.MustacheStatement { + const node = { + type: 'MustacheStatement', + path, + params, + hash, + trusting, + strip, + loc, + }; + + Object.defineProperty(node, 'escaped', { + enumerable: false, + get(this: typeof node): boolean { + deprecate(`The escaped property on mustache nodes is deprecated, use trusting instead`); + return !this.trusting; + }, + set(this: typeof node, value: boolean) { + deprecate(`The escaped property on mustache nodes is deprecated, use trusting instead`); + this.trusting = !value; + }, + }); + + return node as ASTv1.MustacheStatement; +} + +export type PathExpressionParams = Omit; + +export function buildLegacyPath({ head, tail, loc }: PathExpressionParams): ASTv1.PathExpression { + const node = { + type: 'PathExpression', + head, + tail, + get original() { + return [this.head.original, ...this.tail].join('.'); + }, + set original(value: string) { + let [head, ...tail] = asPresentArray(value.split('.')); + this.head = b.head(head, this.head.loc); + this.tail = tail; + }, + loc, + }; + + Object.defineProperty(node, 'parts', { + enumerable: false, + get(this: { original: string }): readonly string[] { + deprecate(`The parts property on path nodes is deprecated, use trusting instead`); + return Object.freeze(this.original.split('.')); + }, + set(this: { original: string }, value: PresentArray) { + deprecate(`The parts property on mustache nodes is deprecated, use trusting instead`); + this.original = value.join('.'); + }, + }); + + Object.defineProperty(node, 'this', { + enumerable: false, + get(this: typeof node): boolean { + deprecate(`The this property on path nodes is deprecated, use head.type instead`); + return this.head.type === 'ThisHead'; + }, + }); + + Object.defineProperty(node, 'data', { + enumerable: false, + get(this: typeof node): boolean { + deprecate(`The data property on path nodes is deprecated, use head.type instead`); + return this.head.type === 'AtHead'; + }, + }); + + return node as ASTv1.PathExpression; +} + +export function buildLegacyLiteral({ + type, + value, + loc, +}: { + type: T['type']; + value: T['value']; + loc: T['loc']; +}): T { + const node = { + type, + value, + loc, + }; + + Object.defineProperty(node, 'original', { + enumerable: false, + get(this: typeof node): T['original'] { + deprecate(`The original property on literal nodes is deprecated, use value instead`); + return this.value; + }, + set(this: typeof node, value: T['original']) { + deprecate(`The original property on literal nodes is deprecated, use value instead`); + this.value = value; + }, + }); + + return node as T; } diff --git a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts index e1bbc500d2..9c34badb89 100644 --- a/packages/@glimmer/syntax/lib/v1/nodes-v1.ts +++ b/packages/@glimmer/syntax/lib/v1/nodes-v1.ts @@ -12,36 +12,35 @@ export interface BaseNode { export interface CommonProgram extends BaseNode { body: Statement[]; - blockParams: string[]; - chained?: boolean; -} - -export interface Program extends CommonProgram { - type: 'Program'; } export interface Block extends CommonProgram { type: 'Block'; - blockParamNodes: BlockParam[]; + params: VarHead[]; + chained?: boolean; + + /** + * string accessor for params.name + */ + blockParams: string[]; } export type EntityEncodingState = 'transformed' | 'raw'; export interface Template extends CommonProgram { type: 'Template'; + blockParams: string[]; } -export type PossiblyDeprecatedBlock = Block | Template; +/** + * @deprecated use Template or Block instead + */ +export type Program = Template | Block; -export interface CallParts { - path: Expression; - params: Expression[]; - hash: Hash; -} +export type CallableExpression = SubExpression | PathExpression; -export interface Call extends BaseNode { - name?: Expression; - path: Expression; +export interface CallParts { + path: CallableExpression; params: Expression[]; hash: Hash; } @@ -57,15 +56,18 @@ export interface MustacheStatement extends BaseNode { path: Expression; params: Expression[]; hash: Hash; - /** @deprecated */ - escaped: boolean; trusting: boolean; strip: StripFlags; + + /** + * @deprecated use trusting instead + */ + escaped: boolean; } export interface BlockStatement extends BaseNode { type: 'BlockStatement'; - path: Expression; + path: CallableExpression; params: Expression[]; hash: Hash; program: Block; @@ -80,20 +82,11 @@ export interface BlockStatement extends BaseNode { export interface ElementModifierStatement extends BaseNode { type: 'ElementModifierStatement'; - path: Expression; + path: CallableExpression; params: Expression[]; hash: Hash; } -export interface PartialStatement extends BaseNode { - type: 'PartialStatement'; - name: PathExpression | SubExpression; - params: Expression[]; - hash: Hash; - indent: string; - strip: StripFlags; -} - export interface CommentStatement extends BaseNode { type: 'CommentStatement'; value: string; @@ -104,62 +97,41 @@ export interface MustacheCommentStatement extends BaseNode { value: string; } -export interface ElementName { - type: 'ElementName'; - name: string; - loc: src.SourceLocation; -} - -export interface ElementStartNode extends BaseNode { - type: 'ElementStartNode'; - value: string; -} - -export interface ElementNameNode extends BaseNode { - type: 'ElementNameNode'; - value: string; -} - -export interface ElementEndNode extends BaseNode { - type: 'ElementEndNode'; - value: string; -} - -export interface ElementPartNode extends BaseNode { - type: 'ElementPartNode'; - value: string; -} - -/* - - ^-- ElementPartNode - ^-- ElementPartNode - ^- ElementPartNode - ^-------- ElementNameNode - ^------------------ ElementStartNode - ^----------- ElementEndNode - */ export interface ElementNode extends BaseNode { type: 'ElementNode'; - tag: string; - nameNode: ElementNameNode; - startTag: ElementStartNode; - endTag: ElementEndNode; - parts: ElementPartNode[]; + path: PathExpression; selfClosing: boolean; attributes: AttrNode[]; - blockParams: string[]; - blockParamNodes: BlockParam[]; + params: VarHead[]; modifiers: ElementModifierStatement[]; comments: MustacheCommentStatement[]; children: Statement[]; + + /** + * span for the open tag + */ + openTag: src.SourceSpan; + + /** + * span for the close tag, null for void or self-closing tags + */ + closeTag: Nullable; + + /** + * string accessor for path.original + */ + tag: string; + + /** + * string accessor for params.name + */ + blockParams: string[]; } export type StatementName = | 'MustacheStatement' | 'CommentStatement' | 'BlockStatement' - | 'PartialStatement' | 'MustacheCommentStatement' | 'TextNode' | 'ElementNode'; @@ -185,28 +157,39 @@ export interface ConcatStatement extends BaseNode { export type ExpressionName = 'SubExpression' | 'PathExpression' | LiteralName; -export interface SubExpression extends Call { +export interface SubExpression extends BaseNode { type: 'SubExpression'; - path: Expression; + path: CallableExpression; params: Expression[]; hash: Hash; } export interface ThisHead { type: 'ThisHead'; - loc: src.SourceLocation; + original: 'this'; + loc: src.SourceSpan; } export interface AtHead { type: 'AtHead'; name: string; - loc: src.SourceLocation; + loc: src.SourceSpan; + + /** + * alias for name + */ + original: string; } export interface VarHead { type: 'VarHead'; name: string; - loc: src.SourceLocation; + loc: src.SourceSpan; + + /** + * alias for name + */ + original: string; } export type PathHead = ThisHead | AtHead | VarHead; @@ -225,15 +208,15 @@ export interface PathExpression extends MinimalPathExpression { /** * @deprecated use `head` and `tail` instead */ - parts: string[]; + parts: readonly string[]; /** * @deprecated use `head.type` instead */ - this: boolean; + readonly this: boolean; /** * @deprecated use `head.type' instead */ - data: boolean; + readonly data: boolean; } export type LiteralName = @@ -246,30 +229,50 @@ export type LiteralName = export interface StringLiteral extends BaseNode { type: 'StringLiteral'; value: string; + + /** + * @deprecated use value instead + */ original: string; } export interface BooleanLiteral extends BaseNode { type: 'BooleanLiteral'; value: boolean; + + /** + * @deprecated use value instead + */ original: boolean; } export interface NumberLiteral extends BaseNode { type: 'NumberLiteral'; value: number; + + /** + * @deprecated use value instead + */ original: number; } export interface UndefinedLiteral extends BaseNode { type: 'UndefinedLiteral'; value: undefined; + + /** + * @deprecated use value instead + */ original: undefined; } export interface NullLiteral extends BaseNode { type: 'NullLiteral'; value: null; + + /** + * @deprecated use value instead + */ original: null; } @@ -284,57 +287,69 @@ export interface HashPair extends BaseNode { value: Expression; } -/** - * a param inside the pipes of elements or mustache blocks, - * ... bar is a BlockParam. - * {{#Foo as |bar|}}... bar is a BlockParam. - */ -export interface BlockParam extends BaseNode { - type: 'BlockParam'; - value: string; -} - export interface StripFlags { open: boolean; close: boolean; } -export type SharedNodes = { +export type Nodes = { + Template: Template; + Block: Block; + + MustacheStatement: MustacheStatement; + BlockStatement: BlockStatement; + ElementModifierStatement: ElementModifierStatement; CommentStatement: CommentStatement; MustacheCommentStatement: MustacheCommentStatement; + ElementNode: ElementNode; + AttrNode: AttrNode; TextNode: TextNode; + + ConcatStatement: ConcatStatement; + SubExpression: SubExpression; + PathExpression: PathExpression; + StringLiteral: StringLiteral; BooleanLiteral: BooleanLiteral; NumberLiteral: NumberLiteral; NullLiteral: NullLiteral; UndefinedLiteral: UndefinedLiteral; - MustacheStatement: MustacheStatement; - ElementModifierStatement: ElementModifierStatement; - PartialStatement: PartialStatement; - AttrNode: AttrNode; - ConcatStatement: ConcatStatement; -}; -export type Nodes = SharedNodes & { - ElementEndNode: ElementEndNode; - ElementStartNode: ElementStartNode; - ElementPartNode: ElementPartNode; - ElementNameNode: ElementNameNode; - Program: Program; - Template: Template; - Block: Block; - BlockStatement: BlockStatement; - ElementNode: ElementNode; - SubExpression: SubExpression; - PathExpression: PathExpression; Hash: Hash; HashPair: HashPair; - BlockParam: BlockParam; }; export type NodeType = keyof Nodes; export type Node = Nodes[NodeType]; +// These "sub-node" cannot appear standalone, they are only used inside another +// "real" AST node to provide richer information. The distinction mostly exists +// for backwards compatibility reason. These nodes are not traversed and do not +// have visitor keys for them, so it won't break existing AST consumers (e.g. +// those that implemented an `All` visitor may not be expecting these new types +// of nodes). +// +// Conceptually, the idea of "sub-node" does make sense, and you can say source +// locations are another kind of these things. However, in these cases, they +// actually fully implement the `BaseNode` interface, and only not extending +// `BaseNode` because the `type` field is not `keyof Nodes` (which is circular +// reasoning). If these are not "real" nodes because they can only appear in +// very limited context, then the same reasoning probably applies for, say, +// HashPair. +// +// If we do eventually make some kind of breaking change here, perhaps with +// some kind of opt-in, then we can consider upgrading these into "real" nodes, +// but for now, this is where they go, and it isn't a huge problem in practice +// because there are little utility in traversing these kind of nodes anyway. +export type SubNodes = { + ThisHead: ThisHead; + AtHead: AtHead; + VarHead: VarHead; +}; + +export type SubNodeType = keyof SubNodes; +export type SubNode = SubNodes[SubNodeType]; + export type Statement = Nodes[StatementName]; export type Statements = Pick; export type Literal = Nodes[LiteralName]; diff --git a/packages/@glimmer/syntax/lib/v1/parser-builders.ts b/packages/@glimmer/syntax/lib/v1/parser-builders.ts index 476a0f6b93..aeb8dd899b 100644 --- a/packages/@glimmer/syntax/lib/v1/parser-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/parser-builders.ts @@ -1,12 +1,10 @@ -import type { Dict, Nullable, PresentArray } from '@glimmer/interfaces'; +import type { Nullable, Optional, PresentArray } from '@glimmer/interfaces'; import { assert } from '@glimmer/util'; -import type { ParserNodeBuilder } from '../parser'; -import type { SourceLocation } from '../source/location'; -import type { SourceOffset, SourceSpan } from '../source/span'; import type * as ASTv1 from './api'; -import { PathExpressionImplV1 } from './legacy-interop'; +import { SourceSpan } from '../source/span'; +import { buildLegacyLiteral, buildLegacyMustache, buildLegacyPath } from './legacy-interop'; const DEFAULT_STRIP = { close: false, @@ -20,7 +18,7 @@ const DEFAULT_STRIP = { * 2. Mandating source locations */ class Builders { - pos(line: number, column: number) { + pos({ line, column }: { line: number; column: number }) { return { line, column, @@ -28,23 +26,28 @@ class Builders { } blockItself({ - body = [], - blockParams = [], + body, + params, chained = false, loc, }: { - body?: ASTv1.Statement[] | undefined; - blockParams?: string[] | undefined; - chained?: boolean | undefined; + body: ASTv1.Statement[]; + params: ASTv1.VarHead[]; + chained?: Optional; loc: SourceSpan; }): ASTv1.Block { return { type: 'Block', - body: body, - blockParams: blockParams, - blockParamNodes: blockParams?.map( - (b) => ({ type: 'BlockParam', value: b }) as ASTv1.BlockParam - ), + body, + params, + get blockParams() { + return this.params.map((p) => p.name); + }, + set blockParams(params: string[]) { + this.params = params.map((name) => { + return b.var({ name, loc: SourceSpan.synthetic(name) }); + }); + }, chained, loc, }; @@ -55,14 +58,14 @@ class Builders { blockParams, loc, }: { - body?: ASTv1.Statement[]; - blockParams?: string[]; + body: ASTv1.Statement[]; + blockParams: string[]; loc: SourceSpan; }): ASTv1.Template { return { type: 'Template', - body: body || [], - blockParams: blockParams || [], + body, + blockParams, loc, }; } @@ -80,18 +83,16 @@ class Builders { hash: ASTv1.Hash; trusting: boolean; loc: SourceSpan; - strip: ASTv1.StripFlags; + strip?: Optional; }): ASTv1.MustacheStatement { - return { - type: 'MustacheStatement', + return buildLegacyMustache({ path, params, hash, - escaped: !trusting, trusting, + strip, loc, - strip: strip || { open: false, close: false }, - }; + }); } block({ @@ -109,11 +110,11 @@ class Builders { params: ASTv1.Expression[]; hash: ASTv1.Hash; defaultBlock: ASTv1.Block; - elseBlock?: Nullable; + elseBlock: Nullable; loc: SourceSpan; - openStrip: ASTv1.StripFlags; - inverseStrip: ASTv1.StripFlags; - closeStrip: ASTv1.StripFlags; + openStrip?: Optional; + inverseStrip?: Optional; + closeStrip?: Optional; }): ASTv1.BlockStatement { return { type: 'BlockStatement', @@ -122,33 +123,42 @@ class Builders { hash, program: defaultBlock, inverse: elseBlock, - loc: loc, - openStrip: openStrip, - inverseStrip: inverseStrip, - closeStrip: closeStrip, + loc, + openStrip, + inverseStrip, + closeStrip, }; } - comment(value: string, loc: SourceOffset): ParserNodeBuilder { + comment({ value, loc }: { value: string; loc: SourceSpan }): ASTv1.CommentStatement { return { type: 'CommentStatement', - value: value, + value, loc, }; } - mustacheComment(value: string, loc: SourceSpan): ASTv1.MustacheCommentStatement { + mustacheComment({ + value, + loc, + }: { + value: string; + loc: SourceSpan; + }): ASTv1.MustacheCommentStatement { return { type: 'MustacheCommentStatement', - value: value, + value, loc, }; } - concat( - parts: PresentArray, - loc: SourceSpan - ): ASTv1.ConcatStatement { + concat({ + parts, + loc, + }: { + parts: PresentArray; + loc: SourceSpan; + }): ASTv1.ConcatStatement { return { type: 'ConcatStatement', parts, @@ -157,42 +167,67 @@ class Builders { } element({ - tag, + path, selfClosing, - attrs, - blockParams, + attributes, modifiers, + params, comments, children, + openTag, + closeTag, loc, - }: BuildElementOptions): ASTv1.ElementNode { + }: { + path: ASTv1.PathExpression; + selfClosing: boolean; + attributes: ASTv1.AttrNode[]; + modifiers: ASTv1.ElementModifierStatement[]; + params: ASTv1.VarHead[]; + children: ASTv1.Statement[]; + comments: ASTv1.MustacheCommentStatement[]; + openTag: SourceSpan; + closeTag: Nullable; + loc: SourceSpan; + }): ASTv1.ElementNode { + let _selfClosing = selfClosing; + return { type: 'ElementNode', - 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((x) => ({ type: 'BlockParam', value: x }) as ASTv1.BlockParam) || [], - modifiers: modifiers || [], - comments: (comments as ASTv1.MustacheCommentStatement[]) || [], - children: children || [], + path, + attributes, + modifiers, + params, + comments, + children, + openTag, + closeTag, loc, + get tag() { + return this.path.original; + }, + set tag(name: string) { + this.path.original = name; + }, + get blockParams() { + return this.params.map((p) => p.name); + }, + set blockParams(params: string[]) { + this.params = params.map((name) => { + return b.var({ name, loc: SourceSpan.synthetic(name) }); + }); + }, + get selfClosing() { + return _selfClosing; + }, + set selfClosing(selfClosing: boolean) { + _selfClosing = selfClosing; + + if (selfClosing) { + this.closeTag = null; + } else { + this.closeTag = SourceSpan.synthetic(``); + } + }, }; } @@ -270,58 +305,103 @@ class Builders { tail: string[]; loc: SourceSpan; }): ASTv1.PathExpression { - let { original: originalHead } = headToString(head); - let original = [...originalHead, ...tail].join('.'); - - return new PathExpressionImplV1(original, head, tail, loc); + return buildLegacyPath({ head, tail, loc }); } - head(head: string, loc: SourceSpan): ASTv1.PathHead { - if (head[0] === '@') { - return this.atName(head, loc); - } else if (head === 'this') { - return this.this(loc); + head({ original, loc }: { original: string; loc: SourceSpan }): ASTv1.PathHead { + if (original === 'this') { + return this.this({ loc }); + } + if (original[0] === '@') { + return this.atName({ name: original, loc }); } else { - return this.var(head, loc); + return this.var({ name: original, loc }); } } - this(loc: SourceSpan): ASTv1.PathHead { + this({ loc }: { loc: SourceSpan }): ASTv1.ThisHead { return { type: 'ThisHead', + get original() { + return 'this' as const; + }, loc, }; } - atName(name: string, loc: SourceSpan): ASTv1.PathHead { - // the `@` should be included so we have a complete source range - assert(name[0] === '@', `call builders.at() with a string that starts with '@'`); - - return { - type: 'AtHead', - name, + atName({ name, loc }: { name: string; loc: SourceSpan }): ASTv1.AtHead { + let _name = ''; + + const node = { + type: 'AtHead' as const, + get name() { + return _name; + }, + set name(value) { + assert(value[0] === '@', `call builders.at() with a string that starts with '@'`); + assert( + value.indexOf('.') === -1, + `builder.at() should not be called with a name with dots in it` + ); + _name = value; + }, + get original() { + return this.name; + }, + set original(value) { + this.name = value; + }, loc, }; - } - var(name: string, loc: SourceSpan): ASTv1.PathHead { - assert(name !== 'this', `You called builders.var() with 'this'. Call builders.this instead`); - assert( - name[0] !== '@', - `You called builders.var() with '${name}'. Call builders.at('${name}') instead` - ); + // trigger the assertions + node.name = name; - return { - type: 'VarHead', - name, + return node; + } + + var({ name, loc }: { name: string; loc: SourceSpan }): ASTv1.VarHead { + let _name = ''; + + const node = { + type: 'VarHead' as const, + get name() { + return _name; + }, + set name(value) { + assert( + value !== 'this', + `You called builders.var() with 'this'. Call builders.this instead` + ); + assert( + value[0] !== '@', + `You called builders.var() with '${name}'. Call builders.at('${name}') instead` + ); + assert( + value.indexOf('.') === -1, + `builder.var() should not be called with a name with dots in it` + ); + _name = value; + }, + get original() { + return this.name; + }, + set original(value) { + this.name = value; + }, loc, }; + + // trigger the assertions + node.name = name; + + return node; } - hash(pairs: ASTv1.HashPair[], loc: SourceSpan): ASTv1.Hash { + hash({ pairs, loc }: { pairs: ASTv1.HashPair[]; loc: SourceSpan }): ASTv1.Hash { return { type: 'Hash', - pairs: pairs || [], + pairs, loc, }; } @@ -337,7 +417,7 @@ class Builders { }): ASTv1.HashPair { return { type: 'HashPair', - key: key, + key, value, loc, }; @@ -350,91 +430,12 @@ class Builders { }: { type: T['type']; value: T['value']; - loc?: SourceLocation; + loc: SourceSpan; }): T { - return { - type, - value, - original: value, - loc, - } as T; - } - - undefined(): ASTv1.UndefinedLiteral { - return this.literal({ type: 'UndefinedLiteral', value: undefined }); + return buildLegacyLiteral({ type, value, loc }); } - - null(): ASTv1.NullLiteral { - return this.literal({ type: 'NullLiteral', value: null }); - } - - string(value: string, loc: SourceSpan): ASTv1.StringLiteral { - return this.literal({ type: 'StringLiteral', value, loc }); - } - - boolean(value: boolean, loc: SourceSpan): ASTv1.BooleanLiteral { - return this.literal({ type: 'BooleanLiteral', value, loc }); - } - - number(value: number, loc: SourceSpan): ASTv1.NumberLiteral { - return this.literal({ type: 'NumberLiteral', value, loc }); - } -} - -// Nodes - -export type ElementParts = - | ['attrs', ...AttrSexp[]] - | ['modifiers', ...ModifierSexp[]] - | ['body', ...ASTv1.Statement[]] - | ['comments', ...ElementComment[]] - | ['as', ...string[]] - | ['loc', SourceLocation]; - -export type PathSexp = string | ['path', string, LocSexp?]; - -export type ModifierSexp = - | string - | [PathSexp, LocSexp?] - | [PathSexp, ASTv1.Expression[], LocSexp?] - | [PathSexp, ASTv1.Expression[], Dict, LocSexp?]; - -export type AttrSexp = [string, ASTv1.AttrNode['value'] | string, LocSexp?]; - -export type LocSexp = ['loc', SourceLocation]; - -export type ElementComment = ASTv1.MustacheCommentStatement | SourceLocation | string; - -export type SexpValue = - | string - | ASTv1.Expression[] - | Dict - | LocSexp - | PathSexp - | undefined; - -export interface BuildElementOptions { - tag: string; - selfClosing: boolean; - attrs: ASTv1.AttrNode[]; - modifiers: ASTv1.ElementModifierStatement[]; - children: ASTv1.Statement[]; - comments: ElementComment[]; - blockParams: string[]; - loc: SourceSpan; } -// Expressions - -function headToString(head: ASTv1.PathHead): { original: string; parts: string[] } { - switch (head.type) { - case 'AtHead': - return { original: head.name, parts: [head.name] }; - case 'ThisHead': - return { original: `this`, parts: [] }; - case 'VarHead': - return { original: head.name, parts: [head.name] }; - } -} +const b = new Builders(); -export default new Builders(); +export default b; diff --git a/packages/@glimmer/syntax/lib/v1/public-builders.ts b/packages/@glimmer/syntax/lib/v1/public-builders.ts index 0a0e0cff8c..556960647c 100644 --- a/packages/@glimmer/syntax/lib/v1/public-builders.ts +++ b/packages/@glimmer/syntax/lib/v1/public-builders.ts @@ -1,13 +1,14 @@ import type { Dict, Nullable } from '@glimmer/interfaces'; -import { asPresentArray, assert, assign, deprecate, isPresentArray } from '@glimmer/util'; +import { asPresentArray, assert, deprecate, isPresentArray } from '@glimmer/util'; import type { SourceLocation, SourcePosition } from '../source/location'; import type * as ASTv1 from './api'; +import { isVoidTag } from '../generation/printer'; import { SYNTHETIC_LOCATION } from '../source/location'; import { Source } from '../source/source'; import { SourceSpan } from '../source/span'; -import { PathExpressionImplV1 } from './legacy-interop'; +import b from './parser-builders'; let _SOURCE: Source | undefined; @@ -23,124 +24,110 @@ function SOURCE(): Source { // Statements -export type BuilderHead = string | ASTv1.Expression; -export type TagDescriptor = string | { name: string; selfClosing: boolean }; +export type BuilderHead = string | ASTv1.CallableExpression; +export type TagDescriptor = + | string + | ASTv1.PathExpression + | { path: ASTv1.PathExpression; selfClosing?: boolean } + | { name: string; selfClosing?: boolean }; function buildMustache( path: BuilderHead | ASTv1.Literal, - params?: ASTv1.Expression[], - hash?: ASTv1.Hash, - raw?: boolean, + params: ASTv1.Expression[] = [], + hash: ASTv1.Hash = buildHash([]), + trusting = false, loc?: SourceLocation, strip?: ASTv1.StripFlags ): ASTv1.MustacheStatement { - if (typeof path === 'string') { - path = buildPath(path); - } - - return { - type: 'MustacheStatement', - path, - params: params || [], - hash: hash || buildHash([]), - escaped: !raw, - trusting: !!raw, + return b.mustache({ + path: buildPath(path), + params, + hash, + trusting, + strip, loc: buildLoc(loc || null), - strip: strip || { open: false, close: false }, - }; + }); } +type PossiblyDeprecatedBlock = ASTv1.Block | ASTv1.Template; + function buildBlock( path: BuilderHead, params: Nullable, hash: Nullable, - _defaultBlock: ASTv1.PossiblyDeprecatedBlock, - _elseBlock?: Nullable, + _defaultBlock: PossiblyDeprecatedBlock, + _elseBlock: Nullable = null, loc?: SourceLocation, openStrip?: ASTv1.StripFlags, inverseStrip?: ASTv1.StripFlags, closeStrip?: ASTv1.StripFlags ): ASTv1.BlockStatement { let defaultBlock: ASTv1.Block; - let elseBlock: Nullable | undefined; + let elseBlock: Nullable = null; if (_defaultBlock.type === 'Template') { deprecate(`b.program is deprecated. Use b.blockItself instead.`); - - defaultBlock = assign({}, _defaultBlock, { type: 'Block' }) as unknown as ASTv1.Block; + defaultBlock = b.blockItself({ + params: buildBlockParams(_defaultBlock.blockParams), + body: _defaultBlock.body, + loc: _defaultBlock.loc, + }); } else { defaultBlock = _defaultBlock; } - if (_elseBlock !== undefined && _elseBlock !== null && _elseBlock.type === 'Template') { + if (_elseBlock?.type === 'Template') { deprecate(`b.program is deprecated. Use b.blockItself instead.`); + assert(_elseBlock.blockParams.length === 0, '{{else}} block cannot have block params'); - elseBlock = assign({}, _elseBlock, { type: 'Block' }) as unknown as ASTv1.Block; + elseBlock = b.blockItself({ + params: [], + body: _elseBlock.body, + loc: _elseBlock.loc, + }); } else { elseBlock = _elseBlock; } - return { - type: 'BlockStatement', + return b.block({ path: buildPath(path), params: params || [], hash: hash || buildHash([]), - program: defaultBlock || null, - inverse: elseBlock || null, + defaultBlock, + elseBlock, loc: buildLoc(loc || null), - openStrip: openStrip || { open: false, close: false }, - inverseStrip: inverseStrip || { open: false, close: false }, - closeStrip: closeStrip || { open: false, close: false }, - }; + openStrip, + inverseStrip, + closeStrip, + }); } function buildElementModifier( - path: BuilderHead | ASTv1.Expression, + path: BuilderHead, params?: ASTv1.Expression[], hash?: ASTv1.Hash, loc?: Nullable ): ASTv1.ElementModifierStatement { - return { - type: 'ElementModifierStatement', + return b.elementModifier({ path: buildPath(path), params: params || [], hash: hash || buildHash([]), loc: buildLoc(loc || null), - }; -} - -function buildPartial( - name: ASTv1.PathExpression, - params?: ASTv1.Expression[], - hash?: ASTv1.Hash, - indent?: string, - loc?: SourceLocation -): ASTv1.PartialStatement { - return { - type: 'PartialStatement', - name: name, - params: params || [], - hash: hash || buildHash([]), - indent: indent || '', - strip: { open: false, close: false }, - loc: buildLoc(loc || null), - }; + }); } function buildComment(value: string, loc?: SourceLocation): ASTv1.CommentStatement { - return { - type: 'CommentStatement', + return b.comment({ value: value, loc: buildLoc(loc || null), - }; + }); } function buildMustacheComment(value: string, loc?: SourceLocation): ASTv1.MustacheCommentStatement { - return { - type: 'MustacheCommentStatement', + return b.mustacheComment({ value: value, loc: buildLoc(loc || null), - }; + }); } function buildConcat( @@ -151,11 +138,10 @@ function buildConcat( throw new Error(`b.concat requires at least one part`); } - return { - type: 'ConcatStatement', - parts: parts || [], + return b.concat({ + parts, loc: buildLoc(loc || null), - }; + }); } // Nodes @@ -194,228 +180,181 @@ export interface BuildElementOptions { attrs?: ASTv1.AttrNode[]; modifiers?: ASTv1.ElementModifierStatement[]; children?: ASTv1.Statement[]; - comments?: ElementComment[]; - blockParams?: string[]; - loc?: SourceSpan; + comments?: ASTv1.MustacheCommentStatement[]; + blockParams?: ASTv1.VarHead[] | string[]; + openTag?: SourceLocation; + closeTag?: Nullable; + loc?: SourceLocation; } function buildElement(tag: TagDescriptor, options: BuildElementOptions = {}): ASTv1.ElementNode { - let { attrs, blockParams, modifiers, comments, children, loc } = options; - - let tagName: string; + let { + attrs, + blockParams, + modifiers, + comments, + children, + openTag, + closeTag: _closeTag, + loc, + } = options; // this is used for backwards compat, prior to `selfClosing` being part of the ElementNode AST - let selfClosing = false; - if (typeof tag === 'object') { + let path: ASTv1.PathExpression; + let selfClosing: boolean | undefined; + + if (typeof tag === 'string') { + if (tag.endsWith('/')) { + path = buildPath(tag.slice(0, -1)); + selfClosing = true; + } else { + path = buildPath(tag); + } + } else if ('type' in tag) { + assert(tag.type === 'PathExpression', `Invalid tag type ${tag.type}`); + path = tag; + } else if ('path' in tag) { + assert(tag.path.type === 'PathExpression', `Invalid tag type ${tag.path.type}`); + path = tag.path; selfClosing = tag.selfClosing; - tagName = tag.name; - } else if (tag.slice(-1) === '/') { - tagName = tag.slice(0, -1); - selfClosing = true; } else { - tagName = tag; + path = buildPath(tag.name); + selfClosing = tag.selfClosing; + } + + if (selfClosing) { + assert( + _closeTag === null || _closeTag === undefined, + 'Cannot build a self-closing tag with a closeTag source location' + ); } - return { - type: 'ElementNode', - tag: tagName, - 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: tagName - .split('.') - .map((t) => ({ type: 'ElementPartNode', value: t }) as ASTv1.ElementPartNode), - selfClosing: selfClosing, + let params = blockParams?.map((param) => { + if (typeof param === 'string') { + return buildVar(param); + } else { + return param; + } + }); + + let closeTag: Nullable = null; + + if (_closeTag) { + closeTag = buildLoc(_closeTag || null); + } else if (_closeTag === undefined) { + closeTag = selfClosing || isVoidTag(path.original) ? null : buildLoc(null); + } + + return b.element({ + path, + selfClosing: selfClosing || false, attributes: attrs || [], - blockParams: blockParams || [], - blockParamNodes: - blockParams?.map((x) => ({ type: 'BlockParam', value: x }) as ASTv1.BlockParam) || [], + params: params || [], modifiers: modifiers || [], - comments: (comments as ASTv1.MustacheCommentStatement[]) || [], + comments: comments || [], children: children || [], + openTag: buildLoc(openTag || null), + closeTag, loc: buildLoc(loc || null), - }; + }); } -function buildAttr( - name: string, - value: ASTv1.AttrNode['value'], - loc?: SourceLocation -): ASTv1.AttrNode { - return { - type: 'AttrNode', +function buildAttr(name: string, value: ASTv1.AttrValue, loc?: SourceLocation): ASTv1.AttrNode { + return b.attr({ name: name, value: value, loc: buildLoc(loc || null), - }; + }); } -function buildText(chars?: string, loc?: SourceLocation): ASTv1.TextNode { - return { - type: 'TextNode', - chars: chars || '', +function buildText(chars = '', loc?: SourceLocation): ASTv1.TextNode { + return b.text({ + chars, loc: buildLoc(loc || null), - }; + }); } // Expressions function buildSexpr( path: BuilderHead, - params?: ASTv1.Expression[], - hash?: ASTv1.Hash, + params: ASTv1.Expression[] = [], + hash: ASTv1.Hash = buildHash([]), loc?: SourceLocation ): ASTv1.SubExpression { - return { - type: 'SubExpression', + return b.sexpr({ path: buildPath(path), - params: params || [], - hash: hash || buildHash([]), + params, + hash, loc: buildLoc(loc || null), - }; -} - -function headToString(head: ASTv1.PathHead): { original: string; parts: string[] } { - switch (head.type) { - case 'AtHead': - return { original: head.name, parts: [head.name] }; - case 'ThisHead': - return { original: `this`, parts: [] }; - case 'VarHead': - return { original: head.name, parts: [head.name] }; - } + }); } -function buildHead( - original: string, - loc: SourceLocation -): { head: ASTv1.PathHead; tail: string[] } { +function buildHead(original: string, loc?: SourceLocation): ASTv1.PathExpression { let [head, ...tail] = asPresentArray(original.split('.')); - let headNode: ASTv1.PathHead; - - if (head === 'this') { - headNode = { - type: 'ThisHead', - loc: buildLoc(loc || null), - }; - } else if (head[0] === '@') { - headNode = { - type: 'AtHead', - name: head, - loc: buildLoc(loc || null), - }; - } else { - headNode = { - type: 'VarHead', - name: head, - loc: buildLoc(loc || null), - }; - } - - return { - head: headNode, - tail, - }; + let headNode = b.head({ original: head, loc: buildLoc(loc || null) }); + return b.path({ head: headNode, tail, loc: buildLoc(loc || null) }); } -function buildThis(loc: SourceLocation): ASTv1.PathHead { - return { - type: 'ThisHead', - loc: buildLoc(loc || null), - }; +function buildThis(loc?: SourceLocation): ASTv1.ThisHead { + return b.this({ loc: buildLoc(loc || null) }); } -function buildAtName(name: string, loc: SourceLocation): ASTv1.PathHead { - // the `@` should be included so we have a complete source range - assert(name[0] === '@', `call builders.at() with a string that starts with '@'`); - - return { - type: 'AtHead', - name, - loc: buildLoc(loc || null), - }; +function buildAtName(name: string, loc?: SourceLocation): ASTv1.AtHead { + return b.atName({ name, loc: buildLoc(loc || null) }); } -function buildVar(name: string, loc: SourceLocation): ASTv1.PathHead { - assert(name !== 'this', `You called builders.var() with 'this'. Call builders.this instead`); - assert( - name[0] !== '@', - `You called builders.var() with '${name}'. Call builders.at('${name}') instead` - ); - - return { - type: 'VarHead', - name, - loc: buildLoc(loc || null), - }; +function buildVar(name: string, loc?: SourceLocation): ASTv1.VarHead { + return b.var({ name, loc: buildLoc(loc || null) }); } -function buildHeadFromString(head: string, loc: SourceLocation): ASTv1.PathHead { - if (head[0] === '@') { - return buildAtName(head, loc); - } else if (head === 'this') { - return buildThis(loc); - } else { - return buildVar(head, loc); - } +function buildHeadFromString(original: string, loc?: SourceLocation): ASTv1.PathHead { + return b.head({ original, loc: buildLoc(loc || null) }); } function buildCleanPath( head: ASTv1.PathHead, - tail: string[], - loc: SourceLocation + tail: string[] = [], + loc?: SourceLocation ): ASTv1.PathExpression { - let { original: originalHead, parts: headParts } = headToString(head); - let parts = [...headParts, ...tail]; - let original = [...originalHead, ...parts].join('.'); - - return new PathExpressionImplV1(original, head, tail, buildLoc(loc || null)); + return b.path({ head, tail, loc: buildLoc(loc || null) }); } function buildPath( path: ASTv1.PathExpression | string | { head: string; tail: string[] }, loc?: SourceLocation ): ASTv1.PathExpression; +function buildPath(path: BuilderHead, loc?: SourceLocation): ASTv1.CallableExpression; +function buildPath(path: BuilderHead | ASTv1.Literal, loc?: SourceLocation): ASTv1.Expression; function buildPath(path: ASTv1.Expression, loc?: SourceLocation): ASTv1.Expression; -function buildPath(path: BuilderHead | ASTv1.Expression, loc?: SourceLocation): ASTv1.Expression; function buildPath( path: BuilderHead | ASTv1.Expression | { head: string; tail: string[] }, loc?: SourceLocation ): ASTv1.Expression { + let span = buildLoc(loc || null); + if (typeof path !== 'string') { if ('type' in path) { return path; } else { - let { head, tail } = buildHead(path.head, SourceSpan.broken()); - assert( - tail.length === 0, + path.head.indexOf('.') === -1, `builder.path({ head, tail }) should not be called with a head with dots in it` ); - let { original: originalHead } = headToString(head); + let { head, tail } = path; - return new PathExpressionImplV1( - [originalHead, ...tail].join('.'), - head, + return b.path({ + head: b.head({ original: head, loc: span.sliceStartChars({ chars: head.length }) }), tail, - buildLoc(loc || null) - ); + loc: buildLoc(loc || null), + }); } } - let { head, tail } = buildHead(path, SourceSpan.broken()); + let { head, tail } = buildHead(path, span); - return new PathExpressionImplV1(path, head, tail, buildLoc(loc || null)); + return b.path({ head, tail, loc: span }); } function buildLiteral( @@ -423,81 +362,81 @@ function buildLiteral( value: T['value'], loc?: SourceLocation ): T { - return { + return b.literal({ type, value, - original: value, loc: buildLoc(loc || null), - } as T; + }); } // Miscellaneous -function buildHash(pairs?: ASTv1.HashPair[], loc?: SourceLocation): ASTv1.Hash { - return { - type: 'Hash', - pairs: pairs || [], +function buildHash(pairs: ASTv1.HashPair[] = [], loc?: SourceLocation): ASTv1.Hash { + return b.hash({ + pairs, loc: buildLoc(loc || null), - }; + }); } function buildPair(key: string, value: ASTv1.Expression, loc?: SourceLocation): ASTv1.HashPair { - return { - type: 'HashPair', - key: key, + return b.pair({ + key, value, loc: buildLoc(loc || null), - }; + }); } function buildProgram( body?: ASTv1.Statement[], blockParams?: string[], loc?: SourceLocation -): ASTv1.Template { - return { - type: 'Template', - body: body || [], - blockParams: blockParams || [], - loc: buildLoc(loc || null), - }; +): ASTv1.Template | ASTv1.Block { + deprecate(`b.program is deprecated. Use b.template or b.blockItself instead.`); + + if (blockParams && blockParams.length) { + return buildBlockItself(body, blockParams, false, loc); + } else { + return buildTemplate(body, [], loc); + } +} + +function buildBlockParams(params: ReadonlyArray): ASTv1.VarHead[] { + return params.map((p) => + typeof p === 'string' ? b.var({ name: p, loc: SourceSpan.synthetic(p) }) : p + ); } function buildBlockItself( - body?: ASTv1.Statement[], - blockParams?: string[], + body: ASTv1.Statement[] = [], + params: Array = [], chained = false, loc?: SourceLocation ): ASTv1.Block { - return { - type: 'Block', - body: body || [], - blockParams: blockParams || [], - blockParamNodes: - blockParams?.map((b) => ({ type: 'BlockParam', value: b }) as ASTv1.BlockParam) || [], + return b.blockItself({ + body, + params: buildBlockParams(params), chained, loc: buildLoc(loc || null), - }; + }); } function buildTemplate( - body?: ASTv1.Statement[], - blockParams?: string[], + body: ASTv1.Statement[] = [], + blockParams: string[] = [], loc?: SourceLocation ): ASTv1.Template { - return { - type: 'Template', - body: body || [], - blockParams: blockParams || [], + return b.template({ + body, + blockParams, loc: buildLoc(loc || null), - }; + }); } function buildPosition(line: number, column: number): SourcePosition { - return { + return b.pos({ line, column, - }; + }); } function buildLoc(loc: Nullable): SourceSpan; @@ -508,8 +447,17 @@ function buildLoc( endColumn?: number, source?: string ): SourceSpan; - -function buildLoc(...args: any[]): SourceSpan { +function buildLoc( + ...args: + | [Nullable] + | [ + startLine: number, + startColumn: number, + endLine?: number | undefined, + endColumn?: number | undefined, + source?: string | undefined, + ] +): SourceSpan { if (args.length === 1) { let loc = args[0]; @@ -528,8 +476,8 @@ function buildLoc(...args: any[]): SourceSpan { column: startColumn, }, end: { - line: endLine, - column: endColumn, + line: endLine || startLine, + column: endColumn || startColumn, }, }); } @@ -538,7 +486,6 @@ function buildLoc(...args: any[]): SourceSpan { export default { mustache: buildMustache, block: buildBlock, - partial: buildPartial, comment: buildComment, mustacheComment: buildMustacheComment, element: buildElement, diff --git a/packages/@glimmer/syntax/lib/v1/visitor-keys.ts b/packages/@glimmer/syntax/lib/v1/visitor-keys.ts index 8f06a5986f..c82dd0d3a9 100644 --- a/packages/@glimmer/syntax/lib/v1/visitor-keys.ts +++ b/packages/@glimmer/syntax/lib/v1/visitor-keys.ts @@ -3,28 +3,21 @@ import type * as ASTv1 from './api'; // ensure stays in sync with typing // ParentNode and ChildKey types are derived from VisitorKeysMap const visitorKeys = { - Program: ['body'], Template: ['body'], Block: ['body'], MustacheStatement: ['path', 'params', 'hash'], BlockStatement: ['path', 'params', 'hash', 'program', 'inverse'], ElementModifierStatement: ['path', 'params', 'hash'], - PartialStatement: ['name', 'params', 'hash'], CommentStatement: [], MustacheCommentStatement: [], ElementNode: ['attributes', 'modifiers', 'children', 'comments'], - ElementStartNode: [], - ElementPartNode: [], - ElementEndNode: [], - ElementNameNode: [], AttrNode: ['value'], TextNode: [], ConcatStatement: ['parts'], SubExpression: ['path', 'params', 'hash'], PathExpression: [], - PathHead: [], StringLiteral: [], BooleanLiteral: [], @@ -34,12 +27,6 @@ const visitorKeys = { Hash: ['pairs'], HashPair: ['value'], - BlockParam: [], - - // v2 new nodes - NamedBlock: ['attributes', 'modifiers', 'children', 'comments'], - SimpleElement: ['attributes', 'modifiers', 'children', 'comments'], - Component: ['head', 'attributes', 'modifiers', 'children', 'comments'], } as const; type VisitorKeysMap = typeof visitorKeys; diff --git a/packages/@glimmer/syntax/lib/v2/normalize.ts b/packages/@glimmer/syntax/lib/v2/normalize.ts index aaf120fac6..df1fea03cc 100644 --- a/packages/@glimmer/syntax/lib/v2/normalize.ts +++ b/packages/@glimmer/syntax/lib/v2/normalize.ts @@ -40,8 +40,8 @@ export function normalize( let normalizeOptions = { strictMode: false, - locals: [], ...options, + locals: ast.blockParams, }; let top = SymbolTable.top( @@ -191,6 +191,11 @@ class ExpressionNormalizer { case 'PathExpression': return this.path(expr, resolution); case 'SubExpression': { + // expr.path used to incorrectly have the type ASTv1.Expression + if (isLiteral(expr.path)) { + assertIllegalLiteral(expr.path, expr.loc); + } + let resolution = this.block.resolutionFor(expr, SexpSyntaxContext); if (resolution.result === 'error') { @@ -323,8 +328,6 @@ class StatementNormalizer { normalize(node: ASTv1.Statement): ASTv2.ContentNode | ASTv2.NamedBlock { switch (node.type) { - case 'PartialStatement': - throw new Error(`Handlebars partial syntax ({{> ...}}) is not allowed in Glimmer`); case 'BlockStatement': return this.BlockStatement(node); case 'ElementNode': @@ -372,27 +375,35 @@ class StatementNormalizer { * Normalizes an ASTv1.MustacheStatement to an ASTv2.AppendStatement */ MustacheStatement(mustache: ASTv1.MustacheStatement): ASTv2.AppendContent { - let { escaped } = mustache; + let { path, params, hash, trusting } = mustache; let loc = this.block.loc(mustache.loc); + let context = AppendSyntaxContext(mustache); + let value: ASTv2.ExpressionNode; - // Normalize the call parts in AppendSyntaxContext - let callParts = this.expr.callParts( - { - path: mustache.path, - params: mustache.params, - hash: mustache.hash, - }, - AppendSyntaxContext(mustache) - ); + if (isLiteral(path)) { + if (params.length === 0 && hash.pairs.length === 0) { + value = this.expr.normalize(path, context); + } else { + assertIllegalLiteral(path, loc); + } + } else { + // Normalize the call parts in AppendSyntaxContext + let callParts = this.expr.callParts( + { + path, + params, + hash, + }, + AppendSyntaxContext(mustache) + ); - let value = callParts.args.isEmpty() - ? callParts.callee - : this.block.builder.sexp(callParts, loc); + value = callParts.args.isEmpty() ? callParts.callee : this.block.builder.sexp(callParts, loc); + } return this.block.builder.append( { table: this.block.table, - trusting: !escaped, + trusting, value, }, loc @@ -406,6 +417,11 @@ class StatementNormalizer { let { program, inverse } = block; let loc = this.block.loc(block.loc); + // block.path used to incorrectly have the type ASTv1.Expression + if (isLiteral(block.path)) { + assertIllegalLiteral(block.path, loc); + } + let resolution = this.block.resolutionFor(block, BlockSyntaxContext); if (resolution.result === 'error') { @@ -515,6 +531,11 @@ class ElementNormalizer { } private modifier(m: ASTv1.ElementModifierStatement): ASTv2.ElementModifier { + // modifier.path used to incorrectly have the type ASTv1.Expression + if (isLiteral(m.path)) { + assertIllegalLiteral(m.path, m.loc); + } + let resolution = this.ctx.resolutionFor(m, ModifierSyntaxContext); if (resolution.result === 'error') { @@ -538,10 +559,21 @@ class ElementNormalizer { * ``` */ private mustacheAttr(mustache: ASTv1.MustacheStatement): ASTv2.ExpressionNode { + let { path, params, hash, loc } = mustache; + let context = AttrValueSyntaxContext(mustache); + + if (isLiteral(path)) { + if (params.length === 0 && hash.pairs.length === 0) { + return this.expr.normalize(path, context); + } else { + assertIllegalLiteral(path, loc); + } + } + // Normalize the call parts in AttrValueSyntaxContext let sexp = this.ctx.builder.sexp( - this.expr.callParts(mustache, AttrValueSyntaxContext(mustache)), - this.ctx.loc(mustache.loc) + this.expr.callParts(mustache as ASTv1.CallParts, AttrValueSyntaxContext(mustache)), + this.ctx.loc(loc) ); // If there are no params or hash, just return the function part as its own expression @@ -562,7 +594,7 @@ class ElementNormalizer { } { switch (part.type) { case 'MustacheStatement': - return { expr: this.mustacheAttr(part), trusting: !part.escaped }; + return { expr: this.mustacheAttr(part), trusting: part.trusting }; case 'TextNode': return { expr: this.ctx.builder.literal(part.chars, this.ctx.loc(part.loc)), @@ -721,7 +753,7 @@ class ElementNormalizer { if (isComponent) { let path = b.path({ - head: b.head(variable, variableLoc), + head: b.head({ original: variable, loc: variableLoc }), tail, loc: pathLoc, }); @@ -950,6 +982,24 @@ class ElementChildren extends Children { } } +function isLiteral(node: ASTv1.Expression): node is ASTv1.Literal { + switch (node.type) { + case 'StringLiteral': + case 'BooleanLiteral': + case 'NumberLiteral': + case 'UndefinedLiteral': + case 'NullLiteral': + return true; + default: + return false; + } +} + +function assertIllegalLiteral(node: ASTv1.Literal, loc: SourceSpan): never { + let value = node.type === 'StringLiteral' ? JSON.stringify(node.value) : String(node.value); + throw generateSyntaxError(`Unexpected literal \`${value}\``, loc); +} + function printPath(node: ASTv1.PathExpression | ASTv1.CallNode): string { if (node.type !== 'PathExpression' && node.path.type === 'PathExpression') { return printPath(node.path); @@ -960,13 +1010,7 @@ function printPath(node: ASTv1.PathExpression | ASTv1.CallNode): string { function printHead(node: ASTv1.PathExpression | ASTv1.CallNode): string { if (node.type === 'PathExpression') { - switch (node.head.type) { - case 'AtHead': - case 'VarHead': - return node.head.name; - case 'ThisHead': - return 'this'; - } + return node.head.original; } else if (node.path.type === 'PathExpression') { return printHead(node.path); } else { diff --git a/packages/@glimmer/syntax/test/loc-node-test.ts b/packages/@glimmer/syntax/test/loc-node-test.ts index d148b717ac..b7ce04eca5 100644 --- a/packages/@glimmer/syntax/test/loc-node-test.ts +++ b/packages/@glimmer/syntax/test/loc-node-test.ts @@ -4,24 +4,28 @@ import { guardArray } from '@glimmer-workspace/test-utils'; QUnit.module('[glimmer-syntax] Parser - Location Info'); -function assertNodeType( - node: AST.Node | null | undefined, +function assertNodeType(node: unknown, type: T): node is AST.Nodes[T]; +function assertNodeType( + node: unknown, type: T -): node is AST.Nodes[T] { - let nodeType = node && node.type; - QUnit.assert.pushResult({ - result: nodeType === type, - actual: nodeType, - expected: type, - message: `expected node type to be ${type} but was ${String(nodeType)}`, - }); +): node is AST.SubNodes[T]; +function assertNodeType(node: unknown, type: string): boolean { + let nodeType: unknown = undefined; + + try { + nodeType = (node as { type?: unknown } | null | undefined)?.type; + } catch { + // no-op + } + + QUnit.assert.strictEqual(nodeType, type, `expected node type to be ${type}`); return nodeType === type; } const { test } = QUnit; function locEqual( - node: AST.Node | null | undefined, + node: AST.Node | AST.SubNode | src.SourceSpan | null | undefined, startLine: number, startColumn: number, endLine: number, @@ -33,11 +37,15 @@ function locEqual( end: { line: endLine, column: endColumn }, }; - QUnit.assert.deepEqual( - node && { start: node.loc.startPosition, end: node.loc.endPosition }, - expected, - message - ); + let actual: src.SourceLocation | null | undefined; + + if (node && 'type' in node) { + actual = node.loc.toJSON(); + } else if (node && 'toJSON' in node) { + actual = node.toJSON(); + } + + QUnit.assert.deepEqual(actual, expected, message); } test('programs', () => { @@ -110,7 +118,7 @@ test('element modifier', () => { } }); -test('html elements', () => { +test('html elements', (assert) => { let ast = parse(`

@@ -121,39 +129,92 @@ test('html elements', () => { `); let [, section] = ast.body; - locEqual(section, 2, 4, 7, 14, 'section element'); + if (assertNodeType(section, 'ElementNode')) { + locEqual(section, 2, 4, 7, 14, 'section element'); + locEqual(section.path, 2, 5, 2, 12); + locEqual(section.path.head, 2, 5, 2, 12); + locEqual(section.openTag, 2, 4, 2, 13, '
'); + locEqual(section.closeTag, 7, 4, 7, 14, '
'); + let [, br, , div] = section.children; - locEqual(br, 3, 6, 3, 10, 'br element'); - locEqual(div, 4, 6, 6, 12, 'div element'); + + if (assertNodeType(br, 'ElementNode')) { + locEqual(br, 3, 6, 3, 10, 'br element'); + locEqual(br.path, 3, 7, 3, 9); + locEqual(br.path.head, 3, 7, 3, 9); + locEqual(br.openTag, 3, 6, 3, 10, '
'); + 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 + '>
'; 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;