diff --git a/.github/workflows/ci-jobs.yml b/.github/workflows/ci-jobs.yml index 0cafa28c79d..716194715cf 100644 --- a/.github/workflows/ci-jobs.yml +++ b/.github/workflows/ci-jobs.yml @@ -37,8 +37,6 @@ jobs: run: pnpm build:types - name: Check internal types run: pnpm type-check:internals - - name: Check @handlebars/parser types - run: pnpm type-check:handlebars - name: Check published types run: pnpm type-check:types @@ -222,7 +220,7 @@ jobs: SHOULD_TRANSPILE_FOR_NODE: true run: pnpm build - name: test - run: pnpm test:node && pnpm --filter "@handlebars/parser" test + run: pnpm test:node blueprint-test: name: Blueprint Tests diff --git a/.github/workflows/glimmer-syntax-prettier-smoke-test.yml b/.github/workflows/glimmer-syntax-prettier-smoke-test.yml index 3b87973841f..391af75c8f9 100644 --- a/.github/workflows/glimmer-syntax-prettier-smoke-test.yml +++ b/.github/workflows/glimmer-syntax-prettier-smoke-test.yml @@ -17,7 +17,6 @@ on: - "packages/@glimmer/interfaces/**" - "packages/@glimmer/util/**" - "packages/@glimmer/wire-format/**" - - "packages/@handlebars/parser/**" pull_request: paths: - ".github/workflows/glimmer-syntax-prettier-smoke-test.yml" @@ -27,7 +26,6 @@ on: - "packages/@glimmer/interfaces/**" - "packages/@glimmer/util/**" - "packages/@glimmer/wire-format/**" - - "packages/@handlebars/parser/**" workflow_dispatch: permissions: @@ -63,6 +61,10 @@ jobs: working-directory: prettier-repo run: yarn add "@glimmer/syntax@file:${{ github.workspace }}/glimmer-syntax.tgz" + - name: Update error snapshots (our error messages differ from Jison's verbose format) + working-directory: prettier-repo + run: yarn jest --updateSnapshot tests/format/handlebars/_errors_/ + - name: Run prettier handlebars tests working-directory: prettier-repo run: yarn jest tests/format/handlebars diff --git a/bench-cli.mjs b/bench-cli.mjs new file mode 100644 index 00000000000..57674d3b176 --- /dev/null +++ b/bench-cli.mjs @@ -0,0 +1,291 @@ +/** + * CLI/build-style benchmark: simulates a real build pass over a project. + * + * IDE benchmark: same template, many iterations (measures JIT-warmed throughput) + * CLI benchmark: many distinct templates, one pass (cold-ish JIT, one-time init cost) + * + * Run: node bench-cli.mjs + */ + +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const currentDistPath = join(__dirname, 'packages/@glimmer/syntax/dist/es/index.js'); +const prDistPath = '/tmp/pr-21313/packages/@glimmer/syntax/dist/es/index.js'; + +// ─── Realistic template corpus ──────────────────────────────────────────────── +// ~50 distinct templates of varying complexity, simulating a real Ember project. + +const TEMPLATES = [ + `
{{this.title}}
`, + `{{@label}}`, + ``, + `{{#if this.isLoading}}{{else}}{{yield}}{{/if}}`, + ``, + ``, + `
{{yield}}
`, + `

{{this.title}}

{{this.description}}

`, + `{{#let (hash name=@name age=@age) as |person|}}{{person.name}}{{/let}}`, + `
`, + + ``, + + ``, + + ` + + + {{#each @columns as |col|}} + + {{/each}} + + + + {{#each this.sortedRows as |row|}} + + {{#each @columns as |col|}} + + {{/each}} + + {{/each}} + +
+ {{col.label}} + {{#if (eq this.sortKey col.key)}} + + {{/if}} +
{{get row col.key}}
`, + + ``, + + ``, + + `{{#each @notifications as |notif|}} + +{{/each}}`, + + ``, + + `
+
+ {{#each this.toolbarButtons as |btn|}} + + {{/each}} +
+
+
+
`, + + `
+
+ +

{{this.monthLabel}} {{this.year}}

+ +
+
+ {{#each this.weeks as |week|}} +
+ {{#each week as |day|}} + + {{/each}} +
+ {{/each}} +
+
`, + + // Extra medium-sized templates to fill out the corpus + ...Array.from( + { length: 30 }, + (_, i) => ` +
+
+

{{@title}}

+ {{#if @subtitle}}

{{@subtitle}}

{{/if}} +
+
+ {{#each @items as |item|}} +
+

{{item.name}}

+ {{#if item.description}} +

{{item.description}}

+ {{/if}} +
+ {{item.category}} + +
+
+ {{/each}} +
+
` + ), +]; + +console.log( + `Corpus: ${TEMPLATES.length} distinct templates, total ${TEMPLATES.reduce((s, t) => s + t.length, 0)} chars\n` +); + +// ─── Measurements ───────────────────────────────────────────────────────────── + +async function measureParser(label, distPath) { + // Measure cold first-parse (includes module load + any lazy init like WASM) + const t0 = performance.now(); + const { preprocess } = await import(distPath); + const loadMs = performance.now() - t0; + + // First parse (triggers WASM init if applicable, cold V8) + const t1 = performance.now(); + preprocess(TEMPLATES[0]); + const firstParseMs = performance.now() - t1; + + // Single-pass build simulation: parse each template once (no repetition) + // Run this 10 times to get stable numbers (simulates running the build tool 10x) + const buildTimes = []; + for (let run = 0; run < 10; run++) { + const start = performance.now(); + for (const tpl of TEMPLATES) preprocess(tpl); + buildTimes.push(performance.now() - start); + } + const buildMin = Math.min(...buildTimes); + const buildMed = buildTimes.slice().sort((a, b) => a - b)[5]; // p50 + + // Extrapolate to a 500-template project + const perTemplate = buildMin / TEMPLATES.length; + const proj500 = perTemplate * 500; + + return { label, loadMs, firstParseMs, buildMin, buildMed, perTemplate, proj500 }; +} + +console.log('Loading and measuring (this takes ~10s)...\n'); + +const current = await measureParser('current branch', currentDistPath); +const pr = await measureParser('PR #21313 (rust)', prDistPath); + +// ─── Output ─────────────────────────────────────────────────────────────────── + +function row(label, cur, prv, unit = 'ms', lowerIsBetter = true) { + const winner = lowerIsBetter + ? cur < prv + ? 'current' + : 'rust-pr' + : cur > prv + ? 'current' + : 'rust-pr'; + const ratio = winner === 'current' ? (prv / cur).toFixed(2) : (cur / prv).toFixed(2); + const arrow = winner === 'current' ? '<' : '>'; + console.log( + ` ${label.padEnd(32)} ${String(cur.toFixed(2) + unit).padStart(10)} ${arrow} ${String(prv.toFixed(2) + unit).padStart(10)} ${ratio}x (${winner} wins)` + ); +} + +console.log( + `${'Metric'.padEnd(32)} ${'current'.padStart(10)} ${'PR#21313'.padStart(10)} winner` +); +console.log('-'.repeat(80)); + +row('Module load (import)', current.loadMs, pr.loadMs); +row('First parse (cold)', current.firstParseMs, pr.firstParseMs); +row(`Build pass (${TEMPLATES.length} tpl, best of 10)`, current.buildMin, pr.buildMin); +row(`Build pass (p50)`, current.buildMed, pr.buildMed); +row('Per-template avg (build)', current.perTemplate, pr.perTemplate, 'ms'); +row('500-template project (proj)', current.proj500, pr.proj500, 'ms'); + +console.log(''); +console.log('Notes:'); +console.log(` current branch : JS pipeline (handlebars v2 parser)`); +console.log(` PR #21313 : Rust/WASM (pest.rs) + JSON bridge + JS post-processing`); +console.log( + ` "build pass" : single-pass over ${TEMPLATES.length} distinct templates (no repeat, simulates CLI)` +); +console.log(` "first parse" : includes any lazy WASM init (one-time per process)`); diff --git a/bench-compare.mjs b/bench-compare.mjs new file mode 100644 index 00000000000..8eb5f98fee4 --- /dev/null +++ b/bench-compare.mjs @@ -0,0 +1,204 @@ +/** + * Parser benchmark: this branch (perf/handlebars-v2-parser) vs PR #21313 (rust-parser-pest) + * + * Run: node bench-compare.mjs + */ + +import { createRequire } from 'module'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const currentDistPath = join(__dirname, 'packages/@glimmer/syntax/dist/es/index.js'); +const prDistPath = '/tmp/pr-21313/packages/@glimmer/syntax/dist/es/index.js'; + +// ─── Templates ──────────────────────────────────────────────────────────────── + +const small = `
{{this.title}}
`; + +const medium = ` +
+

{{this.title}}

+ {{#each this.items as |item index|}} +
+ {{item.name}} + +
+ {{/each}} + {{#if this.showFooter}} +
{{this.footerText}}
+ {{/if}} +
`; + +const large = medium.repeat(10); + +const realWorld = ` +
+
+ {{this.username}} +
+

{{this.displayName}}

+

{{this.bio}}

+ {{this.role}} +
+ {{#if this.isOwnProfile}} + + {{/if}} +
+ + + +
+ {{#if (eq this.activeTab "posts")}} + {{#each this.posts as |post|}} +
+

{{post.title}}

+

{{post.excerpt}}

+
+ + {{post.views}} views +
+
+ {{else}} +

No posts yet.

+ {{/each}} + {{else if (eq this.activeTab "followers")}} + {{#each this.followers as |follower|}} +
+ {{follower.name}} + {{follower.name}} + +
+ {{/each}} + {{/if}} +
+
`; + +const templates = [ + ['small', small], + ['medium', medium], + ['large', large], + ['real-world', realWorld], +]; + +// ─── Benchmark runner ───────────────────────────────────────────────────────── + +function bench(preprocess, name, template, iterations = 1000) { + // warm up + for (let i = 0; i < 50; i++) preprocess(template); + + const start = performance.now(); + for (let i = 0; i < iterations; i++) preprocess(template); + const elapsed = performance.now() - start; + + return { + name, + chars: template.length, + ms: elapsed / iterations, + }; +} + +function printTable(rows) { + const colWidths = [18, 8, 12, 12, 12]; + const headers = ['template', 'chars', 'current (ms)', 'pr#21313 (ms)', 'speedup']; + const sep = colWidths.map((w) => '-'.repeat(w)).join('-+-'); + + const pad = (s, w) => String(s).padEnd(w); + const rpad = (s, w) => String(s).padStart(w); + + console.log('\n' + headers.map((h, i) => pad(h, colWidths[i])).join(' | ')); + console.log(sep); + for (const row of rows) { + const faster = row.current < row.pr ? 'current' : 'rust-pr'; + const ratio = + row.current < row.pr + ? (row.pr / row.current).toFixed(2) + 'x (current wins)' + : (row.current / row.pr).toFixed(2) + 'x (rust wins)'; + console.log( + [ + pad(row.template, colWidths[0]), + rpad(row.chars, colWidths[1]), + rpad(row.current.toFixed(3), colWidths[2]), + rpad(row.pr.toFixed(3), colWidths[3]), + rpad(ratio, colWidths[4] + 20), + ].join(' | ') + ); + } +} + +// ─── Main ───────────────────────────────────────────────────────────────────── + +console.log('Loading parsers...'); + +const { preprocess: preprocessCurrent } = await import(currentDistPath); +const { preprocess: preprocessPR } = await import(prDistPath); + +console.log('Parsers loaded. Running benchmarks...\n'); + +// Verify both produce output (smoke check) +try { + const r1 = preprocessCurrent('
{{foo}}
'); + const r2 = preprocessPR('
{{foo}}
'); + console.log(`Current branch: ${r1.type} (${r1.body.length} top-level nodes)`); + console.log(`PR #21313: ${r2.type} (${r2.body.length} top-level nodes)`); +} catch (e) { + console.error('Smoke check failed:', e.message); + process.exit(1); +} + +console.log(''); + +const N = 1000; +const rows = []; + +for (const [name, tpl] of templates) { + process.stdout.write(` Benchmarking '${name}'...`); + const currentResult = bench(preprocessCurrent, name, tpl, N); + const prResult = bench(preprocessPR, name, tpl, N); + process.stdout.write(' done\n'); + rows.push({ + template: name, + chars: tpl.length, + current: currentResult.ms, + pr: prResult.ms, + }); +} + +printTable(rows); + +// Phase breakdown: measure the PR's WASM parse vs JS post-processing +console.log('\n--- Phase breakdown (PR #21313, medium template) ---'); +const { parseTemplateToJson } = + await import('/tmp/pr-21313/packages/@glimmer/syntax/pkg/universal.mjs'); +const src = medium; +const N2 = 1000; + +// warm up WASM +for (let i = 0; i < 50; i++) parseTemplateToJson(src); + +const startWasm = performance.now(); +for (let i = 0; i < N2; i++) parseTemplateToJson(src); +const wasmMs = (performance.now() - startWasm) / N2; + +const startFull = performance.now(); +for (let i = 0; i < N2; i++) preprocessPR(src); +const fullMs = (performance.now() - startFull) / N2; + +console.log(` WASM parse only: ${wasmMs.toFixed(3)}ms`); +console.log(` Full preprocess() (PR): ${fullMs.toFixed(3)}ms`); +console.log(` JS post-processing cost: ${(fullMs - wasmMs).toFixed(3)}ms`); diff --git a/bench-full-pipeline.mjs b/bench-full-pipeline.mjs new file mode 100644 index 00000000000..243a359e707 --- /dev/null +++ b/bench-full-pipeline.mjs @@ -0,0 +1,208 @@ +/** + * Full compile pipeline benchmark: preprocess() → normalize() → compile() → wire format + * + * Uses ember-template-compiler's precompile() which exercises the entire stack. + * Three-way comparison: main (Jison), v2-parser (this branch), rust/wasm (PR #21313). + * + * Run: node bench-full-pipeline.mjs + */ + +const MAIN = '/tmp/ember-main/dist/packages/ember-template-compiler/index.js'; +const V2 = '/Users/real-world-project/ember.js/dist/packages/ember-template-compiler/index.js'; +const RUST = '/tmp/pr-21313/dist/dev/packages/ember-template-compiler/index.js'; + +// Also import the syntax-only preprocess for the parse-only split +const MAIN_SYNTAX = '/tmp/ember-main/packages/@glimmer/syntax/dist/es/index.js'; +const V2_SYNTAX = '/Users/real-world-project/ember.js/packages/@glimmer/syntax/dist/es/index.js'; +const RUST_SYNTAX = '/tmp/pr-21313/packages/@glimmer/syntax/dist/es/index.js'; + +// ── Templates ────────────────────────────────────────────────────────────────── + +const small = `
{{this.title}}
`; + +const medium = ` +
+

{{this.title}}

+ {{#each this.items as |item index|}} +
+ {{item.name}} + +
+ {{/each}} + {{#if this.showFooter}} +
{{this.footerText}}
+ {{/if}} +
`; + +const realWorld = ` +
+
+ {{this.username}} +

{{this.displayName}}

+

{{this.bio}}

+ {{#if this.isOwnProfile}} + + {{/if}} +
+ +
+ {{#if (eq this.activeTab "posts")}} + {{#each this.posts as |post|}} +
+

{{post.title}}

{{post.excerpt}}

+
{{post.views}} views
+
+ {{else}} +

No posts yet.

+ {{/each}} + {{else if (eq this.activeTab "followers")}} + {{#each this.followers as |follower|}} +
+ {{follower.name}} + {{follower.name}} + +
+ {{/each}} + {{/if}} +
+
`; + +const large = medium.repeat(10); + +const templates = [ + ['small', small, 2000], + ['medium', medium, 1000], + ['real-world', realWorld, 1000], + ['large (10x)', large, 300], +]; + +// ── Helpers ──────────────────────────────────────────────────────────────────── + +function bench(fn, tpl, N) { + for (let i = 0; i < Math.min(50, N); i++) fn(tpl); + const t = performance.now(); + for (let i = 0; i < N; i++) fn(tpl); + return (performance.now() - t) / N; +} + +function pct(part, total) { + return ((part / total) * 100).toFixed(0) + '%'; +} + +// ── Load all parsers ─────────────────────────────────────────────────────────── + +console.log('Loading compilers...'); +const [ + { precompile: compileMain }, + { precompile: compileV2 }, + { precompile: compileRust }, + { preprocess: parseMain }, + { preprocess: parseV2 }, + { preprocess: parseRust }, +] = await Promise.all([ + import(MAIN), + import(V2), + import(RUST), + import(MAIN_SYNTAX), + import(V2_SYNTAX), + import(RUST_SYNTAX), +]); +console.log('Loaded.\n'); + +// ── Section 1: full precompile() ─────────────────────────────────────────────── + +console.log('━'.repeat(90)); +console.log('FULL PIPELINE: precompile() → wire format (ms/call, warmed JIT)'); +console.log('━'.repeat(90)); +console.log( + 'template chars main(Jison) v2-parser rust/wasm v2vsJison v2vsRust' +); +console.log('─'.repeat(90)); + +const fullResults = {}; +for (const [name, tpl, N] of templates) { + const m = bench(compileMain, tpl, N); + const v = bench(compileV2, tpl, N); + const r = bench(compileRust, tpl, N); + fullResults[name] = { m, v, r, chars: tpl.length }; + console.log( + name.padEnd(16) + + String(tpl.length).padStart(6) + + ' ' + + m.toFixed(3).padStart(11) + + ' ' + + v.toFixed(3).padStart(11) + + ' ' + + r.toFixed(3).padStart(10) + + ' ' + + (m / v).toFixed(2).padStart(7) + + 'x ' + + (r / v).toFixed(2).padStart(7) + + 'x' + ); +} + +// ── Section 2: parse-only vs full compile split ──────────────────────────────── + +console.log('\n' + '━'.repeat(90)); +console.log('PARSE vs COMPILE SPLIT (medium template, showing where time goes)'); +console.log('━'.repeat(90)); + +const N_SPLIT = 2000; +const parseOnlyMain = bench(parseMain, medium, N_SPLIT); +const parseOnlyV2 = bench(parseV2, medium, N_SPLIT); +const parseOnlyRust = bench(parseRust, medium, N_SPLIT); +const fullMain = bench(compileMain, medium, N_SPLIT); +const fullV2 = bench(compileV2, medium, N_SPLIT); +const fullRust = bench(compileRust, medium, N_SPLIT); + +const compileOnlyMain = fullMain - parseOnlyMain; +const compileOnlyV2 = fullV2 - parseOnlyV2; +const compileOnlyRust = fullRust - parseOnlyRust; + +console.log('\n main(Jison) v2-parser rust/wasm'); +console.log('─'.repeat(70)); +console.log( + 'parse() ' + + `${parseOnlyMain.toFixed(3)}ms (${pct(parseOnlyMain, fullMain)})`.padEnd(20) + + `${parseOnlyV2.toFixed(3)}ms (${pct(parseOnlyV2, fullV2)})`.padEnd(20) + + `${parseOnlyRust.toFixed(3)}ms (${pct(parseOnlyRust, fullRust)})` +); +console.log( + 'compile only ' + + `${compileOnlyMain.toFixed(3)}ms (${pct(compileOnlyMain, fullMain)})`.padEnd(20) + + `${compileOnlyV2.toFixed(3)}ms (${pct(compileOnlyV2, fullV2)})`.padEnd(20) + + `${compileOnlyRust.toFixed(3)}ms (${pct(compileOnlyRust, fullRust)})` +); +console.log( + 'total ' + + `${fullMain.toFixed(3)}ms`.padEnd(20) + + `${fullV2.toFixed(3)}ms`.padEnd(20) + + `${fullRust.toFixed(3)}ms` +); + +// ── Section 3: 500-template project projection ───────────────────────────────── + +console.log('\n' + '━'.repeat(90)); +console.log('500-TEMPLATE PROJECT (build-time projection, using real-world template timing)'); +console.log('━'.repeat(90)); + +const { m: rwm, v: rwv, r: rwr } = fullResults['real-world']; +const scale = 500; +console.log( + `\n main(Jison): ${(rwm * scale).toFixed(0)}ms total (${rwm.toFixed(3)}ms × ${scale})` +); +console.log( + ` v2-parser: ${(rwv * scale).toFixed(0)}ms total (${rwv.toFixed(3)}ms × ${scale}) — ${(rwm / rwv).toFixed(2)}x faster than Jison` +); +console.log( + ` rust/wasm: ${(rwr * scale).toFixed(0)}ms total (${rwr.toFixed(3)}ms × ${scale}) — ${(rwr / rwv).toFixed(2)}x slower than v2` +); diff --git a/eslint.config.mjs b/eslint.config.mjs index 7407d64b580..5b92444cc22 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -28,9 +28,14 @@ export default [ '**/type-tests/', 'internal-docs/guides/**', 'packages/@glimmer-workspace/**', - 'packages/@handlebars/parser/lib/parser.js', - 'packages/@handlebars/parser/src/**', + 'packages/@glimmer/syntax/lib/parser/v2-parser.js', + 'packages/@glimmer/syntax/lib/parser/whitespace-control.js', + 'packages/@glimmer/syntax/lib/parser/visitor.js', + 'packages/@glimmer/syntax/lib/parser/exception.js', 'tracerbench-testing/', + 'bench-cli.mjs', + 'bench-compare.mjs', + 'bench-full-pipeline.mjs', ], }, pluginJs.configs.recommended, @@ -189,15 +194,6 @@ export default [ 'import/namespace': 'off', }, }, - { - files: ['packages/@handlebars/parser/spec/**/*.js'], - - languageOptions: { - globals: { - ...globals.mocha, - }, - }, - }, { files: [ 'packages/*/tests/**/*.[jt]s', diff --git a/internal-docs/guides/development/build-constraints.md b/internal-docs/guides/development/build-constraints.md index 8e94bd664f6..f2e59cd76d4 100644 --- a/internal-docs/guides/development/build-constraints.md +++ b/internal-docs/guides/development/build-constraints.md @@ -137,7 +137,6 @@ The build system has specific rules for what gets inlined vs treated as external - TypeScript helper library (`tslib`) **Always External:** -- `@handlebars/parser` - `simple-html-tokenizer` - `babel-plugin-debug-macros` - Other `@glimmer/*` packages (to avoid duplication) diff --git a/package.json b/package.json index 008724da9c5..dfe482170d7 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "test:browserstack": "node bin/run-browserstack-tests.js", "test:wip": "vite build --mode development --minify false && testem ci", "type-check:internals": "tsc --noEmit", - "type-check:handlebars": "tsc --noEmit --project packages/@handlebars/parser/tsconfig.json", "type-check:types": "tsc --noEmit --project type-tests", "type-check": "npm-run-all type-check:*" }, diff --git a/packages/@handlebars/parser/lib/exception.js b/packages/@glimmer/syntax/lib/parser/exception.js similarity index 100% rename from packages/@handlebars/parser/lib/exception.js rename to packages/@glimmer/syntax/lib/parser/exception.js diff --git a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts index 6052abbe97f..868af197227 100644 --- a/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts +++ b/packages/@glimmer/syntax/lib/parser/tokenizer-event-handlers.ts @@ -9,7 +9,7 @@ import { localAssert, } from '@glimmer/debug-util'; import { assign } from '@glimmer/util'; -import { parse, parseWithoutProcessing } from '@handlebars/parser'; +import { parse, parseWithoutProcessing } from './v2-parser'; import { EntityParser } from 'simple-html-tokenizer'; import type { EndTag, StartTag } from '../parser'; diff --git a/packages/@glimmer/syntax/lib/parser/v2-parser.js b/packages/@glimmer/syntax/lib/parser/v2-parser.js new file mode 100644 index 00000000000..a93b8b5e497 --- /dev/null +++ b/packages/@glimmer/syntax/lib/parser/v2-parser.js @@ -0,0 +1,1959 @@ +// @ts-nocheck +/** + * Claude-iterated POC for a recursive descent parser for Handlebars templates. + * Drop-in replacement for the Jison-generated parser. + * + * Key optimizations over Jison: + * 1. Index-based scanning (never slices the input string to advance) + * 2. indexOf('{{') for content scanning instead of regex + * 3. charCodeAt dispatch instead of testing 40 regexes per token + * 4. Line/col tracking via indexOf('\n') batching + * 5. No intermediate token objects — parser reads directly from input + */ + +import Exception from './exception.js'; +import WhitespaceControl from './whitespace-control.js'; + +export function parseWithoutProcessing(input, options) { + return v2ParseWithoutProcessing(input, options); +} + +export function parse(input, options) { + const ast = v2ParseWithoutProcessing(input, options); + const strip = new WhitespaceControl(options); + return strip.accept(ast); +} + +// Character codes +const CH_NL = 10; // \n +const CH_CR = 13; // \r +const CH_SPACE = 32; +const CH_TAB = 9; +const CH_BANG = 33; // ! +const CH_DQUOTE = 34; // " +const CH_HASH = 35; // # +const CH_DOLLAR = 36; // $ +const CH_AMP = 38; // & +const CH_SQUOTE = 39; // ' +const CH_LPAREN = 40; // ( +const CH_RPAREN = 41; // ) +const CH_STAR = 42; // * +const CH_DASH = 45; // - +const CH_DOT = 46; // . +const CH_SLASH = 47; // / +const CH_0 = 48; +const CH_9 = 57; +const CH_SEMI = 59; // ; +const CH_EQ = 61; // = +const CH_GT = 62; // > +const CH_AT = 64; // @ +const CH_LBRACKET = 91; // [ +const CH_BACKSLASH = 92; // \\ +const CH_RBRACKET = 93; // ] +const CH_CARET = 94; // ^ +const CH_BACKTICK = 96; // ` +const CH_LBRACE = 123; // { +const CH_PIPE = 124; // | +const CH_RBRACE = 125; // } +const CH_TILDE = 126; // ~ + +/** + * Check if a character code can appear in a Handlebars ID. + * Based on the ID regex: [^\s!"#%-,\.\/;->@\[-\^`\{-~]+ + */ +function isIdChar(c) { + if (c <= CH_SPACE) return false; // whitespace + control + if (c === CH_BANG || c === CH_DQUOTE || c === CH_HASH) return false; + if (c >= 37 && c <= 44) return false; // % & ' ( ) * + , + if (c === CH_DOT || c === CH_SLASH) return false; + if (c >= CH_SEMI && c <= CH_GT) return false; // ; < = > + if (c === CH_AT) return false; + if (c >= CH_LBRACKET && c <= CH_CARET) return false; // [ \ ] ^ + if (c === CH_BACKTICK) return false; + if (c >= CH_LBRACE && c <= CH_TILDE) return false; // { | } ~ + return true; +} + +function isWhitespace(c) { + return c === CH_SPACE || c === CH_TAB || c === CH_NL || c === CH_CR || c === 12; // form feed +} + +/** + * Check if a character is a lookahead character for ID/literal matching. + * LOOKAHEAD = [=~}\s\/.)\]|] + */ +function isLookahead(c) { + return ( + c === CH_EQ || + c === CH_TILDE || + c === CH_RBRACE || + isWhitespace(c) || + c === CH_SLASH || + c === CH_DOT || + c === CH_RPAREN || + c === CH_RBRACKET || + c === CH_PIPE || + c !== c // NaN (past end of string) + ); +} + +/** + * LITERAL_LOOKAHEAD = [~}\s)\]] + */ +function isLiteralLookahead(c) { + return ( + c === CH_TILDE || + c === CH_RBRACE || + isWhitespace(c) || + c === CH_RPAREN || + c === CH_RBRACKET || + c !== c // NaN + ); +} + +/** + * Strip brackets from an ID token: [foo] → foo + */ +function idFromToken(token) { + if (token.charCodeAt(0) === CH_LBRACKET && token.charCodeAt(token.length - 1) === CH_RBRACKET) { + return token.substring(1, token.length - 1); + } + return token; +} + +function stripComment(comment) { + return comment.replace(/^\{\{~?!-?-?/, '').replace(/-?-?~?\}\}$/, ''); +} + +export function v2ParseWithoutProcessing(input, options) { + if (typeof input !== 'string') { + // Pass through already-compiled AST + if (input.type === 'Program') return input; + throw new Error('Expected string or Program AST'); + } + + // === State === + let pos = 0; + let line = 1; + let col = 0; + const len = input.length; + const srcName = options?.srcName ?? undefined; + + // Syntax options + let squareSyntax; + if (typeof options?.syntax?.square === 'function') { + squareSyntax = options.syntax.square; + } else if (options?.syntax?.square === 'node') { + squareSyntax = arrayLiteralNode; + } else { + squareSyntax = 'string'; + } + + let hashSyntax; + if (typeof options?.syntax?.hash === 'function') { + hashSyntax = options.syntax.hash; + } else { + hashSyntax = hashLiteralNode; + } + + // yy-like context for helper callbacks + const yy = { preparePath, id: idFromToken, locInfo: makeLoc }; + + // === Position tracking === + + function advanceTo(target) { + while (pos < target) { + const nl = input.indexOf('\n', pos); + if (nl === -1 || nl >= target) { + col += target - pos; + pos = target; + return; + } + // Count the newline + line++; + col = 0; + pos = nl + 1; + } + } + + function cc(offset) { + return input.charCodeAt(pos + (offset || 0)); + } + + function startsWith(str, offset) { + return input.startsWith(str, pos + (offset || 0)); + } + + function makeLoc(sl, sc, el, ec) { + return { + source: srcName, + start: { line: sl, column: sc }, + end: { line: el || line, column: ec !== undefined ? ec : col }, + }; + } + + function savePos() { + return { line, col }; + } + + function locFrom(start) { + return makeLoc(start.line, start.col, line, col); + } + + function error(msg) { + throw new Exception( + 'Parse error on line ' + line + ':\n' + input.slice(pos, pos + 20) + '\n' + msg, + { + loc: makeLoc(line, col), + } + ); + } + + // === Scanning primitives === + + function skipWs() { + while (pos < len && isWhitespace(cc())) { + if (cc() === CH_NL) { + line++; + col = 0; + pos++; + } else if (cc() === CH_CR) { + line++; + col = 0; + pos++; + if (pos < len && cc() === CH_NL) pos++; // \r\n + } else { + col++; + pos++; + } + } + } + + function scanId() { + const start = pos; + while (pos < len && isIdChar(cc())) { + col++; + pos++; + } + if (pos === start) return null; + return input.substring(start, pos); + } + + function scanEscapedLiteral() { + // We're at '[', scan to matching ']' with backslash escaping + if (cc() !== CH_LBRACKET) return null; + const start = pos; + col++; + pos++; // skip [ + while (pos < len) { + const c = cc(); + if (c === CH_BACKSLASH && pos + 1 < len) { + col += 2; + pos += 2; // skip escaped char + } else if (c === CH_RBRACKET) { + col++; + pos++; // skip ] + const raw = input.substring(start, pos); + return raw.replace(/\\([\\\]])/g, '$1'); + } else if (c === CH_NL) { + line++; + col = 0; + pos++; + } else { + col++; + pos++; + } + } + error('Unterminated escaped literal'); + } + + function scanString() { + const quote = cc(); + if (quote !== CH_DQUOTE && quote !== CH_SQUOTE) return null; + const startPos = pos; + const startP = savePos(); + col++; + pos++; // skip opening quote + let result = ''; + let segStart = pos; + while (pos < len) { + const c = cc(); + if (c === CH_BACKSLASH && pos + 1 < len && cc(1) === quote) { + result += input.substring(segStart, pos); + col += 2; + pos += 2; + result += String.fromCharCode(quote); + segStart = pos; + } else if (c === quote) { + result += input.substring(segStart, pos); + col++; + pos++; // skip closing quote + return { value: result, original: result, loc: locFrom(startP) }; + } else if (c === CH_NL) { + line++; + col = 0; + pos++; + } else { + col++; + pos++; + } + } + error('Unterminated string'); + } + + function scanNumber() { + const start = pos; + if (cc() === CH_DASH) { + col++; + pos++; + } + if (pos >= len || cc() < CH_0 || cc() > CH_9) { + // Not a number, restore + advanceTo(start); // no-op if no dash + pos = start; + col = col - (pos - start); // crude restore + return null; + } + // Actually, let me just save/restore properly + const savedLine = line; + const savedCol = col; + + // Reset to start for proper scanning + pos = start; + line = savedLine; + col = savedCol - (pos === start ? 0 : 1); + + if (cc() === CH_DASH) { + col++; + pos++; + } + while (pos < len && cc() >= CH_0 && cc() <= CH_9) { + col++; + pos++; + } + if (pos < len && cc() === CH_DOT) { + col++; + pos++; + while (pos < len && cc() >= CH_0 && cc() <= CH_9) { + col++; + pos++; + } + } + // Check literal lookahead + if (pos < len && !isLiteralLookahead(cc())) { + // Not a valid number, restore + pos = start; + line = savedLine; + col = savedCol - (pos - start); + return null; + } + return input.substring(start, pos); + } + + // === Content scanning === + + function scanContent() { + if (pos >= len) return null; + const startP = savePos(); + const start = pos; + let result = ''; + let segStart = pos; + + while (pos < len) { + const idx = input.indexOf('{{', pos); + if (idx === -1) { + // Rest is content + advanceTo(len); + result += input.substring(segStart, len); + if (result.length === 0) return null; + return { + type: 'ContentStatement', + original: result, + value: result, + loc: locFrom(startP), + }; + } + + // Check for escaped mustache — only if the backslash is within our scan range + if (idx > pos && input.charCodeAt(idx - 1) === CH_BACKSLASH) { + if (idx > pos + 1 && input.charCodeAt(idx - 2) === CH_BACKSLASH) { + // \\{{ — the \\ is a literal backslash, {{ is a real mustache + // Content includes everything up to \\{{ with one backslash stripped + result += input.substring(segStart, idx - 1); // strip one backslash + advanceTo(idx); // advance to the real {{ (not past it) + if (result.length === 0) return null; + return { + type: 'ContentStatement', + original: result, + value: result, + loc: locFrom(startP), + }; + } + // \{{ — escaped mustache. Jison handles this by: + // 1. Emitting content up to the \ (stripping it) as CONTENT + // 2. Entering emu state which scans to next {{/\{{/\\{{/EOF + // 3. Emitting that chunk as another CONTENT + // + // We match this by: emit what we have so far (up to the \, stripped), + // then advance past \{{ and let the emu scan produce the next content. + + // First: emit content accumulated so far (before the backslash) + advanceTo(idx - 1); + result += input.substring(segStart, idx - 1); + if (result.length > 0) { + return { + type: 'ContentStatement', + original: result, + value: result, + loc: locFrom(startP), + }; + } + + // If no content before the \, advance past the \{{ and scan emu content + advanceTo(idx + 2); // past \{{ + const emuStartP = savePos(); + const emuStart = pos; + const nextMu = findNextMustacheOrEnd(pos); + advanceTo(nextMu); + const emuContent = '{{' + input.substring(emuStart, nextMu); + return { + type: 'ContentStatement', + original: emuContent, + value: emuContent, + loc: makeLoc(startP.line, startP.col, line, col), + }; + } + + // Normal {{ — stop here + advanceTo(idx); + result += input.substring(segStart, idx); + if (result.length === 0) return null; + return { + type: 'ContentStatement', + original: result, + value: result, + loc: locFrom(startP), + }; + } + + result += input.substring(segStart, len); + advanceTo(len); + if (result.length === 0) return null; + return { + type: 'ContentStatement', + original: result, + value: result, + loc: locFrom(startP), + }; + } + + function findNextMustacheOrEnd(from) { + // Emu state: scan for next {{ (escaped or not) or EOF. + // Returns position to stop content at. The main scanContent loop + // will then handle escape detection on the next iteration. + const idx = input.indexOf('{{', from); + if (idx === -1) return len; + // Back up past ALL consecutive backslashes preceding {{ so the next + // scanContent call can correctly classify them (e.g. \\{{ as literal + // backslash + real mustache, not as another escape). + let end = idx; + while (end > from && input.charCodeAt(end - 1) === CH_BACKSLASH) { + end--; + } + return end; + } + + // === Mustache classification === + // After seeing '{{', classify what kind of statement this is. + + function consumeOpen() { + // We're at '{{', consume it and return info about the opener + const openStart = savePos(); + const startPos = pos; + + // Check for {{{{ (raw block) + if (startsWith('{{{{')) { + advanceTo(pos + 4); + // Check if it's a close raw block {{{{/ + if (cc() === CH_SLASH) { + // This shouldn't happen at statement level — it's handled in raw block parsing + error('Unexpected raw block close'); + } + return { kind: 'raw', start: openStart, raw: input.substring(startPos, pos) }; + } + + advanceTo(pos + 2); // skip {{ + + // Check for ~ (left strip) + let leftStrip = false; + if (cc() === CH_TILDE) { + leftStrip = true; + col++; + pos++; + } + + // Check optional leading whitespace before 'else' + const afterStripPos = pos; + const afterStripLine = line; + const afterStripCol = col; + skipWs(); + const wsSkipped = pos > afterStripPos; + + const c = cc(); + + // Check for else keyword + if (startsWith('else')) { + const afterElse = pos + 4; + const charAfterElse = input.charCodeAt(afterElse); + + // Check if it's standalone {{else}} or {{else~}} + if ( + isWhitespace(charAfterElse) || + charAfterElse === CH_TILDE || + charAfterElse === CH_RBRACE + ) { + // Scan past 'else' and whitespace + advanceTo(afterElse); + skipWs(); + + // Check for ~?}} + let rightStrip = false; + if (cc() === CH_TILDE) { + rightStrip = true; + col++; + pos++; + } + if (cc() === CH_RBRACE && cc(1) === CH_RBRACE) { + // Standalone inverse: {{else}} + advanceTo(pos + 2); + const raw = input.substring(startPos, pos); + return { + kind: 'inverse', + start: openStart, + strip: { open: leftStrip, close: rightStrip }, + raw, + }; + } + + // It's {{else something}} — openInverseChain + // We already advanced to afterElse on line 482, and may have + // scanned past whitespace/~ looking for }}. Reset to afterElse + // and re-skip whitespace to position correctly. + // Note: line/col were correctly tracked by advanceTo(afterElse), + // we just need to reset pos and re-advance if we overshot. + if (pos !== afterElse) { + // We overshot — need to recompute. Save the correct state from + // when we were at afterElse. Since advanceTo already tracked + // line/col to afterElse, and then we only moved forward through + // whitespace/~, we need to go back. Recompute from scratch: + pos = afterStripPos; + line = afterStripLine; + col = afterStripCol; + advanceTo(afterElse); + } + skipWs(); + const raw = input.substring(startPos, pos); + return { + kind: 'inverseChain', + start: openStart, + leftStrip, + raw, + }; + } + + // Not followed by appropriate char — it's an identifier starting with 'else' + // Restore position + pos = afterStripPos; + line = afterStripLine; + col = afterStripCol; + } else if (wsSkipped) { + // Restore whitespace if we didn't match 'else' + pos = afterStripPos; + line = afterStripLine; + col = afterStripCol; + } + + switch (c) { + case CH_BANG: { + // Comment: {{! or {{!-- + // We need to match Jison's behavior exactly. + // + // Jison has two comment rules (longest-match semantics): + // 1. Short: {{~?![\s\S]*?}} — matches any {{!...}} up to first }} + // 2. Long: {{~?!-- enters com state, then [\s\S]*?--~?}} matches body + // + // When both match, Jison picks the LONGER match. So: + // - {{!--}} → short wins (7 chars beats 5 chars for long start) + // - {{!-- hello --}} → long wins (the short would only match {{!-- hello --}}, + // but the long matches the full thing) + // + // Strategy: try short first. If starts with --, also try long. + // Pick the longer match. + + // Don't advance past ! yet — we'll compute raw text from startPos + col++; + pos++; + const afterBang = pos; + + // Try short comment: {{! up to first ~?}} + const shortEnd = input.indexOf('}}', afterBang); + if (shortEnd === -1) error('Unterminated comment'); + let shortRStrip = false; + if (shortEnd > 0 && input.charCodeAt(shortEnd - 1) === CH_TILDE) { + shortRStrip = true; + } + const shortMatchEnd = shortEnd + 2; // past }} + + // Check if this might be a long comment (starts with --) + const startsWithDashDash = + input.charCodeAt(afterBang) === CH_DASH && input.charCodeAt(afterBang + 1) === CH_DASH; + + if (startsWithDashDash) { + // Try long comment: find --~?}} after the initial -- + const longSearchStart = afterBang + 2; + let longMatchEnd = -1; + let longRStrip = false; + let searchFrom = longSearchStart; + + while (searchFrom < len) { + const dashIdx = input.indexOf('--', searchFrom); + if (dashIdx === -1) break; + let afterDash = dashIdx + 2; + let thisRStrip = false; + if (afterDash < len && input.charCodeAt(afterDash) === CH_TILDE) { + thisRStrip = true; + afterDash++; + } + if ( + afterDash + 1 < len && + input.charCodeAt(afterDash) === CH_RBRACE && + input.charCodeAt(afterDash + 1) === CH_RBRACE + ) { + longMatchEnd = afterDash + 2; + longRStrip = thisRStrip; + break; + } + searchFrom = dashIdx + 1; + } + + // Pick the longer match + if (longMatchEnd > shortMatchEnd) { + // Long comment wins + const rawText = input.substring(startPos, longMatchEnd); + advanceTo(longMatchEnd); + return { + kind: 'comment', + start: openStart, + value: stripComment(rawText), + strip: { open: leftStrip, close: longRStrip }, + loc: locFrom(openStart), + }; + } + } + + // Short comment wins (or no long comment match) + const rawText = input.substring(startPos, shortMatchEnd); + advanceTo(shortMatchEnd); + return { + kind: 'comment', + start: openStart, + value: stripComment(rawText), + strip: { open: leftStrip, close: shortRStrip }, + loc: locFrom(openStart), + }; + } + + case CH_GT: { + // Partial: {{> + col++; + pos++; + return { + kind: 'partial', + start: openStart, + leftStrip, + raw: input.substring(startPos, pos), + }; + } + + case CH_HASH: { + col++; + pos++; + // Check for {{#> (partial block) + if (cc() === CH_GT) { + col++; + pos++; + return { + kind: 'partialBlock', + start: openStart, + leftStrip, + raw: input.substring(startPos, pos), + }; + } + // Check for {{#* (decorator block) + let isDecorator = false; + if (cc() === CH_STAR) { + isDecorator = true; + col++; + pos++; + } + return { + kind: 'block', + start: openStart, + leftStrip, + isDecorator, + raw: input.substring(startPos, pos), + }; + } + + case CH_SLASH: { + // Close block: {{/ + col++; + pos++; + return { kind: 'close', start: openStart, leftStrip, raw: input.substring(startPos, pos) }; + } + + case CH_CARET: { + // ^ — could be standalone inverse {{^}} or open inverse {{^foo}} + col++; + pos++; + skipWs(); + // Check for ~?}} + let rightStrip = false; + if (cc() === CH_TILDE) { + const savedP = pos; + const savedL = line; + const savedC = col; + rightStrip = true; + col++; + pos++; + if (cc() === CH_RBRACE && cc(1) === CH_RBRACE) { + advanceTo(pos + 2); + return { + kind: 'inverse', + start: openStart, + strip: { open: leftStrip, close: rightStrip }, + raw: input.substring(startPos, pos), + }; + } + // Not }}, restore + pos = savedP; + line = savedL; + col = savedC; + rightStrip = false; + } + if (cc() === CH_RBRACE && cc(1) === CH_RBRACE) { + advanceTo(pos + 2); + return { + kind: 'inverse', + start: openStart, + strip: { open: leftStrip, close: false }, + raw: input.substring(startPos, pos), + }; + } + // It's an open inverse block + return { + kind: 'openInverse', + start: openStart, + leftStrip, + raw: input.substring(startPos, pos), + }; + } + + case CH_LBRACE: { + // Triple stache {{{ (unescaped) + col++; + pos++; + return { + kind: 'unescaped', + start: openStart, + leftStrip, + raw: input.substring(startPos, pos), + }; + } + + case CH_AMP: { + // Unescaped {{& + col++; + pos++; + return { + kind: 'mustache', + start: openStart, + leftStrip, + unescaped: true, + raw: input.substring(startPos, pos), + }; + } + + case CH_STAR: { + // Decorator {{* + col++; + pos++; + return { + kind: 'mustache', + start: openStart, + leftStrip, + isDecorator: true, + raw: input.substring(startPos, pos), + }; + } + + default: { + // Regular mustache {{ + return { + kind: 'mustache', + start: openStart, + leftStrip, + raw: input.substring(startPos, pos), + }; + } + } + } + + function consumeClose() { + // Expect }} or ~}} + skipWs(); + let rightStrip = false; + if (cc() === CH_TILDE) { + rightStrip = true; + col++; + pos++; + } + if (cc() !== CH_RBRACE || cc(1) !== CH_RBRACE) { + error("Expected '}}'"); + } + advanceTo(pos + 2); + return rightStrip; + } + + function consumeUnescapedClose() { + // Expect }}} or ~}}} + skipWs(); + let rightStrip = false; + if (cc() === CH_TILDE) { + rightStrip = true; + col++; + pos++; + } + if (cc() !== CH_RBRACE || cc(1) !== CH_RBRACE || cc(2) !== CH_RBRACE) { + error("Expected '}}}'"); + } + advanceTo(pos + 3); + return rightStrip; + } + + // === Expression parsing === + + function parseExpr() { + skipWs(); + const c = cc(); + + // Sub-expression + if (c === CH_LPAREN) return parseSexprOrPath(); + + // Array literal + if (c === CH_LBRACKET && squareSyntax !== 'string') return parseArrayLiteralOrPath(); + + return parseHelperName(); + } + + function parseSexprOrPath() { + const startP = savePos(); // save pos BEFORE sub-expression + const sexpr = parseSexpr(); + // Peek for separator WITHOUT consuming whitespace — the caller + // owns trailing whitespace (affects loc of containing HashPair etc.) + const savedPos = pos, + savedLine = line, + savedCol = col; + skipWs(); + if (cc() === CH_DOT || cc() === CH_SLASH) { + return parsePath(false, sexpr, startP); + } + // Restore — don't consume trailing whitespace + pos = savedPos; + line = savedLine; + col = savedCol; + return sexpr; + } + + function parseArrayLiteralOrPath() { + const startP = savePos(); // save pos BEFORE array literal + const arr = parseArrayLiteral(); + const savedPos = pos, + savedLine = line, + savedCol = col; + skipWs(); + if (cc() === CH_DOT || cc() === CH_SLASH) { + return parsePath(false, arr, startP); + } + pos = savedPos; + line = savedLine; + col = savedCol; + return arr; + } + + function parseHelperName() { + skipWs(); + const c = cc(); + const startP = savePos(); + + // String literal + if (c === CH_DQUOTE || c === CH_SQUOTE) { + const s = scanString(); + return { type: 'StringLiteral', value: s.value, original: s.value, loc: s.loc }; + } + + // Number literal + if (c === CH_DASH || (c >= CH_0 && c <= CH_9)) { + const savedPos = pos; + const savedLine = line; + const savedCol = col; + const numStr = scanNumber(); + if (numStr !== null && (pos >= len || isLiteralLookahead(cc()))) { + return { + type: 'NumberLiteral', + value: Number(numStr), + original: Number(numStr), + loc: locFrom(startP), + }; + } + // Restore — might be a negative path or ID starting with dash + pos = savedPos; + line = savedLine; + col = savedCol; + } + + // Boolean + if (startsWith('true') && isLiteralLookahead(input.charCodeAt(pos + 4))) { + advanceTo(pos + 4); + return { type: 'BooleanLiteral', value: true, original: true, loc: locFrom(startP) }; + } + if (startsWith('false') && isLiteralLookahead(input.charCodeAt(pos + 5))) { + advanceTo(pos + 5); + return { type: 'BooleanLiteral', value: false, original: false, loc: locFrom(startP) }; + } + + // Undefined + if (startsWith('undefined') && isLiteralLookahead(input.charCodeAt(pos + 9))) { + advanceTo(pos + 9); + return { + type: 'UndefinedLiteral', + original: undefined, + value: undefined, + loc: locFrom(startP), + }; + } + + // Null + if (startsWith('null') && isLiteralLookahead(input.charCodeAt(pos + 4))) { + advanceTo(pos + 4); + return { type: 'NullLiteral', original: null, value: null, loc: locFrom(startP) }; + } + + // Data path (@...) + if (c === CH_AT) { + col++; + pos++; + return parseDataName(startP); + } + + // Path (starting with ID, .., ., or escaped [literal]) + return parsePath(false, false); + } + + function parseDataName(startP) { + // After @, only path segments (IDs) are valid, not numbers. + // In Jison, @ is DATA token, then pathSegments expects ID (not NUMBER). + // Digits are valid ID chars but the Jison lexer matches them as NUMBER first. + // So @0, @1, etc. are parse errors in Jison. + const c = cc(); + if (c >= CH_0 && c <= CH_9) { + error("Expecting 'ID'"); + } + const segments = parsePathSegments(); + return preparePath(true, false, segments, locFrom(startP)); + } + + function parsePath(data, exprHead, exprHeadStartP) { + const startP = exprHeadStartP || savePos(); + + if (exprHead) { + // exprHead sep pathSegments + const sep = scanSep(); + if (!sep) error('Expected separator after sub-expression in path'); + const segments = parsePathSegments(); + return preparePath(false, exprHead, segments, locFrom(startP)); + } + + // pathSegments: ID (sep ID)* + const segments = parsePathSegments(); + return preparePath(data, false, segments, locFrom(startP)); + } + + function parsePathSegments() { + const segments = []; + const first = scanIdOrEscaped(); + if (first === null) error("Expecting 'ID'"); + segments.push({ part: idFromToken(first), original: first }); + + while (pos < len) { + const savedPos = pos; + const savedLine = line; + const savedCol = col; + const sep = scanSep(); + if (!sep) break; + const id = scanIdOrEscaped(); + if (id === null) { + // Trailing separator (e.g. "foo." or "foo/") — restore and stop + // Let downstream (Glimmer) handle the error + pos = savedPos; + line = savedLine; + col = savedCol; + break; + } + segments.push({ part: idFromToken(id), original: id, separator: sep }); + } + + return segments; + } + + function scanIdOrEscaped() { + if (cc() === CH_LBRACKET) { + return scanEscapedLiteral(); + } + // Handle '..' and '.' as valid ID tokens (per Jison lexer rules) + if (cc() === CH_DOT && cc(1) === CH_DOT) { + col += 2; + pos += 2; + return '..'; + } + if (cc() === CH_DOT && isLookahead(cc(1))) { + col++; + pos++; + return '.'; + } + return scanId(); + } + + function scanSep() { + if (cc() === CH_DOT && cc(1) === CH_HASH) { + col += 2; + pos += 2; + return '.#'; + } + if (cc() === CH_DOT || cc() === CH_SLASH) { + const c = input[pos]; + col++; + pos++; + return c; + } + return null; + } + + function preparePath(data, sexpr, parts, loc) { + let original; + if (data) { + original = '@'; + } else if (sexpr) { + original = sexpr.original + '.'; + } else { + original = ''; + } + + const tail = []; + let depth = 0; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i].part; + const isLiteral = parts[i].original !== part; + const separator = parts[i].separator; + const partPrefix = separator === '.#' ? '#' : ''; + + original += (separator || '') + part; + + if (!isLiteral && (part === '..' || part === '.' || part === 'this')) { + if (tail.length > 0) { + throw new Exception('Invalid path: ' + original, { loc }); + } else if (part === '..') { + depth++; + } + } else { + tail.push(`${partPrefix}${part}`); + } + } + + const head = sexpr || tail.shift(); + + return { + type: 'PathExpression', + this: original.startsWith('this.'), + data: !!data, + depth, + head, + tail, + parts: head ? [head, ...tail] : tail, + original, + loc, + }; + } + + // === Hash parsing === + + function isAtHash() { + // Look ahead: current token is ID followed by = + if (!isIdChar(cc()) && cc() !== CH_LBRACKET) return false; + // Scan forward past the ID + let p = pos; + if (input.charCodeAt(p) === CH_LBRACKET) { + // Escaped literal — find closing ] + p++; + while (p < len && input.charCodeAt(p) !== CH_RBRACKET) { + if (input.charCodeAt(p) === CH_BACKSLASH) p++; + p++; + } + p++; // skip ] + } else { + while (p < len && isIdChar(input.charCodeAt(p))) p++; + } + // Skip whitespace + while (p < len && isWhitespace(input.charCodeAt(p))) p++; + return p < len && input.charCodeAt(p) === CH_EQ; + } + + function parseHash() { + const startP = savePos(); + const pairs = []; + let endP; + while (pos < len && isAtHash()) { + pairs.push(parseHashPair()); + endP = savePos(); // capture end BEFORE skipping whitespace + skipWs(); + } + if (pairs.length === 0) return undefined; + return { type: 'Hash', pairs, loc: makeLoc(startP.line, startP.col, endP.line, endP.col) }; + } + + function parseHashPair() { + skipWs(); + const startP = savePos(); + const key = scanIdOrEscaped(); + if (key === null) error('Expected hash key'); + skipWs(); + if (cc() !== CH_EQ) error("Expected '=' in hash"); + col++; + pos++; // skip = + skipWs(); + const value = parseExpr(); + return { type: 'HashPair', key: idFromToken(key), value, loc: locFrom(startP) }; + } + + // === Sub-expression parsing === + + function parseSexpr() { + const startP = savePos(); + if (cc() !== CH_LPAREN) error("Expected '('"); + col++; + pos++; // skip ( + skipWs(); + + // Check for hash-only sexpr: (key=val) + if (isAtHash()) { + const hash = parseHash(); + skipWs(); + if (cc() !== CH_RPAREN) error("Expected ')'"); + col++; + pos++; + const loc = locFrom(startP); + return hashSyntax(hash, loc, { yy, syntax: 'expr' }); + } + + const path = parseExpr(); + const params = []; + let hash = undefined; + + skipWs(); + while (cc() !== CH_RPAREN && pos < len) { + if (isAtHash()) { + hash = parseHash(); + break; + } + params.push(parseExpr()); + skipWs(); + } + + skipWs(); + if (cc() !== CH_RPAREN) error("Expected ')'"); + col++; + pos++; + + return { type: 'SubExpression', path, params, hash, loc: locFrom(startP) }; + } + + // === Array literal === + + function parseArrayLiteral() { + const startP = savePos(); + if (cc() !== CH_LBRACKET) error("Expected '['"); + col++; + pos++; // skip [ + const items = []; + skipWs(); + while (cc() !== CH_RBRACKET && pos < len) { + items.push(parseExpr()); + skipWs(); + } + if (cc() !== CH_RBRACKET) error("Expected ']'"); + col++; + pos++; + const loc = locFrom(startP); + return squareSyntax(items, loc, { yy, syntax: 'expr' }); + } + + // === Block params === + + function parseBlockParams() { + skipWs(); + // Look for 'as |' + if (!startsWith('as')) return null; + const afterAs = pos + 2; + if (afterAs >= len || !isWhitespace(input.charCodeAt(afterAs))) return null; + + // Scan past 'as' + whitespace + let p = afterAs; + while (p < len && isWhitespace(input.charCodeAt(p))) p++; + if (p >= len || input.charCodeAt(p) !== CH_PIPE) return null; + + // It's block params + advanceTo(p + 1); // past 'as' + ws + | + const ids = []; + skipWs(); + while (cc() !== CH_PIPE && pos < len) { + const id = scanId(); + if (id === null) error('Expected block param identifier'); + ids.push(idFromToken(id)); + skipWs(); + } + if (cc() !== CH_PIPE) error("Expected '|' to close block params"); + col++; + pos++; + return ids; + } + + // === Statement parsers === + + function parseProgram(terminators) { + const stmts = []; + while (pos < len) { + // Check if we're at a terminator + if (startsWith('{{')) { + if (isTerminator(terminators)) break; + } + + const content = scanContent(); + if (content) { + stmts.push(content); + continue; + } + + if (pos >= len) break; + + // We're at a {{ + if (isTerminator(terminators)) break; + const stmt = parseOpenStatement(); + if (stmt) stmts.push(stmt); + } + + return prepareProgram(stmts); + } + + function isTerminator(terminators) { + if (!terminators) return false; + // Save position + const savedPos = pos; + const savedLine = line; + const savedCol = col; + + // Check what's after {{ + if (!startsWith('{{')) return false; + + // Peek at the opener type + let p = pos + 2; + + // Skip ~ + if (p < len && input.charCodeAt(p) === CH_TILDE) p++; + + // Skip whitespace (for else detection) + let pw = p; + while (pw < len && isWhitespace(input.charCodeAt(pw))) pw++; + + const c = input.charCodeAt(p); + + for (const t of terminators) { + switch (t) { + case 'close': + if (c === CH_SLASH) return true; + break; + case 'inverse': + // {{^}} or {{^foo + if (c === CH_CARET) return true; + // {{else}} or {{else foo + if (input.startsWith('else', pw)) return true; + break; + } + } + + return false; + } + + function parseOpenStatement() { + const open = consumeOpen(); + + switch (open.kind) { + case 'comment': + return { + type: 'CommentStatement', + value: open.value, + strip: open.strip, + loc: open.loc, + }; + + case 'mustache': + return parseMustache(open); + + case 'unescaped': + return parseUnescapedMustache(open); + + case 'block': + return parseBlock(open); + + case 'openInverse': + return parseInverseBlock(open); + + case 'partial': + return parsePartial(open); + + case 'partialBlock': + return parsePartialBlock(open); + + case 'raw': + return parseRawBlock(open); + + case 'inverse': + // Standalone inverse at statement level — this is an error + // The Jison parser would fail here too + error('Unexpected inverse'); + break; + + case 'close': + error('Unexpected close block'); + break; + + case 'inverseChain': + error('Unexpected inverse chain'); + break; + + default: + error('Unexpected token: ' + open.kind); + } + } + + function parseMustache(open) { + skipWs(); + + // Check for hash-only mustache: {{key=val}} + if (isAtHash()) { + const hash = parseHash(); + const rightStrip = consumeClose(); + const loc = locFrom(open.start); + const strip = { open: open.leftStrip || false, close: rightStrip }; + const wrappedPath = hashSyntax(hash, loc, { yy, syntax: 'expr' }); + return { + type: open.isDecorator ? 'Decorator' : 'MustacheStatement', + path: wrappedPath, + params: [], + hash: undefined, + escaped: determineEscaped(open), + strip, + loc, + }; + } + + const path = parseExpr(); + const params = []; + let hash = undefined; + + skipWs(); + while (pos < len && cc() !== CH_RBRACE && !(cc() === CH_TILDE && cc(1) === CH_RBRACE)) { + if (isAtHash()) { + hash = parseHash(); + break; + } + params.push(parseExpr()); + skipWs(); + } + + const rightStrip = consumeClose(); + const loc = locFrom(open.start); + const strip = { open: open.leftStrip || false, close: rightStrip }; + + return { + type: open.isDecorator ? 'Decorator' : 'MustacheStatement', + path, + params, + hash, + escaped: determineEscaped(open), + strip, + loc, + }; + } + + function determineEscaped(open) { + if (open.unescaped) return false; + if (open.kind === 'unescaped') return false; + const raw = open.raw || ''; + // Check for {{{ or {{& — both are unescaped + const c3 = raw.charAt(2); + const c4 = raw.charAt(3); + if (c3 === '{' || c3 === '&') return false; + if (c3 === '~' && (c4 === '{' || c4 === '&')) return false; + return true; + } + + function parseUnescapedMustache(open) { + skipWs(); + const path = parseExpr(); + const params = []; + let hash = undefined; + + skipWs(); + while ( + pos < len && + !(cc() === CH_RBRACE && cc(1) === CH_RBRACE && cc(2) === CH_RBRACE) && + !(cc() === CH_TILDE && cc(1) === CH_RBRACE) + ) { + if (isAtHash()) { + hash = parseHash(); + break; + } + params.push(parseExpr()); + skipWs(); + } + + const rightStrip = consumeUnescapedClose(); + const loc = locFrom(open.start); + + return { + type: 'MustacheStatement', + path, + params, + hash, + escaped: false, + strip: { open: open.leftStrip || false, close: rightStrip }, + loc, + }; + } + + // === Block parsing === + + function parseBlock(open) { + skipWs(); + const path = parseExpr(); + const params = []; + let hash = undefined; + let blockParams = undefined; + + skipWs(); + while (pos < len && cc() !== CH_RBRACE && !(cc() === CH_TILDE && cc(1) === CH_RBRACE)) { + // Check for block params (as |...|) + if (startsWith('as') && isWhitespace(input.charCodeAt(pos + 2))) { + const bp = parseBlockParams(); + if (bp) { + blockParams = bp; + break; + } + } + if (isAtHash()) { + hash = parseHash(); + skipWs(); + // Still check for block params after hash + if (startsWith('as') && isWhitespace(input.charCodeAt(pos + 2))) { + blockParams = parseBlockParams(); + } + break; + } + params.push(parseExpr()); + skipWs(); + } + + const rightStrip = consumeClose(); + const openInfo = { + open: open.raw, + path, + params, + hash, + blockParams, + strip: { open: open.leftStrip || false, close: rightStrip }, + }; + + // Parse the block body + const program = parseProgram(['close', 'inverse']); + + // Check for inverse + let inverseAndProgram = undefined; + if (pos < len && startsWith('{{')) { + const savedPos = pos; + const savedLine = line; + const savedCol = col; + const nextOpen = consumeOpen(); + + if (nextOpen.kind === 'inverse') { + const inverseProgram = parseProgram(['close']); + inverseAndProgram = { strip: nextOpen.strip, program: inverseProgram }; + } else if (nextOpen.kind === 'inverseChain') { + inverseAndProgram = parseInverseChain(nextOpen); + } else if (nextOpen.kind === 'close') { + // Restore — close will be parsed below + pos = savedPos; + line = savedLine; + col = savedCol; + } else { + pos = savedPos; + line = savedLine; + col = savedCol; + } + } + + // Parse close block + const close = parseCloseBlock(path); + + return buildBlock(openInfo, program, inverseAndProgram, close, false, open.start); + } + + function parseInverseBlock(open) { + // Same as parseBlock but with inverted=true + skipWs(); + const path = parseExpr(); + const params = []; + let hash = undefined; + let blockParams = undefined; + + skipWs(); + while (pos < len && cc() !== CH_RBRACE && !(cc() === CH_TILDE && cc(1) === CH_RBRACE)) { + if (startsWith('as') && isWhitespace(input.charCodeAt(pos + 2))) { + const bp = parseBlockParams(); + if (bp) { + blockParams = bp; + break; + } + } + if (isAtHash()) { + hash = parseHash(); + skipWs(); + if (startsWith('as') && isWhitespace(input.charCodeAt(pos + 2))) { + blockParams = parseBlockParams(); + } + break; + } + params.push(parseExpr()); + skipWs(); + } + + const rightStrip = consumeClose(); + const openInfo = { + path, + params, + hash, + blockParams, + strip: { open: open.leftStrip || false, close: rightStrip }, + }; + + const program = parseProgram(['close', 'inverse']); + + let inverseAndProgram = undefined; + if (pos < len && startsWith('{{')) { + const savedPos = pos; + const savedLine = line; + const savedCol = col; + const nextOpen = consumeOpen(); + + if (nextOpen.kind === 'inverse') { + const inverseProgram = parseProgram(['close']); + inverseAndProgram = { strip: nextOpen.strip, program: inverseProgram }; + } else if (nextOpen.kind === 'inverseChain') { + inverseAndProgram = parseInverseChain(nextOpen); + } else { + pos = savedPos; + line = savedLine; + col = savedCol; + } + } + + const close = parseCloseBlock(path); + + return buildBlock(openInfo, program, inverseAndProgram, close, true, open.start); + } + + function parseInverseChain(chainOpen) { + // chainOpen is an inverseChain opener ({{else if ...}}) + skipWs(); + const path = parseExpr(); + const params = []; + let hash = undefined; + let blockParams = undefined; + + skipWs(); + while (pos < len && cc() !== CH_RBRACE && !(cc() === CH_TILDE && cc(1) === CH_RBRACE)) { + if (startsWith('as') && isWhitespace(input.charCodeAt(pos + 2))) { + const bp = parseBlockParams(); + if (bp) { + blockParams = bp; + break; + } + } + if (isAtHash()) { + hash = parseHash(); + skipWs(); + if (startsWith('as') && isWhitespace(input.charCodeAt(pos + 2))) { + blockParams = parseBlockParams(); + } + break; + } + params.push(parseExpr()); + skipWs(); + } + + const rightStrip = consumeClose(); + const openInfo = { + open: chainOpen.raw, + path, + params, + hash, + blockParams, + strip: { open: chainOpen.leftStrip || false, close: rightStrip }, + }; + + const program = parseProgram(['close', 'inverse']); + + let nestedInverse = undefined; + if (pos < len && startsWith('{{')) { + const savedPos = pos; + const savedLine = line; + const savedCol = col; + const nextOpen = consumeOpen(); + + if (nextOpen.kind === 'inverse') { + const inverseProgram = parseProgram(['close']); + nestedInverse = { strip: nextOpen.strip, program: inverseProgram }; + } else if (nextOpen.kind === 'inverseChain') { + nestedInverse = parseInverseChain(nextOpen); + } else { + pos = savedPos; + line = savedLine; + col = savedCol; + } + } + + // Build the inner block (using close = nestedInverse's last close or the parent's) + // The close strip for chained blocks comes from the parent's close block + const innerBlock = buildBlock( + openInfo, + program, + nestedInverse, + nestedInverse, + false, + chainOpen.start + ); + + const wrapperProgram = prepareProgram([innerBlock], program.loc); + wrapperProgram.chained = true; + + return { strip: openInfo.strip, program: wrapperProgram, chain: true }; + } + + function parseCloseBlock(openPath) { + if (!startsWith('{{')) error('Expected close block'); + const open = consumeOpen(); + if (open.kind !== 'close') error('Expected close block'); + + skipWs(); + const closePath = parseExpr(); + const rightStrip = consumeClose(); + + // Validate close matches open + const openName = openPath.original || openPath.parts?.join?.('/') || ''; + const closeName = closePath.original || closePath.parts?.join?.('/') || ''; + if (openName !== closeName) { + throw new Exception(openName + " doesn't match " + closeName, { loc: openPath.loc }); + } + + return { path: closePath, strip: { open: open.leftStrip || false, close: rightStrip } }; + } + + function buildBlock(openInfo, program, inverseAndProgram, close, inverted, startPos) { + const isDecorator = openInfo.open ? /\*/.test(openInfo.open) : false; + + program.blockParams = openInfo.blockParams; + + let inverse, inverseStrip; + + if (inverseAndProgram) { + if (isDecorator) { + throw new Exception('Unexpected inverse block on decorator', inverseAndProgram); + } + + if (inverseAndProgram.chain) { + inverseAndProgram.program.body[0].closeStrip = close && close.strip; + } + + inverseStrip = inverseAndProgram.strip; + inverse = inverseAndProgram.program; + } + + if (inverted) { + const tmp = inverse; + inverse = program; + program = tmp; + } + + return { + type: isDecorator ? 'DecoratorBlock' : 'BlockStatement', + path: openInfo.path, + params: openInfo.params, + hash: openInfo.hash, + program, + inverse, + openStrip: openInfo.strip, + inverseStrip, + closeStrip: close && close.strip, + loc: locFrom(startPos), + }; + } + + // === Raw block === + + function parseRawBlock(open) { + skipWs(); + const path = parseExpr(); + const params = []; + let hash = undefined; + + skipWs(); + while ( + pos < len && + !(cc() === CH_RBRACE && cc(1) === CH_RBRACE && cc(2) === CH_RBRACE && cc(3) === CH_RBRACE) + ) { + if (isAtHash()) { + hash = parseHash(); + break; + } + params.push(parseExpr()); + skipWs(); + } + + // Consume }}}} + if (!startsWith('}}}}')) error("Expected '}}}}' to close raw block"); + advanceTo(pos + 4); + + // Scan raw content until {{{{/openName}}}} + // In the Jison 'raw' state, EVERYTHING is content except {{{{/name}}}}. + // Nested {{{{ (not followed by /) is also content. + // We track a nesting depth: {{{{ pushes, {{{{/name}}}} pops. + const openName = path.original || path.parts?.join?.('/') || ''; + const contents = []; + let rawDepth = 1; // we're inside one raw block + + while (pos < len) { + const idx = input.indexOf('{{{{', pos); + if (idx === -1) error('Unterminated raw block'); + + // Content before {{{{ + if (idx > pos) { + const contentStart = savePos(); + const text = input.substring(pos, idx); + advanceTo(idx); + contents.push({ + type: 'ContentStatement', + original: text, + value: text, + loc: locFrom(contentStart), + }); + } + + // Check if it's {{{{/ (potential close) + if (input.charCodeAt(idx + 4) === CH_SLASH) { + // Try to match {{{{/openName}}}} + const closeStart = idx + 5; + let closeEnd = closeStart; + while (closeEnd < len && isIdChar(input.charCodeAt(closeEnd))) closeEnd++; + const closeId = input.substring(closeStart, closeEnd); + + if (input.startsWith('}}}}', closeEnd)) { + if (rawDepth === 1) { + if (closeId === openName) { + // This is our close tag + advanceTo(closeEnd + 4); + + // Build the raw block — Jison uses the overall block loc for program too + const loc = locFrom(open.start); + const program = { + type: 'Program', + body: contents, + strip: {}, + loc, + }; + + return { + type: 'BlockStatement', + path, + params, + hash, + program, + openStrip: {}, + inverseStrip: {}, + closeStrip: {}, + loc, + }; + } + // Mismatch: close tag doesn't match open + throw new Exception(openName + " doesn't match " + closeId, { loc: path.loc }); + } + + if (closeId) { + // It's a close for a nested raw block — just decrement depth and treat as content + rawDepth--; + } + } + + // Not our close — treat {{{{/...}}}} as content + const contentStart = savePos(); + const endOfTag = closeEnd + (input.startsWith('}}}}', closeEnd) ? 4 : 0); + const text = input.substring(idx, endOfTag || idx + 5); + advanceTo(endOfTag || idx + 5); + contents.push({ + type: 'ContentStatement', + original: text, + value: text, + loc: locFrom(contentStart), + }); + } else { + // {{{{ not followed by / — nested raw block opener, treat as content + rawDepth++; + const contentStart = savePos(); + advanceTo(idx + 4); + const text = '{{{{'; + contents.push({ + type: 'ContentStatement', + original: text, + value: text, + loc: locFrom(contentStart), + }); + } + } + + error('Unterminated raw block'); + } + + // === Partial === + + function parsePartial(open) { + skipWs(); + const name = parseExpr(); + const params = []; + let hash = undefined; + + skipWs(); + while (pos < len && cc() !== CH_RBRACE && !(cc() === CH_TILDE && cc(1) === CH_RBRACE)) { + if (isAtHash()) { + hash = parseHash(); + break; + } + params.push(parseExpr()); + skipWs(); + } + + const rightStrip = consumeClose(); + + return { + type: 'PartialStatement', + name, + params, + hash, + indent: '', + strip: { open: open.leftStrip || false, close: rightStrip }, + loc: locFrom(open.start), + }; + } + + function parsePartialBlock(open) { + skipWs(); + const name = parseExpr(); + const params = []; + let hash = undefined; + + skipWs(); + while (pos < len && cc() !== CH_RBRACE && !(cc() === CH_TILDE && cc(1) === CH_RBRACE)) { + if (isAtHash()) { + hash = parseHash(); + break; + } + params.push(parseExpr()); + skipWs(); + } + + const rightStrip = consumeClose(); + + const openInfo = { + path: name, + params, + hash, + strip: { open: open.leftStrip || false, close: rightStrip }, + }; + + const program = parseProgram(['close']); + const close = parseCloseBlock(name); + + return { + type: 'PartialBlockStatement', + name: openInfo.path, + params: openInfo.params, + hash: openInfo.hash, + program, + openStrip: openInfo.strip, + closeStrip: close && close.strip, + loc: locFrom(open.start), + }; + } + + // === Program / root === + + function prepareProgram(statements, loc) { + if (!loc && statements.length) { + const firstLoc = statements[0].loc; + const lastLoc = statements[statements.length - 1].loc; + if (firstLoc && lastLoc) { + loc = { + source: firstLoc.source, + start: { line: firstLoc.start.line, column: firstLoc.start.column }, + end: { line: lastLoc.end.line, column: lastLoc.end.column }, + }; + } + } + return { type: 'Program', body: statements, strip: {}, loc: loc || undefined }; + } + + // === Entry point === + const result = parseProgram(null); + + if (pos < len) { + error('Unexpected content after end of template'); + } + + return result; +} + +function arrayLiteralNode(array, loc) { + return { type: 'ArrayLiteral', items: array, loc }; +} + +function hashLiteralNode(hash, loc) { + return { type: 'HashLiteral', pairs: hash.pairs, loc }; +} diff --git a/packages/@handlebars/parser/lib/visitor.js b/packages/@glimmer/syntax/lib/parser/visitor.js similarity index 100% rename from packages/@handlebars/parser/lib/visitor.js rename to packages/@glimmer/syntax/lib/parser/visitor.js diff --git a/packages/@handlebars/parser/lib/whitespace-control.js b/packages/@glimmer/syntax/lib/parser/whitespace-control.js similarity index 100% rename from packages/@handlebars/parser/lib/whitespace-control.js rename to packages/@glimmer/syntax/lib/parser/whitespace-control.js diff --git a/packages/@glimmer/syntax/package.json b/packages/@glimmer/syntax/package.json index 886a9b99963..36db32df47e 100644 --- a/packages/@glimmer/syntax/package.json +++ b/packages/@glimmer/syntax/package.json @@ -35,7 +35,6 @@ "@glimmer/interfaces": "workspace:*", "@glimmer/util": "workspace:*", "@glimmer/wire-format": "workspace:*", - "@handlebars/parser": "workspace:*", "simple-html-tokenizer": "^0.5.11" }, "devDependencies": { diff --git a/packages/@glimmer/syntax/test/parser-error-test.ts b/packages/@glimmer/syntax/test/parser-error-test.ts new file mode 100644 index 00000000000..d529c62afa4 --- /dev/null +++ b/packages/@glimmer/syntax/test/parser-error-test.ts @@ -0,0 +1,100 @@ +import { preprocess as parse } from '@glimmer/syntax'; + +const { module, test } = QUnit; + +module('[glimmer-syntax] Parser - parse error regression fixtures', function () { + // prettier tests/format/handlebars/_errors_/invalid-3.hbs + test('empty mustache {{}} is a parse error (invalid-3.hbs)', (assert) => { + assert.throws( + () => { + parse('\n\n{{}}\n', { meta: { moduleName: 'test-module' } }); + }, + /./u, + 'empty mustache should throw a parse error' + ); + }); + + // prettier tests/format/handlebars/_errors_/invalid.hbs + test('unclosed mustache {{@name} is a parse error (invalid.hbs)', (assert) => { + assert.throws( + () => { + parse('\nx, {{@name}\n', { meta: { moduleName: 'test-module' } }); + }, + /./u, + 'unclosed mustache should throw a parse error' + ); + }); + + // prettier tests/format/handlebars/_errors_/tilde-comments-1.hbs + test('bare tilde mustache {{~}} is a parse error (tilde-comments-1.hbs)', (assert) => { + assert.throws( + () => { + parse('{{~}}\n', { meta: { moduleName: 'test-module' } }); + }, + /./u, + 'bare tilde mustache should throw a parse error' + ); + }); + + // prettier tests/format/handlebars/_errors_/tilde-comments-2.hbs + test('double tilde mustache {{~~}} is a parse error (tilde-comments-2.hbs)', (assert) => { + assert.throws( + () => { + parse('{{~~}}\n', { meta: { moduleName: 'test-module' } }); + }, + /./u, + 'double tilde mustache should throw a parse error' + ); + }); + + // assert-reserved-named-arguments-test: '@' alone is reserved / parse error + test('mustache with bare @ is a parse error ({{@}})', (assert) => { + assert.throws( + () => { + parse('{{@}}', { meta: { moduleName: 'test-module' } }); + }, + /./u, + 'mustache with bare @ should throw a parse error' + ); + }); + + // assert-reserved-named-arguments-test: '@0' is not a valid path + test('mustache with @ is a parse error ({{@0}})', (assert) => { + assert.throws( + () => { + parse('{{@0}}', { meta: { moduleName: 'test-module' } }); + }, + /./u, + '@ is not a valid identifier' + ); + }); + + // assert-reserved-named-arguments-test: '@@', '@=', '@!' etc. + test('mustache with @ is a parse error ({{@@}}, {{@=}}, {{@!}})', (assert) => { + for (const input of ['{{@@}}', '{{@=}}', '{{@!}}']) { + assert.throws( + () => { + parse(input, { meta: { moduleName: 'test-module' } }); + }, + /./u, + `${input} should throw a parse error` + ); + } + }); + + // Jison has a quirk where digit-only segments are rejected as the LAST segment + // (lexer matches NUMBER before ID) but accepted as middle segments (e.g. + // {{foo.0.bar}}). Real Ember templates use .0. as array access: + // {{@equipmentEdgeList.0.node.profile.modelInfo.manufacturer.name}} + // The v2-parser uniformly accepts digit segments in all positions, which is + // more permissive than Jison but doesn't break any real-world templates. + test('digit path segment as middle segment is accepted ({{foo.0.bar}})', (assert) => { + const ast = parse('{{foo.0.bar}}', { meta: { moduleName: 'test-module' } }); + assert.strictEqual(ast.body[0]?.type, 'MustacheStatement'); + }); + + test('digit path segment with data path is accepted ({{@list.0.name}})', (assert) => { + const ast = parse('{{@list.0.name}}', { meta: { moduleName: 'test-module' } }); + assert.strictEqual(ast.body[0]?.type, 'MustacheStatement'); + }); +}); diff --git a/packages/@glimmer/syntax/test/parser-escape-test.ts b/packages/@glimmer/syntax/test/parser-escape-test.ts new file mode 100644 index 00000000000..fc19fa620c1 --- /dev/null +++ b/packages/@glimmer/syntax/test/parser-escape-test.ts @@ -0,0 +1,139 @@ +import type { ASTv1 } from '@glimmer/syntax'; +import { builders as b, preprocess as parse } from '@glimmer/syntax'; + +import { element } from './parser-node-test'; +import { astEqual } from './support'; + +const { module, test } = QUnit; + +module('[glimmer-syntax] Parser - backslash escape sequences', function () { + // k=1: \{{ → escape. Backslash consumed, {{content}} becomes literal text. + test('\\{{ produces literal {{ in a TextNode', () => { + astEqual('\\{{foo}}', b.template([b.text('{{foo}}')])); + }); + + test('\\{{ merges escaped content with following text (emu-state behaviour)', () => { + astEqual('\\{{foo}} bar baz', b.template([b.text('{{foo}} bar baz')])); + }); + + test('text before \\{{ is emitted as a separate TextNode', () => { + astEqual('prefix\\{{foo}} suffix', b.template([b.text('prefix'), b.text('{{foo}} suffix')])); + }); + + test('\\{{ followed by a real mustache stops the emu-state merge', () => { + astEqual('\\{{foo}}{{bar}}', b.template([b.text('{{foo}}'), b.mustache(b.path('bar'))])); + }); + + test('emu-state merge stops at \\{{ (another escape)', () => { + astEqual( + '\\{{foo}} text \\{{bar}} done {{baz}}', + b.template([b.text('{{foo}} text '), b.text('{{bar}} done '), b.mustache(b.path('baz'))]) + ); + }); + + // k=2: \\{{ → real mustache, ONE literal backslash emitted as TextNode. + test('\\\\{{ emits one literal backslash and a real mustache', () => { + astEqual('\\\\{{foo}}', b.template([b.text('\\'), b.mustache(b.path('foo'))])); + }); + + // k=3: \\\{{ → real mustache, TWO literal backslashes emitted as TextNode. + test('\\\\\\{{ emits two literal backslashes and a real mustache', () => { + astEqual('\\\\\\{{foo}}', b.template([b.text('\\\\'), b.mustache(b.path('foo'))])); + }); + + test('full escaped.hbs sequence produces correct AST', () => { + const input = + 'an escaped mustache:\n\\{{my-component}}\na non-escaped mustache:\n' + + '\\\\{{my-component}}\nanother non-escaped mustache:\n\\\\\\{{my-component}}\n'; + astEqual( + input, + b.template([ + b.text('an escaped mustache:\n'), + b.text('{{my-component}}\na non-escaped mustache:\n'), + b.text('\\'), + b.mustache(b.path('my-component')), + b.text('\nanother non-escaped mustache:\n\\\\'), + b.mustache(b.path('my-component')), + b.text('\n'), + ]) + ); + }); + + // Inside HTML elements + + test('\\{{ in element text content produces literal {{', () => { + astEqual('
\\{{foo}}
', b.template([element('div', ['body', b.text('{{foo}}')])])); + }); + + test('\\\\{{ in element text content produces one backslash + real mustache', () => { + astEqual( + '
\\\\{{foo}}
', + b.template([element('div', ['body', b.text('\\'), b.mustache(b.path('foo'))])]) + ); + }); + + // Inside quoted attribute values + + test('\\{{ inside a quoted attribute value emits {{ as literal text', (assert) => { + const ast = parse('
'); + const el = ast.body[0] as ASTv1.ElementNode; + const attr = el.attributes[0] as ASTv1.AttrNode; + const value = attr.value as ASTv1.TextNode; + assert.strictEqual(value.chars, 'foo {{'); + }); + + // Backslash NOT before {{ passes through unchanged + + test('plain backslash not before {{ is preserved in text', () => { + astEqual('foo\\bar', b.template([b.text('foo\\bar')])); + }); + + test('double backslash not before {{ is preserved in text', () => { + astEqual('foo\\\\bar', b.template([b.text('foo\\\\bar')])); + }); + + test('triple backslash not before {{ is preserved in text (backslashes.hbs)', () => { + astEqual('

\\\\\\

', b.template([element('p', ['body', b.text('\\\\\\')])])); + }); + + test('triple backslash + \\\\{{ in element text (backslashes.hbs)', () => { + astEqual( + '

\\\\\\ \\\\{{foo}}

', + b.template([element('p', ['body', b.text('\\\\\\ \\'), b.mustache(b.path('foo'))])]) + ); + }); + + test('plain backslash in attribute value is preserved (backslashes-in-attributes.hbs)', (assert) => { + const ast = parse('

'); + const attr = (ast.body[0] as ASTv1.ElementNode).attributes[0] as ASTv1.AttrNode; + assert.strictEqual((attr.value as ASTv1.TextNode).chars, 'backslash \\\\ in an attribute'); + }); + + test('\\{{ in quoted class attribute value (mustache.hbs)', (assert) => { + const ast = parse('
'); + const attr = (ast.body[0] as ASTv1.ElementNode).attributes[0] as ASTv1.AttrNode; + assert.strictEqual((attr.value as ASTv1.TextNode).chars, ' bar {{'); + }); + + // Unclosed escape + + test('\\{{ without closing }} emits {{ and following text up to end', () => { + astEqual('\\{{ unclosed', b.template([b.text('{{ unclosed')])); + }); + + test('\\{{ without closing }} stops at < (HTML element boundary)', () => { + astEqual( + '
\\{{ unclosed
', + b.template([element('div', ['body', b.text('{{ unclosed')])]) + ); + }); + + // Escaped literal with newline inside brackets + + test('escaped literal [foo\\nbar] preserves newline in path segment', (assert) => { + const ast = parse('{{[foo\nbar]}}'); + const path = (ast.body[0] as ASTv1.MustacheStatement).path as ASTv1.PathExpression; + assert.strictEqual(path.head.type, 'VarHead'); + assert.strictEqual((path.head as ASTv1.VarHead).name, 'foo\nbar'); + }); +}); diff --git a/packages/@glimmer/syntax/test/parser-whitespace-test.ts b/packages/@glimmer/syntax/test/parser-whitespace-test.ts new file mode 100644 index 00000000000..624b4de5206 --- /dev/null +++ b/packages/@glimmer/syntax/test/parser-whitespace-test.ts @@ -0,0 +1,98 @@ +import type { ASTv1 } from '@glimmer/syntax'; +import { preprocess as parse } from '@glimmer/syntax'; + +const { module, test } = QUnit; + +module('[glimmer-syntax] Parser - whitespace control (tilde and standalone)', function () { + // Tilde (whitespace stripping) + + test('tilde on mustache strips adjacent whitespace text nodes, which are then removed', (assert) => { + const ast = parse(' {{~comment~}} '); + assert.strictEqual(ast.body.length, 1, 'empty text nodes are removed after tilde stripping'); + assert.strictEqual(ast.body[0]?.type, 'MustacheStatement'); + }); + + test('tilde on block open/close strips program body content', (assert) => { + const ast = parse('x{{# comment~}} \nfoo\n {{~/comment}}y'); + const block = ast.body[1] as ASTv1.BlockStatement; + assert.strictEqual((block.program.body[0] as ASTv1.TextNode).chars, 'foo'); + }); + + // ignoreStandalone (parseWithoutProcessing equivalent) + + test('ignoreStandalone: tilde still strips adjacent text nodes', (assert) => { + const ast = parse(' {{~comment~}} ', { parseOptions: { ignoreStandalone: true } }); + assert.strictEqual( + ast.body.length, + 1, + 'tilde-stripped empty nodes are removed even without standalone detection' + ); + assert.strictEqual(ast.body[0]?.type, 'MustacheStatement'); + }); + + // Standalone block detection + + test('standalone block: surrounding whitespace text nodes are removed after stripping', (assert) => { + const ast = parse(' {{#comment}} \nfoo\n {{/comment}} '); + assert.strictEqual(ast.body.length, 1, 'surrounding empty text nodes are removed'); + const block = ast.body[0] as ASTv1.BlockStatement; + assert.strictEqual((block.program.body[0] as ASTv1.TextNode).chars, 'foo\n'); + }); + + test('standalone block with else: surrounding nodes removed, inner content standalone-stripped', (assert) => { + const ast = parse(' {{#comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '); + assert.strictEqual(ast.body.length, 1); + const block = ast.body[0] as ASTv1.BlockStatement; + assert.strictEqual((block.program.body[0] as ASTv1.TextNode).chars, 'foo\n'); + assert.strictEqual( + ((block.inverse as ASTv1.Block).body[0] as ASTv1.TextNode).chars, + ' bar \n' + ); + }); + + test('standalone block at start of line: program body strips leading newline', (assert) => { + const ast = parse('{{#comment}} \nfoo\n {{/comment}}'); + const block = ast.body[0] as ASTv1.BlockStatement; + assert.strictEqual((block.program.body[0] as ASTv1.TextNode).chars, 'foo\n'); + }); + + test('standalone block containing mustache: surrounding text is stripped and empty node removed', (assert) => { + const ast = parse('{{#comment}} \n{{foo}}\n {{/comment}}'); + const block = ast.body[0] as ASTv1.BlockStatement; + assert.strictEqual(block.program.body.length, 2); + assert.strictEqual(block.program.body[0]?.type, 'MustacheStatement'); + assert.strictEqual((block.program.body[1] as ASTv1.TextNode).chars, '\n'); + }); + + test('non-standalone block (inline): whitespace is NOT stripped', (assert) => { + const ast = parse('{{#foo}} {{#comment}} \nfoo\n {{/comment}} {{/foo}}'); + const outerBlock = ast.body[0] as ASTv1.BlockStatement; + const innerBlock = outerBlock.program.body[1] as ASTv1.BlockStatement; + assert.strictEqual(innerBlock.type, 'BlockStatement'); + assert.strictEqual((innerBlock.program.body[0] as ASTv1.TextNode).chars, ' \nfoo\n '); + }); + + // Standalone comment detection + + test('standalone comment: trailing whitespace node is removed after stripping', (assert) => { + const ast = parse('{{! comment }} '); + assert.strictEqual(ast.body.length, 1); + assert.strictEqual(ast.body[0]?.type, 'MustacheCommentStatement'); + }); + + test('standalone comment: both surrounding text nodes are removed after stripping', (assert) => { + const ast = parse(' {{! comment }} '); + assert.strictEqual(ast.body.length, 1); + assert.strictEqual(ast.body[0]?.type, 'MustacheCommentStatement'); + }); + + // ignoreStandalone: standalone detection is skipped + + test('ignoreStandalone: standalone block is NOT stripped', (assert) => { + const ast = parse('{{#comment}} \nfoo\n {{/comment}}', { + parseOptions: { ignoreStandalone: true }, + }); + const block = ast.body[0] as ASTv1.BlockStatement; + assert.strictEqual((block.program.body[0] as ASTv1.TextNode).chars, ' \nfoo\n '); + }); +}); diff --git a/packages/@handlebars/parser/CHANGELOG.md b/packages/@handlebars/parser/CHANGELOG.md deleted file mode 100644 index cd310aaf6b9..00000000000 --- a/packages/@handlebars/parser/CHANGELOG.md +++ /dev/null @@ -1,80 +0,0 @@ -# Changelog - -## Release (2025-11-29) - -* @handlebars/parser 2.2.2 (patch) - -#### :bug: Bug Fix -* `@handlebars/parser` - * [#27](https://github.com/handlebars-lang/handlebars-parser/pull/27) Fix commonjs build ([@kaermorchen](https://github.com/kaermorchen)) - -#### Committers: 1 -- Stanislav Romanov ([@kaermorchen](https://github.com/kaermorchen)) - -## Release (2025-08-01) - -* @handlebars/parser 2.2.1 (patch) - -#### :bug: Bug Fix -* `@handlebars/parser` - * [#24](https://github.com/handlebars-lang/handlebars-parser/pull/24) fix node engine and create a test matrix in CI ([@mansona](https://github.com/mansona)) - -#### Committers: 1 -- Chris Manson ([@mansona](https://github.com/mansona)) - -## Release (2025-03-19) - -* @handlebars/parser 2.2.0 (minor) - -#### :rocket: Enhancement -* `@handlebars/parser` - * [#15](https://github.com/handlebars-lang/handlebars-parser/pull/15) Implement hash and array literal syntax ([@wycats](https://github.com/wycats)) - * [#14](https://github.com/handlebars-lang/handlebars-parser/pull/14) Add support for a `#` prefix in path segments ([@wycats](https://github.com/wycats)) - -#### :bug: Bug Fix -* `@handlebars/parser` - * [#6](https://github.com/handlebars-lang/handlebars-parser/pull/6) [bugfix] Don't emit `parts: [undefined]` for `{{this}}` ([@dfreeman](https://github.com/dfreeman)) - -#### :house: Internal -* `@handlebars/parser` - * [#17](https://github.com/handlebars-lang/handlebars-parser/pull/17) Add release plan, remove release-it ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) - -#### Committers: 3 -- Dan Freeman ([@dfreeman](https://github.com/dfreeman)) -- Yehuda Katz ([@wycats](https://github.com/wycats)) -- [@NullVoxPopuli](https://github.com/NullVoxPopuli) - -## v2.1.0 (2021-02-22) - -#### :rocket: Enhancement -* [#4](https://github.com/handlebars-lang/handlebars-parser/pull/4) [FEATURE] Allows SubExpressions to be PathExpression roots ([@pzuraq](https://github.com/pzuraq)) - -#### Committers: 1 -- Chris Garrett ([@pzuraq](https://github.com/pzuraq)) - -## v2.0.0 (2020-12-09) - -#### :boom: Breaking Change -* [#3](https://github.com/handlebars-lang/handlebars-parser/pull/3) Make sub-expressions callable ([@pzuraq](https://github.com/pzuraq)) - -#### Committers: 1 -- Chris Garrett ([@pzuraq](https://github.com/pzuraq)) - -## v1.1.0 (2020-09-18) - -#### :rocket: Enhancement -* [#2](https://github.com/handlebars-lang/handlebars-parser/pull/2) [FEAT] Adds types ([@pzuraq](https://github.com/pzuraq)) - -#### Committers: 1 -- Chris Garrett ([@pzuraq](https://github.com/pzuraq)) - - -## v1.0.0 (2020-09-14) - -#### :rocket: Enhancement -* [#1](https://github.com/handlebars-lang/handlebars-parser/pull/1) [FEAT] Adds initial setup, lint, ci ([@pzuraq](https://github.com/pzuraq)) - -#### Committers: 1 -- Chris Garrett ([@pzuraq](https://github.com/pzuraq)) - - diff --git a/packages/@handlebars/parser/README.md b/packages/@handlebars/parser/README.md deleted file mode 100644 index f77e27ef052..00000000000 --- a/packages/@handlebars/parser/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Handlebars Parser - -The official Handlebars.js parser. This package contains the definition for -for the Handlebars language AST, and the parser for parsing into that AST. diff --git a/packages/@handlebars/parser/lib/helpers.js b/packages/@handlebars/parser/lib/helpers.js deleted file mode 100644 index fb2998e2ebe..00000000000 --- a/packages/@handlebars/parser/lib/helpers.js +++ /dev/null @@ -1,224 +0,0 @@ -import Exception from './exception.js'; - -function validateClose(open, close) { - close = close.path ? close.path.original : close; - - if (open.path.original !== close) { - let errorNode = { loc: open.path.loc }; - - throw new Exception(open.path.original + " doesn't match " + close, errorNode); - } -} - -export function SourceLocation(source, locInfo) { - this.source = source; - this.start = { - line: locInfo.first_line, - column: locInfo.first_column, - }; - this.end = { - line: locInfo.last_line, - column: locInfo.last_column, - }; -} - -export function id(token) { - if (/^\[.*\]$/.test(token)) { - return token.substring(1, token.length - 1); - } else { - return token; - } -} - -export function stripFlags(open, close) { - return { - open: open.charAt(2) === '~', - close: close.charAt(close.length - 3) === '~', - }; -} - -export function stripComment(comment) { - return comment.replace(/^\{\{~?!-?-?/, '').replace(/-?-?~?\}\}$/, ''); -} - -export function preparePath(data, sexpr, parts, loc) { - loc = this.locInfo(loc); - - let original; - - if (data) { - original = '@'; - } else if (sexpr) { - original = sexpr.original + '.'; - } else { - original = ''; - } - - let tail = []; - let depth = 0; - - for (let i = 0, l = parts.length; i < l; i++) { - let part = parts[i].part; - // If we have [] syntax then we do not treat path references as operators, - // i.e. foo.[this] resolves to approximately context.foo['this'] - let isLiteral = parts[i].original !== part; - let separator = parts[i].separator; - - let partPrefix = separator === '.#' ? '#' : ''; - - original += (separator || '') + part; - - if (!isLiteral && (part === '..' || part === '.' || part === 'this')) { - if (tail.length > 0) { - throw new Exception('Invalid path: ' + original, { loc }); - } else if (part === '..') { - depth++; - } - } else { - tail.push(`${partPrefix}${part}`); - } - } - - let head = sexpr || tail.shift(); - - return { - type: 'PathExpression', - this: original.startsWith('this.'), - data, - depth, - head, - tail, - parts: head ? [head, ...tail] : tail, - original, - loc, - }; -} - -export function prepareMustache(path, params, hash, open, strip, locInfo) { - // Must use charAt to support IE pre-10 - let escapeFlag = open.charAt(3) || open.charAt(2), - escaped = escapeFlag !== '{' && escapeFlag !== '&'; - - let decorator = /\*/.test(open); - return { - type: decorator ? 'Decorator' : 'MustacheStatement', - path, - params, - hash, - escaped, - strip, - loc: this.locInfo(locInfo), - }; -} - -export function prepareRawBlock(openRawBlock, contents, close, locInfo) { - validateClose(openRawBlock, close); - - locInfo = this.locInfo(locInfo); - let program = { - type: 'Program', - body: contents, - strip: {}, - loc: locInfo, - }; - - return { - type: 'BlockStatement', - path: openRawBlock.path, - params: openRawBlock.params, - hash: openRawBlock.hash, - program, - openStrip: {}, - inverseStrip: {}, - closeStrip: {}, - loc: locInfo, - }; -} - -export function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { - if (close && close.path) { - validateClose(openBlock, close); - } - - let decorator = /\*/.test(openBlock.open); - - program.blockParams = openBlock.blockParams; - - let inverse, inverseStrip; - - if (inverseAndProgram) { - if (decorator) { - throw new Exception('Unexpected inverse block on decorator', inverseAndProgram); - } - - if (inverseAndProgram.chain) { - inverseAndProgram.program.body[0].closeStrip = close.strip; - } - - inverseStrip = inverseAndProgram.strip; - inverse = inverseAndProgram.program; - } - - if (inverted) { - inverted = inverse; - inverse = program; - program = inverted; - } - - return { - type: decorator ? 'DecoratorBlock' : 'BlockStatement', - path: openBlock.path, - params: openBlock.params, - hash: openBlock.hash, - program, - inverse, - openStrip: openBlock.strip, - inverseStrip, - closeStrip: close && close.strip, - loc: this.locInfo(locInfo), - }; -} - -export function prepareProgram(statements, loc) { - if (!loc && statements.length) { - const firstLoc = statements[0].loc, - lastLoc = statements[statements.length - 1].loc; - - /* istanbul ignore else */ - if (firstLoc && lastLoc) { - loc = { - source: firstLoc.source, - start: { - line: firstLoc.start.line, - column: firstLoc.start.column, - }, - end: { - line: lastLoc.end.line, - column: lastLoc.end.column, - }, - }; - } - } - - return { - type: 'Program', - body: statements, - strip: {}, - loc: loc, - }; -} - -export function preparePartialBlock(open, program, close, locInfo) { - validateClose(open, close); - - return { - type: 'PartialBlockStatement', - name: open.path, - params: open.params, - hash: open.hash, - program, - openStrip: open.strip, - closeStrip: close && close.strip, - loc: this.locInfo(locInfo), - }; -} diff --git a/packages/@handlebars/parser/lib/index.js b/packages/@handlebars/parser/lib/index.js deleted file mode 100644 index cf22bff15b9..00000000000 --- a/packages/@handlebars/parser/lib/index.js +++ /dev/null @@ -1,6 +0,0 @@ -export { default as Visitor } from './visitor.js'; -export { default as WhitespaceControl } from './whitespace-control.js'; -export { default as parser } from './parser.js'; -export { default as Exception } from './exception.js'; -export { print, PrintVisitor } from './printer.js'; -export { parse, parseWithoutProcessing } from './parse.js'; diff --git a/packages/@handlebars/parser/lib/parse.js b/packages/@handlebars/parser/lib/parse.js deleted file mode 100644 index 9927b5f4d73..00000000000 --- a/packages/@handlebars/parser/lib/parse.js +++ /dev/null @@ -1,73 +0,0 @@ -import parser from './parser.js'; -import WhitespaceControl from './whitespace-control.js'; -import * as Helpers from './helpers.js'; - -let baseHelpers = {}; - -for (let helper in Helpers) { - if (Object.prototype.hasOwnProperty.call(Helpers, helper)) { - baseHelpers[helper] = Helpers[helper]; - } -} - -export function parseWithoutProcessing(input, options) { - // Just return if an already-compiled AST was passed in. - if (input.type === 'Program') { - return input; - } - - parser.yy = baseHelpers; - - // Altering the shared object here, but this is ok as parser is a sync operation - parser.yy.locInfo = function (locInfo) { - return new Helpers.SourceLocation(options && options.srcName, locInfo); - }; - - let squareSyntax; - - if (typeof options?.syntax?.square === 'function') { - squareSyntax = options.syntax.square; - } else if (options?.syntax?.square === 'node') { - squareSyntax = arrayLiteralNode; - } else { - squareSyntax = 'string'; - } - - let hashSyntax; - - if (typeof options?.syntax?.hash === 'function') { - hashSyntax = options.syntax.hash; - } else { - hashSyntax = hashLiteralNode; - } - - parser.yy.syntax = { - square: squareSyntax, - hash: hashSyntax, - }; - - return parser.parse(input); -} - -function arrayLiteralNode(array, loc) { - return { - type: 'ArrayLiteral', - items: array, - loc, - }; -} - -function hashLiteralNode(hash, loc) { - return { - type: 'HashLiteral', - pairs: hash.pairs, - loc, - }; -} - -export function parse(input, options) { - let ast = parseWithoutProcessing(input, options); - let strip = new WhitespaceControl(options); - - return strip.accept(ast); -} diff --git a/packages/@handlebars/parser/lib/parser.js b/packages/@handlebars/parser/lib/parser.js deleted file mode 100644 index 136e73cc256..00000000000 --- a/packages/@handlebars/parser/lib/parser.js +++ /dev/null @@ -1,2032 +0,0 @@ -// @ts-nocheck -/* parser generated by jison 0.4.18 */ -/* - Returns a Parser object of the following structure: - - Parser: { - yy: {} - } - - Parser.prototype: { - yy: {}, - trace: function(), - symbols_: {associative list: name ==> number}, - terminals_: {associative list: number ==> name}, - productions_: [...], - performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), - table: [...], - defaultActions: {...}, - parseError: function(str, hash), - parse: function(input), - - lexer: { - EOF: 1, - parseError: function(str, hash), - setInput: function(input), - input: function(), - unput: function(str), - more: function(), - less: function(n), - pastInput: function(), - upcomingInput: function(), - showPosition: function(), - test_match: function(regex_match_array, rule_index), - next: function(), - lex: function(), - begin: function(condition), - popState: function(), - _currentRules: function(), - topState: function(), - pushState: function(condition), - - options: { - ranges: boolean (optional: true ==> token location info will include a .range[] member) - flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) - backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) - }, - - performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), - rules: [...], - conditions: {associative list: name ==> set}, - } - } - - - token location info (@$, _$, etc.): { - first_line: n, - last_line: n, - first_column: n, - last_column: n, - range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) - } - - - the parseError function receives a 'hash' object with these members for lexer and parser errors: { - text: (matched text) - token: (the produced terminal token, if any) - line: (yylineno) - } - while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { - loc: (yylloc) - expected: (string describing the set of expected tokens) - recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) - } -*/ -var parser = (function () { - var o = function (k, v, o, l) { - for (o = o || {}, l = k.length; l--; o[k[l]] = v); - return o; - }, - $V0 = [2, 52], - $V1 = [1, 20], - $V2 = [5, 14, 15, 19, 29, 34, 39, 44, 47, 48, 53, 57, 61], - $V3 = [1, 44], - $V4 = [1, 40], - $V5 = [1, 43], - $V6 = [1, 33], - $V7 = [1, 34], - $V8 = [1, 35], - $V9 = [1, 36], - $Va = [1, 37], - $Vb = [1, 42], - $Vc = [1, 46], - $Vd = [14, 15, 19, 29, 34, 39, 44, 47, 48, 53, 57, 61], - $Ve = [14, 15, 19, 29, 34, 44, 47, 48, 53, 57, 61], - $Vf = [15, 18], - $Vg = [14, 15, 19, 29, 34, 47, 48, 53, 57, 61], - $Vh = [33, 67, 73, 75, 84, 85, 86, 87, 88, 89], - $Vi = [23, 33, 56, 67, 68, 73, 75, 77, 79, 84, 85, 86, 87, 88, 89], - $Vj = [1, 62], - $Vk = [1, 63], - $Vl = [23, 33, 56, 68, 73, 79], - $Vm = [23, 33, 56, 67, 68, 73, 75, 77, 79, 84, 85, 86, 87, 88, 89, 92, 93], - $Vn = [2, 51], - $Vo = [1, 64], - $Vp = [67, 73, 75, 77, 84, 85, 86, 87, 88, 89], - $Vq = [56, 67, 73, 75, 84, 85, 86, 87, 88, 89], - $Vr = [1, 75], - $Vs = [1, 76], - $Vt = [1, 83], - $Vu = [33, 67, 73, 75, 79, 84, 85, 86, 87, 88, 89], - $Vv = [23, 67, 73, 75, 84, 85, 86, 87, 88, 89], - $Vw = [67, 68, 73, 75, 84, 85, 86, 87, 88, 89], - $Vx = [33, 79], - $Vy = [1, 134], - $Vz = [73, 81]; - var parser = { - trace: function trace() {}, - yy: {}, - symbols_: { - error: 2, - root: 3, - program: 4, - EOF: 5, - program_repetition0: 6, - statement: 7, - mustache: 8, - block: 9, - rawBlock: 10, - partial: 11, - partialBlock: 12, - content: 13, - COMMENT: 14, - CONTENT: 15, - openRawBlock: 16, - rawBlock_repetition0: 17, - END_RAW_BLOCK: 18, - OPEN_RAW_BLOCK: 19, - helperName: 20, - openRawBlock_repetition0: 21, - openRawBlock_option0: 22, - CLOSE_RAW_BLOCK: 23, - openBlock: 24, - block_option0: 25, - closeBlock: 26, - openInverse: 27, - block_option1: 28, - OPEN_BLOCK: 29, - openBlock_repetition0: 30, - openBlock_option0: 31, - openBlock_option1: 32, - CLOSE: 33, - OPEN_INVERSE: 34, - openInverse_repetition0: 35, - openInverse_option0: 36, - openInverse_option1: 37, - openInverseChain: 38, - OPEN_INVERSE_CHAIN: 39, - openInverseChain_repetition0: 40, - openInverseChain_option0: 41, - openInverseChain_option1: 42, - inverseAndProgram: 43, - INVERSE: 44, - inverseChain: 45, - inverseChain_option0: 46, - OPEN_ENDBLOCK: 47, - OPEN: 48, - hash: 49, - expr: 50, - mustache_repetition0: 51, - mustache_option0: 52, - OPEN_UNESCAPED: 53, - mustache_repetition1: 54, - mustache_option1: 55, - CLOSE_UNESCAPED: 56, - OPEN_PARTIAL: 57, - partial_repetition0: 58, - partial_option0: 59, - openPartialBlock: 60, - OPEN_PARTIAL_BLOCK: 61, - openPartialBlock_repetition0: 62, - openPartialBlock_option0: 63, - exprHead: 64, - arrayLiteral: 65, - sexpr: 66, - OPEN_SEXPR: 67, - CLOSE_SEXPR: 68, - sexpr_repetition0: 69, - sexpr_option0: 70, - hash_repetition_plus0: 71, - hashSegment: 72, - ID: 73, - EQUALS: 74, - OPEN_ARRAY: 75, - arrayLiteral_repetition0: 76, - CLOSE_ARRAY: 77, - blockParams: 78, - OPEN_BLOCK_PARAMS: 79, - blockParams_repetition_plus0: 80, - CLOSE_BLOCK_PARAMS: 81, - path: 82, - dataName: 83, - STRING: 84, - NUMBER: 85, - BOOLEAN: 86, - UNDEFINED: 87, - NULL: 88, - DATA: 89, - pathSegments: 90, - sep: 91, - SEP: 92, - PRIVATE_SEP: 93, - $accept: 0, - $end: 1, - }, - terminals_: { - 2: 'error', - 5: 'EOF', - 14: 'COMMENT', - 15: 'CONTENT', - 18: 'END_RAW_BLOCK', - 19: 'OPEN_RAW_BLOCK', - 23: 'CLOSE_RAW_BLOCK', - 29: 'OPEN_BLOCK', - 33: 'CLOSE', - 34: 'OPEN_INVERSE', - 39: 'OPEN_INVERSE_CHAIN', - 44: 'INVERSE', - 47: 'OPEN_ENDBLOCK', - 48: 'OPEN', - 53: 'OPEN_UNESCAPED', - 56: 'CLOSE_UNESCAPED', - 57: 'OPEN_PARTIAL', - 61: 'OPEN_PARTIAL_BLOCK', - 67: 'OPEN_SEXPR', - 68: 'CLOSE_SEXPR', - 73: 'ID', - 74: 'EQUALS', - 75: 'OPEN_ARRAY', - 77: 'CLOSE_ARRAY', - 79: 'OPEN_BLOCK_PARAMS', - 81: 'CLOSE_BLOCK_PARAMS', - 84: 'STRING', - 85: 'NUMBER', - 86: 'BOOLEAN', - 87: 'UNDEFINED', - 88: 'NULL', - 89: 'DATA', - 92: 'SEP', - 93: 'PRIVATE_SEP', - }, - productions_: [ - 0, - [3, 2], - [4, 1], - [7, 1], - [7, 1], - [7, 1], - [7, 1], - [7, 1], - [7, 1], - [7, 1], - [13, 1], - [10, 3], - [16, 5], - [9, 4], - [9, 4], - [24, 6], - [27, 6], - [38, 6], - [43, 2], - [45, 3], - [45, 1], - [26, 3], - [8, 3], - [8, 5], - [8, 5], - [11, 5], - [12, 3], - [60, 5], - [50, 1], - [50, 1], - [64, 1], - [64, 1], - [66, 3], - [66, 5], - [49, 1], - [72, 3], - [65, 3], - [78, 3], - [20, 1], - [20, 1], - [20, 1], - [20, 1], - [20, 1], - [20, 1], - [20, 1], - [83, 2], - [91, 1], - [91, 1], - [82, 3], - [82, 1], - [90, 3], - [90, 1], - [6, 0], - [6, 2], - [17, 0], - [17, 2], - [21, 0], - [21, 2], - [22, 0], - [22, 1], - [25, 0], - [25, 1], - [28, 0], - [28, 1], - [30, 0], - [30, 2], - [31, 0], - [31, 1], - [32, 0], - [32, 1], - [35, 0], - [35, 2], - [36, 0], - [36, 1], - [37, 0], - [37, 1], - [40, 0], - [40, 2], - [41, 0], - [41, 1], - [42, 0], - [42, 1], - [46, 0], - [46, 1], - [51, 0], - [51, 2], - [52, 0], - [52, 1], - [54, 0], - [54, 2], - [55, 0], - [55, 1], - [58, 0], - [58, 2], - [59, 0], - [59, 1], - [62, 0], - [62, 2], - [63, 0], - [63, 1], - [69, 0], - [69, 2], - [70, 0], - [70, 1], - [71, 1], - [71, 2], - [76, 0], - [76, 2], - [80, 1], - [80, 2], - ], - performAction: function anonymous( - yytext, - yyleng, - yylineno, - yy, - yystate /* action[1] */, - $$ /* vstack */, - _$ /* lstack */ - ) { - /* this == yyval */ - - var $0 = $$.length - 1; - switch (yystate) { - case 1: - return $$[$0 - 1]; - break; - case 2: - this.$ = yy.prepareProgram($$[$0]); - break; - case 3: - case 4: - case 5: - case 6: - case 7: - case 8: - case 20: - case 28: - case 29: - case 30: - case 31: - case 38: - case 39: - case 46: - case 47: - this.$ = $$[$0]; - break; - case 9: - this.$ = { - type: 'CommentStatement', - value: yy.stripComment($$[$0]), - strip: yy.stripFlags($$[$0], $$[$0]), - loc: yy.locInfo(this._$), - }; - - break; - case 10: - this.$ = { - type: 'ContentStatement', - original: $$[$0], - value: $$[$0], - loc: yy.locInfo(this._$), - }; - - break; - case 11: - this.$ = yy.prepareRawBlock($$[$0 - 2], $$[$0 - 1], $$[$0], this._$); - break; - case 12: - this.$ = { path: $$[$0 - 3], params: $$[$0 - 2], hash: $$[$0 - 1] }; - break; - case 13: - this.$ = yy.prepareBlock($$[$0 - 3], $$[$0 - 2], $$[$0 - 1], $$[$0], false, this._$); - break; - case 14: - this.$ = yy.prepareBlock($$[$0 - 3], $$[$0 - 2], $$[$0 - 1], $$[$0], true, this._$); - break; - case 15: - this.$ = { - open: $$[$0 - 5], - path: $$[$0 - 4], - params: $$[$0 - 3], - hash: $$[$0 - 2], - blockParams: $$[$0 - 1], - strip: yy.stripFlags($$[$0 - 5], $$[$0]), - }; - break; - case 16: - case 17: - this.$ = { - path: $$[$0 - 4], - params: $$[$0 - 3], - hash: $$[$0 - 2], - blockParams: $$[$0 - 1], - strip: yy.stripFlags($$[$0 - 5], $$[$0]), - }; - break; - case 18: - this.$ = { strip: yy.stripFlags($$[$0 - 1], $$[$0 - 1]), program: $$[$0] }; - break; - case 19: - var inverse = yy.prepareBlock($$[$0 - 2], $$[$0 - 1], $$[$0], $$[$0], false, this._$), - program = yy.prepareProgram([inverse], $$[$0 - 1].loc); - program.chained = true; - - this.$ = { strip: $$[$0 - 2].strip, program: program, chain: true }; - - break; - case 21: - this.$ = { path: $$[$0 - 1], strip: yy.stripFlags($$[$0 - 2], $$[$0]) }; - break; - case 22: - this.$ = yy.prepareMustache( - yy.syntax.hash($$[$0 - 1], yy.locInfo(this._$), { yy, syntax: 'expr' }), - [], - undefined, - $$[$0 - 2], - yy.stripFlags($$[$0 - 2], $$[$0]), - this._$ - ); - break; - case 23: - case 24: - this.$ = yy.prepareMustache( - $$[$0 - 3], - $$[$0 - 2], - $$[$0 - 1], - $$[$0 - 4], - yy.stripFlags($$[$0 - 4], $$[$0]), - this._$ - ); - break; - case 25: - this.$ = { - type: 'PartialStatement', - name: $$[$0 - 3], - params: $$[$0 - 2], - hash: $$[$0 - 1], - indent: '', - strip: yy.stripFlags($$[$0 - 4], $$[$0]), - loc: yy.locInfo(this._$), - }; - - break; - case 26: - this.$ = yy.preparePartialBlock($$[$0 - 2], $$[$0 - 1], $$[$0], this._$); - break; - case 27: - this.$ = { - path: $$[$0 - 3], - params: $$[$0 - 2], - hash: $$[$0 - 1], - strip: yy.stripFlags($$[$0 - 4], $$[$0]), - }; - break; - case 32: - this.$ = yy.syntax.hash($$[$0 - 1], yy.locInfo(this._$), { yy, syntax: 'expr' }); - break; - case 33: - this.$ = { - type: 'SubExpression', - path: $$[$0 - 3], - params: $$[$0 - 2], - hash: $$[$0 - 1], - loc: yy.locInfo(this._$), - }; - - break; - case 34: - this.$ = { type: 'Hash', pairs: $$[$0], loc: yy.locInfo(this._$) }; - break; - case 35: - this.$ = { - type: 'HashPair', - key: yy.id($$[$0 - 2]), - value: $$[$0], - loc: yy.locInfo(this._$), - }; - break; - case 36: - this.$ = yy.syntax.square($$[$0 - 1], yy.locInfo(this._$), { yy, syntax: 'expr' }); - break; - case 37: - this.$ = yy.id($$[$0 - 1]); - break; - case 40: - this.$ = { - type: 'StringLiteral', - value: $$[$0], - original: $$[$0], - loc: yy.locInfo(this._$), - }; - break; - case 41: - this.$ = { - type: 'NumberLiteral', - value: Number($$[$0]), - original: Number($$[$0]), - loc: yy.locInfo(this._$), - }; - break; - case 42: - this.$ = { - type: 'BooleanLiteral', - value: $$[$0] === 'true', - original: $$[$0] === 'true', - loc: yy.locInfo(this._$), - }; - break; - case 43: - this.$ = { - type: 'UndefinedLiteral', - original: undefined, - value: undefined, - loc: yy.locInfo(this._$), - }; - break; - case 44: - this.$ = { type: 'NullLiteral', original: null, value: null, loc: yy.locInfo(this._$) }; - break; - case 45: - this.$ = yy.preparePath(true, false, $$[$0], this._$); - break; - case 48: - this.$ = yy.preparePath(false, $$[$0 - 2], $$[$0], this._$); - break; - case 49: - this.$ = yy.preparePath(false, false, $$[$0], this._$); - break; - case 50: - $$[$0 - 2].push({ part: yy.id($$[$0]), original: $$[$0], separator: $$[$0 - 1] }); - this.$ = $$[$0 - 2]; - break; - case 51: - this.$ = [{ part: yy.id($$[$0]), original: $$[$0] }]; - break; - case 52: - case 54: - case 56: - case 64: - case 70: - case 76: - case 84: - case 88: - case 92: - case 96: - case 100: - case 106: - this.$ = []; - break; - case 53: - case 55: - case 57: - case 65: - case 71: - case 77: - case 85: - case 89: - case 93: - case 97: - case 101: - case 105: - case 107: - case 109: - $$[$0 - 1].push($$[$0]); - break; - case 104: - case 108: - this.$ = [$$[$0]]; - break; - } - }, - table: [ - o([5, 14, 15, 19, 29, 34, 48, 53, 57, 61], $V0, { 3: 1, 4: 2, 6: 3 }), - { 1: [3] }, - { 5: [1, 4] }, - o([5, 39, 44, 47], [2, 2], { - 7: 5, - 8: 6, - 9: 7, - 10: 8, - 11: 9, - 12: 10, - 13: 11, - 24: 15, - 27: 16, - 16: 17, - 60: 19, - 14: [1, 12], - 15: $V1, - 19: [1, 23], - 29: [1, 21], - 34: [1, 22], - 48: [1, 13], - 53: [1, 14], - 57: [1, 18], - 61: [1, 24], - }), - { 1: [2, 1] }, - o($V2, [2, 53]), - o($V2, [2, 3]), - o($V2, [2, 4]), - o($V2, [2, 5]), - o($V2, [2, 6]), - o($V2, [2, 7]), - o($V2, [2, 8]), - o($V2, [2, 9]), - { - 20: 28, - 49: 25, - 50: 26, - 64: 29, - 65: 38, - 66: 39, - 67: $V3, - 71: 27, - 72: 30, - 73: $V4, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - { - 20: 28, - 50: 45, - 64: 29, - 65: 38, - 66: 39, - 67: $V3, - 73: $Vc, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - o($Vd, $V0, { 6: 3, 4: 47 }), - o($Ve, $V0, { 6: 3, 4: 48 }), - o($Vf, [2, 54], { 17: 49 }), - { - 20: 28, - 50: 50, - 64: 29, - 65: 38, - 66: 39, - 67: $V3, - 73: $Vc, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - o($Vg, $V0, { 6: 3, 4: 51 }), - o([5, 14, 15, 18, 19, 29, 34, 39, 44, 47, 48, 53, 57, 61], [2, 10]), - { - 20: 52, - 64: 53, - 65: 38, - 66: 39, - 67: $V3, - 73: $Vc, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - { - 20: 54, - 64: 53, - 65: 38, - 66: 39, - 67: $V3, - 73: $Vc, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - { - 20: 55, - 64: 53, - 65: 38, - 66: 39, - 67: $V3, - 73: $Vc, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - { - 20: 28, - 50: 56, - 64: 29, - 65: 38, - 66: 39, - 67: $V3, - 73: $Vc, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - { 33: [1, 57] }, - o($Vh, [2, 84], { 51: 58 }), - o([23, 33, 56, 68, 79], [2, 34], { 72: 59, 73: [1, 60] }), - o($Vi, [2, 28]), - o($Vi, [2, 29], { 91: 61, 92: $Vj, 93: $Vk }), - o($Vl, [2, 104]), - o($Vi, [2, 38]), - o($Vi, [2, 39]), - o($Vi, [2, 40]), - o($Vi, [2, 41]), - o($Vi, [2, 42]), - o($Vi, [2, 43]), - o($Vi, [2, 44]), - o($Vm, [2, 30]), - o($Vm, [2, 31]), - o([23, 33, 56, 67, 68, 73, 75, 79, 84, 85, 86, 87, 88, 89, 92, 93], $Vn, { 74: $Vo }), - o($Vi, [2, 49], { 91: 65, 92: $Vj, 93: $Vk }), - { 73: $Vc, 90: 66 }, - o($Vp, [2, 106], { 76: 67 }), - { - 20: 28, - 49: 68, - 50: 69, - 64: 29, - 65: 38, - 66: 39, - 67: $V3, - 71: 27, - 72: 30, - 73: $V4, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - o($Vq, [2, 88], { 54: 70 }), - o($Vm, $Vn), - { 25: 71, 38: 73, 39: $Vr, 43: 74, 44: $Vs, 45: 72, 47: [2, 60] }, - { 28: 77, 43: 78, 44: $Vs, 47: [2, 62] }, - { 13: 80, 15: $V1, 18: [1, 79] }, - o($Vh, [2, 92], { 58: 81 }), - { 26: 82, 47: $Vt }, - o($Vu, [2, 64], { 30: 84 }), - { 91: 61, 92: $Vj, 93: $Vk }, - o($Vu, [2, 70], { 35: 85 }), - o($Vv, [2, 56], { 21: 86 }), - o($Vh, [2, 96], { 62: 87 }), - o($V2, [2, 22]), - { - 20: 28, - 33: [2, 86], - 49: 90, - 50: 89, - 52: 88, - 64: 29, - 65: 38, - 66: 39, - 67: $V3, - 71: 27, - 72: 30, - 73: $V4, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - o($Vl, [2, 105]), - { 74: $Vo }, - { 73: $Vc, 90: 91 }, - { 73: [2, 46] }, - { 73: [2, 47] }, - { - 20: 28, - 50: 92, - 64: 29, - 65: 38, - 66: 39, - 67: $V3, - 73: $Vc, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - { 73: [1, 93] }, - o($Vi, [2, 45], { 91: 65, 92: $Vj, 93: $Vk }), - { - 20: 28, - 50: 95, - 64: 29, - 65: 38, - 66: 39, - 67: $V3, - 73: $Vc, - 75: $V5, - 77: [1, 94], - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - { 68: [1, 96] }, - o($Vw, [2, 100], { 69: 97 }), - { - 20: 28, - 49: 100, - 50: 99, - 55: 98, - 56: [2, 90], - 64: 29, - 65: 38, - 66: 39, - 67: $V3, - 71: 27, - 72: 30, - 73: $V4, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - { 26: 101, 47: $Vt }, - { 47: [2, 61] }, - o($Vd, $V0, { 6: 3, 4: 102 }), - { 47: [2, 20] }, - { - 20: 103, - 64: 53, - 65: 38, - 66: 39, - 67: $V3, - 73: $Vc, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - o($Vg, $V0, { 6: 3, 4: 104 }), - { 26: 105, 47: $Vt }, - { 47: [2, 63] }, - o($V2, [2, 11]), - o($Vf, [2, 55]), - { - 20: 28, - 33: [2, 94], - 49: 108, - 50: 107, - 59: 106, - 64: 29, - 65: 38, - 66: 39, - 67: $V3, - 71: 27, - 72: 30, - 73: $V4, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - o($V2, [2, 26]), - { - 20: 109, - 64: 53, - 65: 38, - 66: 39, - 67: $V3, - 73: $Vc, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - o($Vx, [2, 66], { - 71: 27, - 20: 28, - 64: 29, - 72: 30, - 82: 31, - 83: 32, - 65: 38, - 66: 39, - 90: 41, - 31: 110, - 50: 111, - 49: 112, - 67: $V3, - 73: $V4, - 75: $V5, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - }), - o($Vx, [2, 72], { - 71: 27, - 20: 28, - 64: 29, - 72: 30, - 82: 31, - 83: 32, - 65: 38, - 66: 39, - 90: 41, - 36: 113, - 50: 114, - 49: 115, - 67: $V3, - 73: $V4, - 75: $V5, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - }), - { - 20: 28, - 22: 116, - 23: [2, 58], - 49: 118, - 50: 117, - 64: 29, - 65: 38, - 66: 39, - 67: $V3, - 71: 27, - 72: 30, - 73: $V4, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - { - 20: 28, - 33: [2, 98], - 49: 121, - 50: 120, - 63: 119, - 64: 29, - 65: 38, - 66: 39, - 67: $V3, - 71: 27, - 72: 30, - 73: $V4, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - { 33: [1, 122] }, - o($Vh, [2, 85]), - { 33: [2, 87] }, - o($Vi, [2, 48], { 91: 65, 92: $Vj, 93: $Vk }), - o($Vl, [2, 35]), - o($Vm, [2, 50]), - o($Vm, [2, 36]), - o($Vp, [2, 107]), - o($Vm, [2, 32]), - { - 20: 28, - 49: 125, - 50: 124, - 64: 29, - 65: 38, - 66: 39, - 67: $V3, - 68: [2, 102], - 70: 123, - 71: 27, - 72: 30, - 73: $V4, - 75: $V5, - 82: 31, - 83: 32, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - 90: 41, - }, - { 56: [1, 126] }, - o($Vq, [2, 89]), - { 56: [2, 91] }, - o($V2, [2, 13]), - { 38: 73, 39: $Vr, 43: 74, 44: $Vs, 45: 128, 46: 127, 47: [2, 82] }, - o($Vu, [2, 76], { 40: 129 }), - { 47: [2, 18] }, - o($V2, [2, 14]), - { 33: [1, 130] }, - o($Vh, [2, 93]), - { 33: [2, 95] }, - { 33: [1, 131] }, - { 32: 132, 33: [2, 68], 78: 133, 79: $Vy }, - o($Vu, [2, 65]), - o($Vx, [2, 67]), - { 33: [2, 74], 37: 135, 78: 136, 79: $Vy }, - o($Vu, [2, 71]), - o($Vx, [2, 73]), - { 23: [1, 137] }, - o($Vv, [2, 57]), - { 23: [2, 59] }, - { 33: [1, 138] }, - o($Vh, [2, 97]), - { 33: [2, 99] }, - o($V2, [2, 23]), - { 68: [1, 139] }, - o($Vw, [2, 101]), - { 68: [2, 103] }, - o($V2, [2, 24]), - { 47: [2, 19] }, - { 47: [2, 83] }, - o($Vx, [2, 78], { - 71: 27, - 20: 28, - 64: 29, - 72: 30, - 82: 31, - 83: 32, - 65: 38, - 66: 39, - 90: 41, - 41: 140, - 50: 141, - 49: 142, - 67: $V3, - 73: $V4, - 75: $V5, - 84: $V6, - 85: $V7, - 86: $V8, - 87: $V9, - 88: $Va, - 89: $Vb, - }), - o($V2, [2, 25]), - o($V2, [2, 21]), - { 33: [1, 143] }, - { 33: [2, 69] }, - { 73: [1, 145], 80: 144 }, - { 33: [1, 146] }, - { 33: [2, 75] }, - o($Vf, [2, 12]), - o($Vg, [2, 27]), - o($Vm, [2, 33]), - { 33: [2, 80], 42: 147, 78: 148, 79: $Vy }, - o($Vu, [2, 77]), - o($Vx, [2, 79]), - o($Vd, [2, 15]), - { 73: [1, 150], 81: [1, 149] }, - o($Vz, [2, 108]), - o($Ve, [2, 16]), - { 33: [1, 151] }, - { 33: [2, 81] }, - { 33: [2, 37] }, - o($Vz, [2, 109]), - o($Vd, [2, 17]), - ], - defaultActions: { - 4: [2, 1], - 62: [2, 46], - 63: [2, 47], - 72: [2, 61], - 74: [2, 20], - 78: [2, 63], - 90: [2, 87], - 100: [2, 91], - 104: [2, 18], - 108: [2, 95], - 118: [2, 59], - 121: [2, 99], - 125: [2, 103], - 127: [2, 19], - 128: [2, 83], - 133: [2, 69], - 136: [2, 75], - 148: [2, 81], - 149: [2, 37], - }, - parseError: function parseError(str, hash) { - if (hash.recoverable) { - this.trace(str); - } else { - var error = new Error(str); - error.hash = hash; - throw error; - } - }, - parse: function parse(input) { - var self = this, - stack = [0], - tstack = [], - vstack = [null], - lstack = [], - table = this.table, - yytext = '', - yylineno = 0, - yyleng = 0, - recovering = 0, - TERROR = 2, - EOF = 1; - var args = lstack.slice.call(arguments, 1); - var lexer = Object.create(this.lexer); - var sharedState = { yy: {} }; - for (var k in this.yy) { - if (Object.prototype.hasOwnProperty.call(this.yy, k)) { - sharedState.yy[k] = this.yy[k]; - } - } - lexer.setInput(input, sharedState.yy); - sharedState.yy.lexer = lexer; - sharedState.yy.parser = this; - if (typeof lexer.yylloc == 'undefined') { - lexer.yylloc = {}; - } - var yyloc = lexer.yylloc; - lstack.push(yyloc); - var ranges = lexer.options && lexer.options.ranges; - if (typeof sharedState.yy.parseError === 'function') { - this.parseError = sharedState.yy.parseError; - } else { - this.parseError = Object.getPrototypeOf(this).parseError; - } - function popStack(n) { - stack.length = stack.length - 2 * n; - vstack.length = vstack.length - n; - lstack.length = lstack.length - n; - } - _token_stack: var lex = function () { - var token; - token = lexer.lex() || EOF; - if (typeof token !== 'number') { - token = self.symbols_[token] || token; - } - return token; - }; - var symbol, - preErrorSymbol, - state, - action, - a, - r, - yyval = {}, - p, - len, - newState, - expected; - while (true) { - state = stack[stack.length - 1]; - if (this.defaultActions[state]) { - action = this.defaultActions[state]; - } else { - if (symbol === null || typeof symbol == 'undefined') { - symbol = lex(); - } - action = table[state] && table[state][symbol]; - } - if (typeof action === 'undefined' || !action.length || !action[0]) { - var errStr = ''; - expected = []; - for (p in table[state]) { - if (this.terminals_[p] && p > TERROR) { - expected.push("'" + this.terminals_[p] + "'"); - } - } - if (lexer.showPosition) { - errStr = - 'Parse error on line ' + - (yylineno + 1) + - ':\n' + - lexer.showPosition() + - '\nExpecting ' + - expected.join(', ') + - ", got '" + - (this.terminals_[symbol] || symbol) + - "'"; - } else { - errStr = - 'Parse error on line ' + - (yylineno + 1) + - ': Unexpected ' + - (symbol == EOF ? 'end of input' : "'" + (this.terminals_[symbol] || symbol) + "'"); - } - this.parseError(errStr, { - text: lexer.match, - token: this.terminals_[symbol] || symbol, - line: lexer.yylineno, - loc: yyloc, - expected: expected, - }); - } - if (action[0] instanceof Array && action.length > 1) { - throw new Error( - 'Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol - ); - } - switch (action[0]) { - case 1: - stack.push(symbol); - vstack.push(lexer.yytext); - lstack.push(lexer.yylloc); - stack.push(action[1]); - symbol = null; - if (!preErrorSymbol) { - yyleng = lexer.yyleng; - yytext = lexer.yytext; - yylineno = lexer.yylineno; - yyloc = lexer.yylloc; - if (recovering > 0) { - recovering--; - } - } else { - symbol = preErrorSymbol; - preErrorSymbol = null; - } - break; - case 2: - len = this.productions_[action[1]][1]; - yyval.$ = vstack[vstack.length - len]; - yyval._$ = { - first_line: lstack[lstack.length - (len || 1)].first_line, - last_line: lstack[lstack.length - 1].last_line, - first_column: lstack[lstack.length - (len || 1)].first_column, - last_column: lstack[lstack.length - 1].last_column, - }; - if (ranges) { - yyval._$.range = [ - lstack[lstack.length - (len || 1)].range[0], - lstack[lstack.length - 1].range[1], - ]; - } - r = this.performAction.apply( - yyval, - [yytext, yyleng, yylineno, sharedState.yy, action[1], vstack, lstack].concat(args) - ); - if (typeof r !== 'undefined') { - return r; - } - if (len) { - stack = stack.slice(0, -1 * len * 2); - vstack = vstack.slice(0, -1 * len); - lstack = lstack.slice(0, -1 * len); - } - stack.push(this.productions_[action[1]][0]); - vstack.push(yyval.$); - lstack.push(yyval._$); - newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; - stack.push(newState); - break; - case 3: - return true; - } - } - return true; - }, - }; - /* generated by jison-lex 0.3.4 */ - var lexer = (function () { - var lexer = { - EOF: 1, - - parseError: function parseError(str, hash) { - if (this.yy.parser) { - this.yy.parser.parseError(str, hash); - } else { - throw new Error(str); - } - }, - - // resets the lexer, sets new input - setInput: function (input, yy) { - this.yy = yy || this.yy || {}; - this._input = input; - this._more = this._backtrack = this.done = false; - this.yylineno = this.yyleng = 0; - this.yytext = this.matched = this.match = ''; - this.conditionStack = ['INITIAL']; - this.yylloc = { - first_line: 1, - first_column: 0, - last_line: 1, - last_column: 0, - }; - if (this.options.ranges) { - this.yylloc.range = [0, 0]; - } - this.offset = 0; - return this; - }, - - // consumes and returns one char from the input - input: function () { - var ch = this._input[0]; - this.yytext += ch; - this.yyleng++; - this.offset++; - this.match += ch; - this.matched += ch; - var lines = ch.match(/(?:\r\n?|\n).*/g); - if (lines) { - this.yylineno++; - this.yylloc.last_line++; - } else { - this.yylloc.last_column++; - } - if (this.options.ranges) { - this.yylloc.range[1]++; - } - - this._input = this._input.slice(1); - return ch; - }, - - // unshifts one char (or a string) into the input - unput: function (ch) { - var len = ch.length; - var lines = ch.split(/(?:\r\n?|\n)/g); - - this._input = ch + this._input; - this.yytext = this.yytext.substr(0, this.yytext.length - len); - //this.yyleng -= len; - this.offset -= len; - var oldLines = this.match.split(/(?:\r\n?|\n)/g); - this.match = this.match.substr(0, this.match.length - 1); - this.matched = this.matched.substr(0, this.matched.length - 1); - - if (lines.length - 1) { - this.yylineno -= lines.length - 1; - } - var r = this.yylloc.range; - - this.yylloc = { - first_line: this.yylloc.first_line, - last_line: this.yylineno + 1, - first_column: this.yylloc.first_column, - last_column: lines - ? (lines.length === oldLines.length ? this.yylloc.first_column : 0) + - oldLines[oldLines.length - lines.length].length - - lines[0].length - : this.yylloc.first_column - len, - }; - - if (this.options.ranges) { - this.yylloc.range = [r[0], r[0] + this.yyleng - len]; - } - this.yyleng = this.yytext.length; - return this; - }, - - // When called from action, caches matched text and appends it on next action - more: function () { - this._more = true; - return this; - }, - - // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. - reject: function () { - if (this.options.backtrack_lexer) { - this._backtrack = true; - } else { - return this.parseError( - 'Lexical error on line ' + - (this.yylineno + 1) + - '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + - this.showPosition(), - { - text: '', - token: null, - line: this.yylineno, - } - ); - } - return this; - }, - - // retain first n characters of the match - less: function (n) { - this.unput(this.match.slice(n)); - }, - - // displays already matched input, i.e. for error messages - pastInput: function () { - var past = this.matched.substr(0, this.matched.length - this.match.length); - return (past.length > 20 ? '...' : '') + past.substr(-20).replace(/\n/g, ''); - }, - - // displays upcoming input, i.e. for error messages - upcomingInput: function () { - var next = this.match; - if (next.length < 20) { - next += this._input.substr(0, 20 - next.length); - } - return (next.substr(0, 20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ''); - }, - - // displays the character position where the lexing error occurred, i.e. for error messages - showPosition: function () { - var pre = this.pastInput(); - var c = new Array(pre.length + 1).join('-'); - return pre + this.upcomingInput() + '\n' + c + '^'; - }, - - // test the lexed token: return FALSE when not a match, otherwise return token - test_match: function (match, indexed_rule) { - var token, lines, backup; - - if (this.options.backtrack_lexer) { - // save context - backup = { - yylineno: this.yylineno, - yylloc: { - first_line: this.yylloc.first_line, - last_line: this.last_line, - first_column: this.yylloc.first_column, - last_column: this.yylloc.last_column, - }, - yytext: this.yytext, - match: this.match, - matches: this.matches, - matched: this.matched, - yyleng: this.yyleng, - offset: this.offset, - _more: this._more, - _input: this._input, - yy: this.yy, - conditionStack: this.conditionStack.slice(0), - done: this.done, - }; - if (this.options.ranges) { - backup.yylloc.range = this.yylloc.range.slice(0); - } - } - - lines = match[0].match(/(?:\r\n?|\n).*/g); - if (lines) { - this.yylineno += lines.length; - } - this.yylloc = { - first_line: this.yylloc.last_line, - last_line: this.yylineno + 1, - first_column: this.yylloc.last_column, - last_column: lines - ? lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length - : this.yylloc.last_column + match[0].length, - }; - this.yytext += match[0]; - this.match += match[0]; - this.matches = match; - this.yyleng = this.yytext.length; - if (this.options.ranges) { - this.yylloc.range = [this.offset, (this.offset += this.yyleng)]; - } - this._more = false; - this._backtrack = false; - this._input = this._input.slice(match[0].length); - this.matched += match[0]; - token = this.performAction.call( - this, - this.yy, - this, - indexed_rule, - this.conditionStack[this.conditionStack.length - 1] - ); - if (this.done && this._input) { - this.done = false; - } - if (token) { - return token; - } else if (this._backtrack) { - // recover context - for (var k in backup) { - this[k] = backup[k]; - } - return false; // rule action called reject() implying the next rule should be tested instead. - } - return false; - }, - - // return next match in input - next: function () { - if (this.done) { - return this.EOF; - } - if (!this._input) { - this.done = true; - } - - var token, match, tempMatch, index; - if (!this._more) { - this.yytext = ''; - this.match = ''; - } - var rules = this._currentRules(); - for (var i = 0; i < rules.length; i++) { - tempMatch = this._input.match(this.rules[rules[i]]); - if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { - match = tempMatch; - index = i; - if (this.options.backtrack_lexer) { - token = this.test_match(tempMatch, rules[i]); - if (token !== false) { - return token; - } else if (this._backtrack) { - match = false; - continue; // rule action called reject() implying a rule MISmatch. - } else { - // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) - return false; - } - } else if (!this.options.flex) { - break; - } - } - } - if (match) { - token = this.test_match(match, rules[index]); - if (token !== false) { - return token; - } - // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) - return false; - } - if (this._input === '') { - return this.EOF; - } else { - return this.parseError( - 'Lexical error on line ' + - (this.yylineno + 1) + - '. Unrecognized text.\n' + - this.showPosition(), - { - text: '', - token: null, - line: this.yylineno, - } - ); - } - }, - - // return next match that has a token - lex: function lex() { - var r = this.next(); - if (r) { - return r; - } else { - return this.lex(); - } - }, - - // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) - begin: function begin(condition) { - this.conditionStack.push(condition); - }, - - // pop the previously active lexer condition state off the condition stack - popState: function popState() { - var n = this.conditionStack.length - 1; - if (n > 0) { - return this.conditionStack.pop(); - } else { - return this.conditionStack[0]; - } - }, - - // produce the lexer rule set which is active for the currently active lexer condition state - _currentRules: function _currentRules() { - if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { - return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; - } else { - return this.conditions['INITIAL'].rules; - } - }, - - // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available - topState: function topState(n) { - n = this.conditionStack.length - 1 - Math.abs(n || 0); - if (n >= 0) { - return this.conditionStack[n]; - } else { - return 'INITIAL'; - } - }, - - // alias for begin(condition) - pushState: function pushState(condition) { - this.begin(condition); - }, - - // return the number of states currently on the stack - stateStackSize: function stateStackSize() { - return this.conditionStack.length; - }, - options: {}, - performAction: function anonymous(yy, yy_, $avoiding_name_collisions, YY_START) { - function strip(start, end) { - return (yy_.yytext = yy_.yytext.substring(start, yy_.yyleng - end + start)); - } - - var YYSTATE = YY_START; - switch ($avoiding_name_collisions) { - case 0: - if (yy_.yytext.slice(-2) === '\\\\') { - strip(0, 1); - this.begin('mu'); - } else if (yy_.yytext.slice(-1) === '\\') { - strip(0, 1); - this.begin('emu'); - } else { - this.begin('mu'); - } - if (yy_.yytext) return 15; - - break; - case 1: - return 15; - break; - case 2: - this.popState(); - return 15; - - break; - case 3: - this.begin('raw'); - return 15; - break; - case 4: - this.popState(); - // Should be using `this.topState()` below, but it currently - // returns the second top instead of the first top. Opened an - // issue about it at https://github.com/zaach/jison/issues/291 - if (this.conditionStack[this.conditionStack.length - 1] === 'raw') { - return 15; - } else { - strip(5, 9); - return 18; - } - - break; - case 5: - return 15; - break; - case 6: - this.popState(); - return 14; - - break; - case 7: - return 67; - break; - case 8: - return 68; - break; - case 9: - if (yy.syntax.square === 'string') { - this.unput(yy_.yytext); - // escaped literal - this.begin('escl'); - } else { - return 75; - } - - break; - case 10: - return 77; - break; - case 11: - return 19; - break; - case 12: - this.popState(); - this.begin('raw'); - return 23; - - break; - case 13: - return 57; - break; - case 14: - return 61; - break; - case 15: - return 29; - break; - case 16: - return 47; - break; - case 17: - this.popState(); - return 44; - break; - case 18: - this.popState(); - return 44; - break; - case 19: - return 34; - break; - case 20: - return 39; - break; - case 21: - return 53; - break; - case 22: - return 48; - break; - case 23: - this.unput(yy_.yytext); - this.popState(); - this.begin('com'); - - break; - case 24: - this.popState(); - return 14; - - break; - case 25: - return 48; - break; - case 26: - return 74; - break; - case 27: - return 73; - break; - case 28: - return 73; - break; - case 29: - return 93; - break; - case 30: - return 92; - break; - case 31: // ignore whitespace - break; - case 32: - this.popState(); - return 56; - break; - case 33: - this.popState(); - return 33; - break; - case 34: - yy_.yytext = strip(1, 2).replace(/\\"/g, '"'); - return 84; - break; - case 35: - yy_.yytext = strip(1, 2).replace(/\\'/g, "'"); - return 84; - break; - case 36: - return 89; - break; - case 37: - return 86; - break; - case 38: - return 86; - break; - case 39: - return 87; - break; - case 40: - return 88; - break; - case 41: - return 85; - break; - case 42: - return 79; - break; - case 43: - return 81; - break; - case 44: - return 73; - break; - case 45: - yy_.yytext = yy_.yytext.replace(/\\([\\\]])/g, '$1'); - this.popState(); - return 73; - - break; - case 46: - return 'INVALID'; - break; - case 47: - return 5; - break; - } - }, - rules: [ - /^(?:[^\x00]*?(?=(\{\{)))/, - /^(?:[^\x00]+)/, - /^(?:[^\x00]{2,}?(?=(\{\{|\\\{\{|\\\\\{\{|$)))/, - /^(?:\{\{\{\{(?=[^/]))/, - /^(?:\{\{\{\{\/[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.])\}\}\}\})/, - /^(?:[^\x00]+?(?=(\{\{\{\{)))/, - /^(?:[\s\S]*?--(~)?\}\})/, - /^(?:\()/, - /^(?:\))/, - /^(?:\[)/, - /^(?:\])/, - /^(?:\{\{\{\{)/, - /^(?:\}\}\}\})/, - /^(?:\{\{(~)?>)/, - /^(?:\{\{(~)?#>)/, - /^(?:\{\{(~)?#\*?)/, - /^(?:\{\{(~)?\/)/, - /^(?:\{\{(~)?\^\s*(~)?\}\})/, - /^(?:\{\{(~)?\s*else\s*(~)?\}\})/, - /^(?:\{\{(~)?\^)/, - /^(?:\{\{(~)?\s*else\b)/, - /^(?:\{\{(~)?\{)/, - /^(?:\{\{(~)?&)/, - /^(?:\{\{(~)?!--)/, - /^(?:\{\{(~)?![\s\S]*?\}\})/, - /^(?:\{\{(~)?\*?)/, - /^(?:=)/, - /^(?:\.\.)/, - /^(?:\.(?=([=~}\s\/.)\]|])))/, - /^(?:\.#)/, - /^(?:[\/.])/, - /^(?:\s+)/, - /^(?:\}(~)?\}\})/, - /^(?:(~)?\}\})/, - /^(?:"(\\["]|[^"])*")/, - /^(?:'(\\[']|[^'])*')/, - /^(?:@)/, - /^(?:true(?=([~}\s)\]])))/, - /^(?:false(?=([~}\s)\]])))/, - /^(?:undefined(?=([~}\s)\]])))/, - /^(?:null(?=([~}\s)\]])))/, - /^(?:-?[0-9]+(?:\.[0-9]+)?(?=([~}\s)\]])))/, - /^(?:as\s+\|)/, - /^(?:\|)/, - /^(?:([^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=([=~}\s\/.)\]|]))))/, - /^(?:\[(\\\]|[^\]])*\])/, - /^(?:.)/, - /^(?:$)/, - ], - conditions: { - mu: { - rules: [ - 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, - ], - inclusive: false, - }, - emu: { rules: [2], inclusive: false }, - com: { rules: [6], inclusive: false }, - raw: { rules: [3, 4, 5], inclusive: false }, - escl: { rules: [45], inclusive: false }, - INITIAL: { rules: [0, 1, 47], inclusive: true }, - }, - }; - return lexer; - })(); - parser.lexer = lexer; - function Parser() { - this.yy = {}; - } - Parser.prototype = parser; - parser.Parser = Parser; - return new Parser(); -})(); -export default parser; diff --git a/packages/@handlebars/parser/lib/printer.js b/packages/@handlebars/parser/lib/printer.js deleted file mode 100644 index bea44d3a6bf..00000000000 --- a/packages/@handlebars/parser/lib/printer.js +++ /dev/null @@ -1,204 +0,0 @@ -import Visitor from './visitor.js'; - -export function print(ast) { - return new PrintVisitor().accept(ast); -} - -export function PrintVisitor() { - this.padding = 0; -} - -PrintVisitor.prototype = new Visitor(); - -PrintVisitor.prototype.pad = function (string) { - let out = ''; - - for (let i = 0, l = this.padding; i < l; i++) { - out += ' '; - } - - out += string + '\n'; - return out; -}; - -PrintVisitor.prototype.Program = function (program) { - let out = '', - body = program.body, - i, - l; - - if (program.blockParams) { - let blockParams = 'BLOCK PARAMS: ['; - for (i = 0, l = program.blockParams.length; i < l; i++) { - blockParams += ' ' + program.blockParams[i]; - } - blockParams += ' ]'; - out += this.pad(blockParams); - } - - for (i = 0, l = body.length; i < l; i++) { - out += this.accept(body[i]); - } - - this.padding--; - - return out; -}; - -PrintVisitor.prototype.MustacheStatement = function (mustache) { - if (mustache.params.length > 0 || mustache.hash) { - return this.pad('{{ ' + this.callBody(mustache) + ' }}'); - } else { - return this.pad('{{ ' + this.accept(mustache.path) + ' }}'); - } -}; -PrintVisitor.prototype.Decorator = function (mustache) { - return this.pad('{{ DIRECTIVE ' + this.callBody(mustache) + ' }}'); -}; - -PrintVisitor.prototype.BlockStatement = PrintVisitor.prototype.DecoratorBlock = function (block) { - let out = ''; - - out += this.pad((block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:'); - this.padding++; - out += this.pad(this.callBody(block)); - if (block.program) { - out += this.pad('PROGRAM:'); - this.padding++; - out += this.accept(block.program); - this.padding--; - } - if (block.inverse) { - if (block.program) { - this.padding++; - } - out += this.pad('{{^}}'); - this.padding++; - out += this.accept(block.inverse); - this.padding--; - if (block.program) { - this.padding--; - } - } - this.padding--; - - return out; -}; - -PrintVisitor.prototype.PartialStatement = function (partial) { - let content = 'PARTIAL:' + partial.name.original; - if (partial.params[0]) { - content += ' ' + this.accept(partial.params[0]); - } - if (partial.hash) { - content += ' ' + this.accept(partial.hash); - } - return this.pad('{{> ' + content + ' }}'); -}; -PrintVisitor.prototype.PartialBlockStatement = function (partial) { - let content = 'PARTIAL BLOCK:' + partial.name.original; - if (partial.params[0]) { - content += ' ' + this.accept(partial.params[0]); - } - if (partial.hash) { - content += ' ' + this.accept(partial.hash); - } - - content += ' ' + this.pad('PROGRAM:'); - this.padding++; - content += this.accept(partial.program); - this.padding--; - - return this.pad('{{> ' + content + ' }}'); -}; - -PrintVisitor.prototype.ContentStatement = function (content) { - return this.pad("CONTENT[ '" + content.value + "' ]"); -}; - -PrintVisitor.prototype.CommentStatement = function (comment) { - return this.pad("{{! '" + comment.value + "' }}"); -}; - -PrintVisitor.prototype.SubExpression = function (sexpr) { - return `(${this.callBody(sexpr)})`; -}; - -PrintVisitor.prototype.callBody = function (callExpr) { - let params = callExpr.params, - paramStrings = [], - hash; - - for (let i = 0, l = params.length; i < l; i++) { - paramStrings.push(this.accept(params[i])); - } - - params = paramStrings.length === 0 ? '' : ' [' + paramStrings.join(', ') + ']'; - - hash = callExpr.hash ? ' ' + this.accept(callExpr.hash) : ''; - - return this.accept(callExpr.path) + params + hash; -}; - -PrintVisitor.prototype.PathExpression = function (id) { - let head = typeof id.head === 'string' ? id.head : `[${this.accept(id.head)}]`; - let path = [head, ...id.tail].join('/'); - return 'p%' + prefix(id) + path; -}; - -function prefix(path) { - if (path.data) { - return '@'; - } else if (path.this) { - return 'this.'; - } else { - return ''; - } -} - -PrintVisitor.prototype.StringLiteral = function (string) { - return '"' + string.value + '"'; -}; - -PrintVisitor.prototype.NumberLiteral = function (number) { - return 'n%' + number.value; -}; - -PrintVisitor.prototype.BooleanLiteral = function (bool) { - return 'b%' + bool.value; -}; - -PrintVisitor.prototype.UndefinedLiteral = function () { - return 'UNDEFINED'; -}; - -PrintVisitor.prototype.NullLiteral = function () { - return 'NULL'; -}; - -PrintVisitor.prototype.ArrayLiteral = function (array) { - return `Array[${array.items.map((item) => this.accept(item)).join(', ')}]`; -}; - -PrintVisitor.prototype.HashLiteral = function (hash) { - return `Hash{${this.hashPairs(hash)}}`; -}; - -PrintVisitor.prototype.Hash = function (hash) { - return `HASH{${this.hashPairs(hash)}}`; -}; - -PrintVisitor.prototype.hashPairs = function (hash) { - let pairs = hash.pairs, - joinedPairs = []; - - for (let i = 0, l = pairs.length; i < l; i++) { - joinedPairs.push(this.HashPair(pairs[i])); - } - - return joinedPairs.join(' '); -}; - -PrintVisitor.prototype.HashPair = function (pair) { - return pair.key + '=' + this.accept(pair.value); -}; diff --git a/packages/@handlebars/parser/package.json b/packages/@handlebars/parser/package.json deleted file mode 100644 index 4072441c423..00000000000 --- a/packages/@handlebars/parser/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@handlebars/parser", - "version": "2.2.2", - "description": "The parser for the Handlebars language", - "homepage": "https://github.com/handlebars-lang/handlebars-parser#readme", - "bugs": { - "url": "https://github.com/handlebars-lang/handlebars-parser/issues" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/handlebars-lang/handlebars-parser.git" - }, - "license": "ISC", - "author": "", - "private": true, - "type": "module", - "exports": { - ".": { - "types": "./types/index.d.ts", - "default": "./lib/index.js" - } - }, - "main": "lib/index.js", - "module": "lib/index.js", - "types": "types/index.d.ts", - "scripts": { - "build:jison": "jison -m js src/handlebars.yy src/handlebars.l -o lib/parser.js", - "build:parser": "npm-run-all build:jison build:parser-suffix build:parser-ts-nocheck", - "build:parser-suffix": "combine-files lib/parser.js,src/parser-suffix.js lib/parser.js", - "build:parser-ts-nocheck": "node -e \"const fs=require('fs');const f='lib/parser.js';fs.writeFileSync(f,'// @ts-nocheck\\n'+fs.readFileSync(f,'utf8'))\"", - "test": "mocha --inline-diffs spec" - }, - "devDependencies": { - "combine-files": "^1.1.8", - "jison": "^0.4.18", - "mocha": "^11.0.0", - "npm-run-all2": "^8.0.0" - } -} diff --git a/packages/@handlebars/parser/spec/ast.js b/packages/@handlebars/parser/spec/ast.js deleted file mode 100644 index aeb32a47cc3..00000000000 --- a/packages/@handlebars/parser/spec/ast.js +++ /dev/null @@ -1,291 +0,0 @@ -import { parse, parseWithoutProcessing } from '../lib/index.js'; -import { equals } from './utils.js'; - -describe('ast', function () { - describe('whitespace control', function () { - describe('parse', function () { - it('mustache', function () { - let ast = parse(' {{~comment~}} '); - - equals(ast.body[0].value, ''); - equals(ast.body[2].value, ''); - }); - - it('block statements', function () { - let ast = parse(' {{# comment~}} \nfoo\n {{~/comment}}'); - - equals(ast.body[0].value, ''); - equals(ast.body[1].program.body[0].value, 'foo'); - }); - }); - - describe('parseWithoutProcessing', function () { - it('mustache', function () { - let ast = parseWithoutProcessing(' {{~comment~}} '); - - equals(ast.body[0].value, ' '); - equals(ast.body[2].value, ' '); - }); - - it('block statements', function () { - let ast = parseWithoutProcessing(' {{# comment~}} \nfoo\n {{~/comment}}'); - - equals(ast.body[0].value, ' '); - equals(ast.body[1].program.body[0].value, ' \nfoo\n '); - }); - }); - }); - - describe('node details', function () { - describe('paths', function () { - it('{{this}}', function () { - let path = parse('{{this}}').body[0].path; - equals(path.original, 'this'); - equals(path.head, undefined); - equals(path.tail.length, 0); - equals(path.parts.length, 0); - }); - - it('{{this.bar}}', function () { - let path = parse('{{this.bar}}').body[0].path; - equals(path.original, 'this.bar'); - equals(path.head, 'bar'); - equals(path.tail.length, 0); - equals(path.parts.length, 1); - equals(path.parts[0], 'bar'); - }); - - it('{{this.#bar}}', function () { - let path = parse('{{this.#bar}}').body[0].path; - equals(path.original, 'this.#bar'); - equals(path.head, '#bar'); - equals(path.tail.length, 0); - equals(path.parts.length, 1); - equals(path.parts[0], '#bar'); - }); - - it('{{foo.bar}}', function () { - let path = parse('{{foo.bar}}').body[0].path; - equals(path.original, 'foo.bar'); - equals(path.head, 'foo'); - equals(path.tail.length, 1); - equals(path.tail[0], 'bar'); - equals(path.parts.length, 2); - equals(path.parts[0], 'foo'); - equals(path.parts[1], 'bar'); - }); - - it('{{foo.#bar}}', function () { - let path = parse('{{foo.#bar}}').body[0].path; - equals(path.original, 'foo.#bar'); - equals(path.head, 'foo'); - equals(path.tail.length, 1); - equals(path.tail[0], '#bar'); - equals(path.parts.length, 2); - equals(path.parts[0], 'foo'); - equals(path.parts[1], '#bar'); - }); - }); - }); - - describe('standalone flags', function () { - describe('mustache', function () { - it('does not mark mustaches as standalone', function () { - let ast = parse(' {{comment}} '); - equals(!!ast.body[0].value, true); - equals(!!ast.body[2].value, true); - }); - }); - describe('blocks - parseWithoutProcessing', function () { - it('block mustaches', function () { - let ast = parseWithoutProcessing( - ' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} ' - ), - block = ast.body[1]; - - equals(ast.body[0].value, ' '); - - equals(block.program.body[0].value, ' \nfoo\n '); - equals(block.inverse.body[0].value, ' \n bar \n '); - - equals(ast.body[2].value, ' '); - }); - it('initial block mustaches', function () { - let ast = parseWithoutProcessing('{{# comment}} \nfoo\n {{/comment}}'), - block = ast.body[0]; - - equals(block.program.body[0].value, ' \nfoo\n '); - }); - it('mustaches with children', function () { - let ast = parseWithoutProcessing('{{# comment}} \n{{foo}}\n {{/comment}}'), - block = ast.body[0]; - - equals(block.program.body[0].value, ' \n'); - equals(block.program.body[1].path.original, 'foo'); - equals(block.program.body[2].value, '\n '); - }); - it('nested block mustaches', function () { - let ast = parseWithoutProcessing( - '{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}' - ), - body = ast.body[0].program.body, - block = body[1]; - - equals(body[0].value, ' \n'); - - equals(block.program.body[0].value, ' \nfoo\n '); - equals(block.inverse.body[0].value, ' \n bar \n '); - }); - it('column 0 block mustaches', function () { - let ast = parseWithoutProcessing( - 'test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} ' - ), - block = ast.body[1]; - - equals(ast.body[0].omit, undefined); - - equals(block.program.body[0].value, ' \nfoo\n '); - equals(block.inverse.body[0].value, ' \n bar \n '); - - equals(ast.body[2].value, ' '); - }); - }); - describe('blocks', function () { - it('marks block mustaches as standalone', function () { - let ast = parse(' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '), - block = ast.body[1]; - - equals(ast.body[0].value, ''); - - equals(block.program.body[0].value, 'foo\n'); - equals(block.inverse.body[0].value, ' bar \n'); - - equals(ast.body[2].value, ''); - }); - it('marks initial block mustaches as standalone', function () { - let ast = parse('{{# comment}} \nfoo\n {{/comment}}'), - block = ast.body[0]; - - equals(block.program.body[0].value, 'foo\n'); - }); - it('marks mustaches with children as standalone', function () { - let ast = parse('{{# comment}} \n{{foo}}\n {{/comment}}'), - block = ast.body[0]; - - equals(block.program.body[0].value, ''); - equals(block.program.body[1].path.original, 'foo'); - equals(block.program.body[2].value, '\n'); - }); - it('marks nested block mustaches as standalone', function () { - let ast = parse( - '{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}' - ), - body = ast.body[0].program.body, - block = body[1]; - - equals(body[0].value, ''); - - equals(block.program.body[0].value, 'foo\n'); - equals(block.inverse.body[0].value, ' bar \n'); - - equals(body[0].value, ''); - }); - it('does not mark nested block mustaches as standalone', function () { - let ast = parse( - '{{#foo}} {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} {{/foo}}' - ), - body = ast.body[0].program.body, - block = body[1]; - - equals(body[0].omit, undefined); - - equals(block.program.body[0].value, ' \nfoo\n'); - equals(block.inverse.body[0].value, ' bar \n '); - - equals(body[0].omit, undefined); - }); - it('does not mark nested initial block mustaches as standalone', function () { - let ast = parse('{{#foo}}{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}}{{/foo}}'), - body = ast.body[0].program.body, - block = body[0]; - - equals(block.program.body[0].value, ' \nfoo\n'); - equals(block.inverse.body[0].value, ' bar \n '); - - equals(body[0].omit, undefined); - }); - - it('marks column 0 block mustaches as standalone', function () { - let ast = parse('test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} '), - block = ast.body[1]; - - equals(ast.body[0].omit, undefined); - - equals(block.program.body[0].value, 'foo\n'); - equals(block.inverse.body[0].value, ' bar \n'); - - equals(ast.body[2].value, ''); - }); - }); - describe('partials - parseWithoutProcessing', function () { - it('simple partial', function () { - let ast = parseWithoutProcessing('{{> partial }} '); - equals(ast.body[1].value, ' '); - }); - it('indented partial', function () { - let ast = parseWithoutProcessing(' {{> partial }} '); - equals(ast.body[0].value, ' '); - equals(ast.body[1].indent, ''); - equals(ast.body[2].value, ' '); - }); - }); - describe('partials', function () { - it('marks partial as standalone', function () { - let ast = parse('{{> partial }} '); - equals(ast.body[1].value, ''); - }); - it('marks indented partial as standalone', function () { - let ast = parse(' {{> partial }} '); - equals(ast.body[0].value, ''); - equals(ast.body[1].indent, ' '); - equals(ast.body[2].value, ''); - }); - it('marks those around content as not standalone', function () { - let ast = parse('a{{> partial }}'); - equals(ast.body[0].omit, undefined); - - ast = parse('{{> partial }}a'); - equals(ast.body[1].omit, undefined); - }); - }); - describe('comments - parseWithoutProcessing', function () { - it('simple comment', function () { - let ast = parseWithoutProcessing('{{! comment }} '); - equals(ast.body[1].value, ' '); - }); - it('indented comment', function () { - let ast = parseWithoutProcessing(' {{! comment }} '); - equals(ast.body[0].value, ' '); - equals(ast.body[2].value, ' '); - }); - }); - describe('comments', function () { - it('marks comment as standalone', function () { - let ast = parse('{{! comment }} '); - equals(ast.body[1].value, ''); - }); - it('marks indented comment as standalone', function () { - let ast = parse(' {{! comment }} '); - equals(ast.body[0].value, ''); - equals(ast.body[2].value, ''); - }); - it('marks those around content as not standalone', function () { - let ast = parse('a{{! comment }}'); - equals(ast.body[0].omit, undefined); - - ast = parse('{{! comment }}a'); - equals(ast.body[1].omit, undefined); - }); - }); - }); -}); diff --git a/packages/@handlebars/parser/spec/parser.js b/packages/@handlebars/parser/spec/parser.js deleted file mode 100644 index 970b2350a4e..00000000000 --- a/packages/@handlebars/parser/spec/parser.js +++ /dev/null @@ -1,500 +0,0 @@ -import { parse, print } from '../lib/index.js'; -import { equals, equalsAst, shouldThrow } from './utils.js'; - -describe('parser', function () { - function astFor(template) { - let ast = parse(template); - return print(ast); - } - - it('parses simple mustaches', function () { - equalsAst('{{123}}', '{{ n%123 }}'); - equalsAst('{{"foo"}}', '{{ "foo" }}'); - equalsAst('{{false}}', '{{ b%false }}'); - equalsAst('{{true}}', '{{ b%true }}'); - equalsAst('{{foo}}', '{{ p%foo }}'); - equalsAst('{{foo?}}', '{{ p%foo? }}'); - equalsAst('{{foo_}}', '{{ p%foo_ }}'); - equalsAst('{{foo-}}', '{{ p%foo- }}'); - equalsAst('{{foo:}}', '{{ p%foo: }}'); - }); - - it('parses simple mustaches with data', function () { - equalsAst('{{@foo}}', '{{ p%@foo }}'); - }); - - it('parses simple mustaches with data paths', function () { - equalsAst('{{@../foo}}', '{{ p%@foo }}'); - }); - - it('parses mustaches with paths', function () { - equalsAst('{{foo/bar}}', '{{ p%foo/bar }}'); - equalsAst('{{foo.bar}}', '{{ p%foo/bar }}'); - equalsAst('{{foo.#bar}}', '{{ p%foo/#bar }}'); - equalsAst('{{@foo.#bar}}', '{{ p%@foo/#bar }}'); - - equalsAst('{{this/foo}}', '{{ p%foo }}'); - equalsAst('{{this.foo}}', '{{ p%this.foo }}'); - equalsAst('{{this.#foo}}', '{{ p%this.#foo }}'); - }); - - it('parses mustaches with - in a path', function () { - equalsAst('{{foo-bar}}', '{{ p%foo-bar }}'); - }); - - it('parses mustaches with escaped [] in a path', function () { - equalsAst('{{[foo[\\]]}}', '{{ p%foo[] }}'); - }); - - it('parses escaped \\\\ in path', function () { - equalsAst('{{[foo\\\\]}}', '{{ p%foo\\ }}'); - }); - - it('parses hash literals', function () { - equalsAst('{{(foo=bar)}}', '{{ Hash{foo=p%bar} }}'); - equalsAst('{{(foo=bar)}}', '{{ p%@hello }}', { - options: { - syntax: { - hash: (hash, loc, { yy }) => { - return yy.preparePath(true, false, [{ part: yy.id('hello'), original: 'hello' }], loc); - }, - }, - }, - }); - }); - - it('parses array literals', function () { - equalsAst('{{[foo bar]}}', '{{ Array[p%foo, p%bar] }}', { - options: { syntax: { square: 'node' } }, - }); - - equalsAst('{{[foo bar].baz}}', '{{ p%[Array[p%foo, p%bar]]/baz }}', { - options: { syntax: { square: 'node' } }, - }); - }); - - it('parses mustaches that are hash literals', function () { - equalsAst('{{foo=bar}}', '{{ Hash{foo=p%bar} }}'); - equalsAst('{{foo=bar}}', `{{ "HASH{foo=p%bar}" }}`, { - options: { - syntax: { - hash: (hash, loc) => { - return { - type: 'StringLiteral', - original: print(hash), - value: print(hash), - loc, - }; - }, - }, - }, - }); - }); - - it('parses mustaches with parameters', function () { - equalsAst('{{foo bar}}', '{{ p%foo [p%bar] }}'); - equalsAst('{{this.foo bar}}', '{{ p%this.foo [p%bar] }}'); - equalsAst('{{this.foo this.bar}}', '{{ p%this.foo [p%this.bar] }}'); - equalsAst('{{this.#foo this.#bar}}', '{{ p%this.#foo [p%this.#bar] }}'); - equalsAst('{{foo.#bar foo.#baz}}', '{{ p%foo/#bar [p%foo/#baz] }}'); - equalsAst('{{@foo.#bar @foo.#baz}}', '{{ p%@foo/#bar [p%@foo/#baz] }}'); - }); - - it('parses mustaches with string parameters', function () { - equalsAst('{{foo bar "baz" }}', '{{ p%foo [p%bar, "baz"] }}'); - equalsAst('{{this.foo bar "baz" }}', '{{ p%this.foo [p%bar, "baz"] }}'); - equalsAst('{{this.#foo bar "baz" }}', '{{ p%this.#foo [p%bar, "baz"] }}'); - equalsAst('{{@item.#foo bar "baz" }}', '{{ p%@item/#foo [p%bar, "baz"] }}'); - }); - - it('parses mustaches with NUMBER parameters', function () { - equalsAst('{{foo 1}}', '{{ p%foo [n%1] }}'); - equalsAst('{{this.foo 1}}', '{{ p%this.foo [n%1] }}'); - equalsAst('{{this.#foo 1}}', '{{ p%this.#foo [n%1] }}'); - }); - - it('parses mustaches with BOOLEAN parameters', function () { - equalsAst('{{foo true}}', '{{ p%foo [b%true] }}'); - equalsAst('{{foo false}}', '{{ p%foo [b%false] }}'); - }); - - it('parses mustaches with undefined and null paths', function () { - equalsAst('{{undefined}}', '{{ UNDEFINED }}'); - equalsAst('{{null}}', '{{ NULL }}'); - }); - - it('parses mustaches with undefined and null parameters', function () { - equalsAst('{{foo undefined null}}', '{{ p%foo [UNDEFINED, NULL] }}'); - }); - - it('parses mustaches with DATA parameters', function () { - equalsAst('{{foo @bar}}', '{{ p%foo [p%@bar] }}'); - }); - - it('parses mustaches with hash arguments', function () { - equalsAst('{{foo bar=baz}}', '{{ p%foo HASH{bar=p%baz} }}'); - equalsAst('{{foo bar=1}}', '{{ p%foo HASH{bar=n%1} }}'); - equalsAst('{{foo bar=true}}', '{{ p%foo HASH{bar=b%true} }}'); - equalsAst('{{foo bar=false}}', '{{ p%foo HASH{bar=b%false} }}'); - equalsAst('{{foo bar=@baz}}', '{{ p%foo HASH{bar=p%@baz} }}'); - - equalsAst('{{foo bar=baz bat=bam}}', '{{ p%foo HASH{bar=p%baz bat=p%bam} }}'); - equalsAst('{{foo bar=baz bat="bam"}}', '{{ p%foo HASH{bar=p%baz bat="bam"} }}'); - - equalsAst("{{foo bat='bam'}}", '{{ p%foo HASH{bat="bam"} }}'); - - equalsAst('{{foo omg bar=baz bat="bam"}}', '{{ p%foo [p%omg] HASH{bar=p%baz bat="bam"} }}'); - equalsAst( - '{{foo omg bar=baz bat="bam" baz=1}}', - '{{ p%foo [p%omg] HASH{bar=p%baz bat="bam" baz=n%1} }}' - ); - equalsAst( - '{{foo omg bar=baz bat="bam" baz=true}}', - '{{ p%foo [p%omg] HASH{bar=p%baz bat="bam" baz=b%true} }}' - ); - equalsAst( - '{{foo omg bar=baz bat="bam" baz=false}}', - '{{ p%foo [p%omg] HASH{bar=p%baz bat="bam" baz=b%false} }}' - ); - }); - - it('parses contents followed by a mustache', function () { - equalsAst('foo bar {{baz}}', "CONTENT[ 'foo bar ' ]\n{{ p%baz }}"); - }); - - it('parses a partial', function () { - equalsAst('{{> foo }}', '{{> PARTIAL:foo }}'); - equalsAst('{{> "foo" }}', '{{> PARTIAL:foo }}'); - equalsAst('{{> 1 }}', '{{> PARTIAL:1 }}'); - }); - - it('parses a partial with context', function () { - equalsAst('{{> foo bar}}', '{{> PARTIAL:foo p%bar }}'); - }); - - it('parses a partial with hash', function () { - equalsAst('{{> foo bar=bat}}', '{{> PARTIAL:foo HASH{bar=p%bat} }}'); - }); - - it('parses a partial with context and hash', function () { - equalsAst('{{> foo bar bat=baz}}', '{{> PARTIAL:foo p%bar HASH{bat=p%baz} }}'); - }); - - it('parses a partial with a complex name', function () { - equalsAst('{{> shared/partial?.bar}}', '{{> PARTIAL:shared/partial?.bar }}'); - }); - - it('parsers partial blocks', function () { - equalsAst('{{#> foo}}bar{{/foo}}', "{{> PARTIAL BLOCK:foo PROGRAM:\n CONTENT[ 'bar' ]\n }}"); - }); - it('should handle parser block mismatch', function () { - shouldThrow( - function () { - astFor('{{#> goodbyes}}{{/hellos}}'); - }, - Error, - /goodbyes doesn't match hellos/ - ); - }); - it('parsers partial blocks with arguments', function () { - equalsAst( - '{{#> foo context hash=value}}bar{{/foo}}', - "{{> PARTIAL BLOCK:foo p%context HASH{hash=p%value} PROGRAM:\n CONTENT[ 'bar' ]\n }}" - ); - }); - - it('parses a comment', function () { - equalsAst('{{! this is a comment }}', "{{! ' this is a comment ' }}"); - }); - - it('parses a multi-line comment', function () { - equalsAst('{{!\nthis is a multi-line comment\n}}', "{{! '\nthis is a multi-line comment\n' }}"); - }); - - it('parses an inverse section', function () { - equalsAst( - '{{#foo}} bar {{^}} baz {{/foo}}', - "BLOCK:\n p%foo\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]" - ); - }); - - it('parses an inverse (else-style) section', function () { - equalsAst( - '{{#foo}} bar {{else}} baz {{/foo}}', - "BLOCK:\n p%foo\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]" - ); - }); - - it('parses multiple inverse sections', function () { - equalsAst( - '{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}', - "BLOCK:\n p%foo\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n p%if [p%bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]" - ); - }); - - it('parses empty blocks', function () { - equalsAst('{{#foo}}{{/foo}}', 'BLOCK:\n p%foo\n PROGRAM:'); - }); - - it('parses empty blocks with empty inverse section', function () { - equalsAst('{{#foo}}{{^}}{{/foo}}', 'BLOCK:\n p%foo\n PROGRAM:\n {{^}}'); - }); - - it('parses empty blocks with empty inverse (else-style) section', function () { - equalsAst('{{#foo}}{{else}}{{/foo}}', 'BLOCK:\n p%foo\n PROGRAM:\n {{^}}'); - }); - - it('parses non-empty blocks with empty inverse section', function () { - equalsAst( - '{{#foo}} bar {{^}}{{/foo}}', - "BLOCK:\n p%foo\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}" - ); - }); - - it('parses non-empty blocks with empty inverse (else-style) section', function () { - equalsAst( - '{{#foo}} bar {{else}}{{/foo}}', - "BLOCK:\n p%foo\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}" - ); - }); - - it('parses empty blocks with non-empty inverse section', function () { - equalsAst( - '{{#foo}}{{^}} bar {{/foo}}', - "BLOCK:\n p%foo\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]" - ); - }); - - it('parses empty blocks with non-empty inverse (else-style) section', function () { - equalsAst( - '{{#foo}}{{else}} bar {{/foo}}', - "BLOCK:\n p%foo\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]" - ); - }); - - it('parses a standalone inverse section', function () { - equalsAst('{{^foo}}bar{{/foo}}', "BLOCK:\n p%foo\n {{^}}\n CONTENT[ 'bar' ]"); - }); - - it('throws on old inverse section', function () { - shouldThrow(function () { - astFor('{{else foo}}bar{{/foo}}'); - }, Error); - }); - - it('parses block with block params', function () { - equalsAst( - '{{#foo as |bar baz|}}content{{/foo}}', - "BLOCK:\n p%foo\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]" - ); - }); - - it('parses mustaches with sub-expressions as the callable', function () { - equalsAst('{{(my-helper foo)}}', '{{ (p%my-helper [p%foo]) }}'); - }); - - it('parses mustaches with sub-expressions as the callable (with args)', function () { - equalsAst('{{(my-helper foo) bar}}', '{{ (p%my-helper [p%foo]) [p%bar] }}'); - }); - - it('parses sub-expressions with a sub-expression as the callable', function () { - equalsAst('{{((my-helper foo))}}', '{{ ((p%my-helper [p%foo])) }}'); - }); - - it('parses sub-expressions with a sub-expression as the callable (with args)', function () { - equalsAst('{{((my-helper foo) bar)}}', '{{ ((p%my-helper [p%foo]) [p%bar]) }}'); - }); - - it('parses arguments with a sub-expression as the callable (with args)', function () { - equalsAst( - '{{my-helper ((foo) bar) baz=((foo bar))}}', - '{{ p%my-helper [((p%foo) [p%bar])] HASH{baz=((p%foo [p%bar]))} }}' - ); - }); - - it('parses paths with sub-expressions as the root', function () { - equalsAst('{{(my-helper foo).bar}}', '{{ p%[(p%my-helper [p%foo])]/bar }}'); - }); - - it('parses paths with sub-expressions as the root as a callable', function () { - equalsAst('{{((my-helper foo).bar baz)}}', '{{ (p%[(p%my-helper [p%foo])]/bar [p%baz]) }}'); - }); - - it('parses paths with sub-expressions as the root as an argument', function () { - equalsAst('{{(foo (my-helper bar).baz)}}', '{{ (p%foo [p%[(p%my-helper [p%bar])]/baz]) }}'); - }); - - it('parses paths with sub-expressions as the root as a named argument', function () { - equalsAst( - '{{(foo bar=(my-helper baz).qux)}}', - '{{ (p%foo HASH{bar=p%[(p%my-helper [p%baz])]/qux}) }}' - ); - }); - - it('parses inverse block with block params', function () { - equalsAst( - '{{^foo as |bar baz|}}content{{/foo}}', - "BLOCK:\n p%foo\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]" - ); - }); - it('parses chained inverse block with block params', function () { - equalsAst( - '{{#foo}}{{else foo as |bar baz|}}content{{/foo}}', - "BLOCK:\n p%foo\n PROGRAM:\n {{^}}\n BLOCK:\n p%foo\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]" - ); - }); - it("raises if there's a Parse error", function () { - shouldThrow( - function () { - astFor('foo{{^}}bar'); - }, - Error, - /Parse error on line 1/ - ); - shouldThrow( - function () { - astFor('{{foo}'); - }, - Error, - /Parse error on line 1/ - ); - shouldThrow( - function () { - astFor('{{foo &}}'); - }, - Error, - /Parse error on line 1/ - ); - shouldThrow( - function () { - astFor('{{#goodbyes}}{{/hellos}}'); - }, - Error, - /goodbyes doesn't match hellos/ - ); - - shouldThrow( - function () { - astFor('{{{{goodbyes}}}} {{{{/hellos}}}}'); - }, - Error, - /goodbyes doesn't match hellos/ - ); - }); - - it('should handle invalid paths', function () { - shouldThrow( - function () { - astFor('{{foo/../bar}}'); - }, - Error, - /Invalid path: foo\/\.\. - 1:2/ - ); - shouldThrow( - function () { - astFor('{{foo/./bar}}'); - }, - Error, - /Invalid path: foo\/\. - 1:2/ - ); - shouldThrow( - function () { - astFor('{{foo/this/bar}}'); - }, - Error, - /Invalid path: foo\/this - 1:2/ - ); - }); - - it('knows how to report the correct line number in errors', function () { - shouldThrow( - function () { - astFor('hello\nmy\n{{foo}'); - }, - Error, - /Parse error on line 3/ - ); - shouldThrow( - function () { - astFor('hello\n\nmy\n\n{{foo}'); - }, - Error, - /Parse error on line 5/ - ); - }); - - it('knows how to report the correct line number in errors when the first character is a newline', function () { - shouldThrow( - function () { - astFor('\n\nhello\n\nmy\n\n{{foo}'); - }, - Error, - /Parse error on line 7/ - ); - }); - - describe('externally compiled AST', function () { - it('can pass through an already-compiled AST', function () { - equals( - astFor({ - type: 'Program', - body: [{ type: 'ContentStatement', value: 'Hello' }], - }), - "CONTENT[ 'Hello' ]\n" - ); - }); - }); - - describe('directives', function () { - it('should parse block directives', function () { - equalsAst('{{#* foo}}{{/foo}}', 'DIRECTIVE BLOCK:\n p%foo\n PROGRAM:'); - }); - it('should parse directives', function () { - equalsAst('{{* foo}}', '{{ DIRECTIVE p%foo }}'); - }); - it('should fail if directives have inverse', function () { - shouldThrow( - function () { - astFor('{{#* foo}}{{^}}{{/foo}}'); - }, - Error, - /Unexpected inverse/ - ); - }); - }); - - it('GH1024 - should track program location properly', function () { - let p = parse( - '\n' + - ' {{#if foo}}\n' + - ' {{bar}}\n' + - ' {{else}} {{baz}}\n' + - '\n' + - ' {{/if}}\n' + - ' ' - ); - - // We really need a deep equals but for now this should be stable... - equals( - JSON.stringify(p.loc), - JSON.stringify({ - start: { line: 1, column: 0 }, - end: { line: 7, column: 4 }, - }) - ); - equals( - JSON.stringify(p.body[1].program.loc), - JSON.stringify({ - start: { line: 2, column: 13 }, - end: { line: 4, column: 7 }, - }) - ); - equals( - JSON.stringify(p.body[1].inverse.loc), - JSON.stringify({ - start: { line: 4, column: 15 }, - end: { line: 6, column: 5 }, - }) - ); - }); -}); diff --git a/packages/@handlebars/parser/spec/utils.js b/packages/@handlebars/parser/spec/utils.js deleted file mode 100644 index d3739bca271..00000000000 --- a/packages/@handlebars/parser/spec/utils.js +++ /dev/null @@ -1,107 +0,0 @@ -import { parse, print } from '../lib/index.js'; - -let AssertError; -if (Error.captureStackTrace) { - AssertError = function AssertError(message, caller) { - Error.prototype.constructor.call(this, message); - this.message = message; - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, caller || AssertError); - } - }; - - AssertError.prototype = new Error(); -} else { - AssertError = Error; -} - -/** - * @todo Use chai's expect-style API instead (`expect(actualValue).to.equal(expectedValue)`) - * @see https://www.chaijs.com/api/bdd/ - */ -export function equals(actual, expected, msg) { - if (actual !== expected) { - const error = new AssertError( - `\n Actual: ${actual} Expected: ${expected}` + (msg ? `\n${msg}` : ''), - equals - ); - error.expected = expected; - error.actual = actual; - throw error; - } -} - -export function equalsAst(source, expected, options) { - const msg = typeof options === 'string' ? options : options?.msg; - const parserOptions = typeof options === 'string' ? undefined : options?.options; - const ast = astFor(source, parserOptions); - const padding = ` `.repeat(8); - - if (ast !== `${expected}\n`) { - let sourceMsg = `${padding}Source: ${source}`; - if (parserOptions) { - let formattedOptions = printOptions(parserOptions).split('\n').join(`\n${padding}`); - - sourceMsg += `\n${padding}Options: ${formattedOptions}`; - } - const error = new AssertError(`\n${sourceMsg}${msg ? `\n${msg}` : ''}\n`, equalsAst); - - error.expected = expected; - error.actual = ast; - throw error; - } -} - -function printOptions(options) { - if (!options) { - return ''; - } - - let outOptions = {}; - - if (options.srcName) { - outOptions.srcName = options.srcName; - } - if (options.syntax) { - outOptions.syntax = {}; - - if (options.syntax.hash) { - outOptions.syntax.hash = `{function ${options.syntax.hash.name ?? 'anonymous'}}`; - } - if (options.syntax.square) { - outOptions.syntax.square = `{function ${options.syntax.square.name ?? 'anonymous'}}`; - } - } - - return JSON.stringify(outOptions, null, 2); -} - -/** - * @todo Use chai's expect-style API instead (`expect(actualValue).to.equal(expectedValue)`) - * @see https://www.chaijs.com/api/bdd/#method_throw - */ -export function shouldThrow(callback, type, msg) { - let failed; - try { - callback(); - failed = true; - } catch (caught) { - if (type && !(caught instanceof type)) { - throw new AssertError('Type failure: ' + caught); - } - if (msg && !(msg.test ? msg.test(caught.message) : msg === caught.message)) { - throw new AssertError( - 'Throw mismatch: Expected ' + caught.message + ' to match ' + msg + '\n\n' + caught.stack, - shouldThrow - ); - } - } - if (failed) { - throw new AssertError('It failed to throw', shouldThrow); - } -} -function astFor(template, options = {}) { - let ast = parse(template, options); - return print(ast); -} diff --git a/packages/@handlebars/parser/spec/visitor.js b/packages/@handlebars/parser/spec/visitor.js deleted file mode 100644 index d72ad3e0edc..00000000000 --- a/packages/@handlebars/parser/spec/visitor.js +++ /dev/null @@ -1,155 +0,0 @@ -import { Visitor, parse, print, Exception } from '../lib/index.js'; -import { equals, shouldThrow } from './utils.js'; - -describe('Visitor', function () { - it('should provide coverage', function () { - // Simply run the thing and make sure it does not fail and that all of the - // stub methods are executed - let visitor = new Visitor(); - visitor.accept( - parse( - '{{foo}}{{#foo (bar 1 "1" true undefined null) foo=@data}}{{!comment}}{{> bar }} {{/foo}}' - ) - ); - visitor.accept(parse('{{#> bar }} {{/bar}}')); - visitor.accept(parse('{{#* bar }} {{/bar}}')); - visitor.accept(parse('{{* bar }}')); - }); - - it('should traverse to stubs', function () { - let visitor = new Visitor(); - - visitor.StringLiteral = function (string) { - equals(string.value, '2'); - }; - visitor.NumberLiteral = function (number) { - equals(number.value, 1); - }; - visitor.BooleanLiteral = function (bool) { - equals(bool.value, true); - - equals(this.parents.length, 3); - equals(this.parents[0].type, 'SubExpression'); - equals(this.parents[1].type, 'BlockStatement'); - equals(this.parents[2].type, 'Program'); - }; - visitor.PathExpression = function (id) { - equals(/(foo\.)?bar$/.test(id.original), true); - }; - visitor.ContentStatement = function (content) { - equals(content.value, ' '); - }; - visitor.CommentStatement = function (comment) { - equals(comment.value, 'comment'); - }; - - visitor.accept( - parse('{{#foo.bar (foo.bar 1 "2" true) foo=@foo.bar}}{{!comment}}{{> bar }} {{/foo.bar}}') - ); - }); - - describe('mutating', function () { - describe('fields', function () { - it('should replace value', function () { - let visitor = new Visitor(); - - visitor.mutating = true; - visitor.StringLiteral = function (string) { - return { type: 'NumberLiteral', value: 42, loc: string.loc }; - }; - - let ast = parse('{{foo foo="foo"}}'); - visitor.accept(ast); - equals(print(ast), '{{ p%foo HASH{foo=n%42} }}\n'); - }); - it('should treat undefined resonse as identity', function () { - let visitor = new Visitor(); - visitor.mutating = true; - - let ast = parse('{{foo foo=42}}'); - visitor.accept(ast); - equals(print(ast), '{{ p%foo HASH{foo=n%42} }}\n'); - }); - it('should remove false responses', function () { - let visitor = new Visitor(); - - visitor.mutating = true; - visitor.Hash = function () { - return false; - }; - - let ast = parse('{{foo foo=42}}'); - visitor.accept(ast); - equals(print(ast), '{{ p%foo }}\n'); - }); - it('should throw when removing required values', function () { - shouldThrow( - function () { - let visitor = new Visitor(); - - visitor.mutating = true; - visitor.PathExpression = function () { - return false; - }; - - let ast = parse('{{foo 42}}'); - visitor.accept(ast); - }, - Exception, - 'MustacheStatement requires path' - ); - }); - it('should throw when returning non-node responses', function () { - shouldThrow( - function () { - let visitor = new Visitor(); - - visitor.mutating = true; - visitor.PathExpression = function () { - return {}; - }; - - let ast = parse('{{foo 42}}'); - visitor.accept(ast); - }, - Exception, - 'Unexpected node type "undefined" found when accepting path on MustacheStatement' - ); - }); - }); - describe('arrays', function () { - it('should replace value', function () { - let visitor = new Visitor(); - - visitor.mutating = true; - visitor.StringLiteral = function (string) { - return { type: 'NumberLiteral', value: 42, loc: string.locInfo }; - }; - - let ast = parse('{{foo "foo"}}'); - visitor.accept(ast); - equals(print(ast), '{{ p%foo [n%42] }}\n'); - }); - it('should treat undefined resonse as identity', function () { - let visitor = new Visitor(); - visitor.mutating = true; - - let ast = parse('{{foo 42}}'); - visitor.accept(ast); - equals(print(ast), '{{ p%foo [n%42] }}\n'); - }); - it('should remove false responses', function () { - let visitor = new Visitor(); - - visitor.mutating = true; - visitor.NumberLiteral = function () { - return false; - }; - - let ast = parse('{{foo 42}}'); - visitor.accept(ast); - equals(print(ast), '{{ p%foo }}\n'); - }); - }); - }); -}); diff --git a/packages/@handlebars/parser/src/handlebars.l b/packages/@handlebars/parser/src/handlebars.l deleted file mode 100644 index c23971f2968..00000000000 --- a/packages/@handlebars/parser/src/handlebars.l +++ /dev/null @@ -1,144 +0,0 @@ - -%x mu emu com raw escl - -%{ - -function strip(start, end) { - return yytext = yytext.substring(start, yyleng - end + start); -} - -%} - -LEFT_STRIP "~" -RIGHT_STRIP "~" - -LOOKAHEAD [=~}\s\/.)\]|] -LITERAL_LOOKAHEAD [~}\s)\]] - -/* -ID is the inverse of control characters. -Control characters ranges: - [\s] Whitespace - [!"#%-,\./] !, ", #, %, &, ', (, ), *, +, ,, ., /, Exceptions in range: $, - - [;->@] ;, <, =, >, @, Exceptions in range: :, ? - [\[-\^`] [, \, ], ^, `, Exceptions in range: _ - [\{-~] {, |, }, ~ -*/ -ID [^\s!"#%-,\.\/;->@\[-\^`\{-~]+/{LOOKAHEAD} - -%% - -[^\x00]*?/("{{") { - if(yytext.slice(-2) === "\\\\") { - strip(0,1); - this.begin("mu"); - } else if(yytext.slice(-1) === "\\") { - strip(0,1); - this.begin("emu"); - } else { - this.begin("mu"); - } - if(yytext) return 'CONTENT'; - } - -[^\x00]+ return 'CONTENT'; - -// marks CONTENT up to the next mustache or escaped mustache -[^\x00]{2,}?/("{{"|"\\{{"|"\\\\{{"|<>) { - this.popState(); - return 'CONTENT'; - } - -// nested raw block will create stacked 'raw' condition -"{{{{"/[^/] this.begin('raw'); return 'CONTENT'; -"{{{{/"[^\s!"#%-,\.\/;->@\[-\^`\{-~]+/[=}\s\/.]"}}}}" { - this.popState(); - // Should be using `this.topState()` below, but it currently - // returns the second top instead of the first top. Opened an - // issue about it at https://github.com/zaach/jison/issues/291 - if (this.conditionStack[this.conditionStack.length-1] === 'raw') { - return 'CONTENT'; - } else { - strip(5, 9); - return 'END_RAW_BLOCK'; - } - } -[^\x00]+?/("{{{{") { return 'CONTENT'; } - -[\s\S]*?"--"{RIGHT_STRIP}?"}}" { - this.popState(); - return 'COMMENT'; -} - -"(" return 'OPEN_SEXPR'; -")" return 'CLOSE_SEXPR'; - -"[" { - if (yy.syntax.square === 'string') { - this.unput(yytext); - // escaped literal - this.begin('escl'); - } else { - return 'OPEN_ARRAY'; - } -} -"]" return 'CLOSE_ARRAY'; - - -"{{{{" { return 'OPEN_RAW_BLOCK'; } -"}}}}" { - this.popState(); - this.begin('raw'); - return 'CLOSE_RAW_BLOCK'; - } -"{{"{LEFT_STRIP}?">" return 'OPEN_PARTIAL'; -"{{"{LEFT_STRIP}?"#>" return 'OPEN_PARTIAL_BLOCK'; -"{{"{LEFT_STRIP}?"#""*"? return 'OPEN_BLOCK'; -"{{"{LEFT_STRIP}?"/" return 'OPEN_ENDBLOCK'; -"{{"{LEFT_STRIP}?"^"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; -"{{"{LEFT_STRIP}?\s*"else"\s*{RIGHT_STRIP}?"}}" this.popState(); return 'INVERSE'; -"{{"{LEFT_STRIP}?"^" return 'OPEN_INVERSE'; -"{{"{LEFT_STRIP}?\s*"else" return 'OPEN_INVERSE_CHAIN'; -"{{"{LEFT_STRIP}?"{" return 'OPEN_UNESCAPED'; -"{{"{LEFT_STRIP}?"&" return 'OPEN'; -"{{"{LEFT_STRIP}?"!--" { - this.unput(yytext); - this.popState(); - this.begin('com'); -} -"{{"{LEFT_STRIP}?"!"[\s\S]*?"}}" { - this.popState(); - return 'COMMENT'; -} -"{{"{LEFT_STRIP}?"*"? return 'OPEN'; - -"=" return 'EQUALS'; -".." return 'ID'; -"."/{LOOKAHEAD} return 'ID'; -".#" return 'PRIVATE_SEP'; -[\/.] return 'SEP'; -\s+ // ignore whitespace -"}"{RIGHT_STRIP}?"}}" this.popState(); return 'CLOSE_UNESCAPED'; -{RIGHT_STRIP}?"}}" this.popState(); return 'CLOSE'; -'"'("\\"["]|[^"])*'"' yytext = strip(1,2).replace(/\\"/g,'"'); return 'STRING'; -"'"("\\"[']|[^'])*"'" yytext = strip(1,2).replace(/\\'/g,"'"); return 'STRING'; -"@" return 'DATA'; -"true"/{LITERAL_LOOKAHEAD} return 'BOOLEAN'; -"false"/{LITERAL_LOOKAHEAD} return 'BOOLEAN'; -"undefined"/{LITERAL_LOOKAHEAD} return 'UNDEFINED'; -"null"/{LITERAL_LOOKAHEAD} return 'NULL'; -\-?[0-9]+(?:\.[0-9]+)?/{LITERAL_LOOKAHEAD} return 'NUMBER'; -"as"\s+"|" return 'OPEN_BLOCK_PARAMS'; -"|" return 'CLOSE_BLOCK_PARAMS'; - -{ID} return 'ID'; - -'['('\\]'|[^\]])*']' { - yytext = yytext.replace(/\\([\\\]])/g,'$1'); - this.popState(); - return 'ID'; -} - -. return 'INVALID'; - -<> return 'EOF'; diff --git a/packages/@handlebars/parser/src/handlebars.yy b/packages/@handlebars/parser/src/handlebars.yy deleted file mode 100644 index ed26c2c5026..00000000000 --- a/packages/@handlebars/parser/src/handlebars.yy +++ /dev/null @@ -1,179 +0,0 @@ -%start root - -%ebnf - -%% - -root - : program EOF { return $1; } - ; - -program - : statement* -> yy.prepareProgram($1) - ; - -statement - : mustache -> $1 - | block -> $1 - | rawBlock -> $1 - | partial -> $1 - | partialBlock -> $1 - | content -> $1 - | COMMENT { - $$ = { - type: 'CommentStatement', - value: yy.stripComment($1), - strip: yy.stripFlags($1, $1), - loc: yy.locInfo(@$) - }; - }; - -content - : CONTENT { - $$ = { - type: 'ContentStatement', - original: $1, - value: $1, - loc: yy.locInfo(@$) - }; - }; - -rawBlock - : openRawBlock content* END_RAW_BLOCK -> yy.prepareRawBlock($1, $2, $3, @$) - ; - -openRawBlock - : OPEN_RAW_BLOCK helperName expr* hash? CLOSE_RAW_BLOCK -> { path: $2, params: $3, hash: $4 } - ; - -block - : openBlock program inverseChain? closeBlock -> yy.prepareBlock($1, $2, $3, $4, false, @$) - | openInverse program inverseAndProgram? closeBlock -> yy.prepareBlock($1, $2, $3, $4, true, @$) - ; - -openBlock - : OPEN_BLOCK helperName expr* hash? blockParams? CLOSE -> { open: $1, path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } - ; - -openInverse - : OPEN_INVERSE helperName expr* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } - ; - -openInverseChain - : OPEN_INVERSE_CHAIN helperName expr* hash? blockParams? CLOSE -> { path: $2, params: $3, hash: $4, blockParams: $5, strip: yy.stripFlags($1, $6) } - ; - -inverseAndProgram - : INVERSE program -> { strip: yy.stripFlags($1, $1), program: $2 } - ; - -inverseChain - : openInverseChain program inverseChain? { - var inverse = yy.prepareBlock($1, $2, $3, $3, false, @$), - program = yy.prepareProgram([inverse], $2.loc); - program.chained = true; - - $$ = { strip: $1.strip, program: program, chain: true }; - } - | inverseAndProgram -> $1 - ; - -closeBlock - : OPEN_ENDBLOCK helperName CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)} - ; - -mustache - // Parsing out the '&' escape token at AST level saves ~500 bytes after min due to the removal of one parser node. - // This also allows for handler unification as all mustache node instances can utilize the same handler - : OPEN hash CLOSE -> yy.prepareMustache(yy.syntax.hash($2, yy.locInfo(@$), { yy, syntax: 'expr' }), [], undefined, $1, yy.stripFlags($1, $3), @$) - | OPEN expr expr* hash? CLOSE -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$) - | OPEN_UNESCAPED expr expr* hash? CLOSE_UNESCAPED -> yy.prepareMustache($2, $3, $4, $1, yy.stripFlags($1, $5), @$) - ; - -partial - : OPEN_PARTIAL expr expr* hash? CLOSE { - $$ = { - type: 'PartialStatement', - name: $2, - params: $3, - hash: $4, - indent: '', - strip: yy.stripFlags($1, $5), - loc: yy.locInfo(@$) - }; - } - ; -partialBlock - : openPartialBlock program closeBlock -> yy.preparePartialBlock($1, $2, $3, @$) - ; -openPartialBlock - : OPEN_PARTIAL_BLOCK expr expr* hash? CLOSE -> { path: $2, params: $3, hash: $4, strip: yy.stripFlags($1, $5) } - ; - -expr - : helperName -> $1 - | exprHead -> $1 - ; - -exprHead - : arrayLiteral -> $1 - | sexpr -> $1 - ; - - -sexpr - : OPEN_SEXPR hash CLOSE_SEXPR -> yy.syntax.hash($2, yy.locInfo(@$), { yy, syntax: 'expr' }) - | OPEN_SEXPR expr expr* hash? CLOSE_SEXPR { - $$ = { - type: 'SubExpression', - path: $2, - params: $3, - hash: $4, - loc: yy.locInfo(@$) - }; - }; - -hash - : hashSegment+ -> {type: 'Hash', pairs: $1, loc: yy.locInfo(@$)} - ; - -hashSegment - : ID EQUALS expr -> {type: 'HashPair', key: yy.id($1), value: $3, loc: yy.locInfo(@$)} - ; - -arrayLiteral - : OPEN_ARRAY expr* CLOSE_ARRAY -> yy.syntax.square($2, yy.locInfo(@$), { yy, syntax: 'expr' }) - ; - -blockParams - : OPEN_BLOCK_PARAMS ID+ CLOSE_BLOCK_PARAMS -> yy.id($2) - ; - -helperName - : path -> $1 - | dataName -> $1 - | STRING -> {type: 'StringLiteral', value: $1, original: $1, loc: yy.locInfo(@$)} - | NUMBER -> {type: 'NumberLiteral', value: Number($1), original: Number($1), loc: yy.locInfo(@$)} - | BOOLEAN -> {type: 'BooleanLiteral', value: $1 === 'true', original: $1 === 'true', loc: yy.locInfo(@$)} - | UNDEFINED -> {type: 'UndefinedLiteral', original: undefined, value: undefined, loc: yy.locInfo(@$)} - | NULL -> {type: 'NullLiteral', original: null, value: null, loc: yy.locInfo(@$)} - ; - -dataName - : DATA pathSegments -> yy.preparePath(true, false, $2, @$) - ; - -sep - : SEP -> $1 - | PRIVATE_SEP -> $1 - ; - -path - : exprHead sep pathSegments -> yy.preparePath(false, $1, $3, @$) - | pathSegments -> yy.preparePath(false, false, $1, @$) - ; - -pathSegments - : pathSegments sep ID { $1.push({part: yy.id($3), original: $3, separator: $2}); $$ = $1; } - | ID -> [{part: yy.id($1), original: $1}] - ; diff --git a/packages/@handlebars/parser/src/parser-suffix.js b/packages/@handlebars/parser/src/parser-suffix.js deleted file mode 100644 index ef471187e39..00000000000 --- a/packages/@handlebars/parser/src/parser-suffix.js +++ /dev/null @@ -1 +0,0 @@ -export default parser; diff --git a/packages/@handlebars/parser/tsconfig.json b/packages/@handlebars/parser/tsconfig.json deleted file mode 100644 index eb6cd98e79d..00000000000 --- a/packages/@handlebars/parser/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - // Compilation Configuration - "target": "es2017", - "inlineSources": true, - "inlineSourceMap": true, - "outDir": "dist", - "rootDir": "lib", - "esModuleInterop": true, - "moduleResolution": "bundler", - "verbatimModuleSyntax": true, - // Enhance Strictness - "strict": true, - "skipLibCheck": true, - "suppressImplicitAnyIndexErrors": false, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, - "newLine": "LF", - "allowJs": true - }, - "include": ["lib/**/*.js", "lib/**/*.d.ts"], - "exclude": ["lib/parser.js", "dist", "node_modules", ".vscode"] -} diff --git a/packages/@handlebars/parser/types/ast.d.ts b/packages/@handlebars/parser/types/ast.d.ts deleted file mode 100644 index b9bb2b6c3e7..00000000000 --- a/packages/@handlebars/parser/types/ast.d.ts +++ /dev/null @@ -1,146 +0,0 @@ -export interface Node { - type: string; - loc: SourceLocation; -} - -export interface SourceLocation { - source: string; - start: Position; - end: Position; -} - -export interface Position { - line: number; - column: number; -} - -export interface Program extends Node { - body: Statement[]; - blockParams: string[]; -} - -export interface Statement extends Node {} - -export interface MustacheStatement extends Statement { - type: 'MustacheStatement'; - path: SubExpression | PathExpression | Literal; - params: Expression[]; - hash: Hash; - escaped: boolean; - strip: StripFlags; -} - -export interface Decorator extends MustacheStatement { } - -export interface BlockStatement extends Statement { - type: 'BlockStatement'; - path: PathExpression; - params: Expression[]; - hash: Hash; - program: Program; - inverse: Program; - openStrip: StripFlags; - inverseStrip: StripFlags; - closeStrip: StripFlags; -} - -export interface DecoratorBlock extends BlockStatement { } - -export interface PartialStatement extends Statement { - type: 'PartialStatement'; - name: PathExpression | SubExpression; - params: Expression[]; - hash: Hash; - indent: string; - strip: StripFlags; -} - -export interface PartialBlockStatement extends Statement { - type: 'PartialBlockStatement'; - name: PathExpression | SubExpression; - params: Expression[]; - hash: Hash; - program: Program; - openStrip: StripFlags; - closeStrip: StripFlags; -} - -export interface ContentStatement extends Statement { - type: 'ContentStatement'; - value: string; - original: StripFlags; -} - -export interface CommentStatement extends Statement { - type: 'CommentStatement'; - value: string; - strip: StripFlags; -} - -export interface Expression extends Node {} - -export interface SubExpression extends Expression { - type: 'SubExpression'; - path: SubExpression | PathExpression; - params: Expression[]; - hash: Hash; -} - -export interface PathExpression extends Expression { - type: 'PathExpression'; - data: boolean; - depth: number; - parts: (string | SubExpression)[]; - head: SubExpression | string; - tail: string[]; - original: string; -} - -export interface Literal extends Expression {} -export interface StringLiteral extends Literal { - type: 'StringLiteral'; - value: string; - original: string; -} - -export interface BooleanLiteral extends Literal { - type: 'BooleanLiteral'; - value: boolean; - original: boolean; -} - -export interface NumberLiteral extends Literal { - type: 'NumberLiteral'; - value: number; - original: number; -} - -export interface UndefinedLiteral extends Literal { - type: 'UndefinedLiteral'; -} - -export interface NullLiteral extends Literal { - type: 'NullLiteral'; -} - -export interface Hash extends Node { - type: 'Hash'; - pairs: HashPair[]; -} - -export interface HashPair extends Node { - type: 'HashPair'; - key: string; - value: Expression; -} - -export interface StripFlags { - open: boolean; - close: boolean; -} - -export interface helpers { - helperExpression(node: Node): boolean; - scopeId(path: PathExpression): boolean; - simpleId(path: PathExpression): boolean; -} diff --git a/packages/@handlebars/parser/types/index.d.ts b/packages/@handlebars/parser/types/index.d.ts deleted file mode 100644 index 88ed676ddcc..00000000000 --- a/packages/@handlebars/parser/types/index.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as AST from './ast'; - -export { AST }; - -export interface ParseOptions { - srcName?: string; - ignoreStandalone?: boolean; -} - -export function parse(input: string, options?: ParseOptions): AST.Program; -export function parseWithoutProcessing(input: string, options?: ParseOptions): AST.Program; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8905238c41c..4114bf7fbc5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,7 +155,7 @@ importers: version: 2.1.0 ember-cli-dependency-checker: specifier: ^3.3.1 - version: 3.3.3(ember-cli@6.11.2(@babel/core@7.29.0)(@types/node@22.19.15)) + version: 3.3.3(ember-cli@6.11.2(@babel/core@7.29.0)(@types/node@22.19.15)(ejs@3.1.10)(handlebars@4.7.9)(underscore@1.13.8)) ember-cli-yuidoc: specifier: ^0.9.1 version: 0.9.1 @@ -2153,9 +2153,6 @@ importers: '@glimmer/wire-format': specifier: workspace:* version: link:../wire-format - '@handlebars/parser': - specifier: workspace:* - version: link:../../@handlebars/parser simple-html-tokenizer: specifier: ^0.5.11 version: 0.5.11 @@ -2328,21 +2325,6 @@ importers: specifier: ^5.7.3 version: 5.9.3 - packages/@handlebars/parser: - devDependencies: - combine-files: - specifier: ^1.1.8 - version: 1.1.8 - jison: - specifier: ^0.4.18 - version: 0.4.18 - mocha: - specifier: ^11.0.0 - version: 11.7.5 - npm-run-all2: - specifier: ^8.0.0 - version: 8.0.4 - packages/@types/js-reporters: {} packages/ember: @@ -2815,7 +2797,7 @@ importers: version: 3.0.0 ember-cli-dependency-checker: specifier: ^3.3.3 - version: 3.3.3(ember-cli@6.11.2(@babel/core@7.29.0)(@types/node@22.19.15)) + version: 3.3.3(ember-cli@6.11.2(@babel/core@7.29.0)(@types/node@22.19.15)(ejs@3.1.10)(handlebars@4.7.9)(underscore@1.13.8)) ember-cli-deprecation-workflow: specifier: ^3.4.0 version: 3.4.0(ember-source@) @@ -5856,13 +5838,6 @@ packages: engines: {node: '>= 8'} hasBin: true - JSONSelect@0.4.0: - resolution: {integrity: sha512-VRLR3Su35MH+XV2lrvh9O7qWoug/TUyj9tLDjn9rtpUCNnILLrHjgd/tB0KrhugCxUpj3UqoLqfYb3fLJdIQQQ==} - engines: {node: '>=0.4.7'} - - JSV@4.0.2: - resolution: {integrity: sha512-ZJ6wx9xaKJ3yFUhq5/sk82PJMuUyLk277I8mQeyDgCTjGdjWJIvPfaU5LIXaMuaN2UO1X3kZH4+lgphublZUHw==} - abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -6693,10 +6668,6 @@ packages: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} - cjson@0.3.0: - resolution: {integrity: sha512-bBRQcCIHzI1IVH59fR0bwGrFmi3Btb/JNwM/n401i1DnYgWndpsUBiQRAddLflkZage20A2d25OAWZZk0vBRlA==} - engines: {node: '>= 0.3.0'} - clean-base-url@1.0.0: resolution: {integrity: sha512-9q6ZvUAhbKOSRFY7A/irCQ/rF0KIpa3uXpx6izm8+fp7b2H4hLeUJ+F1YYk9+gDQ/X8Q0MEyYs+tG3cht//HTg==} @@ -6819,10 +6790,6 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - colors@0.5.1: - resolution: {integrity: sha512-XjsuUwpDeY98+yz959OlUK6m7mLBM+1MEG5oaenfuQnNnrQk1WvtcvFgN3FNDP3f2NmZ211t0mNEfSEN1h0eIg==} - engines: {node: '>=0.1.90'} - colors@1.0.3: resolution: {integrity: sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==} engines: {node: '>=0.1.90'} @@ -6831,10 +6798,6 @@ packages: resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} engines: {node: '>=0.1.90'} - combine-files@1.1.8: - resolution: {integrity: sha512-Ut/1sjySj6DyjyWZ5fQNThTTAZlj7968tozHqSGXFEcyaQeQUKFdb1HZDcNrzGRfAbKERw2S2m/+5x6zupBU6A==} - hasBin: true - combined-stream@0.0.7: resolution: {integrity: sha512-qfexlmLp9MyrkajQVyjEDb0Vj+KhRgR/rxLiVhaihlT+ZkX0lReqtH6Ack40CvMDERR4b5eFp3CreskpBs1Pig==} engines: {node: '>= 0.8'} @@ -7411,9 +7374,6 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ebnf-parser@0.1.10: - resolution: {integrity: sha512-urvSxVQ6XJcoTpc+/x2pWhhuOX4aljCNQpwzw+ifZvV1andZkAmiJc3Rq1oGEAQmcjiLceyMXOy1l8ms8qs2fQ==} - editions@1.3.4: resolution: {integrity: sha512-gzao+mxnYDzIysXKMQi/+M1mjy/rjestjg6OPoYTtI+3Izp23oiGZitsl9lPDPiTGXbcSIk1iJWhliSaglxnUg==} engines: {node: '>=0.8'} @@ -7733,11 +7693,6 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - escodegen@1.3.3: - resolution: {integrity: sha512-z9FWgKc48wjMlpzF5ymKS1AF8OIgnKLp9VyN7KbdtyrP/9lndwUFqCtMm+TAJmJf7KJFFYc4cFJfVTTGkKEwsA==} - engines: {node: '>=0.10.0'} - hasBin: true - eslint-compat-utils@0.5.1: resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} engines: {node: '>=12'} @@ -7879,11 +7834,6 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - esprima@1.1.1: - resolution: {integrity: sha512-qxxB994/7NtERxgXdFgLHIs9M6bhLXc6qtUmWZ3L8+gTQ9qaoyki2887P2IqAYsoENyr8SUbTutStDniOHSDHg==} - engines: {node: '>=0.4.0'} - hasBin: true - esprima@3.0.0: resolution: {integrity: sha512-xoBq/MIShSydNZOkjkoCEjqod963yHNXTLC40ypBhop6yPqflPz/vTinmCfSrGcywVLnSftRf6a0kJLdFdzemw==} engines: {node: '>=0.10.0'} @@ -7902,10 +7852,6 @@ packages: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} - estraverse@1.5.1: - resolution: {integrity: sha512-FpCjJDfmo3vsc/1zKSeqR5k42tcIhxFIlvq+h9j0fO2q/h2uLKyweq7rYJ+0CoVvrGQOxIS5wyBrW/+vF58BUQ==} - engines: {node: '>=0.4.0'} - estraverse@4.3.0: resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} engines: {node: '>=4.0'} @@ -7917,10 +7863,6 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - esutils@1.0.0: - resolution: {integrity: sha512-x/iYH53X3quDwfHRz4y8rn4XcEwwCJeWsul9pF1zldMbGtgOtMNBEOuYWwB1EQlK2LRa1fev3YAgym/RElp5Cg==} - engines: {node: '>=0.10.0'} - esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -9059,16 +9001,6 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} - jison-lex@0.3.4: - resolution: {integrity: sha512-EBh5wrXhls1cUwROd5DcDHR1sG7CdsCFSqY1027+YA1RGxz+BX2TDLAhdsQf40YEtFDGoiO0Qm8PpnBl2EzDJw==} - engines: {node: '>=0.4'} - hasBin: true - - jison@0.4.18: - resolution: {integrity: sha512-FKkCiJvozgC7VTHhMJ00a0/IApSxhlGsFIshLW6trWJ8ONX2TQJBBz6DlcO1Gffy4w9LT+uL+PA+CVnUSJMF7w==} - engines: {node: '>=0.4'} - hasBin: true - js-reporters@2.1.0: resolution: {integrity: sha512-Q4GcEcPSb6ovhqp91claM3WPbSntQxbIn+3JiJgEXturys2ttWgs31VC60Yja+2unpNOH2A2qyjWFU2thCQ8sg==} engines: {node: '>=10'} @@ -9167,11 +9099,6 @@ packages: jsonify@0.0.1: resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} - jsonlint@1.6.0: - resolution: {integrity: sha512-x6YLBe6NjdpmIeiklwQOxsZuYj/SOWkT33GlTpaG1UdFGjdWjPcxJ1CWZAX3wA7tarz8E2YHF6KiW5HTapPlXw==} - engines: {node: '>= 0.6'} - hasBin: true - jstat@1.9.6: resolution: {integrity: sha512-rPBkJbK2TnA8pzs93QcDDPlKcrtZWuuCo2dVR0TFLOJSxhqfWOVCSp8aV3/oSbn+4uY4yw1URtLpHQedtmXfug==} @@ -9218,9 +9145,6 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lex-parser@0.1.4: - resolution: {integrity: sha512-DuAEISsr1H4LOpmFLkyMc8YStiRWZCO8hMsoXAXSbgyfvs2WQhSt0+/FBv3ZU/JBFZMGcE+FWzEBSzwUU7U27w==} - lighthouse-logger@2.0.2: resolution: {integrity: sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==} @@ -9740,10 +9664,6 @@ packages: resolution: {integrity: sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==} engines: {node: '>=6'} - nomnom@1.5.2: - resolution: {integrity: sha512-fiVbT7BqxiQqjlR9U3FDGOSERFCKoXVCdxV2FwZuNN7/cmJ42iQx35nUFOAFDcyvemu9Adp+IlsCGlKQYLmBKw==} - deprecated: Package no longer supported. Contact support@npmjs.com for more info. - nopt@3.0.6: resolution: {integrity: sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==} hasBin: true @@ -10914,10 +10834,6 @@ packages: resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} deprecated: See https://github.com/lydell/source-map-url#deprecated - source-map@0.1.43: - resolution: {integrity: sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==} - engines: {node: '>=0.8.0'} - source-map@0.4.4: resolution: {integrity: sha512-Y8nIfcb1s/7DcobUz1yOO1GSp7gyL+D9zLHDehT7iRESqGSxjJ448Sg7rvfgsRJCnKLdSl11uGf0s9X80cH0/A==} engines: {node: '>=0.8.0'} @@ -11509,9 +11425,6 @@ packages: underscore.string@3.3.6: resolution: {integrity: sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==} - underscore@1.1.7: - resolution: {integrity: sha512-w4QtCHoLBXw1mjofIDoMyexaEdWGMedWNDhlWTtT1V1lCRqi65Pnoygkh6+WRdr+Bm8ldkBNkNeCsXGMlQS9HQ==} - underscore@1.13.8: resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} @@ -15553,10 +15466,6 @@ snapshots: dependencies: isexe: 2.0.0 - JSONSelect@0.4.0: {} - - JSV@4.0.2: {} - abbrev@1.1.1: {} accepts@1.3.8: @@ -16769,10 +16678,6 @@ snapshots: ci-info@4.4.0: {} - cjson@0.3.0: - dependencies: - jsonlint: 1.6.0 - clean-base-url@1.0.0: {} clean-css@5.3.3: @@ -16884,14 +16789,10 @@ snapshots: colorette@2.0.20: {} - colors@0.5.1: {} - colors@1.0.3: {} colors@1.4.0: {} - combine-files@1.1.8: {} - combined-stream@0.0.7: dependencies: delayed-stream: 0.0.5 @@ -17323,8 +17224,6 @@ snapshots: eastasianwidth@0.2.0: {} - ebnf-parser@0.1.10: {} - editions@1.3.4: {} editions@2.3.1: @@ -17461,7 +17360,7 @@ snapshots: transitivePeerDependencies: - supports-color - ember-cli-dependency-checker@3.3.3(ember-cli@6.11.2(@babel/core@7.29.0)(@types/node@22.19.15)): + ember-cli-dependency-checker@3.3.3(ember-cli@6.11.2(@babel/core@7.29.0)(@types/node@22.19.15)(ejs@3.1.10)(handlebars@4.7.9)(underscore@1.13.8)): dependencies: chalk: 2.4.2 ember-cli: 6.11.2(@babel/core@7.29.0)(@types/node@22.19.15)(ejs@3.1.10)(handlebars@4.7.9)(underscore@1.13.8) @@ -18066,14 +17965,6 @@ snapshots: escape-string-regexp@4.0.0: {} - escodegen@1.3.3: - dependencies: - esprima: 1.1.1 - estraverse: 1.5.1 - esutils: 1.0.0 - optionalDependencies: - source-map: 0.1.43 - eslint-compat-utils@0.5.1(eslint@9.39.4): dependencies: eslint: 9.39.4 @@ -18263,8 +18154,6 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 - esprima@1.1.1: {} - esprima@3.0.0: {} esprima@4.0.1: {} @@ -18277,16 +18166,12 @@ snapshots: dependencies: estraverse: 5.3.0 - estraverse@1.5.1: {} - estraverse@4.3.0: {} estraverse@5.3.0: {} estree-walker@2.0.2: {} - esutils@1.0.0: {} - esutils@2.0.3: {} etag@1.8.1: {} @@ -19664,22 +19549,6 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jison-lex@0.3.4: - dependencies: - lex-parser: 0.1.4 - nomnom: 1.5.2 - - jison@0.4.18: - dependencies: - JSONSelect: 0.4.0 - cjson: 0.3.0 - ebnf-parser: 0.1.10 - escodegen: 1.3.3 - esprima: 1.1.1 - jison-lex: 0.3.4 - lex-parser: 0.1.4 - nomnom: 1.5.2 - js-reporters@2.1.0: {} js-string-escape@1.0.1: {} @@ -19802,11 +19671,6 @@ snapshots: jsonify@0.0.1: {} - jsonlint@1.6.0: - dependencies: - JSV: 4.0.2 - nomnom: 1.5.2 - jstat@1.9.6: {} keyv@3.1.0: @@ -19853,8 +19717,6 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lex-parser@0.1.4: {} - lighthouse-logger@2.0.2: dependencies: debug: 4.4.3(supports-color@8.1.1) @@ -20378,11 +20240,6 @@ snapshots: node-watch@0.7.3: {} - nomnom@1.5.2: - dependencies: - colors: 0.5.1 - underscore: 1.1.7 - nopt@3.0.6: dependencies: abbrev: 1.1.1 @@ -21726,11 +21583,6 @@ snapshots: source-map-url@0.4.1: {} - source-map@0.1.43: - dependencies: - amdefine: 1.0.1 - optional: true - source-map@0.4.4: dependencies: amdefine: 1.0.1 @@ -22577,8 +22429,6 @@ snapshots: sprintf-js: 1.1.3 util-deprecate: 1.0.2 - underscore@1.1.7: {} - underscore@1.13.8: {} undici-types@6.21.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ea6c625d4b4..9814be2538d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,7 +3,6 @@ packages: - 'packages/@ember/*' - 'packages/@glimmer/*' - 'packages/@glimmer-workspace/*' - - 'packages/@handlebars/*' - 'packages/@types/*' - 'packages/*/*/test' - 'smoke-tests/*' diff --git a/rollup.config.mjs b/rollup.config.mjs index 3ab820dbdfa..3db627d39ab 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -216,9 +216,6 @@ function packages() { // "exposedDependencies" since they used to actually be dependencies. '@glimmer-workspace/**', '@glimmer/**', - - // @handlebars/parser is a hidden dependency, not an explicit entrypoint - '@handlebars/**', ], cwd: 'packages', }); @@ -283,7 +280,6 @@ export function hiddenDependencies() { findFromProject('@glimmer/syntax', 'simple-html-tokenizer'), 'module' ).path, - '@handlebars/parser': resolve(packageCache.appRoot, 'packages/@handlebars/parser/lib/index.js'), ...walkGlimmerDeps(['@glimmer/compiler']), 'decorator-transforms/runtime': resolve( findFromProject('decorator-transforms').root,