From 0f0d1b126f12728384fa7ff485b455b16e8f4bea Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Thu, 24 Oct 2024 10:38:22 -0700 Subject: [PATCH 1/9] Initial stab at infra updates and TypeScript The commit migrates a single file to TypeScript, but also updates infrastructure that is necessary to make it work. --- .eslintrc.cjs | 65 +++++++ lib/exception.js | 68 ------- lib/exception.ts | 67 +++++++ lib/helpers.js | 28 +-- lib/index.js | 12 +- lib/parse.js | 8 +- lib/printer.js | 88 ++++----- lib/visitor.js | 36 ++-- lib/whitespace-control.js | 145 +++++++------- package.json | 33 +++- pnpm-lock.yaml | 278 ++++++++++----------------- scripts/compile-parser.cjs | 19 ++ spec/{.eslintrc.js => .eslintrc.cjs} | 2 +- spec/ast.js | 122 ++++++------ spec/parser.js | 102 +++++----- spec/utils.js | 73 ++++--- spec/visitor.js | 70 +++---- tsconfig.json | 19 +- 18 files changed, 641 insertions(+), 594 deletions(-) create mode 100644 .eslintrc.cjs delete mode 100644 lib/exception.js create mode 100644 lib/exception.ts create mode 100644 scripts/compile-parser.cjs rename spec/{.eslintrc.js => .eslintrc.cjs} (61%) diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..2f3a7f3 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,65 @@ +module.exports = { + extends: ['eslint:recommended', 'plugin:compat/recommended', 'prettier'], + globals: { + self: false, + }, + env: { + node: true, + es6: true, + }, + rules: { + 'no-console': 'warn', + + // temporarily disabled until the violating places are fixed. + 'no-func-assign': 'off', + 'no-sparse-arrays': 'off', + + // Best Practices // + //----------------// + 'default-case': 'warn', + 'guard-for-in': 'warn', + 'no-alert': 'error', + 'no-caller': 'error', + 'no-div-regex': 'warn', + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-floating-decimal': 'error', + 'no-implied-eval': 'error', + 'no-iterator': 'error', + 'no-labels': 'error', + 'no-lone-blocks': 'error', + 'no-loop-func': 'error', + 'no-multi-str': 'warn', + 'no-global-assign': 'error', + 'no-new': 'error', + 'no-new-func': 'error', + 'no-new-wrappers': 'error', + 'no-octal-escape': 'error', + 'no-process-env': 'error', + 'no-proto': 'error', + 'no-return-assign': 'error', + 'no-script-url': 'error', + 'no-self-compare': 'error', + 'no-sequences': 'error', + 'no-throw-literal': 'error', + 'no-unused-expressions': 'error', + 'no-warning-comments': 'warn', + 'no-with': 'error', + radix: 'error', + + // Variables // + //-----------// + 'no-label-var': 'error', + 'no-undef-init': 'error', + 'no-use-before-define': ['error', 'nofunc'], + + // ECMAScript 6 // + //--------------// + 'no-var': 'error', + }, + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, +}; diff --git a/lib/exception.js b/lib/exception.js deleted file mode 100644 index b2fdc00..0000000 --- a/lib/exception.js +++ /dev/null @@ -1,68 +0,0 @@ -const errorProps = [ - 'description', - 'fileName', - 'lineNumber', - 'endLineNumber', - 'message', - 'name', - 'number', - 'stack' -]; - -function Exception(message, node) { - let loc = node && node.loc, - line, - endLineNumber, - column, - endColumn; - - if (loc) { - line = loc.start.line; - endLineNumber = loc.end.line; - column = loc.start.column; - endColumn = loc.end.column; - - message += ' - ' + line + ':' + column; - } - - let tmp = Error.prototype.constructor.call(this, message); - - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (let idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } - - /* istanbul ignore else */ - if (Error.captureStackTrace) { - Error.captureStackTrace(this, Exception); - } - - try { - if (loc) { - this.lineNumber = line; - this.endLineNumber = endLineNumber; - - // Work around issue under safari where we can't directly set the column value - /* istanbul ignore next */ - if (Object.defineProperty) { - Object.defineProperty(this, 'column', { - value: column, - enumerable: true - }); - Object.defineProperty(this, 'endColumn', { - value: endColumn, - enumerable: true - }); - } else { - this.column = column; - this.endColumn = endColumn; - } - } - } catch (nop) { - /* Ignore if the browser is very particular */ - } -} - -Exception.prototype = new Error(); - -export default Exception; diff --git a/lib/exception.ts b/lib/exception.ts new file mode 100644 index 0000000..f65f30e --- /dev/null +++ b/lib/exception.ts @@ -0,0 +1,67 @@ +import type { BaseNode } from './types/types.d.ts'; + +export default class Exception extends Error { + readonly lineNumber: number | undefined; + readonly endLineNumber: number | undefined; + readonly column: number | undefined; + readonly endColumn: number | undefined; + + readonly description: string | undefined; + + constructor(message: string, node?: BaseNode) { + const loc = node?.loc; + let line; + let endLineNumber; + let column; + let endColumn; + + if (loc) { + line = loc.start.line; + endLineNumber = loc.end.line; + column = loc.start.column; + endColumn = loc.end.column; + + message += ' - ' + line + ':' + column; + } + + super(message); + + /* istanbul ignore else */ + if (hasCaptureStackTrace(Error)) { + Error.captureStackTrace(this, Exception); + } + + try { + if (loc) { + this.lineNumber = line; + this.endLineNumber = endLineNumber; + + // Work around issue under safari where we can't directly set the column value + /* istanbul ignore next */ + if (Object.defineProperty) { + Object.defineProperty(this, 'column', { + value: column, + enumerable: true, + }); + Object.defineProperty(this, 'endColumn', { + value: endColumn, + enumerable: true, + }); + } else { + this.column = column; + this.endColumn = endColumn; + } + } + } catch (nop) { + /* Ignore if the browser is very particular */ + } + } +} + +type CapturableError = typeof Error & { + captureStackTrace: (error: Error, constructor: Function) => void; +}; + +function hasCaptureStackTrace(error: typeof Error): error is CapturableError { + return 'captureStackTrace' in error; +} diff --git a/lib/helpers.js b/lib/helpers.js index de2806d..ef567e2 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -1,4 +1,4 @@ -import Exception from './exception'; +import Exception from './exception.js'; function validateClose(open, close) { close = close.path ? close.path.original : close; @@ -17,11 +17,11 @@ export function SourceLocation(source, locInfo) { this.source = source; this.start = { line: locInfo.first_line, - column: locInfo.first_column + column: locInfo.first_column, }; this.end = { line: locInfo.last_line, - column: locInfo.last_column + column: locInfo.last_column, }; } @@ -36,7 +36,7 @@ export function id(token) { export function stripFlags(open, close) { return { open: open.charAt(2) === '~', - close: close.charAt(close.length - 3) === '~' + close: close.charAt(close.length - 3) === '~', }; } @@ -93,7 +93,7 @@ export function preparePath(data, sexpr, parts, loc) { tail, parts: head ? [head, ...tail] : tail, original, - loc + loc, }; } @@ -110,7 +110,7 @@ export function prepareMustache(path, params, hash, open, strip, locInfo) { hash, escaped, strip, - loc: this.locInfo(locInfo) + loc: this.locInfo(locInfo), }; } @@ -122,7 +122,7 @@ export function prepareRawBlock(openRawBlock, contents, close, locInfo) { type: 'Program', body: contents, strip: {}, - loc: locInfo + loc: locInfo, }; return { @@ -134,7 +134,7 @@ export function prepareRawBlock(openRawBlock, contents, close, locInfo) { openStrip: {}, inverseStrip: {}, closeStrip: {}, - loc: locInfo + loc: locInfo, }; } @@ -188,7 +188,7 @@ export function prepareBlock( openStrip: openBlock.strip, inverseStrip, closeStrip: close && close.strip, - loc: this.locInfo(locInfo) + loc: this.locInfo(locInfo), }; } @@ -203,12 +203,12 @@ export function prepareProgram(statements, loc) { source: firstLoc.source, start: { line: firstLoc.start.line, - column: firstLoc.start.column + column: firstLoc.start.column, }, end: { line: lastLoc.end.line, - column: lastLoc.end.column - } + column: lastLoc.end.column, + }, }; } } @@ -217,7 +217,7 @@ export function prepareProgram(statements, loc) { type: 'Program', body: statements, strip: {}, - loc: loc + loc: loc, }; } @@ -232,6 +232,6 @@ export function preparePartialBlock(open, program, close, locInfo) { program, openStrip: open.strip, closeStrip: close && close.strip, - loc: this.locInfo(locInfo) + loc: this.locInfo(locInfo), }; } diff --git a/lib/index.js b/lib/index.js index a9813bb..cf22bff 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,6 @@ -export { default as Visitor } from './visitor'; -export { default as WhitespaceControl } from './whitespace-control'; -export { default as parser } from './parser'; -export { default as Exception } from './exception'; -export { print, PrintVisitor } from './printer'; -export { parse, parseWithoutProcessing } from './parse'; +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/lib/parse.js b/lib/parse.js index ce29867..6b5f03d 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1,6 +1,6 @@ -import parser from './parser'; -import WhitespaceControl from './whitespace-control'; -import * as Helpers from './helpers'; +import parser from './parser.js'; +import WhitespaceControl from './whitespace-control.js'; +import * as Helpers from './helpers.js'; let baseHelpers = {}; @@ -19,7 +19,7 @@ export function parseWithoutProcessing(input, options) { parser.yy = baseHelpers; // Altering the shared object here, but this is ok as parser is a sync operation - parser.yy.locInfo = function(locInfo) { + parser.yy.locInfo = function (locInfo) { return new Helpers.SourceLocation(options && options.srcName, locInfo); }; diff --git a/lib/printer.js b/lib/printer.js index cca457e..9e23499 100644 --- a/lib/printer.js +++ b/lib/printer.js @@ -1,5 +1,5 @@ /* eslint-disable new-cap */ -import Visitor from './visitor'; +import Visitor from './visitor.js'; export function print(ast) { return new PrintVisitor().accept(ast); @@ -11,7 +11,7 @@ export function PrintVisitor() { PrintVisitor.prototype = new Visitor(); -PrintVisitor.prototype.pad = function(string) { +PrintVisitor.prototype.pad = function (string) { let out = ''; for (let i = 0, l = this.padding; i < l; i++) { @@ -22,7 +22,7 @@ PrintVisitor.prototype.pad = function(string) { return out; }; -PrintVisitor.prototype.Program = function(program) { +PrintVisitor.prototype.Program = function (program) { let out = '', body = program.body, i, @@ -46,47 +46,46 @@ PrintVisitor.prototype.Program = function(program) { return out; }; -PrintVisitor.prototype.MustacheStatement = function(mustache) { +PrintVisitor.prototype.MustacheStatement = function (mustache) { return this.pad('{{ ' + this.SubExpression(mustache) + ' }}'); }; -PrintVisitor.prototype.Decorator = function(mustache) { +PrintVisitor.prototype.Decorator = function (mustache) { return this.pad('{{ DIRECTIVE ' + this.SubExpression(mustache) + ' }}'); }; -PrintVisitor.prototype.BlockStatement = PrintVisitor.prototype.DecoratorBlock = function( - block -) { - let out = ''; +PrintVisitor.prototype.BlockStatement = PrintVisitor.prototype.DecoratorBlock = + function (block) { + let out = ''; - out += this.pad( - (block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:' - ); - this.padding++; - out += this.pad(this.SubExpression(block)); - if (block.program) { - out += this.pad('PROGRAM:'); + out += this.pad( + (block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:' + ); this.padding++; - out += this.accept(block.program); - this.padding--; - } - if (block.inverse) { + out += this.pad(this.SubExpression(block)); if (block.program) { + out += this.pad('PROGRAM:'); this.padding++; + out += this.accept(block.program); + this.padding--; } - out += this.pad('{{^}}'); - this.padding++; - out += this.accept(block.inverse); - this.padding--; - if (block.program) { + 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--; + this.padding--; - return out; -}; + return out; + }; -PrintVisitor.prototype.PartialStatement = function(partial) { +PrintVisitor.prototype.PartialStatement = function (partial) { let content = 'PARTIAL:' + partial.name.original; if (partial.params[0]) { content += ' ' + this.accept(partial.params[0]); @@ -96,7 +95,7 @@ PrintVisitor.prototype.PartialStatement = function(partial) { } return this.pad('{{> ' + content + ' }}'); }; -PrintVisitor.prototype.PartialBlockStatement = function(partial) { +PrintVisitor.prototype.PartialBlockStatement = function (partial) { let content = 'PARTIAL BLOCK:' + partial.name.original; if (partial.params[0]) { content += ' ' + this.accept(partial.params[0]); @@ -113,15 +112,15 @@ PrintVisitor.prototype.PartialBlockStatement = function(partial) { return this.pad('{{> ' + content + ' }}'); }; -PrintVisitor.prototype.ContentStatement = function(content) { +PrintVisitor.prototype.ContentStatement = function (content) { return this.pad("CONTENT[ '" + content.value + "' ]"); }; -PrintVisitor.prototype.CommentStatement = function(comment) { +PrintVisitor.prototype.CommentStatement = function (comment) { return this.pad("{{! '" + comment.value + "' }}"); }; -PrintVisitor.prototype.SubExpression = function(sexpr) { +PrintVisitor.prototype.SubExpression = function (sexpr) { let params = sexpr.params, paramStrings = [], hash; @@ -137,10 +136,11 @@ PrintVisitor.prototype.SubExpression = function(sexpr) { return this.accept(sexpr.path) + ' ' + params + hash; }; -PrintVisitor.prototype.PathExpression = function(id) { - let head = typeof id.head === 'string' ? id.head : `[${this.accept(id.head)}]`; +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; + return 'p%' + prefix(id) + path; }; function prefix(path) { @@ -153,27 +153,27 @@ function prefix(path) { } } -PrintVisitor.prototype.StringLiteral = function(string) { +PrintVisitor.prototype.StringLiteral = function (string) { return '"' + string.value + '"'; }; -PrintVisitor.prototype.NumberLiteral = function(number) { +PrintVisitor.prototype.NumberLiteral = function (number) { return 'n%' + number.value; }; -PrintVisitor.prototype.BooleanLiteral = function(bool) { +PrintVisitor.prototype.BooleanLiteral = function (bool) { return 'b%' + bool.value; }; -PrintVisitor.prototype.UndefinedLiteral = function() { +PrintVisitor.prototype.UndefinedLiteral = function () { return 'UNDEFINED'; }; -PrintVisitor.prototype.NullLiteral = function() { +PrintVisitor.prototype.NullLiteral = function () { return 'NULL'; }; -PrintVisitor.prototype.Hash = function(hash) { +PrintVisitor.prototype.Hash = function (hash) { let pairs = hash.pairs, joinedPairs = []; @@ -183,7 +183,7 @@ PrintVisitor.prototype.Hash = function(hash) { return 'HASH{' + joinedPairs.join(' ') + '}'; }; -PrintVisitor.prototype.HashPair = function(pair) { +PrintVisitor.prototype.HashPair = function (pair) { return pair.key + '=' + this.accept(pair.value); }; /* eslint-enable new-cap */ diff --git a/lib/visitor.js b/lib/visitor.js index 0f7826b..e9b5d68 100644 --- a/lib/visitor.js +++ b/lib/visitor.js @@ -1,4 +1,4 @@ -import Exception from './exception'; +import Exception from './exception.js'; function Visitor() { this.parents = []; @@ -9,7 +9,7 @@ Visitor.prototype = { mutating: false, // Visits a given value. If mutating, will replace the value if necessary. - acceptKey: function(node, name) { + acceptKey: function (node, name) { let value = this.accept(node[name]); if (this.mutating) { // Hacky sanity check: This may have a few false positives for type for the helper @@ -30,7 +30,7 @@ Visitor.prototype = { // Performs an accept operation with added sanity check to ensure // required keys are not removed. - acceptRequired: function(node, name) { + acceptRequired: function (node, name) { this.acceptKey(node, name); if (!node[name]) { @@ -40,7 +40,7 @@ Visitor.prototype = { // Traverses a given array. If mutating, empty responses will be removed // for child elements. - acceptArray: function(array) { + acceptArray: function (array) { for (let i = 0, l = array.length; i < l; i++) { this.acceptKey(array, i); @@ -52,7 +52,7 @@ Visitor.prototype = { } }, - accept: function(object) { + accept: function (object) { if (!object) { return; } @@ -78,7 +78,7 @@ Visitor.prototype = { } }, - Program: function(program) { + Program: function (program) { this.acceptArray(program.body); }, @@ -89,31 +89,31 @@ Visitor.prototype = { DecoratorBlock: visitBlock, PartialStatement: visitPartial, - PartialBlockStatement: function(partial) { + PartialBlockStatement: function (partial) { visitPartial.call(this, partial); this.acceptKey(partial, 'program'); }, - ContentStatement: function(/* content */) {}, - CommentStatement: function(/* comment */) {}, + ContentStatement: function (/* content */) {}, + CommentStatement: function (/* comment */) {}, SubExpression: visitSubExpression, - PathExpression: function(/* path */) {}, + PathExpression: function (/* path */) {}, - StringLiteral: function(/* string */) {}, - NumberLiteral: function(/* number */) {}, - BooleanLiteral: function(/* bool */) {}, - UndefinedLiteral: function(/* literal */) {}, - NullLiteral: function(/* literal */) {}, + StringLiteral: function (/* string */) {}, + NumberLiteral: function (/* number */) {}, + BooleanLiteral: function (/* bool */) {}, + UndefinedLiteral: function (/* literal */) {}, + NullLiteral: function (/* literal */) {}, - Hash: function(hash) { + Hash: function (hash) { this.acceptArray(hash.pairs); }, - HashPair: function(pair) { + HashPair: function (pair) { this.acceptRequired(pair, 'value'); - } + }, }; function visitSubExpression(mustache) { diff --git a/lib/whitespace-control.js b/lib/whitespace-control.js index e85d66a..8d98c14 100644 --- a/lib/whitespace-control.js +++ b/lib/whitespace-control.js @@ -1,11 +1,11 @@ -import Visitor from './visitor'; +import Visitor from './visitor.js'; function WhitespaceControl(options = {}) { this.options = options; } WhitespaceControl.prototype = new Visitor(); -WhitespaceControl.prototype.Program = function(program) { +WhitespaceControl.prototype.Program = function (program) { const doStandalone = !this.options.ignoreStandalone; let isRoot = !this.isRootSeen; @@ -62,88 +62,87 @@ WhitespaceControl.prototype.Program = function(program) { return program; }; -WhitespaceControl.prototype.BlockStatement = WhitespaceControl.prototype.DecoratorBlock = WhitespaceControl.prototype.PartialBlockStatement = function( - block -) { - this.accept(block.program); - this.accept(block.inverse); - - // Find the inverse program that is involved with whitespace stripping. - let program = block.program || block.inverse, - inverse = block.program && block.inverse, - firstInverse = inverse, - lastInverse = inverse; - - if (inverse && inverse.chained) { - firstInverse = inverse.body[0].program; - - // Walk the inverse chain to find the last inverse that is actually in the chain. - while (lastInverse.chained) { - lastInverse = lastInverse.body[lastInverse.body.length - 1].program; - } - } +WhitespaceControl.prototype.BlockStatement = + WhitespaceControl.prototype.DecoratorBlock = + WhitespaceControl.prototype.PartialBlockStatement = + function (block) { + this.accept(block.program); + this.accept(block.inverse); + + // Find the inverse program that is involved with whitespace stripping. + let program = block.program || block.inverse, + inverse = block.program && block.inverse, + firstInverse = inverse, + lastInverse = inverse; + + if (inverse && inverse.chained) { + firstInverse = inverse.body[0].program; + + // Walk the inverse chain to find the last inverse that is actually in the chain. + while (lastInverse.chained) { + lastInverse = lastInverse.body[lastInverse.body.length - 1].program; + } + } - let strip = { - open: block.openStrip.open, - close: block.closeStrip.close, + let strip = { + open: block.openStrip.open, + close: block.closeStrip.close, - // Determine the standalone candidacy. Basically flag our content as being possibly standalone - // so our parent can determine if we actually are standalone - openStandalone: isNextWhitespace(program.body), - closeStandalone: isPrevWhitespace((firstInverse || program).body) - }; + // Determine the standalone candidacy. Basically flag our content as being possibly standalone + // so our parent can determine if we actually are standalone + openStandalone: isNextWhitespace(program.body), + closeStandalone: isPrevWhitespace((firstInverse || program).body), + }; - if (block.openStrip.close) { - omitRight(program.body, null, true); - } + if (block.openStrip.close) { + omitRight(program.body, null, true); + } - if (inverse) { - let inverseStrip = block.inverseStrip; + if (inverse) { + let inverseStrip = block.inverseStrip; - if (inverseStrip.open) { - omitLeft(program.body, null, true); - } + if (inverseStrip.open) { + omitLeft(program.body, null, true); + } - if (inverseStrip.close) { - omitRight(firstInverse.body, null, true); - } - if (block.closeStrip.open) { - omitLeft(lastInverse.body, null, true); - } + if (inverseStrip.close) { + omitRight(firstInverse.body, null, true); + } + if (block.closeStrip.open) { + omitLeft(lastInverse.body, null, true); + } - // Find standalone else statements - if ( - !this.options.ignoreStandalone && - isPrevWhitespace(program.body) && - isNextWhitespace(firstInverse.body) - ) { - omitLeft(program.body); - omitRight(firstInverse.body); - } - } else if (block.closeStrip.open) { - omitLeft(program.body, null, true); - } + // Find standalone else statements + if ( + !this.options.ignoreStandalone && + isPrevWhitespace(program.body) && + isNextWhitespace(firstInverse.body) + ) { + omitLeft(program.body); + omitRight(firstInverse.body); + } + } else if (block.closeStrip.open) { + omitLeft(program.body, null, true); + } - return strip; -}; + return strip; + }; -WhitespaceControl.prototype.Decorator = WhitespaceControl.prototype.MustacheStatement = function( - mustache -) { - return mustache.strip; -}; +WhitespaceControl.prototype.Decorator = + WhitespaceControl.prototype.MustacheStatement = function (mustache) { + return mustache.strip; + }; -WhitespaceControl.prototype.PartialStatement = WhitespaceControl.prototype.CommentStatement = function( - node -) { - /* istanbul ignore next */ - let strip = node.strip || {}; - return { - inlineStandalone: true, - open: strip.open, - close: strip.close +WhitespaceControl.prototype.PartialStatement = + WhitespaceControl.prototype.CommentStatement = function (node) { + /* istanbul ignore next */ + let strip = node.strip || {}; + return { + inlineStandalone: true, + open: strip.open, + close: strip.close, + }; }; -}; function isPrevWhitespace(body, i, isRoot) { if (i === undefined) { diff --git a/package.json b/package.json index 29acaa3..bc373f8 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "@handlebars/parser", + "type": "module", "version": "2.1.0", "description": "The parser for the Handlebars language", "homepage": "https://github.com/handlebars-lang/handlebars-parser#readme", @@ -15,16 +16,35 @@ "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "types": "types/index.d.ts", + "exports": { + ".": { + "require": { + "types": "./types/index.d.ts", + "default": "./dist/cjs/index.js" + }, + "import": { + "types": "./types/index.d.ts", + "default": "./dist/esm/index.js" + }, + "default": { + "types": "./types/index.d.ts", + "default": "./dist/cjs/index.js" + } + } + }, "scripts": { "lint": "eslint .", "prepublishOnly": "pnpm run build", "build": "npm-run-all build:parser build:esm build:cjs", - "build:cjs": "tsc --module commonjs --target es5 --outDir dist/cjs", - "build:esm": "tsc --module es2015 --target es5 --outDir dist/esm", - "build:jison": "jison -m js src/handlebars.yy src/handlebars.l -o lib/parser.js", + "build:cjs": "tsc --module nodenext --moduleResolution nodenext --target es5 --outDir dist/cjs", + "build:esm": "tsc --module es2020 --outDir dist/esm", + "build:jison": "node scripts/compile-parser.cjs", "build:parser": "npm-run-all build:jison build:parser-suffix", "build:parser-suffix": "combine-files lib/parser.js,src/parser-suffix.js lib/parser.js", - "test": "pnpm run build && mocha spec --require esm" + "pretest": "pnpm run build", + "test": "mocha --inline-diffs spec", + "pretest:bail": "pnpm run build", + "test:bail": "mocha --bail --inline-diffs spec" }, "prettier": { "tabWidth": 2, @@ -35,10 +55,9 @@ "combine-files": "^1.1.8", "eslint": "^8.57.1", "eslint-config-prettier": "^8.10.0", - "eslint-plugin-compat": "^3.13.0", - "esm": "^3.2.25", + "eslint-plugin-compat": "^6.0.1", "jison": "^0.4.18", - "mocha": "^8.1.3", + "mocha": "^10.7.3", "npm-run-all": "^4.1.5", "prettier": "^2.1.1", "release-it": "^14.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cef0bd9..a2e74b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,17 +18,14 @@ importers: specifier: ^8.10.0 version: 8.10.0(eslint@8.57.1) eslint-plugin-compat: - specifier: ^3.13.0 - version: 3.13.0(eslint@8.57.1) - esm: - specifier: ^3.2.25 - version: 3.2.25 + specifier: ^6.0.1 + version: 6.0.1(eslint@8.57.1) jison: specifier: ^0.4.18 version: 0.4.18 mocha: - specifier: ^8.1.3 - version: 8.4.0 + specifier: ^10.7.3 + version: 10.7.3 npm-run-all: specifier: ^4.1.5 version: 4.1.5 @@ -93,8 +90,8 @@ packages: '@iarna/toml@2.2.5': resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==} - '@mdn/browser-compat-data@3.3.14': - resolution: {integrity: sha512-n2RC9d6XatVbWFdHLimzzUJxJ1KY8LdjqrW6YvGPiRmsHkhOUx74/Ct10x5Yo7bC/Jvqx7cDEW8IMPv/+vwEzA==} + '@mdn/browser-compat-data@5.6.9': + resolution: {integrity: sha512-xbpYnhcx48qe1p8qimSCUu79QPhK6STaj5mUJ7A0VRCxgfZ5boJ4L/Vy9e5lOPquPSQ1tWZ6mOO+01VzLJg2iA==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -196,9 +193,6 @@ packages: '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} - '@ungap/promise-all-settled@1.1.2': - resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} - '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} @@ -253,18 +247,14 @@ packages: ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} - ansi-colors@4.1.1: - resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} - ansi-regex@3.0.1: - resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} - engines: {node: '>=4'} - ansi-regex@4.1.1: resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} engines: {node: '>=6'} @@ -310,8 +300,8 @@ packages: resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} engines: {node: '>= 0.4'} - ast-metadata-inferer@0.7.0: - resolution: {integrity: sha512-OkMLzd8xelb3gmnp6ToFvvsHLtS6CbagTkFQvQ+ZYFe3/AIl9iKikNR9G7pY3GfOR/2Xc222hwBjzI7HLkE76Q==} + ast-metadata-inferer@0.8.0: + resolution: {integrity: sha512-jOMKcHht9LxYIEQu+RVd22vtgrPaVCtDRQ/16IGmurdzxvYbDd5ynxjnyrzLnieG96eTcAyaoj/wN/4/1FyyeA==} ast-types@0.13.4: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} @@ -357,6 +347,9 @@ packages: brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -433,8 +426,8 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - chokidar@3.5.1: - resolution: {integrity: sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==} + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} chownr@1.1.4: @@ -529,9 +522,6 @@ packages: resolution: {integrity: sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==} deprecated: This package is no longer supported. - core-js@3.38.1: - resolution: {integrity: sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==} - core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -571,15 +561,6 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} - debug@4.3.1: - resolution: {integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.3.2: resolution: {integrity: sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==} engines: {node: '>=6.0'} @@ -679,8 +660,8 @@ packages: resolution: {integrity: sha512-sarumrIS8/WEcRudIG0PQRSJQ7TLX6WAPrYg4SZtaYSoc5wMXzL1f2HU2dO7G/9X87yk7LgGk8fkKxTm7ZweGQ==} engines: {node: '>=4.0.0'} - diff@5.0.0: - resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} engines: {node: '>=0.3.1'} dir-glob@3.0.1: @@ -790,11 +771,11 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-compat@3.13.0: - resolution: {integrity: sha512-cv8IYMuTXm7PIjMVDN2y4k/KVnKZmoNGHNq27/9dLstOLydKblieIv+oe2BN2WthuXnFNhaNvv3N1Bvl4dbIGA==} - engines: {node: '>=9.x'} + eslint-plugin-compat@6.0.1: + resolution: {integrity: sha512-0MeIEuoy8kWkOhW38kK8hU4vkb6l/VvyjpuYDymYOXmUY9NvTgyErF16lYuX+HPS5hkmym7lfA+XpYZiWYWmYA==} + engines: {node: '>=18.x'} peerDependencies: - eslint: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + eslint: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 eslint-scope@7.2.2: resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} @@ -810,10 +791,6 @@ packages: deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true - esm@3.2.25: - resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} - engines: {node: '>=6'} - espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1025,14 +1002,15 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@7.1.6: - resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} - deprecated: Glob versions prior to v9 are no longer supported - glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported + global-dirs@2.1.0: resolution: {integrity: sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==} engines: {node: '>=8'} @@ -1045,6 +1023,10 @@ packages: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} + globals@15.11.0: + resolution: {integrity: sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -1074,10 +1056,6 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - growl@1.10.5: - resolution: {integrity: sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==} - engines: {node: '>=4.x'} - has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -1475,10 +1453,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.0.0: - resolution: {integrity: sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==} - hasBin: true - js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1569,10 +1543,6 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - log-symbols@4.0.0: - resolution: {integrity: sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==} - engines: {node: '>=10'} - log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -1647,12 +1617,13 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - minimatch@3.0.4: - resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} - minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1697,9 +1668,9 @@ packages: engines: {node: '>=10'} hasBin: true - mocha@8.4.0: - resolution: {integrity: sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==} - engines: {node: '>= 10.12.0'} + mocha@10.7.3: + resolution: {integrity: sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==} + engines: {node: '>= 14.0.0'} hasBin: true move-concurrently@1.0.1: @@ -1718,11 +1689,6 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.1.20: - resolution: {integrity: sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -2070,8 +2036,8 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - readdirp@3.5.0: - resolution: {integrity: sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==} + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} rechoir@0.6.2: @@ -2218,8 +2184,8 @@ packages: engines: {node: '>=10'} hasBin: true - serialize-javascript@5.0.1: - resolution: {integrity: sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==} + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -2338,10 +2304,6 @@ packages: resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} engines: {node: '>=4'} - string-width@2.1.1: - resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} - engines: {node: '>=4'} - string-width@3.1.0: resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==} engines: {node: '>=6'} @@ -2374,10 +2336,6 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-ansi@4.0.0: - resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} - engines: {node: '>=4'} - strip-ansi@5.2.0: resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} engines: {node: '>=6'} @@ -2618,9 +2576,6 @@ packages: engines: {node: '>= 8'} hasBin: true - wide-align@1.1.3: - resolution: {integrity: sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==} - widest-line@3.1.0: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} @@ -2641,8 +2596,8 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - workerpool@6.1.0: - resolution: {integrity: sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==} + workerpool@6.5.1: + resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} wrap-ansi@5.1.0: resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} @@ -2738,7 +2693,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -2754,7 +2709,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -2765,7 +2720,7 @@ snapshots: '@iarna/toml@2.2.5': {} - '@mdn/browser-compat-data@3.3.14': {} + '@mdn/browser-compat-data@5.6.9': {} '@nodelib/fs.scandir@2.1.5': dependencies: @@ -2908,8 +2863,6 @@ snapshots: dependencies: '@types/node': 22.7.5 - '@ungap/promise-all-settled@1.1.2': {} - '@ungap/structured-clone@1.2.0': {} JSONSelect@0.4.0: {} @@ -2934,7 +2887,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -2961,14 +2914,12 @@ snapshots: dependencies: string-width: 4.2.3 - ansi-colors@4.1.1: {} + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 - ansi-regex@3.0.1: {} - ansi-regex@4.1.1: {} ansi-regex@5.0.1: {} @@ -3019,9 +2970,9 @@ snapshots: is-array-buffer: 3.0.4 is-shared-array-buffer: 1.0.3 - ast-metadata-inferer@0.7.0: + ast-metadata-inferer@0.8.0: dependencies: - '@mdn/browser-compat-data': 3.3.14 + '@mdn/browser-compat-data': 5.6.9 ast-types@0.13.4: dependencies: @@ -3082,6 +3033,10 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -3189,7 +3144,7 @@ snapshots: chardet@0.7.0: {} - chokidar@3.5.1: + chokidar@3.6.0: dependencies: anymatch: 3.1.3 braces: 3.0.3 @@ -3197,7 +3152,7 @@ snapshots: is-binary-path: 2.1.0 is-glob: 4.0.3 normalize-path: 3.0.0 - readdirp: 3.5.0 + readdirp: 3.6.0 optionalDependencies: fsevents: 2.3.3 @@ -3292,8 +3247,6 @@ snapshots: rimraf: 2.7.1 run-queue: 1.0.3 - core-js@3.38.1: {} - core-util-is@1.0.3: {} cosmiconfig@7.0.0: @@ -3348,12 +3301,6 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 - debug@4.3.1(supports-color@8.1.1): - dependencies: - ms: 2.1.2 - optionalDependencies: - supports-color: 8.1.1 - debug@4.3.2(supports-color@7.2.0): dependencies: ms: 2.1.2 @@ -3364,9 +3311,11 @@ snapshots: dependencies: ms: 2.1.2 - debug@4.3.7: + debug@4.3.7(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 decamelize@1.2.0: {} @@ -3435,7 +3384,7 @@ snapshots: lodash.find: 4.6.0 pify: 2.3.0 - diff@5.0.0: {} + diff@5.2.0: {} dir-glob@3.0.1: dependencies: @@ -3594,17 +3543,17 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-plugin-compat@3.13.0(eslint@8.57.1): + eslint-plugin-compat@6.0.1(eslint@8.57.1): dependencies: - '@mdn/browser-compat-data': 3.3.14 - ast-metadata-inferer: 0.7.0 + '@mdn/browser-compat-data': 5.6.9 + ast-metadata-inferer: 0.8.0 browserslist: 4.24.0 caniuse-lite: 1.0.30001668 - core-js: 3.38.1 eslint: 8.57.1 find-up: 5.0.0 + globals: 15.11.0 lodash.memoize: 4.1.2 - semver: 7.3.5 + semver: 7.6.3 eslint-scope@7.2.2: dependencies: @@ -3626,7 +3575,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -3656,8 +3605,6 @@ snapshots: transitivePeerDependencies: - supports-color - esm@3.2.25: {} - espree@9.6.1: dependencies: acorn: 8.12.1 @@ -3877,7 +3824,7 @@ snapshots: dependencies: '@tootallnate/once': 1.1.2 data-uri-to-buffer: 3.0.1 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) file-uri-to-path: 2.0.0 fs-extra: 8.1.0 ftp: 0.3.10 @@ -3905,23 +3852,22 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@7.1.6: + glob@7.2.3: dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.0.4 + minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - glob@7.2.3: + glob@8.1.0: dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 3.1.2 + minimatch: 5.1.6 once: 1.4.0 - path-is-absolute: 1.0.1 global-dirs@2.1.0: dependencies: @@ -3935,6 +3881,8 @@ snapshots: dependencies: type-fest: 0.20.2 + globals@15.11.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -3996,8 +3944,6 @@ snapshots: graphemer@1.4.0: {} - growl@1.10.5: {} - has-bigints@1.0.2: {} has-flag@3.0.0: {} @@ -4041,7 +3987,7 @@ snapshots: http-proxy-agent@3.0.0: dependencies: agent-base: 5.1.1 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -4049,7 +3995,7 @@ snapshots: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -4061,14 +4007,14 @@ snapshots: https-proxy-agent@4.0.0: dependencies: agent-base: 5.1.1 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -4373,10 +4319,6 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.0.0: - dependencies: - argparse: 2.0.1 - js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -4472,10 +4414,6 @@ snapshots: lodash@4.17.21: {} - log-symbols@4.0.0: - dependencies: - chalk: 4.1.2 - log-symbols@4.1.0: dependencies: chalk: 4.1.2 @@ -4549,13 +4487,13 @@ snapshots: mimic-response@3.1.0: {} - minimatch@3.0.4: + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 - minimatch@3.1.2: + minimatch@5.1.6: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 2.0.1 minimist@1.2.8: {} @@ -4600,32 +4538,27 @@ snapshots: mkdirp@1.0.4: {} - mocha@8.4.0: + mocha@10.7.3: dependencies: - '@ungap/promise-all-settled': 1.1.2 - ansi-colors: 4.1.1 + ansi-colors: 4.1.3 browser-stdout: 1.3.1 - chokidar: 3.5.1 - debug: 4.3.1(supports-color@8.1.1) - diff: 5.0.0 + chokidar: 3.6.0 + debug: 4.3.7(supports-color@8.1.1) + diff: 5.2.0 escape-string-regexp: 4.0.0 find-up: 5.0.0 - glob: 7.1.6 - growl: 1.10.5 + glob: 8.1.0 he: 1.2.0 - js-yaml: 4.0.0 - log-symbols: 4.0.0 - minimatch: 3.0.4 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.1.6 ms: 2.1.3 - nanoid: 3.1.20 - serialize-javascript: 5.0.1 + serialize-javascript: 6.0.2 strip-json-comments: 3.1.1 supports-color: 8.1.1 - which: 2.0.2 - wide-align: 1.1.3 - workerpool: 6.1.0 + workerpool: 6.5.1 yargs: 16.2.0 - yargs-parser: 20.2.4 + yargs-parser: 20.2.9 yargs-unparser: 2.0.0 move-concurrently@1.0.1: @@ -4649,8 +4582,6 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.1.20: {} - natural-compare@1.4.0: {} netmask@2.0.2: {} @@ -4809,7 +4740,7 @@ snapshots: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) get-uri: 3.0.2 http-proxy-agent: 4.0.1 https-proxy-agent: 5.0.1 @@ -4935,7 +4866,7 @@ snapshots: proxy-agent@5.0.0: dependencies: agent-base: 6.0.2 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) http-proxy-agent: 4.0.1 https-proxy-agent: 5.0.1 lru-cache: 5.1.1 @@ -5020,7 +4951,7 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 - readdirp@3.5.0: + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -5224,7 +5155,7 @@ snapshots: semver@7.6.3: {} - serialize-javascript@5.0.1: + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -5295,7 +5226,7 @@ snapshots: socks-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.7 + debug: 4.3.7(supports-color@8.1.1) socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -5349,11 +5280,6 @@ snapshots: strict-uri-encode@2.0.0: {} - string-width@2.1.1: - dependencies: - is-fullwidth-code-point: 2.0.0 - strip-ansi: 4.0.0 - string-width@3.1.0: dependencies: emoji-regex: 7.0.3 @@ -5402,10 +5328,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - strip-ansi@4.0.0: - dependencies: - ansi-regex: 3.0.1 - strip-ansi@5.2.0: dependencies: ansi-regex: 4.1.1 @@ -5664,10 +5586,6 @@ snapshots: dependencies: isexe: 2.0.0 - wide-align@1.1.3: - dependencies: - string-width: 2.1.1 - widest-line@3.1.0: dependencies: string-width: 4.2.3 @@ -5685,7 +5603,7 @@ snapshots: word-wrap@1.2.5: {} - workerpool@6.1.0: {} + workerpool@6.5.1: {} wrap-ansi@5.1.0: dependencies: diff --git a/scripts/compile-parser.cjs b/scripts/compile-parser.cjs new file mode 100644 index 0000000..dfb9f16 --- /dev/null +++ b/scripts/compile-parser.cjs @@ -0,0 +1,19 @@ +const cli = require('jison/lib/cli'); + +// This is a workaround for https://github.com/zaach/jison/pull/352 having never been merged +const oldProcessGrammars = cli.processGrammars; + +cli.processGrammars = function (...args) { + const grammar = oldProcessGrammars.call(this, ...args); + grammar.options = grammar.options ?? {}; + grammar.options['token-stack'] = true; + return grammar; +}; + +cli.main({ + moduleType: 'js', + file: 'src/handlebars.yy', + lexfile: 'src/handlebars.l', + outfile: 'lib/parser.js', + 'token-stack': true, +}); diff --git a/spec/.eslintrc.js b/spec/.eslintrc.cjs similarity index 61% rename from spec/.eslintrc.js rename to spec/.eslintrc.cjs index 9fa44c3..f078531 100644 --- a/spec/.eslintrc.js +++ b/spec/.eslintrc.cjs @@ -1,5 +1,5 @@ module.exports = { - extends: ['../.eslintrc.js'], + extends: ['../.eslintrc.cjs'], env: { mocha: true, }, diff --git a/spec/ast.js b/spec/ast.js index fb45f00..31c4c44 100644 --- a/spec/ast.js +++ b/spec/ast.js @@ -1,17 +1,17 @@ -import { parse, parseWithoutProcessing } from '../dist/esm'; -import { equals } from './utils'; +import { parse, parseWithoutProcessing } from '../dist/esm/index.js'; +import { equals } from './utils.js'; -describe('ast', function() { - describe('whitespace control', function() { - describe('parse', function() { - it('mustache', function() { +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() { + it('block statements', function () { let ast = parse(' {{# comment~}} \nfoo\n {{~/comment}}'); equals(ast.body[0].value, ''); @@ -19,15 +19,15 @@ describe('ast', function() { }); }); - describe('parseWithoutProcessing', function() { - it('mustache', function() { + describe('parseWithoutProcessing', function () { + it('mustache', function () { let ast = parseWithoutProcessing(' {{~comment~}} '); equals(ast.body[0].value, ' '); equals(ast.body[2].value, ' '); }); - it('block statements', function() { + it('block statements', function () { let ast = parseWithoutProcessing( ' {{# comment~}} \nfoo\n {{~/comment}}' ); @@ -90,19 +90,19 @@ describe('ast', function() { }); }); - describe('standalone flags', function() { - describe('mustache', function() { - it('does not mark mustaches as standalone', function() { + 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() { + describe('blocks - parseWithoutProcessing', function () { + it('block mustaches', function () { let ast = parseWithoutProcessing( - ' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} ' - ), + ' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} ' + ), block = ast.body[1]; equals(ast.body[0].value, ' '); @@ -112,28 +112,26 @@ describe('ast', function() { equals(ast.body[2].value, ' '); }); - it('initial block mustaches', function() { - let ast = parseWithoutProcessing( - '{{# comment}} \nfoo\n {{/comment}}' - ), + 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() { + it('mustaches with children', function () { let ast = parseWithoutProcessing( - '{{# comment}} \n{{foo}}\n {{/comment}}' - ), + '{{# 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() { + it('nested block mustaches', function () { let ast = parseWithoutProcessing( - '{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}' - ), + '{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}' + ), body = ast.body[0].program.body, block = body[1]; @@ -142,10 +140,10 @@ describe('ast', function() { equals(block.program.body[0].value, ' \nfoo\n '); equals(block.inverse.body[0].value, ' \n bar \n '); }); - it('column 0 block mustaches', function() { + it('column 0 block mustaches', function () { let ast = parseWithoutProcessing( - 'test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} ' - ), + 'test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} ' + ), block = ast.body[1]; equals(ast.body[0].omit, undefined); @@ -156,11 +154,11 @@ describe('ast', function() { equals(ast.body[2].value, ' '); }); }); - describe('blocks', function() { - it('marks block mustaches as standalone', function() { + describe('blocks', function () { + it('marks block mustaches as standalone', function () { let ast = parse( - ' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} ' - ), + ' {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} ' + ), block = ast.body[1]; equals(ast.body[0].value, ''); @@ -170,13 +168,13 @@ describe('ast', function() { equals(ast.body[2].value, ''); }); - it('marks initial block mustaches as standalone', function() { + 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() { + it('marks mustaches with children as standalone', function () { let ast = parse('{{# comment}} \n{{foo}}\n {{/comment}}'), block = ast.body[0]; @@ -184,10 +182,10 @@ describe('ast', function() { equals(block.program.body[1].path.original, 'foo'); equals(block.program.body[2].value, '\n'); }); - it('marks nested block mustaches as standalone', function() { + it('marks nested block mustaches as standalone', function () { let ast = parse( - '{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}' - ), + '{{#foo}} \n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} \n{{/foo}}' + ), body = ast.body[0].program.body, block = body[1]; @@ -198,10 +196,10 @@ describe('ast', function() { equals(body[0].value, ''); }); - it('does not mark nested block mustaches as standalone', function() { + it('does not mark nested block mustaches as standalone', function () { let ast = parse( - '{{#foo}} {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} {{/foo}}' - ), + '{{#foo}} {{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} {{/foo}}' + ), body = ast.body[0].program.body, block = body[1]; @@ -212,10 +210,10 @@ describe('ast', function() { equals(body[0].omit, undefined); }); - it('does not mark nested initial block mustaches as standalone', function() { + it('does not mark nested initial block mustaches as standalone', function () { let ast = parse( - '{{#foo}}{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}}{{/foo}}' - ), + '{{#foo}}{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}}{{/foo}}' + ), body = ast.body[0].program.body, block = body[0]; @@ -225,10 +223,10 @@ describe('ast', function() { equals(body[0].omit, undefined); }); - it('marks column 0 block mustaches as standalone', function() { + it('marks column 0 block mustaches as standalone', function () { let ast = parse( - 'test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} ' - ), + 'test\n{{# comment}} \nfoo\n {{else}} \n bar \n {{/comment}} ' + ), block = ast.body[1]; equals(ast.body[0].omit, undefined); @@ -239,30 +237,30 @@ describe('ast', function() { equals(ast.body[2].value, ''); }); }); - describe('partials - parseWithoutProcessing', function() { - it('simple partial', function() { + describe('partials - parseWithoutProcessing', function () { + it('simple partial', function () { let ast = parseWithoutProcessing('{{> partial }} '); equals(ast.body[1].value, ' '); }); - it('indented partial', function() { + 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() { + 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() { + 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() { + it('marks those around content as not standalone', function () { let ast = parse('a{{> partial }}'); equals(ast.body[0].omit, undefined); @@ -270,28 +268,28 @@ describe('ast', function() { equals(ast.body[1].omit, undefined); }); }); - describe('comments - parseWithoutProcessing', function() { - it('simple comment', function() { + describe('comments - parseWithoutProcessing', function () { + it('simple comment', function () { let ast = parseWithoutProcessing('{{! comment }} '); equals(ast.body[1].value, ' '); }); - it('indented comment', function() { + 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() { + 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() { + 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() { + it('marks those around content as not standalone', function () { let ast = parse('a{{! comment }}'); equals(ast.body[0].omit, undefined); diff --git a/spec/parser.js b/spec/parser.js index 2c2c476..e96815b 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -1,5 +1,5 @@ -import { parse, print } from '../dist/esm'; -import { equals, equalsAst, shouldThrow } from './utils'; +import { parse, print } from '../dist/esm/index.js'; +import { equals, equalsAst, shouldThrow } from './utils.js'; describe('parser', function () { function astFor(template) { @@ -96,30 +96,30 @@ describe('parser', function () { equalsAst( '{{foo bar=baz bat=bam}}', - '{{ p%foo [] HASH{bar=p%baz bat=p%bam} }}', + '{{ p%foo [] HASH{bar=p%baz bat=p%bam} }}' ); equalsAst( '{{foo bar=baz bat="bam"}}', - '{{ p%foo [] HASH{bar=p%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"} }}', + '{{ 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} }}', + '{{ 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} }}', + '{{ 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} }}', + '{{ p%foo [p%omg] HASH{bar=p%baz bat="bam" baz=b%false} }}' ); }); @@ -144,21 +144,21 @@ describe('parser', function () { it('parses a partial with context and hash', function () { equalsAst( '{{> foo bar bat=baz}}', - '{{> PARTIAL:foo p%bar HASH{bat=p%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 }}', + '{{> PARTIAL:shared/partial?.bar }}' ); }); it('parsers partial blocks', function () { equalsAst( '{{#> foo}}bar{{/foo}}', - "{{> PARTIAL BLOCK:foo PROGRAM:\n CONTENT[ 'bar' ]\n }}", + "{{> PARTIAL BLOCK:foo PROGRAM:\n CONTENT[ 'bar' ]\n }}" ); }); it('should handle parser block mismatch', function () { @@ -167,13 +167,13 @@ describe('parser', function () { astFor('{{#> goodbyes}}{{/hellos}}'); }, Error, - /goodbyes doesn't match hellos/, + /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 }}", + "{{> PARTIAL BLOCK:foo p%context HASH{hash=p%value} PROGRAM:\n CONTENT[ 'bar' ]\n }}" ); }); @@ -184,28 +184,28 @@ describe('parser', function () { it('parses a multi-line comment', function () { equalsAst( '{{!\nthis is a multi-line comment\n}}', - "{{! '\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 ' ]", + "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 ' ]", + "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 ' ]", + "BLOCK:\n p%foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n p%if [p%bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]" ); }); @@ -216,49 +216,49 @@ describe('parser', function () { it('parses empty blocks with empty inverse section', function () { equalsAst( '{{#foo}}{{^}}{{/foo}}', - 'BLOCK:\n p%foo []\n PROGRAM:\n {{^}}', + '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 {{^}}', + '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 {{^}}", + "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 {{^}}", + "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 ' ]", + "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 ' ]", + "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' ]", + "BLOCK:\n p%foo []\n {{^}}\n CONTENT[ 'bar' ]" ); }); @@ -271,7 +271,7 @@ describe('parser', function () { 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' ]", + "BLOCK:\n p%foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]" ); }); @@ -290,55 +290,55 @@ describe('parser', function () { 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] [] }}', + '{{ 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] []} }}', + '{{ 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 [] }}', + '{{ 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] [] }}', + '{{ 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] [] }}', + '{{ 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} [] }}', + '{{ 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' ]", + "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' ]", + "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 () { @@ -347,28 +347,28 @@ describe('parser', function () { astFor('foo{{^}}bar'); }, Error, - /Parse error on line 1/, + /Parse error on line 1/ ); shouldThrow( function () { astFor('{{foo}'); }, Error, - /Parse error on line 1/, + /Parse error on line 1/ ); shouldThrow( function () { astFor('{{foo &}}'); }, Error, - /Parse error on line 1/, + /Parse error on line 1/ ); shouldThrow( function () { astFor('{{#goodbyes}}{{/hellos}}'); }, Error, - /goodbyes doesn't match hellos/, + /goodbyes doesn't match hellos/ ); shouldThrow( @@ -376,7 +376,7 @@ describe('parser', function () { astFor('{{{{goodbyes}}}} {{{{/hellos}}}}'); }, Error, - /goodbyes doesn't match hellos/, + /goodbyes doesn't match hellos/ ); }); @@ -386,21 +386,21 @@ describe('parser', function () { astFor('{{foo/../bar}}'); }, Error, - /Invalid path: foo\/\.\. - 1:2/, + /Invalid path: foo\/\.\. - 1:2/ ); shouldThrow( function () { astFor('{{foo/./bar}}'); }, Error, - /Invalid path: foo\/\. - 1:2/, + /Invalid path: foo\/\. - 1:2/ ); shouldThrow( function () { astFor('{{foo/this/bar}}'); }, Error, - /Invalid path: foo\/this - 1:2/, + /Invalid path: foo\/this - 1:2/ ); }); @@ -410,14 +410,14 @@ describe('parser', function () { astFor('hello\nmy\n{{foo}'); }, Error, - /Parse error on line 3/, + /Parse error on line 3/ ); shouldThrow( function () { astFor('hello\n\nmy\n\n{{foo}'); }, Error, - /Parse error on line 5/, + /Parse error on line 5/ ); }); @@ -427,7 +427,7 @@ describe('parser', function () { astFor('\n\nhello\n\nmy\n\n{{foo}'); }, Error, - /Parse error on line 7/, + /Parse error on line 7/ ); }); @@ -438,7 +438,7 @@ describe('parser', function () { type: 'Program', body: [{ type: 'ContentStatement', value: 'Hello' }], }), - "CONTENT[ 'Hello' ]\n", + "CONTENT[ 'Hello' ]\n" ); }); }); @@ -447,7 +447,7 @@ describe('parser', function () { it('should parse block directives', function () { equalsAst( '{{#* foo}}{{/foo}}', - 'DIRECTIVE BLOCK:\n p%foo []\n PROGRAM:', + 'DIRECTIVE BLOCK:\n p%foo []\n PROGRAM:' ); }); it('should parse directives', function () { @@ -459,7 +459,7 @@ describe('parser', function () { astFor('{{#* foo}}{{^}}{{/foo}}'); }, Error, - /Unexpected inverse/, + /Unexpected inverse/ ); }); }); @@ -472,7 +472,7 @@ describe('parser', function () { ' {{else}} {{baz}}\n' + '\n' + ' {{/if}}\n' + - ' ', + ' ' ); // We really need a deep equals but for now this should be stable... @@ -481,21 +481,21 @@ describe('parser', function () { 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/spec/utils.js b/spec/utils.js index ad09270..cb58adf 100644 --- a/spec/utils.js +++ b/spec/utils.js @@ -1,4 +1,4 @@ -import { parse, print } from '../dist/esm'; +import { parse, print } from '../dist/esm/index.js'; let AssertError; if (Error.captureStackTrace) { @@ -23,7 +23,8 @@ if (Error.captureStackTrace) { export function equals(actual, expected, msg) { if (actual !== expected) { throw new AssertError( - `\n Actual: ${actual} Expected: ${expected}` + (msg ? `\n${msg}` : ''), + `\n Actual: ${actual} Expected: ${expected}` + + (msg ? `\n${msg}` : ''), equals ); } @@ -34,10 +35,10 @@ export function equalsAst(source, expected, msg) { if (ast !== `${expected}\n`) { throw new AssertError( - `\n Source: ${source}\n\n Actual: ${ast} Expected: ${expected}\n` + (msg ? `\n${msg}` : ''), + `\n Source: ${source}\n\n Actual: ${ast} Expected: ${expected}\n` + + (msg ? `\n${msg}` : ''), equals ); - } } @@ -52,28 +53,56 @@ export function shouldThrow(callback, type, msg) { 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, + const error = new AssertError( + `An error was thrown, but it had the wrong type. Original error:\n${snippet( + caught.stack + )}`, shouldThrow ); + error.expected = type.name; + error.actual = caught.constructor.name; + throw error; + } + + if (msg) { + if (typeof msg === 'string') { + if (msg !== caught.message) { + const error = new AssertError( + `Error message didn't match.\n\n${snippet(caught.stack)}` + + shouldThrow + ); + error.expected = msg; + error.actual = caught.message; + throw error; + } + } else if (msg instanceof RegExp) { + if (!msg.test(caught.message)) { + const error = new AssertError( + `Error message didn't match.\n\n${snippet(caught.stack)}` + + shouldThrow + ); + error.expected = msg; + error.actual = caught.message; + throw error; + } + } } } + if (failed) { - throw new AssertError('It failed to throw', shouldThrow); + throw new AssertError('Expected a thrown exception', shouldThrow); } } - function astFor(template) { - let ast = parse(template); - return print(ast); - } +function astFor(template) { + let ast = parse(template); + return print(ast); +} + +function snippet(string) { + return string + .split('\n') + .map(function (line) { + return ' | ' + line; + }) + .join('\n'); +} diff --git a/spec/visitor.js b/spec/visitor.js index 77a1dc6..9d04924 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -1,8 +1,8 @@ -import { Visitor, parse, print, Exception } from '../dist/esm'; -import { equals, shouldThrow } from './utils'; +import { Visitor, parse, print, Exception } from '../dist/esm/index.js'; +import { equals, shouldThrow } from './utils.js'; -describe('Visitor', function() { - it('should provide coverage', function() { +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(); @@ -16,16 +16,16 @@ describe('Visitor', function() { visitor.accept(parse('{{* bar }}')); }); - it('should traverse to stubs', function() { + it('should traverse to stubs', function () { let visitor = new Visitor(); - visitor.StringLiteral = function(string) { + visitor.StringLiteral = function (string) { equals(string.value, '2'); }; - visitor.NumberLiteral = function(number) { + visitor.NumberLiteral = function (number) { equals(number.value, 1); }; - visitor.BooleanLiteral = function(bool) { + visitor.BooleanLiteral = function (bool) { equals(bool.value, true); equals(this.parents.length, 3); @@ -33,13 +33,13 @@ describe('Visitor', function() { equals(this.parents[1].type, 'BlockStatement'); equals(this.parents[2].type, 'Program'); }; - visitor.PathExpression = function(id) { + visitor.PathExpression = function (id) { equals(/(foo\.)?bar$/.test(id.original), true); }; - visitor.ContentStatement = function(content) { + visitor.ContentStatement = function (content) { equals(content.value, ' '); }; - visitor.CommentStatement = function(comment) { + visitor.CommentStatement = function (comment) { equals(comment.value, 'comment'); }; @@ -50,39 +50,33 @@ describe('Visitor', function() { ); }); - describe('mutating', function() { - describe('fields', function() { - it('should replace value', function() { + describe('mutating', function () { + describe('fields', function () { + it('should replace value', function () { let visitor = new Visitor(); visitor.mutating = true; - visitor.StringLiteral = function(string) { + 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' - ); + equals(print(ast), '{{ p%foo [] HASH{foo=n%42} }}\n'); }); - it('should treat undefined resonse as identity', function() { + 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' - ); + equals(print(ast), '{{ p%foo [] HASH{foo=n%42} }}\n'); }); - it('should remove false responses', function() { + it('should remove false responses', function () { let visitor = new Visitor(); visitor.mutating = true; - visitor.Hash = function() { + visitor.Hash = function () { return false; }; @@ -90,13 +84,13 @@ describe('Visitor', function() { visitor.accept(ast); equals(print(ast), '{{ p%foo [] }}\n'); }); - it('should throw when removing required values', function() { + it('should throw when removing required values', function () { shouldThrow( - function() { + function () { let visitor = new Visitor(); visitor.mutating = true; - visitor.PathExpression = function() { + visitor.PathExpression = function () { return false; }; @@ -107,13 +101,13 @@ describe('Visitor', function() { 'MustacheStatement requires path' ); }); - it('should throw when returning non-node responses', function() { + it('should throw when returning non-node responses', function () { shouldThrow( - function() { + function () { let visitor = new Visitor(); visitor.mutating = true; - visitor.PathExpression = function() { + visitor.PathExpression = function () { return {}; }; @@ -125,12 +119,12 @@ describe('Visitor', function() { ); }); }); - describe('arrays', function() { - it('should replace value', function() { + describe('arrays', function () { + it('should replace value', function () { let visitor = new Visitor(); visitor.mutating = true; - visitor.StringLiteral = function(string) { + visitor.StringLiteral = function (string) { return { type: 'NumberLiteral', value: 42, loc: string.locInfo }; }; @@ -138,7 +132,7 @@ describe('Visitor', function() { visitor.accept(ast); equals(print(ast), '{{ p%foo [n%42] }}\n'); }); - it('should treat undefined resonse as identity', function() { + it('should treat undefined resonse as identity', function () { let visitor = new Visitor(); visitor.mutating = true; @@ -146,11 +140,11 @@ describe('Visitor', function() { visitor.accept(ast); equals(print(ast), '{{ p%foo [n%42] }}\n'); }); - it('should remove false responses', function() { + it('should remove false responses', function () { let visitor = new Visitor(); visitor.mutating = true; - visitor.NumberLiteral = function() { + visitor.NumberLiteral = function () { return false; }; diff --git a/tsconfig.json b/tsconfig.json index 715c1c6..ae08869 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,18 +8,25 @@ "baseUrl": "lib", "rootDir": "lib", "esModuleInterop": true, - "moduleResolution": "node", - + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, // Enhance Strictness "strict": true, "suppressImplicitAnyIndexErrors": false, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, - "newLine": "LF", "allowJs": true }, - "include": ["lib/**/*.js"], - "exclude": ["dist", "node_modules", ".vscode"] -} + "include": [ + "lib/**/*.js", + "lib/**/*.d.ts" + ], + "exclude": [ + "dist", + "lib/parser.js", + "node_modules", + ".vscode" + ] +} \ No newline at end of file From d5c7da5563d5d2f0ad454c02e6f59488dd420313 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Thu, 24 Oct 2024 15:13:42 -0700 Subject: [PATCH 2/9] Migrate helper to TS --- lib/exception.ts | 4 +- lib/helpers.js | 237 ----------------------------------- lib/helpers.ts | 298 +++++++++++++++++++++++++++++++++++++++++++++ lib/types.d.ts | 235 ----------------------------------- lib/types/ast.d.ts | 174 ++++++++++++++++++++++++++ lib/utils.ts | 5 + package.json | 2 +- spec/parser.js | 35 ++---- spec/utils.js | 23 +++- src/handlebars.yy | 6 +- tsconfig.json | 3 +- 11 files changed, 518 insertions(+), 504 deletions(-) delete mode 100644 lib/helpers.js create mode 100644 lib/helpers.ts delete mode 100644 lib/types.d.ts create mode 100644 lib/types/ast.d.ts create mode 100644 lib/utils.ts diff --git a/lib/exception.ts b/lib/exception.ts index f65f30e..12b887c 100644 --- a/lib/exception.ts +++ b/lib/exception.ts @@ -1,4 +1,4 @@ -import type { BaseNode } from './types/types.d.ts'; +import type { HasLocation } from './types/ast.js'; export default class Exception extends Error { readonly lineNumber: number | undefined; @@ -8,7 +8,7 @@ export default class Exception extends Error { readonly description: string | undefined; - constructor(message: string, node?: BaseNode) { + constructor(message: string, node?: HasLocation) { const loc = node?.loc; let line; let endLineNumber; diff --git a/lib/helpers.js b/lib/helpers.js deleted file mode 100644 index ef567e2..0000000 --- a/lib/helpers.js +++ /dev/null @@ -1,237 +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/lib/helpers.ts b/lib/helpers.ts new file mode 100644 index 0000000..6929e72 --- /dev/null +++ b/lib/helpers.ts @@ -0,0 +1,298 @@ +import Exception from './exception.js'; +import type { + CloseBlock, + InverseChain, + LocInfo, + OpenBlock, + OpenPartialBlock, + OpenRawBlock, + Part, + SourcePosition, + YY, +} from './types/types.js'; +import type * as ast from './types/ast.js'; +import { assert } from './utils.js'; + +function validateClose( + open: OpenBlock | OpenRawBlock | OpenPartialBlock, + close: CloseBlock | string +) { + const closeString = typeof close === 'string' ? close : close.path.original; + + if (open.path.type !== 'PathExpression') { + throw new Exception(`Unexpected block open (expected a path)`, open.path); + } + + if (open.path.original !== closeString) { + throw new Exception( + `${open.path.original} doesn't match ${closeString}`, + open.path + ); + } +} + +export class SourceLocation implements ast.SourceLocation { + source: string | undefined; + start: SourcePosition; + end: SourcePosition; + + constructor(source: string | undefined, locInfo: 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: string) { + if (/^\[.*\]$/.test(token)) { + return token.substring(1, token.length - 1); + } else { + return token; + } +} + +export function stripFlags(open: string, close: string) { + return { + open: open.charAt(2) === '~', + close: close.charAt(close.length - 3) === '~', + }; +} + +export function stripComment(comment: string) { + return comment.replace(/^\{\{~?!-?-?/, '').replace(/-?-?~?\}\}$/, ''); +} + +export function preparePath( + this: YY, + data: boolean, + sexpr: ast.PathExpression | false, + parts: Part[], + locInfo: LocInfo +) { + const loc = this.locInfo(locInfo); + + 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( + this: YY, + path: ast.PathExpression, + params: ast.Expr[], + hash: ast.Hash, + openToken: string, + strip: ast.StripFlags, + locInfo: LocInfo +) { + // Must use charAt to support IE pre-10 + let escapeFlag = openToken.charAt(3) || openToken.charAt(2), + escaped = escapeFlag !== '{' && escapeFlag !== '&'; + + let decorator = /\*/.test(openToken); + return { + type: decorator ? 'Decorator' : 'MustacheStatement', + path, + params, + hash, + escaped, + strip, + loc: this.locInfo(locInfo), + }; +} + +export function prepareRawBlock( + this: YY, + openRawBlock: OpenRawBlock, + contents: ast.Statement[], + closeToken: string, + locInfo: LocInfo +): ast.BlockStatement { + validateClose(openRawBlock, closeToken); + + const loc = this.locInfo(locInfo); + let program: ast.Program = { + type: 'Program', + body: contents, + strip: {}, + loc, + }; + + assert(openRawBlock.path.type === 'PathExpression', 'Mustache path'); + + return { + type: 'BlockStatement', + path: openRawBlock.path, + params: openRawBlock.params, + hash: openRawBlock.hash, + program, + openStrip: {}, + inverseStrip: {}, + closeStrip: {}, + loc, + }; +} + +export function prepareBlock( + this: YY, + openBlock: OpenBlock, + program: ast.Program, + inverseAndProgram: InverseChain, + close: CloseBlock, + inverted: boolean, + locInfo: 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) { + const first = inverseAndProgram.program.body[0]; + + assert( + first.type === 'BlockStatement', + `BUG: the first statement after an 'else' chain must be a block statement. This should be enforced by the parser and this error should never occur.` + ); + + if (first.type === 'BlockStatement') { + first.closeStrip = close.strip; + } + } + + inverseStrip = inverseAndProgram.strip; + inverse = inverseAndProgram.program; + } + + if (inverted) { + const initialInverse = inverse as ast.Program; + inverse = program; + program = initialInverse; + } + + 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: ast.Statement[], + loc?: ast.SourceLocation +) { + if (!loc && statements.length) { + const firstLoc = statements[0].loc; + const lastLoc = statements[statements.length - 1].loc; + + if (firstLoc === lastLoc) { + loc = firstLoc; + } else { + loc = new SourceLocation(firstLoc.source, { + first_line: firstLoc.start.line, + first_column: firstLoc.start.column, + last_line: lastLoc.end.line, + last_column: lastLoc.end.column, + }); + } + } + + return { + type: 'Program', + body: statements, + strip: {}, + loc, + }; +} + +export function preparePartialBlock( + this: YY, + open: OpenPartialBlock, + program: ast.Program, + close: CloseBlock, + locInfo: 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/lib/types.d.ts b/lib/types.d.ts deleted file mode 100644 index afe4272..0000000 --- a/lib/types.d.ts +++ /dev/null @@ -1,235 +0,0 @@ -export interface BaseNode { - type: string; - loc: SourceLocation; -} - -export interface InverseChain { - strip: StripFlags; - program: Program; - chain?: boolean; -} - -export interface Program { - type: 'Program'; - /** - * The root node of a program has no `loc` if it's empty. - */ - loc: SourceLocation | undefined; - blockParams?: string[]; - body: Statement[]; - chained?: boolean; - strip: StripFlags; -} - -export interface CommentStatement extends BaseNode { - type: 'CommentStatement'; - value: string; - strip: StripFlags; -} - -export interface PartialStatement extends BaseNode { - type: 'PartialStatement'; - name: Expression; - params: Expression[]; - hash: Hash; - indent: string; - strip: StripFlags; -} - -export interface BlockStatement extends BaseNode { - type: 'BlockStatement'; - path: Expression; - params: Expression[]; - hash: Hash; - program: Program | undefined; - inverse?: Program | undefined; - openStrip: StripFlags; - inverseStrip: StripFlags | undefined; - closeStrip: StripFlags; -} - -export interface DecoratorBlock extends BaseNode { - type: 'DecoratorBlock'; - path: Expression; - params: Expression[]; - hash: Hash; - program: Program; - inverse?: undefined; - inverseStrip?: undefined; - openStrip: StripFlags; - closeStrip: StripFlags; -} - -export interface PartialBlockStatement extends BaseNode { - type: 'PartialBlockStatement'; - name: Expression; - params: Expression[]; - hash: Hash; - program: Program; - inverse?: undefined; - inverseStrip?: undefined; - openStrip: StripFlags; - closeStrip: StripFlags; -} - -export type Statement = - | MustacheStatement - | Content - | BlockStatement - | PartialStatement - | PartialBlockStatement; - -export interface MustacheStatement extends BaseNode { - type: 'Decorator' | 'MustacheStatement'; - path: Expression; - params: Expression[]; - hash: Hash; - escaped: boolean; - strip: StripFlags; -} - -export interface PathExpression extends BaseNode { - readonly original: string; - readonly this: boolean; - readonly data: boolean; - readonly depth: number; - readonly parts: (string | SubExpression)[]; - readonly head: string | SubExpression | undefined; - readonly tail: string[]; -} - -export interface SubExpression extends BaseNode { - readonly original: string; -} - -export interface Hash { - readonly pairs: HashPair[]; -} - -export interface StripFlags { - readonly open?: boolean; - readonly close?: boolean; - readonly openStandalone?: boolean; - readonly closeStandalone?: boolean; - readonly inlineStandalone?: boolean; -} - -export interface HashPair { - readonly key: string; - readonly value: Expression; -} - -export interface ParserPart { - readonly part: string; - readonly original: string; - readonly separator: string; -} - -export interface Content extends BaseNode { - type: 'ContentStatement'; - original: string; - value: string; -} - -export type Expression = SubExpression | PathExpression; - -export interface SourcePosition { - line: number; - column: number; -} - -export interface SourceLocation { - source: string | undefined; - start: SourcePosition; - end: SourcePosition; -} - -export interface CallNode { - path: Expression; - params: Expression[]; - hash: Hash; -} - -export interface OpenPartial { - strip: StripFlags; -} - -export interface OpenPartialBlock extends CallNode { - strip: StripFlags; -} - -export interface OpenRawBlock extends CallNode, BaseNode {} - -export interface OpenBlock extends CallNode { - open: string; - blockParams: string[]; - strip: StripFlags; -} - -export interface OpenInverse extends CallNode { - blockParams: string[]; - strip: StripFlags; -} - -export interface CloseBlock { - readonly path: PathExpression; - strip: StripFlags; -} - -export type AcceptedNode = Program; - -/// JISON TYPES /// - -export interface Parser { - parse: (input: string) => Program; - yy: YY; -} - -export interface YY { - locInfo(locInfo: LocInfo): SourceLocation; - preparePath( - this: YY, - data: boolean, - sexpr: { expr: SubExpression; sep: string } | false, - parts: ParserPart[], - locInfo: LocInfo, - ): PathExpression; - - prepareMustache( - this: YY, - path: PathExpression, - params: Expression[], - hash: Hash, - open: string, - strip: StripFlags, - locInfo: LocInfo, - ): MustacheStatement; - - prepareRawBlock( - this: YY, - openRawBlock: OpenRawBlock, - contents: Content[], - close: string, - locInfo: LocInfo, - ): BlockStatement; - - prepareBlock( - this: YY, - openBlock: OpenBlock, - program: Program, - inverseChain: InverseChain, - close: CloseBlock, - inverted: boolean, - locInfo: LocInfo, - ): BlockStatement | DecoratorBlock; -} - -/** - * The `LocInfo` object comes from the generated `jison` parser. - */ -export interface LocInfo { - first_line: number; - first_column: number; - last_line: number; - last_column: number; -} diff --git a/lib/types/ast.d.ts b/lib/types/ast.d.ts new file mode 100644 index 0000000..c33b909 --- /dev/null +++ b/lib/types/ast.d.ts @@ -0,0 +1,174 @@ +export type Literal = CollectionLiteral | PrimitiveLiteral; +export type PrimitiveLiteral = + | StringLiteral + | BooleanLiteral + | NumberLiteral + | UndefinedLiteral + | NullLiteral; +export type CollectionLiteral = HashLiteral | ArrayLiteral; +export type Expr = SubExpression | PathExpression | Literal; + +export interface HasLocation { + loc: SourceLocation; +} + +export interface Node extends HasLocation { + type: string; +} + +export interface SourceLocation { + source?: string | undefined; + start: Position; + end: Position; +} + +export interface Position { + line: number; + column: number; +} + +export interface Program extends Node { + body: Statement[]; + blockParams?: string[]; + /** @compat */ + strip: {}; +} + +export interface Statement extends Node {} + +export interface MustacheStatement extends Statement, WithArgsNode { + type: 'MustacheStatement'; + path: CollectionLiteral | SubExpression | PathExpression; + escaped: boolean; + strip: StripFlags; +} + +export interface Decorator extends MustacheStatement {} + +export interface BlockStatement extends Statement, WithArgsNode { + type: 'BlockStatement'; + /** + * This is very restricted compared to other call nodes + * because the opening path must be repeated as part of + * the block close (i.e. {{#foo}}{{/foo}}). + */ + path: PathExpression; + program: Program; + inverse?: Program; + openStrip: StripFlags; + inverseStrip: StripFlags; + closeStrip: StripFlags; +} + +export interface DecoratorBlock extends BlockStatement {} + +export interface PartialStatement extends Statement, WithArgsNode { + type: 'PartialStatement'; + name: PathExpression | SubExpression; + indent: string; + strip: StripFlags; +} + +export interface PartialBlockStatement extends Statement, WithArgsNode { + type: 'PartialBlockStatement'; + name: PathExpression | SubExpression; + 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 BaseExpression extends Node {} + +export interface SubExpression extends BaseExpression, WithArgsNode { + type: 'SubExpression'; + path: CollectionLiteral | SubExpression | PathExpression; +} + +export interface PathExpression extends BaseExpression { + type: 'PathExpression'; + data: boolean; + depth: number; + parts: (string | SubExpression)[]; + head: SubExpression | string; + tail: string[]; + original: string; +} + +export interface BasePrimitiveLiteral extends BaseExpression {} + +export interface StringLiteral extends BasePrimitiveLiteral { + type: 'StringLiteral'; + value: string; + original: string; +} + +export interface BooleanLiteral extends BasePrimitiveLiteral { + type: 'BooleanLiteral'; + value: boolean; + original: boolean; +} + +export interface NumberLiteral extends BasePrimitiveLiteral { + type: 'NumberLiteral'; + value: number; + original: number; +} + +export interface UndefinedLiteral extends BasePrimitiveLiteral { + type: 'UndefinedLiteral'; +} + +export interface NullLiteral extends BasePrimitiveLiteral { + type: 'NullLiteral'; +} + +export interface Hash extends Node { + type: 'Hash'; + pairs: HashPair[]; +} + +export interface BaseCollectionLiteral extends BaseExpression {} + +export interface HashLiteral extends BaseCollectionLiteral { + type: 'HashLiteral'; + pairs: HashPair[]; +} + +export interface ArrayLiteral extends BaseCollectionLiteral { + type: 'ArrayLiteral'; + items: Expr[]; +} + +export interface HashPair extends Node { + type: 'HashPair'; + key: string; + value: Expr; +} + +export interface StripFlags { + open?: boolean; + close?: boolean; +} + +export interface WithArgsNode { + params: Expr[]; + hash: Hash; +} + +export interface helpers { + helperExpression(node: Node): boolean; + scopeId(path: PathExpression): boolean; + simpleId(path: PathExpression): boolean; +} diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..6dd6c8a --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,5 @@ +export function assert(condition: unknown, msg?: string): asserts condition { + if (!condition) { + throw new Error(msg ?? 'Assertion failed'); + } +} diff --git a/package.json b/package.json index bc373f8..4639609 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "lint": "eslint .", "prepublishOnly": "pnpm run build", "build": "npm-run-all build:parser build:esm build:cjs", - "build:cjs": "tsc --module nodenext --moduleResolution nodenext --target es5 --outDir dist/cjs", + "build:cjs": "tsc --module nodenext --moduleResolution nodenext --target es2020 --outDir dist/cjs", "build:esm": "tsc --module es2020 --outDir dist/esm", "build:jison": "node scripts/compile-parser.cjs", "build:parser": "npm-run-all build:jison build:parser-suffix", diff --git a/spec/parser.js b/spec/parser.js index e96815b..1b81376 100644 --- a/spec/parser.js +++ b/spec/parser.js @@ -1,5 +1,5 @@ import { parse, print } from '../dist/esm/index.js'; -import { equals, equalsAst, shouldThrow } from './utils.js'; +import { equals, equalsAst, equalsJSON, shouldThrow } from './utils.js'; describe('parser', function () { function astFor(template) { @@ -476,26 +476,17 @@ describe('parser', function () { ); // 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 }, - }) - ); + equalsJSON(p.loc, { + start: { line: 1, column: 0 }, + end: { line: 7, column: 4 }, + }); + equalsJSON(p.body[1].program.loc, { + start: { line: 2, column: 13 }, + end: { line: 4, column: 7 }, + }); + equalsJSON(p.body[1].inverse.loc, { + start: { line: 4, column: 15 }, + end: { line: 6, column: 5 }, + }); }); }); diff --git a/spec/utils.js b/spec/utils.js index cb58adf..9309a42 100644 --- a/spec/utils.js +++ b/spec/utils.js @@ -22,11 +22,28 @@ if (Error.captureStackTrace) { */ export function equals(actual, expected, msg) { if (actual !== expected) { - throw new AssertError( - `\n Actual: ${actual} Expected: ${expected}` + - (msg ? `\n${msg}` : ''), + const error = new AssertError( + msg ?? `Expected actual to equal expected.`, equals ); + error.expected = expected; + error.actual = actual; + throw error; + } +} + +export function equalsJSON(actual, expected, msg) { + const actualJSON = JSON.stringify(actual, null, 2); + const expectedJSON = JSON.stringify(expected, null, 2); + + if (actualJSON !== expectedJSON) { + const error = new AssertError( + msg ?? `Expected equivalent JSON serialization.`, + equalsJSON + ); + error.expected = expectedJSON; + error.actual = actualJSON; + throw error; } } diff --git a/src/handlebars.yy b/src/handlebars.yy index 278dc73..4072a8f 100644 --- a/src/handlebars.yy +++ b/src/handlebars.yy @@ -64,7 +64,7 @@ openInverseChain ; inverseAndProgram - : INVERSE program -> { strip: yy.stripFlags($1, $1), program: $2 } + : INVERSE program -> { strip: yy.stripFlags($1, $1), program: $2, loc: yy.locInfo(@$) } ; inverseChain @@ -73,13 +73,13 @@ inverseChain program = yy.prepareProgram([inverse], $2.loc); program.chained = true; - $$ = { strip: $1.strip, program: program, chain: true }; + $$ = { strip: $1.strip, program: program, chain: true, loc: yy.locInfo(@$) }; } | inverseAndProgram -> $1 ; closeBlock - : OPEN_ENDBLOCK helperName CLOSE -> {path: $2, strip: yy.stripFlags($1, $3)} + : OPEN_ENDBLOCK helperName CLOSE -> {path: $2, strip: yy.stripFlags($1, $3), loc: yy.locInfo(@$)} ; mustache diff --git a/tsconfig.json b/tsconfig.json index ae08869..75cd9e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,10 +17,11 @@ "noUnusedParameters": true, "noImplicitReturns": true, "newLine": "LF", - "allowJs": true + "allowJs": true, }, "include": [ "lib/**/*.js", + "lib/**/*.ts", "lib/**/*.d.ts" ], "exclude": [ From 25b2c4767ddf6593ae6955ea000a6c51d61abd37 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Thu, 24 Oct 2024 16:39:10 -0700 Subject: [PATCH 3/9] Migrate visitor to TS --- lib/types/ast.d.ts | 50 +++++++-- lib/visitor.js | 136 ------------------------ lib/visitor.ts | 250 +++++++++++++++++++++++++++++++++++++++++++++ spec/visitor.js | 4 +- 4 files changed, 295 insertions(+), 145 deletions(-) delete mode 100644 lib/visitor.js create mode 100644 lib/visitor.ts diff --git a/lib/types/ast.d.ts b/lib/types/ast.d.ts index c33b909..5dff7f7 100644 --- a/lib/types/ast.d.ts +++ b/lib/types/ast.d.ts @@ -5,8 +5,43 @@ export type PrimitiveLiteral = | NumberLiteral | UndefinedLiteral | NullLiteral; +export type Statement = + | MustacheStatement + | Decorator + | BlockStatement + | PartialStatement + | ContentStatement + | CommentStatement; export type CollectionLiteral = HashLiteral | ArrayLiteral; export type Expr = SubExpression | PathExpression | Literal; +export type Internal = Hash | HashPair; + +export type VisitableNode = + | Program + | MustacheStatement + | Decorator + | BlockStatement + | DecoratorBlock + | PartialStatement + | PartialBlockStatement + | ContentStatement + | CommentStatement + | SubExpression + | PathExpression + | Literal + | Internal; + +export type VisitableChildren = { + [P in VisitableNode['type']]: Extract< + VisitableNode, + { type: P } + > extends infer N + ? { + [K in keyof N]: N[K] extends VisitableNode ? K : never; + }[keyof N] & + string + : never; +}; export interface HasLocation { loc: SourceLocation; @@ -28,15 +63,16 @@ export interface Position { } export interface Program extends Node { + type: 'Program'; body: Statement[]; blockParams?: string[]; /** @compat */ strip: {}; } -export interface Statement extends Node {} +export interface BaseStatement extends Node {} -export interface MustacheStatement extends Statement, WithArgsNode { +export interface MustacheStatement extends BaseStatement, WithArgsNode { type: 'MustacheStatement'; path: CollectionLiteral | SubExpression | PathExpression; escaped: boolean; @@ -45,7 +81,7 @@ export interface MustacheStatement extends Statement, WithArgsNode { export interface Decorator extends MustacheStatement {} -export interface BlockStatement extends Statement, WithArgsNode { +export interface BlockStatement extends BaseStatement, WithArgsNode { type: 'BlockStatement'; /** * This is very restricted compared to other call nodes @@ -62,14 +98,14 @@ export interface BlockStatement extends Statement, WithArgsNode { export interface DecoratorBlock extends BlockStatement {} -export interface PartialStatement extends Statement, WithArgsNode { +export interface PartialStatement extends BaseStatement, WithArgsNode { type: 'PartialStatement'; name: PathExpression | SubExpression; indent: string; strip: StripFlags; } -export interface PartialBlockStatement extends Statement, WithArgsNode { +export interface PartialBlockStatement extends BaseStatement, WithArgsNode { type: 'PartialBlockStatement'; name: PathExpression | SubExpression; program: Program; @@ -77,13 +113,13 @@ export interface PartialBlockStatement extends Statement, WithArgsNode { closeStrip: StripFlags; } -export interface ContentStatement extends Statement { +export interface ContentStatement extends BaseStatement { type: 'ContentStatement'; value: string; original: StripFlags; } -export interface CommentStatement extends Statement { +export interface CommentStatement extends BaseStatement { type: 'CommentStatement'; value: string; strip: StripFlags; diff --git a/lib/visitor.js b/lib/visitor.js deleted file mode 100644 index e9b5d68..0000000 --- a/lib/visitor.js +++ /dev/null @@ -1,136 +0,0 @@ -import Exception from './exception.js'; - -function Visitor() { - this.parents = []; -} - -Visitor.prototype = { - constructor: Visitor, - mutating: false, - - // Visits a given value. If mutating, will replace the value if necessary. - acceptKey: function (node, name) { - let value = this.accept(node[name]); - if (this.mutating) { - // Hacky sanity check: This may have a few false positives for type for the helper - // methods but will generally do the right thing without a lot of overhead. - if (value && !Visitor.prototype[value.type]) { - throw new Exception( - 'Unexpected node type "' + - value.type + - '" found when accepting ' + - name + - ' on ' + - node.type - ); - } - node[name] = value; - } - }, - - // Performs an accept operation with added sanity check to ensure - // required keys are not removed. - acceptRequired: function (node, name) { - this.acceptKey(node, name); - - if (!node[name]) { - throw new Exception(node.type + ' requires ' + name); - } - }, - - // Traverses a given array. If mutating, empty responses will be removed - // for child elements. - acceptArray: function (array) { - for (let i = 0, l = array.length; i < l; i++) { - this.acceptKey(array, i); - - if (!array[i]) { - array.splice(i, 1); - i--; - l--; - } - } - }, - - accept: function (object) { - if (!object) { - return; - } - - /* istanbul ignore next: Sanity code */ - if (!this[object.type]) { - throw new Exception('Unknown type: ' + object.type, object); - } - - if (this.current) { - this.parents.unshift(this.current); - } - this.current = object; - - let ret = this[object.type](object); - - this.current = this.parents.shift(); - - if (!this.mutating || ret) { - return ret; - } else if (ret !== false) { - return object; - } - }, - - Program: function (program) { - this.acceptArray(program.body); - }, - - MustacheStatement: visitSubExpression, - Decorator: visitSubExpression, - - BlockStatement: visitBlock, - DecoratorBlock: visitBlock, - - PartialStatement: visitPartial, - PartialBlockStatement: function (partial) { - visitPartial.call(this, partial); - - this.acceptKey(partial, 'program'); - }, - - ContentStatement: function (/* content */) {}, - CommentStatement: function (/* comment */) {}, - - SubExpression: visitSubExpression, - - PathExpression: function (/* path */) {}, - - StringLiteral: function (/* string */) {}, - NumberLiteral: function (/* number */) {}, - BooleanLiteral: function (/* bool */) {}, - UndefinedLiteral: function (/* literal */) {}, - NullLiteral: function (/* literal */) {}, - - Hash: function (hash) { - this.acceptArray(hash.pairs); - }, - HashPair: function (pair) { - this.acceptRequired(pair, 'value'); - }, -}; - -function visitSubExpression(mustache) { - this.acceptRequired(mustache, 'path'); - this.acceptArray(mustache.params); - this.acceptKey(mustache, 'hash'); -} -function visitBlock(block) { - visitSubExpression.call(this, block); - - this.acceptKey(block, 'program'); - this.acceptKey(block, 'inverse'); -} -function visitPartial(partial) { - this.acceptRequired(partial, 'name'); - this.acceptArray(partial.params); - this.acceptKey(partial, 'hash'); -} - -export default Visitor; diff --git a/lib/visitor.ts b/lib/visitor.ts new file mode 100644 index 0000000..3f8c108 --- /dev/null +++ b/lib/visitor.ts @@ -0,0 +1,250 @@ +import Exception from './exception.js'; +import type * as ast from './types/ast.js'; + +export default class Visitor { + parents: ast.VisitableNode[] = []; + mutating = false; + current: ast.VisitableNode | undefined; + + acceptField(node: N, name: keyof N): void { + this.#acceptKey(node, name, node); + } + + acceptItem( + array: ast.VisitableNode[], + item: number, + parent: ast.VisitableNode + ): void { + this.#acceptKey(array, item, parent); + } + + #acceptKey( + container: ast.VisitableNode | ast.VisitableNode[], + name: string | symbol | number, + parent: ast.VisitableNode + ) { + const node = Reflect.get(container, name) as ast.VisitableNode; + let value = this.accept(node); + + if (this.mutating) { + if (isObject(value) && !this.#isVisitable(value)) { + throw new Exception( + `${unexpectedVisitorReturn(value)} when accepting ${String( + name + )} on ${parent.type}`, + node + ); + } + Reflect.set(container, name, value); + } + } + + acceptRequired( + node: N, + name: ast.VisitableChildren[N['type']] + ): void { + const original = node[name] as ast.VisitableNode; + this.acceptField(node, name); + + if (!node[name]) { + throw new Exception( + `Visitor removed \`${name}\` (${original.loc.start.line}:${original.loc.start.column}) from ${node.type}, but \`${name}\` is required`, + node + ); + } + } + + // Traverses a given array. If mutating, empty responses will be removed + // for child elements. + acceptArray(array: ast.VisitableNode[], parent: ast.VisitableNode) { + for (let i = 0, l = array.length; i < l; i++) { + this.#acceptKey(array, i, parent); + + if (!array[i]) { + array.splice(i, 1); + i--; + l--; + } + } + } + + accept(node: ast.VisitableNode | null | undefined): unknown { + const obj = node; + + if (!obj) { + return undefined; + } + + if (!this[obj.type]) { + throw new Exception('Unknown type: ' + obj.type, obj); + } + + if (this.current) { + this.parents.unshift(this.current); + } + + this.current = obj; + + const visit = this[obj.type] as ( + node: typeof obj + ) => ReturnType['type']]>; + + let ret = visit.call(this, obj); + + this.current = this.parents.shift(); + + if (!this.mutating || ret) { + return ret; + } else if (ret !== false) { + return obj; + } else { + return; + } + } + + Program(program: ast.Program): unknown { + this.acceptArray(program.body, program); + return; + } + + MustacheStatement(mustache: ast.MustacheStatement): unknown { + this.#visitCallNode(mustache); + return; + } + + Decorator(mustache: ast.Decorator): unknown { + this.#visitCallNode(mustache); + return; + } + + BlockStatement(block: ast.BlockStatement): unknown { + this.#visitBlock(block); + return; + } + + DecoratorBlock(block: ast.DecoratorBlock): unknown { + this.#visitBlock(block); + return; + } + + PartialStatement(partial: ast.PartialStatement): unknown { + this.#visitPartial(partial); + return; + } + + PartialBlockStatement(partial: ast.PartialBlockStatement): unknown { + this.#visitPartial(partial); + this.acceptField(partial, 'program'); + return; + } + + ContentStatement(_content: ast.ContentStatement): unknown { + return; + } + + CommentStatement(_comment: ast.CommentStatement): unknown { + return; + } + + SubExpression(sexpr: ast.SubExpression): unknown { + this.#visitCallNode(sexpr); + return; + } + + PathExpression(_path: ast.PathExpression): unknown { + return; + } + + StringLiteral(_string: ast.StringLiteral): unknown { + return; + } + + NumberLiteral(_number: ast.NumberLiteral): unknown { + return; + } + + BooleanLiteral(_boolean: ast.BooleanLiteral): unknown { + return; + } + + UndefinedLiteral(_undefined: ast.UndefinedLiteral): unknown { + return; + } + + NullLiteral(_null: ast.NullLiteral): unknown { + return; + } + + ArrayLiteral(array: ast.ArrayLiteral): unknown { + this.acceptArray(array.items, array); + return; + } + + HashLiteral(hash: ast.HashLiteral): unknown { + this.acceptArray(hash.pairs, hash); + return; + } + + Hash(hash: ast.Hash): unknown { + this.acceptArray(hash.pairs, hash); + return; + } + + HashPair(pair: ast.HashPair): unknown { + this.acceptRequired(pair, 'value'); + return; + } + + #visitCallNode( + mustache: + | ast.SubExpression + | ast.MustacheStatement + | ast.Decorator + | ast.BlockStatement + | ast.DecoratorBlock + ) { + this.acceptRequired(mustache, 'path'); + this.acceptArray(mustache.params, mustache); + this.acceptField(mustache, 'hash'); + } + + #visitBlock(block: ast.BlockStatement | ast.DecoratorBlock) { + this.#visitCallNode(block); + + this.acceptField(block, 'program'); + this.acceptField(block, 'inverse'); + } + + #visitPartial(partial: ast.PartialStatement | ast.PartialBlockStatement) { + this.acceptRequired(partial, 'name'); + this.acceptArray(partial.params, partial); + this.acceptField(partial, 'hash'); + } + + #isVisitable(obj: VisitorReturn): obj is ast.Node { + const type = Reflect.get(obj, 'type'); + if (type === undefined || typeof type !== 'string') { + return false; + } + + return Reflect.has(this, type); + } +} + +type VisitorReturn = { type?: unknown }; + +function isObject(obj: unknown): obj is VisitorReturn { + return !!obj; +} + +function unexpectedVisitorReturn(value: VisitorReturn) { + if (value.type === undefined) { + return `Unexpected visitor return value (no 'type' property)`; + } else if (typeof value.type !== 'string') { + return `Unexpected visitor return value (type is ${ + value.type === null ? 'null' : typeof value.type + }, not a string)`; + } else { + return `Unexpected visitor return value (type of "${value.type}" is not a visitable node type)`; + } +} diff --git a/spec/visitor.js b/spec/visitor.js index 9d04924..8b988f5 100644 --- a/spec/visitor.js +++ b/spec/visitor.js @@ -98,7 +98,7 @@ describe('Visitor', function () { visitor.accept(ast); }, Exception, - 'MustacheStatement requires path' + 'Visitor removed `path` (1:2) from MustacheStatement, but `path` is required - 1:0' ); }); it('should throw when returning non-node responses', function () { @@ -115,7 +115,7 @@ describe('Visitor', function () { visitor.accept(ast); }, Exception, - 'Unexpected node type "undefined" found when accepting path on MustacheStatement' + `Unexpected visitor return value (no 'type' property) when accepting path on MustacheStatement - 1:2` ); }); }); From 070923d32cbfd2f0a03f2e1428d0c00a7520669a Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Thu, 24 Oct 2024 17:25:42 -0700 Subject: [PATCH 4/9] Migrate more files to TS --- lib/helpers.ts | 477 +++++++++++++++++++++++---------------------- lib/parse.js | 34 ---- lib/parse.ts | 61 ++++++ lib/printer.js | 189 ------------------ lib/printer.ts | 200 +++++++++++++++++++ lib/types/ast.d.ts | 18 +- lib/visitor.ts | 32 +-- spec/utils.js | 9 +- 8 files changed, 538 insertions(+), 482 deletions(-) delete mode 100644 lib/parse.js create mode 100644 lib/parse.ts delete mode 100644 lib/printer.js create mode 100644 lib/printer.ts diff --git a/lib/helpers.ts b/lib/helpers.ts index 6929e72..49dc67d 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -8,291 +8,294 @@ import type { OpenRawBlock, Part, SourcePosition, - YY, } from './types/types.js'; import type * as ast from './types/ast.js'; import { assert } from './utils.js'; +import type { ParseOptions } from './parse.js'; -function validateClose( - open: OpenBlock | OpenRawBlock | OpenPartialBlock, - close: CloseBlock | string -) { - const closeString = typeof close === 'string' ? close : close.path.original; - - if (open.path.type !== 'PathExpression') { - throw new Exception(`Unexpected block open (expected a path)`, open.path); +export class ParserHelpers { + #options: ParseOptions; + constructor(options: ParseOptions) { + this.#options = options; } - if (open.path.original !== closeString) { - throw new Exception( - `${open.path.original} doesn't match ${closeString}`, - open.path - ); - } -} + locInfo = (locInfo: LocInfo) => { + return new SourceLocation(this.#options.srcName, locInfo); + }; -export class SourceLocation implements ast.SourceLocation { - source: string | undefined; - start: SourcePosition; - end: SourcePosition; + id = (token: string) => { + if (/^\[.*\]$/.test(token)) { + return token.substring(1, token.length - 1); + } else { + return token; + } + }; - constructor(source: string | undefined, locInfo: LocInfo) { - this.source = source; - this.start = { - line: locInfo.first_line, - column: locInfo.first_column, + stripFlags = (open: string, close: string) => { + return { + open: open.charAt(2) === '~', + close: close.charAt(close.length - 3) === '~', }; - this.end = { - line: locInfo.last_line, - column: locInfo.last_column, - }; - } -} - -export function id(token: string) { - if (/^\[.*\]$/.test(token)) { - return token.substring(1, token.length - 1); - } else { - return token; - } -} + }; -export function stripFlags(open: string, close: string) { - return { - open: open.charAt(2) === '~', - close: close.charAt(close.length - 3) === '~', + stripComment = (comment: string) => { + return comment.replace(/^\{\{~?!-?-?/, '').replace(/-?-?~?\}\}$/, ''); }; -} -export function stripComment(comment: string) { - return comment.replace(/^\{\{~?!-?-?/, '').replace(/-?-?~?\}\}$/, ''); -} + preparePath = ( + data: boolean, + sexpr: ast.PathExpression | false, + parts: Part[], + locInfo: LocInfo + ) => { + const loc = this.locInfo(locInfo); -export function preparePath( - this: YY, - data: boolean, - sexpr: ast.PathExpression | false, - parts: Part[], - locInfo: LocInfo -) { - const loc = this.locInfo(locInfo); + let original; - let original; - - if (data) { - original = '@'; - } else if (sexpr) { - original = sexpr.original + '.'; - } else { - original = ''; - } + if (data) { + original = '@'; + } else if (sexpr) { + original = sexpr.original + '.'; + } else { + original = ''; + } - let tail = []; - let depth = 0; + 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; + 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 === '.#' ? '#' : ''; + let partPrefix = separator === '.#' ? '#' : ''; - original += (separator || '') + part; + original += (separator || '') + part; - if (!isLiteral && (part === '..' || part === '.' || part === 'this')) { - if (tail.length > 0) { - throw new Exception('Invalid path: ' + original, { loc }); - } else if (part === '..') { - depth++; + 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}`); } - } 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, + 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( - this: YY, - path: ast.PathExpression, - params: ast.Expr[], - hash: ast.Hash, - openToken: string, - strip: ast.StripFlags, - locInfo: LocInfo -) { - // Must use charAt to support IE pre-10 - let escapeFlag = openToken.charAt(3) || openToken.charAt(2), - escaped = escapeFlag !== '{' && escapeFlag !== '&'; - - let decorator = /\*/.test(openToken); - return { - type: decorator ? 'Decorator' : 'MustacheStatement', - path, - params, - hash, - escaped, - strip, - loc: this.locInfo(locInfo), + prepareMustache = ( + path: ast.PathExpression, + params: ast.Expr[], + hash: ast.Hash, + openToken: string, + strip: ast.StripFlags, + locInfo: LocInfo + ) => { + // Must use charAt to support IE pre-10 + let escapeFlag = openToken.charAt(3) || openToken.charAt(2), + escaped = escapeFlag !== '{' && escapeFlag !== '&'; + + let decorator = /\*/.test(openToken); + return { + type: decorator ? 'Decorator' : 'MustacheStatement', + path, + params, + hash, + escaped, + strip, + loc: this.locInfo(locInfo), + }; }; -} -export function prepareRawBlock( - this: YY, - openRawBlock: OpenRawBlock, - contents: ast.Statement[], - closeToken: string, - locInfo: LocInfo -): ast.BlockStatement { - validateClose(openRawBlock, closeToken); - - const loc = this.locInfo(locInfo); - let program: ast.Program = { - type: 'Program', - body: contents, - strip: {}, - loc, - }; + prepareRawBlock = ( + openRawBlock: OpenRawBlock, + contents: ast.Statement[], + closeToken: string, + locInfo: LocInfo + ): ast.BlockStatement => { + validateClose(openRawBlock, closeToken); + + const loc = this.locInfo(locInfo); + let program: ast.Program = { + type: 'Program', + body: contents, + strip: {}, + loc, + }; - assert(openRawBlock.path.type === 'PathExpression', 'Mustache path'); - - return { - type: 'BlockStatement', - path: openRawBlock.path, - params: openRawBlock.params, - hash: openRawBlock.hash, - program, - openStrip: {}, - inverseStrip: {}, - closeStrip: {}, - loc, + assert(openRawBlock.path.type === 'PathExpression', 'Mustache path'); + + return { + type: 'BlockStatement', + path: openRawBlock.path, + params: openRawBlock.params, + hash: openRawBlock.hash, + program, + openStrip: {}, + inverseStrip: {}, + closeStrip: {}, + loc, + }; }; -} -export function prepareBlock( - this: YY, - openBlock: OpenBlock, - program: ast.Program, - inverseAndProgram: InverseChain, - close: CloseBlock, - inverted: boolean, - locInfo: LocInfo -) { - if (close && close.path) { - validateClose(openBlock, close); - } + prepareBlock = ( + openBlock: OpenBlock, + program: ast.Program, + inverseAndProgram: InverseChain, + close: CloseBlock, + inverted: boolean, + locInfo: LocInfo + ) => { + if (close && close.path) { + validateClose(openBlock, close); + } - let decorator = /\*/.test(openBlock.open); + let decorator = /\*/.test(openBlock.open); - program.blockParams = openBlock.blockParams; + program.blockParams = openBlock.blockParams; - let inverse, inverseStrip; + let inverse, inverseStrip; - if (inverseAndProgram) { - if (decorator) { - throw new Exception( - 'Unexpected inverse block on decorator', - inverseAndProgram - ); - } + if (inverseAndProgram) { + if (decorator) { + throw new Exception( + 'Unexpected inverse block on decorator', + inverseAndProgram + ); + } - if (inverseAndProgram.chain) { - const first = inverseAndProgram.program.body[0]; + if (inverseAndProgram.chain) { + const first = inverseAndProgram.program.body[0]; - assert( - first.type === 'BlockStatement', - `BUG: the first statement after an 'else' chain must be a block statement. This should be enforced by the parser and this error should never occur.` - ); + assert( + first.type === 'BlockStatement', + `BUG: the first statement after an 'else' chain must be a block statement. This should be enforced by the parser and this error should never occur.` + ); - if (first.type === 'BlockStatement') { - first.closeStrip = close.strip; + if (first.type === 'BlockStatement') { + first.closeStrip = close.strip; + } } + + inverseStrip = inverseAndProgram.strip; + inverse = inverseAndProgram.program; } - inverseStrip = inverseAndProgram.strip; - inverse = inverseAndProgram.program; - } + if (inverted) { + const initialInverse = inverse as ast.Program; + inverse = program; + program = initialInverse; + } - if (inverted) { - const initialInverse = inverse as ast.Program; - inverse = program; - program = initialInverse; - } + 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), + }; + }; - 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), + prepareProgram = (statements: ast.Statement[], loc?: ast.SourceLocation) => { + if (!loc && statements.length) { + const firstLoc = statements[0].loc; + const lastLoc = statements[statements.length - 1].loc; + + if (firstLoc === lastLoc) { + loc = firstLoc; + } else { + loc = new SourceLocation(firstLoc.source, { + first_line: firstLoc.start.line, + first_column: firstLoc.start.column, + last_line: lastLoc.end.line, + last_column: lastLoc.end.column, + }); + } + } + + return { + type: 'Program', + body: statements, + strip: {}, + loc, + }; + }; + + preparePartialBlock = ( + open: OpenPartialBlock, + program: ast.Program, + close: CloseBlock, + locInfo: 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), + }; }; } -export function prepareProgram( - statements: ast.Statement[], - loc?: ast.SourceLocation +function validateClose( + open: OpenBlock | OpenRawBlock | OpenPartialBlock, + close: CloseBlock | string ) { - if (!loc && statements.length) { - const firstLoc = statements[0].loc; - const lastLoc = statements[statements.length - 1].loc; + const closeString = typeof close === 'string' ? close : close.path.original; - if (firstLoc === lastLoc) { - loc = firstLoc; - } else { - loc = new SourceLocation(firstLoc.source, { - first_line: firstLoc.start.line, - first_column: firstLoc.start.column, - last_line: lastLoc.end.line, - last_column: lastLoc.end.column, - }); - } + if (open.path.type !== 'PathExpression') { + throw new Exception(`Unexpected block open (expected a path)`, open.path); } - return { - type: 'Program', - body: statements, - strip: {}, - loc, - }; + if (open.path.original !== closeString) { + throw new Exception( + `${open.path.original} doesn't match ${closeString}`, + open.path + ); + } } -export function preparePartialBlock( - this: YY, - open: OpenPartialBlock, - program: ast.Program, - close: CloseBlock, - locInfo: 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), - }; +export class SourceLocation implements ast.SourceLocation { + source: string | undefined; + start: SourcePosition; + end: SourcePosition; + + constructor(source: string | undefined, locInfo: LocInfo) { + this.source = source; + this.start = { + line: locInfo.first_line, + column: locInfo.first_column, + }; + this.end = { + line: locInfo.last_line, + column: locInfo.last_column, + }; + } } diff --git a/lib/parse.js b/lib/parse.js deleted file mode 100644 index 6b5f03d..0000000 --- a/lib/parse.js +++ /dev/null @@ -1,34 +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); - }; - - return parser.parse(input); -} - -export function parse(input, options) { - let ast = parseWithoutProcessing(input, options); - let strip = new WhitespaceControl(options); - - return strip.accept(ast); -} diff --git a/lib/parse.ts b/lib/parse.ts new file mode 100644 index 0000000..23e25aa --- /dev/null +++ b/lib/parse.ts @@ -0,0 +1,61 @@ +import { ParserHelpers } from './helpers.js'; +import PARSER from './parser.js'; +import type * as ast from './types/ast.js'; +import type { BaseNode } from './types/types.js'; +import WhitespaceControl from './whitespace-control.js'; + +const parser = PARSER as { + parse: (input: string) => ast.Program; + yy: ParserHelpers; +}; + +export function parseWithoutProcessing( + input: string | T, + options: ParseOptions = {} +) { + // Just return if an already-compiled AST was passed in. + if (typeof input !== 'string') { + return input; + } + + parser.yy = new ParserHelpers(options); + + return parser.parse(input); +} + +export function parse( + input: string | T, + options: ParseOptions = {} +) { + let ast = parseWithoutProcessing(input, options); + let strip = new WhitespaceControl(options); + + // @ts-expect-error + return strip.accept(ast); +} +export interface ParseOptions { + srcName?: string; + syntax?: SyntaxOptions; +} + +export interface SyntaxOptions { + hash?: + | 'node' + | (( + hash: ast.Hash, + loc: ast.SourceLocation, + options: SyntaxFnOptions + ) => BaseNode); + square?: + | 'string' + | 'node' + | (( + params: ast.Expr[], + loc: ast.SourceLocation, + options: SyntaxFnOptions + ) => BaseNode); +} + +export interface SyntaxFnOptions { + yy: ParserHelpers; +} diff --git a/lib/printer.js b/lib/printer.js deleted file mode 100644 index 9e23499..0000000 --- a/lib/printer.js +++ /dev/null @@ -1,189 +0,0 @@ -/* eslint-disable new-cap */ -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) { - return this.pad('{{ ' + this.SubExpression(mustache) + ' }}'); -}; -PrintVisitor.prototype.Decorator = function (mustache) { - return this.pad('{{ DIRECTIVE ' + this.SubExpression(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.SubExpression(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) { - let params = sexpr.params, - paramStrings = [], - hash; - - for (let i = 0, l = params.length; i < l; i++) { - paramStrings.push(this.accept(params[i])); - } - - params = '[' + paramStrings.join(', ') + ']'; - - hash = sexpr.hash ? ' ' + this.accept(sexpr.hash) : ''; - - return this.accept(sexpr.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.Hash = function (hash) { - let pairs = hash.pairs, - joinedPairs = []; - - for (let i = 0, l = pairs.length; i < l; i++) { - joinedPairs.push(this.accept(pairs[i])); - } - - return 'HASH{' + joinedPairs.join(' ') + '}'; -}; -PrintVisitor.prototype.HashPair = function (pair) { - return pair.key + '=' + this.accept(pair.value); -}; -/* eslint-enable new-cap */ diff --git a/lib/printer.ts b/lib/printer.ts new file mode 100644 index 0000000..2cb4f24 --- /dev/null +++ b/lib/printer.ts @@ -0,0 +1,200 @@ +import Visitor from './visitor.js'; +import type * as ast from './types/ast.js'; + +export function print(ast: ast.VisitableNode) { + return new PrintVisitor().accept(ast); +} + +export class PrintVisitor extends Visitor { + padding = 0; + + pad(string: string) { + let out = ''; + + for (let i = 0, l = this.padding; i < l; i++) { + out += ' '; + } + + out += string + '\n'; + return out; + } + + Program(program: ast.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; + } + + MustacheStatement(mustache: ast.MustacheStatement) { + return this.pad('{{ ' + this.callNode(mustache) + ' }}'); + } + + Decorator(mustache: ast.Decorator) { + return this.pad('{{ DIRECTIVE ' + this.callNode(mustache) + ' }}'); + } + + BlockStatement(block: ast.BlockStatement): string { + return this.#BlockStatement(block); + } + + DecoratorBlock(block: ast.DecoratorBlock): string { + return this.#BlockStatement(block); + } + + #BlockStatement(block: ast.BlockStatement | ast.DecoratorBlock) { + let out = ''; + + out += this.pad( + (block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:' + ); + this.padding++; + out += this.pad(this.callNode(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; + } + + PartialStatement(partial: ast.PartialStatement) { + // @ts-expect-error + 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 + ' }}'); + } + + PartialBlockStatement(partial: ast.PartialBlockStatement) { + // @ts-expect-error + 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 + ' }}'); + } + + ContentStatement(content: ast.ContentStatement) { + return this.pad("CONTENT[ '" + content.value + "' ]"); + } + + CommentStatement(comment: ast.CommentStatement) { + return this.pad("{{! '" + comment.value + "' }}"); + } + + SubExpression(subExpression: ast.SubExpression) { + return this.callNode(subExpression); + } + + callNode(sexpr: ast.CallNode) { + const params = sexpr.params; + const paramStrings = []; + + for (let i = 0, l = params.length; i < l; i++) { + paramStrings.push(this.accept(params[i])); + } + + const paramsString = '[' + paramStrings.join(', ') + ']'; + const hashString = sexpr.hash ? ' ' + this.accept(sexpr.hash) : ''; + + return `${this.accept(sexpr.path)} ${paramsString}${hashString}`; + } + + PathExpression(id: ast.PathExpression) { + let head = + typeof id.head === 'string' ? id.head : `[${this.accept(id.head)}]`; + let path = [head, ...id.tail].join('/'); + return 'p%' + prefix(id) + path; + } + + StringLiteral(string: ast.StringLiteral) { + return '"' + string.value + '"'; + } + + NumberLiteral(number: ast.NumberLiteral) { + return 'n%' + number.value; + } + + BooleanLiteral(bool: ast.BooleanLiteral) { + return 'b%' + bool.value; + } + + UndefinedLiteral() { + return 'UNDEFINED'; + } + + NullLiteral() { + return 'NULL'; + } + + Hash(hash: ast.Hash) { + let pairs = hash.pairs, + joinedPairs = []; + + for (let i = 0, l = pairs.length; i < l; i++) { + joinedPairs.push(this.accept(pairs[i])); + } + + return 'HASH{' + joinedPairs.join(' ') + '}'; + } + + HashPair(pair: ast.HashPair) { + return pair.key + '=' + this.accept(pair.value); + } +} + +function prefix(path: ast.PathExpression) { + if (path.data) { + return '@'; + } else if (path.this) { + return 'this.'; + } else { + return ''; + } +} diff --git a/lib/types/ast.d.ts b/lib/types/ast.d.ts index 5dff7f7..034ceae 100644 --- a/lib/types/ast.d.ts +++ b/lib/types/ast.d.ts @@ -15,6 +15,12 @@ export type Statement = export type CollectionLiteral = HashLiteral | ArrayLiteral; export type Expr = SubExpression | PathExpression | Literal; export type Internal = Hash | HashPair; +export type CallNode = + | SubExpression + | MustacheStatement + | Decorator + | BlockStatement + | DecoratorBlock; export type VisitableNode = | Program @@ -81,8 +87,7 @@ export interface MustacheStatement extends BaseStatement, WithArgsNode { export interface Decorator extends MustacheStatement {} -export interface BlockStatement extends BaseStatement, WithArgsNode { - type: 'BlockStatement'; +export interface BaseBlockStatement extends BaseStatement, WithArgsNode { /** * This is very restricted compared to other call nodes * because the opening path must be repeated as part of @@ -96,7 +101,13 @@ export interface BlockStatement extends BaseStatement, WithArgsNode { closeStrip: StripFlags; } -export interface DecoratorBlock extends BlockStatement {} +export interface BlockStatement extends BaseBlockStatement { + type: 'BlockStatement'; +} + +export interface DecoratorBlock extends BaseBlockStatement { + type: 'DecoratorBlock'; +} export interface PartialStatement extends BaseStatement, WithArgsNode { type: 'PartialStatement'; @@ -135,6 +146,7 @@ export interface SubExpression extends BaseExpression, WithArgsNode { export interface PathExpression extends BaseExpression { type: 'PathExpression'; data: boolean; + this: boolean; depth: number; parts: (string | SubExpression)[]; head: SubExpression | string; diff --git a/lib/visitor.ts b/lib/visitor.ts index 3f8c108..11a12c3 100644 --- a/lib/visitor.ts +++ b/lib/visitor.ts @@ -108,12 +108,12 @@ export default class Visitor { } MustacheStatement(mustache: ast.MustacheStatement): unknown { - this.#visitCallNode(mustache); + this.callNode(mustache); return; } Decorator(mustache: ast.Decorator): unknown { - this.#visitCallNode(mustache); + this.callNode(mustache); return; } @@ -146,8 +146,15 @@ export default class Visitor { return; } - SubExpression(sexpr: ast.SubExpression): unknown { - this.#visitCallNode(sexpr); + SubExpression(sexpr: ast.SubExpression): unknown; + /** + * Passing a `CallNode` to `SubExpression` is deprecated. + * + * @deprecated Call {@linkcode callNode} instead of SubExpression if you aren't passing a SubExpression. + */ + SubExpression(sexpr: ast.CallNode): unknown; + SubExpression(sexpr: ast.CallNode): unknown { + this.callNode(sexpr); return; } @@ -195,21 +202,14 @@ export default class Visitor { return; } - #visitCallNode( - mustache: - | ast.SubExpression - | ast.MustacheStatement - | ast.Decorator - | ast.BlockStatement - | ast.DecoratorBlock - ) { - this.acceptRequired(mustache, 'path'); - this.acceptArray(mustache.params, mustache); - this.acceptField(mustache, 'hash'); + callNode(callNode: ast.CallNode) { + this.acceptRequired(callNode, 'path'); + this.acceptArray(callNode.params, callNode); + this.acceptField(callNode, 'hash'); } #visitBlock(block: ast.BlockStatement | ast.DecoratorBlock) { - this.#visitCallNode(block); + this.callNode(block); this.acceptField(block, 'program'); this.acceptField(block, 'inverse'); diff --git a/spec/utils.js b/spec/utils.js index 9309a42..890c368 100644 --- a/spec/utils.js +++ b/spec/utils.js @@ -51,11 +51,14 @@ export function equalsAst(source, expected, msg) { const ast = astFor(source); if (ast !== `${expected}\n`) { - throw new AssertError( - `\n Source: ${source}\n\n Actual: ${ast} Expected: ${expected}\n` + - (msg ? `\n${msg}` : ''), + const error = new AssertError( + `\n Source: ${source}` + (msg ? `\n${msg}` : ''), equals ); + + error.expected = expected; + error.actual = ast; + throw error; } } From 1b23b820449ca8c3f6593194d9aa6fcfb48ffb76 Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Thu, 24 Oct 2024 22:24:52 -0700 Subject: [PATCH 5/9] Convert remaining lib files --- lib/{index.js => index.ts} | 0 lib/parse.ts | 2 +- lib/whitespace-control.js | 233 ------------------------------ lib/whitespace-control.ts | 285 +++++++++++++++++++++++++++++++++++++ tsconfig.json | 3 - 5 files changed, 286 insertions(+), 237 deletions(-) rename lib/{index.js => index.ts} (100%) delete mode 100644 lib/whitespace-control.js create mode 100644 lib/whitespace-control.ts diff --git a/lib/index.js b/lib/index.ts similarity index 100% rename from lib/index.js rename to lib/index.ts diff --git a/lib/parse.ts b/lib/parse.ts index 23e25aa..d205111 100644 --- a/lib/parse.ts +++ b/lib/parse.ts @@ -30,11 +30,11 @@ export function parse( let ast = parseWithoutProcessing(input, options); let strip = new WhitespaceControl(options); - // @ts-expect-error return strip.accept(ast); } export interface ParseOptions { srcName?: string; + ignoreStandalone?: boolean; syntax?: SyntaxOptions; } diff --git a/lib/whitespace-control.js b/lib/whitespace-control.js deleted file mode 100644 index 8d98c14..0000000 --- a/lib/whitespace-control.js +++ /dev/null @@ -1,233 +0,0 @@ -import Visitor from './visitor.js'; - -function WhitespaceControl(options = {}) { - this.options = options; -} -WhitespaceControl.prototype = new Visitor(); - -WhitespaceControl.prototype.Program = function (program) { - const doStandalone = !this.options.ignoreStandalone; - - let isRoot = !this.isRootSeen; - this.isRootSeen = true; - - let body = program.body; - for (let i = 0, l = body.length; i < l; i++) { - let current = body[i], - strip = this.accept(current); - - if (!strip) { - continue; - } - - let _isPrevWhitespace = isPrevWhitespace(body, i, isRoot), - _isNextWhitespace = isNextWhitespace(body, i, isRoot), - openStandalone = strip.openStandalone && _isPrevWhitespace, - closeStandalone = strip.closeStandalone && _isNextWhitespace, - inlineStandalone = - strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace; - - if (strip.close) { - omitRight(body, i, true); - } - if (strip.open) { - omitLeft(body, i, true); - } - - if (doStandalone && inlineStandalone) { - omitRight(body, i); - - if (omitLeft(body, i)) { - // If we are on a standalone node, save the indent info for partials - if (current.type === 'PartialStatement') { - // Pull out the whitespace from the final line - current.indent = /([ \t]+$)/.exec(body[i - 1].original)[1]; - } - } - } - if (doStandalone && openStandalone) { - omitRight((current.program || current.inverse).body); - - // Strip out the previous content node if it's whitespace only - omitLeft(body, i); - } - if (doStandalone && closeStandalone) { - // Always strip the next node - omitRight(body, i); - - omitLeft((current.inverse || current.program).body); - } - } - - return program; -}; - -WhitespaceControl.prototype.BlockStatement = - WhitespaceControl.prototype.DecoratorBlock = - WhitespaceControl.prototype.PartialBlockStatement = - function (block) { - this.accept(block.program); - this.accept(block.inverse); - - // Find the inverse program that is involved with whitespace stripping. - let program = block.program || block.inverse, - inverse = block.program && block.inverse, - firstInverse = inverse, - lastInverse = inverse; - - if (inverse && inverse.chained) { - firstInverse = inverse.body[0].program; - - // Walk the inverse chain to find the last inverse that is actually in the chain. - while (lastInverse.chained) { - lastInverse = lastInverse.body[lastInverse.body.length - 1].program; - } - } - - let strip = { - open: block.openStrip.open, - close: block.closeStrip.close, - - // Determine the standalone candidacy. Basically flag our content as being possibly standalone - // so our parent can determine if we actually are standalone - openStandalone: isNextWhitespace(program.body), - closeStandalone: isPrevWhitespace((firstInverse || program).body), - }; - - if (block.openStrip.close) { - omitRight(program.body, null, true); - } - - if (inverse) { - let inverseStrip = block.inverseStrip; - - if (inverseStrip.open) { - omitLeft(program.body, null, true); - } - - if (inverseStrip.close) { - omitRight(firstInverse.body, null, true); - } - if (block.closeStrip.open) { - omitLeft(lastInverse.body, null, true); - } - - // Find standalone else statements - if ( - !this.options.ignoreStandalone && - isPrevWhitespace(program.body) && - isNextWhitespace(firstInverse.body) - ) { - omitLeft(program.body); - omitRight(firstInverse.body); - } - } else if (block.closeStrip.open) { - omitLeft(program.body, null, true); - } - - return strip; - }; - -WhitespaceControl.prototype.Decorator = - WhitespaceControl.prototype.MustacheStatement = function (mustache) { - return mustache.strip; - }; - -WhitespaceControl.prototype.PartialStatement = - WhitespaceControl.prototype.CommentStatement = function (node) { - /* istanbul ignore next */ - let strip = node.strip || {}; - return { - inlineStandalone: true, - open: strip.open, - close: strip.close, - }; - }; - -function isPrevWhitespace(body, i, isRoot) { - if (i === undefined) { - i = body.length; - } - - // Nodes that end with newlines are considered whitespace (but are special - // cased for strip operations) - let prev = body[i - 1], - sibling = body[i - 2]; - if (!prev) { - return isRoot; - } - - if (prev.type === 'ContentStatement') { - return (sibling || !isRoot ? /\r?\n\s*?$/ : /(^|\r?\n)\s*?$/).test( - prev.original - ); - } -} -function isNextWhitespace(body, i, isRoot) { - if (i === undefined) { - i = -1; - } - - let next = body[i + 1], - sibling = body[i + 2]; - if (!next) { - return isRoot; - } - - if (next.type === 'ContentStatement') { - return (sibling || !isRoot ? /^\s*?\r?\n/ : /^\s*?(\r?\n|$)/).test( - next.original - ); - } -} - -// Marks the node to the right of the position as omitted. -// I.e. {{foo}}' ' will mark the ' ' node as omitted. -// -// If i is undefined, then the first child will be marked as such. -// -// If multiple is truthy then all whitespace will be stripped out until non-whitespace -// content is met. -function omitRight(body, i, multiple) { - let current = body[i == null ? 0 : i + 1]; - if ( - !current || - current.type !== 'ContentStatement' || - (!multiple && current.rightStripped) - ) { - return; - } - - let original = current.value; - current.value = current.value.replace( - multiple ? /^\s+/ : /^[ \t]*\r?\n?/, - '' - ); - current.rightStripped = current.value !== original; -} - -// Marks the node to the left of the position as omitted. -// I.e. ' '{{foo}} will mark the ' ' node as omitted. -// -// If i is undefined then the last child will be marked as such. -// -// If multiple is truthy then all whitespace will be stripped out until non-whitespace -// content is met. -function omitLeft(body, i, multiple) { - let current = body[i == null ? body.length - 1 : i - 1]; - if ( - !current || - current.type !== 'ContentStatement' || - (!multiple && current.leftStripped) - ) { - return; - } - - // We omit the last node if it's whitespace only and not preceded by a non-content node. - let original = current.value; - current.value = current.value.replace(multiple ? /\s+$/ : /[ \t]+$/, ''); - current.leftStripped = current.value !== original; - return current.leftStripped; -} - -export default WhitespaceControl; diff --git a/lib/whitespace-control.ts b/lib/whitespace-control.ts new file mode 100644 index 0000000..12c5a1d --- /dev/null +++ b/lib/whitespace-control.ts @@ -0,0 +1,285 @@ +import Visitor from './visitor.js'; +import type * as ast from './types/ast.js'; + +interface WhitespaceControlOptions { + ignoreStandalone?: boolean; +} + +export default class WhitespaceControl extends Visitor { + options: WhitespaceControlOptions; + isRootSeen = false; + + constructor(options: WhitespaceControlOptions) { + super(); + this.options = options; + } + + Program(program: ast.Program) { + const doStandalone = !this.options.ignoreStandalone; + + let isRoot = !this.isRootSeen; + this.isRootSeen = true; + + let body = program.body; + for (let i = 0, l = body.length; i < l; i++) { + let current = body[i]; + const strip = this.accept(current) as ast.StripFlags | undefined; + + if (!strip) { + continue; + } + + let _isPrevWhitespace = isPrevWhitespace(body, i, isRoot); + let _isNextWhitespace = isNextWhitespace(body, i, isRoot); + let openStandalone = strip.openStandalone && _isPrevWhitespace; + let closeStandalone = strip.closeStandalone && _isNextWhitespace; + let inlineStandalone = + strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace; + + if (strip.close) { + omitRight(body, i, true); + } + + if (strip.open) { + omitLeft(body, i, true); + } + + if (doStandalone && inlineStandalone) { + omitRight(body, i); + + if (omitLeft(body, i)) { + // If we are on a standalone node, save the indent info for partials + if (current.type === 'PartialStatement') { + // Pull out the whitespace from the final line + // @ts-expect-error + current.indent = /([ \t]+$)/.exec(body[i - 1].original)[1]; + } + } + } + if (doStandalone && openStandalone) { + // @ts-expect-error + omitRight((current.program || current.inverse).body); + + // Strip out the previous content node if it's whitespace only + omitLeft(body, i); + } + if (doStandalone && closeStandalone) { + // Always strip the next node + omitRight(body, i); + + // @ts-expect-error + omitLeft((current.inverse || current.program).body); + } + } + + return program; + } + + BlockStatement(block: ast.BlockStatement) { + return this.#blockStatement(block); + } + + DecoratorBlock(block: ast.DecoratorBlock) { + return this.#blockStatement(block); + } + + PartialBlockStatement(block: ast.PartialBlockStatement) { + return this.#blockStatement(block); + } + + #blockStatement( + block: ast.PartialBlockStatement | ast.BlockStatement | ast.DecoratorBlock + ) { + this.accept(block.program); + this.accept(block.inverse); + + // Find the inverse program that is involved with whitespace stripping. + let program = block.program || block.inverse; + let inverse = block.program && block.inverse; + let firstInverse = inverse; + let lastInverse = inverse; + + if (inverse?.chained) { + // @ts-expect-error + firstInverse = inverse.body[0].program; + + // Walk the inverse chain to find the last inverse that is actually in the chain. + while (lastInverse?.chained) { + // @ts-expect-error + lastInverse = lastInverse.body[lastInverse.body.length - 1].program; + } + } + + let strip = { + open: block.openStrip.open, + close: block.closeStrip.close, + + // Determine the standalone candidacy. Basically flag our content as being possibly standalone + // so our parent can determine if we actually are standalone + openStandalone: isNextWhitespace(program.body), + closeStandalone: isPrevWhitespace((firstInverse || program).body), + }; + + if (block.openStrip.close) { + omitRight(program.body, null, true); + } + + if (inverse) { + let inverseStrip = block.inverseStrip; + + if (inverseStrip?.open) { + omitLeft(program.body, null, true); + } + + if (inverseStrip?.close && firstInverse) { + omitRight(firstInverse.body, null, true); + } + if (block.closeStrip.open && lastInverse) { + omitLeft(lastInverse.body, null, true); + } + + // Find standalone else statements + if ( + !this.options.ignoreStandalone && + isPrevWhitespace(program.body) && + // @ts-expect-error + isNextWhitespace(firstInverse.body) + ) { + omitLeft(program.body); + // @ts-expect-error + omitRight(firstInverse.body); + } + } else if (block.closeStrip.open) { + omitLeft(program.body, null, true); + } + + return strip; + } + + Decorator(node: ast.Decorator) { + return node.strip; + } + + MustacheStatement(node: ast.MustacheStatement) { + return node.strip; + } + + PartialStatement(node: ast.PartialStatement) { + return this.#statement(node); + } + + CommentStatement(node: ast.CommentStatement) { + return this.#statement(node); + } + + #statement(node: ast.PartialStatement | ast.CommentStatement) { + let strip = node.strip || {}; + return { + inlineStandalone: true, + open: strip.open, + close: strip.close, + }; + } +} + +function isPrevWhitespace(body: ast.Statement[], i?: number, isRoot?: boolean) { + if (i === undefined) { + i = body.length; + } + + // Nodes that end with newlines are considered whitespace (but are special + // cased for strip operations) + let prev = body[i - 1], + sibling = body[i - 2]; + if (!prev) { + return isRoot; + } + + if (prev.type === 'ContentStatement') { + return (sibling || !isRoot ? /\r?\n\s*?$/ : /(^|\r?\n)\s*?$/).test( + prev.original + ); + } +} + +function isNextWhitespace(body: ast.Statement[], i?: number, isRoot?: boolean) { + if (i === undefined) { + i = -1; + } + + let next = body[i + 1], + sibling = body[i + 2]; + if (!next) { + return isRoot; + } + + if (next.type === 'ContentStatement') { + return (sibling || !isRoot ? /^\s*?\r?\n/ : /^\s*?(\r?\n|$)/).test( + next.original + ); + } +} + +// Marks the node to the right of the position as omitted. +// I.e. {{foo}}' ' will mark the ' ' node as omitted. +// +// If i is undefined, then the first child will be marked as such. +// +// If multiple is truthy then all whitespace will be stripped out until non-whitespace +// content is met. +function omitRight( + body: ast.Statement[], + i?: number | null, + multiple?: boolean +) { + let current = body[i == null ? 0 : i + 1]; + if ( + !current || + current.type !== 'ContentStatement' || + (!multiple && current.rightStripped) + ) { + return; + } + + let original = current.value; + current.value = current.value.replace( + multiple ? /^\s+/ : /^[ \t]*\r?\n?/, + '' + ); + + current.rightStripped = current.value !== original; +} + +// Marks the node to the left of the position as omitted. +// I.e. ' '{{foo}} will mark the ' ' node as omitted. +// +// If i is undefined then the last child will be marked as such. +// +// If multiple is truthy then all whitespace will be stripped out until non-whitespace +// content is met. +function omitLeft( + body: ast.Statement[], + i?: number | null, + multiple?: boolean +) { + let current = body[i == null ? body.length - 1 : i - 1]; + if ( + !current || + current.type !== 'ContentStatement' || + (!multiple && current.leftStripped) + ) { + return; + } + + // We omit the last node if it's whitespace only and not preceded by a non-content node. + let original = current.value; + current.value = current.value.replace(multiple ? /\s+$/ : /[ \t]+$/, ''); + current.leftStripped = current.value !== original; + return current.leftStripped; +} + +// export default WhitespaceControl; + +function hasInverse( + block: ast.PartialBlockStatement | ast.BlockStatement | ast.DecoratorBlock +) {} diff --git a/tsconfig.json b/tsconfig.json index 75cd9e6..42a856f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,9 +13,6 @@ // Enhance Strictness "strict": true, "suppressImplicitAnyIndexErrors": false, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true, "newLine": "LF", "allowJs": true, }, From 5605176a797e8397a0dd44e7e431a54e464e6c4a Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Thu, 24 Oct 2024 15:13:42 -0700 Subject: [PATCH 6/9] Migrate helper to TS --- lib/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers.ts b/lib/helpers.ts index 18cd1e8..ee8adf9 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -20,7 +20,7 @@ export class ParserHelpers { constructor(options: ParseOptions) { this.#options = options; - let squareSyntax; + let squareSyntax: SyntaxOptions['square']; if (typeof options?.syntax?.square === 'function') { squareSyntax = options.syntax.square; From dc41ec84030c91c9a441b0cf4e6e16818ffcfc9d Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Thu, 24 Oct 2024 16:39:10 -0700 Subject: [PATCH 7/9] Migrate visitor to TS --- lib/visitor.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/visitor.ts b/lib/visitor.ts index 11a12c3..9311587 100644 --- a/lib/visitor.ts +++ b/lib/visitor.ts @@ -108,12 +108,12 @@ export default class Visitor { } MustacheStatement(mustache: ast.MustacheStatement): unknown { - this.callNode(mustache); + this.#visitCallNode(mustache); return; } Decorator(mustache: ast.Decorator): unknown { - this.callNode(mustache); + this.#visitCallNode(mustache); return; } @@ -154,7 +154,7 @@ export default class Visitor { */ SubExpression(sexpr: ast.CallNode): unknown; SubExpression(sexpr: ast.CallNode): unknown { - this.callNode(sexpr); + this.#visitCallNode(sexpr); return; } @@ -202,14 +202,21 @@ export default class Visitor { return; } - callNode(callNode: ast.CallNode) { - this.acceptRequired(callNode, 'path'); - this.acceptArray(callNode.params, callNode); - this.acceptField(callNode, 'hash'); + #visitCallNode( + mustache: + | ast.SubExpression + | ast.MustacheStatement + | ast.Decorator + | ast.BlockStatement + | ast.DecoratorBlock + ) { + this.acceptRequired(mustache, 'path'); + this.acceptArray(mustache.params, mustache); + this.acceptField(mustache, 'hash'); } #visitBlock(block: ast.BlockStatement | ast.DecoratorBlock) { - this.callNode(block); + this.#visitCallNode(block); this.acceptField(block, 'program'); this.acceptField(block, 'inverse'); From 2d9f005220db311c3fbbb585f610157df431816d Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Thu, 24 Oct 2024 17:25:42 -0700 Subject: [PATCH 8/9] Migrate more files to TS --- lib/printer.ts | 17 +-- lib/types/ast.d.ts | 10 +- lib/types/types.d.ts | 240 +++++++++++++++++++++++++++++++++++++++++++ lib/visitor.ts | 23 ++--- 4 files changed, 266 insertions(+), 24 deletions(-) create mode 100644 lib/types/types.d.ts diff --git a/lib/printer.ts b/lib/printer.ts index a0c03a9..eaeedaf 100644 --- a/lib/printer.ts +++ b/lib/printer.ts @@ -43,10 +43,9 @@ export class PrintVisitor extends Visitor { return out; } - callBody(callExpr: ast.CallNode) { - let params = callExpr.params, - paramStrings = [], - hash; + callNode(callExpr: ast.CallNode) { + let params = callExpr.params; + let paramStrings = []; for (let i = 0, l = params.length; i < l; i++) { paramStrings.push(this.accept(params[i])); @@ -62,7 +61,7 @@ export class PrintVisitor extends Visitor { MustacheStatement(mustache: ast.MustacheStatement) { if (mustache.params.length > 0 || mustache.hash) { - return this.pad('{{ ' + this.callBody(mustache) + ' }}'); + return this.pad('{{ ' + this.callNode(mustache) + ' }}'); } else { return this.pad('{{ ' + this.accept(mustache.path) + ' }}'); } @@ -87,7 +86,7 @@ export class PrintVisitor extends Visitor { (block.type === 'DecoratorBlock' ? 'DIRECTIVE ' : '') + 'BLOCK:' ); this.padding++; - out += this.pad(this.callBody(block)); + out += this.pad(this.callNode(block)); if (block.program) { out += this.pad('PROGRAM:'); this.padding++; @@ -150,7 +149,7 @@ export class PrintVisitor extends Visitor { } SubExpression(subExpression: ast.SubExpression) { - return this.callBody(subExpression); + return `(${this.callNode(subExpression)})`; } PathExpression(id: ast.PathExpression) { @@ -188,6 +187,10 @@ export class PrintVisitor extends Visitor { return `Hash{${this.hashPairs(hash.pairs)}}`; } + Hash(hash: ast.Hash) { + return `HASH{${this.hashPairs(hash.pairs)}}`; + } + hashPairs(pairs: ast.HashPair[]) { const joinedPairs = []; diff --git a/lib/types/ast.d.ts b/lib/types/ast.d.ts index 034ceae..1c8f470 100644 --- a/lib/types/ast.d.ts +++ b/lib/types/ast.d.ts @@ -74,6 +74,7 @@ export interface Program extends Node { blockParams?: string[]; /** @compat */ strip: {}; + chained?: boolean; } export interface BaseStatement extends Node {} @@ -116,7 +117,7 @@ export interface PartialStatement extends BaseStatement, WithArgsNode { strip: StripFlags; } -export interface PartialBlockStatement extends BaseStatement, WithArgsNode { +export interface PartialBlockStatement extends BaseBlockStatement { type: 'PartialBlockStatement'; name: PathExpression | SubExpression; program: Program; @@ -127,7 +128,9 @@ export interface PartialBlockStatement extends BaseStatement, WithArgsNode { export interface ContentStatement extends BaseStatement { type: 'ContentStatement'; value: string; - original: StripFlags; + original: string; + rightStripped?: boolean; + leftStripped?: boolean; } export interface CommentStatement extends BaseStatement { @@ -208,6 +211,9 @@ export interface HashPair extends Node { export interface StripFlags { open?: boolean; close?: boolean; + openStandalone?: boolean; + closeStandalone?: boolean; + inlineStandalone?: boolean; } export interface WithArgsNode { diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts new file mode 100644 index 0000000..2eafe56 --- /dev/null +++ b/lib/types/types.d.ts @@ -0,0 +1,240 @@ +import * as ast from './ast.js'; + +export type BaseNode = ast.Node; + +export interface InverseChain extends ast.HasLocation { + strip: StripFlags; + program: Program; + chain?: boolean; +} + +export interface Part { + part: string; + original: string; + separator?: string; +} + +export interface Program { + type: 'Program'; + /** + * The root node of a program has no `loc` if it's empty. + */ + loc: SourceLocation | undefined; + blockParams?: string[]; + body: Statement[]; + chained?: boolean; + strip: StripFlags; +} + +export interface CommentStatement extends BaseNode { + type: 'CommentStatement'; + value: string; + strip: StripFlags; +} + +export interface PartialStatement extends BaseNode { + type: 'PartialStatement'; + name: Expression; + params: Expression[]; + hash: Hash; + indent: string; + strip: StripFlags; +} + +export interface BlockStatement extends BaseNode { + type: 'BlockStatement'; + path: Expression; + params: Expression[]; + hash: Hash; + program: Program | undefined; + inverse?: Program | undefined; + openStrip: StripFlags; + inverseStrip: StripFlags | undefined; + closeStrip: StripFlags; +} + +export interface DecoratorBlock extends BaseNode { + type: 'DecoratorBlock'; + path: Expression; + params: Expression[]; + hash: Hash; + program: Program; + inverse?: undefined; + inverseStrip?: undefined; + openStrip: StripFlags; + closeStrip: StripFlags; +} + +export interface PartialBlockStatement extends BaseNode { + type: 'PartialBlockStatement'; + name: Expression; + params: Expression[]; + hash: Hash; + program: Program; + inverse?: undefined; + inverseStrip?: undefined; + openStrip: StripFlags; + closeStrip: StripFlags; +} + +export type Statement = + | MustacheStatement + | Content + | BlockStatement + | PartialStatement + | PartialBlockStatement; + +export interface MustacheStatement extends BaseNode { + type: 'Decorator' | 'MustacheStatement'; + path: Expression; + params: Expression[]; + hash: Hash; + escaped: boolean; + strip: StripFlags; +} + +export interface PathExpression extends BaseNode { + readonly original: string; + readonly this: boolean; + readonly data: boolean; + readonly depth: number; + readonly parts: (string | SubExpression)[]; + readonly head: string | SubExpression | undefined; + readonly tail: string[]; +} + +export interface SubExpression extends BaseNode { + readonly original: string; +} + +export interface Hash { + readonly pairs: HashPair[]; +} + +export interface StripFlags { + readonly open?: boolean; + readonly close?: boolean; + readonly openStandalone?: boolean; + readonly closeStandalone?: boolean; + readonly inlineStandalone?: boolean; +} + +export interface HashPair { + readonly key: string; + readonly value: Expression; +} + +export interface ParserPart { + readonly part: string; + readonly original: string; + readonly separator: string; +} + +export interface Content extends BaseNode { + type: 'ContentStatement'; + original: string; + value: string; +} + +export type Expression = SubExpression | PathExpression; + +export interface SourcePosition { + line: number; + column: number; +} + +export interface SourceLocation { + source: string | undefined; + start: SourcePosition; + end: SourcePosition; +} + +export interface CallNode { + path: ast.Expr; + params: ast.Expr[]; + hash: ast.Hash; +} + +export interface OpenPartial { + strip: ast.StripFlags; +} + +export interface OpenPartialBlock extends CallNode { + strip: ast.StripFlags; +} + +export interface OpenRawBlock extends CallNode, BaseNode {} + +export interface OpenBlock extends CallNode { + open: string; + blockParams: string[]; + strip: StripFlags; +} + +export interface OpenInverse extends CallNode { + blockParams: string[]; + strip: StripFlags; +} + +export interface CloseBlock { + readonly path: PathExpression; + strip: StripFlags; +} + +export type AcceptedNode = Program; + +/// JISON TYPES /// + +export interface Parser { + parse: (input: string) => Program; + yy: YY; +} + +export interface YY { + locInfo(locInfo: LocInfo): SourceLocation; + preparePath( + this: YY, + data: boolean, + sexpr: { expr: SubExpression; sep: string } | false, + parts: ParserPart[], + locInfo: LocInfo + ): PathExpression; + + prepareMustache( + this: YY, + path: PathExpression, + params: Expression[], + hash: Hash, + open: string, + strip: StripFlags, + locInfo: LocInfo + ): MustacheStatement; + + prepareRawBlock( + this: YY, + openRawBlock: OpenRawBlock, + contents: Content[], + close: string, + locInfo: LocInfo + ): BlockStatement; + + prepareBlock( + this: YY, + openBlock: OpenBlock, + program: Program, + inverseChain: InverseChain, + close: CloseBlock, + inverted: boolean, + locInfo: LocInfo + ): BlockStatement | DecoratorBlock; +} + +/** + * The `LocInfo` object comes from the generated `jison` parser. + */ +export interface LocInfo { + first_line: number; + first_column: number; + last_line: number; + last_column: number; +} diff --git a/lib/visitor.ts b/lib/visitor.ts index 9311587..11a12c3 100644 --- a/lib/visitor.ts +++ b/lib/visitor.ts @@ -108,12 +108,12 @@ export default class Visitor { } MustacheStatement(mustache: ast.MustacheStatement): unknown { - this.#visitCallNode(mustache); + this.callNode(mustache); return; } Decorator(mustache: ast.Decorator): unknown { - this.#visitCallNode(mustache); + this.callNode(mustache); return; } @@ -154,7 +154,7 @@ export default class Visitor { */ SubExpression(sexpr: ast.CallNode): unknown; SubExpression(sexpr: ast.CallNode): unknown { - this.#visitCallNode(sexpr); + this.callNode(sexpr); return; } @@ -202,21 +202,14 @@ export default class Visitor { return; } - #visitCallNode( - mustache: - | ast.SubExpression - | ast.MustacheStatement - | ast.Decorator - | ast.BlockStatement - | ast.DecoratorBlock - ) { - this.acceptRequired(mustache, 'path'); - this.acceptArray(mustache.params, mustache); - this.acceptField(mustache, 'hash'); + callNode(callNode: ast.CallNode) { + this.acceptRequired(callNode, 'path'); + this.acceptArray(callNode.params, callNode); + this.acceptField(callNode, 'hash'); } #visitBlock(block: ast.BlockStatement | ast.DecoratorBlock) { - this.#visitCallNode(block); + this.callNode(block); this.acceptField(block, 'program'); this.acceptField(block, 'inverse'); From bc09c39fb3f28afe17a90072e502c34aa1b6c83c Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Fri, 25 Oct 2024 14:54:01 -0700 Subject: [PATCH 9/9] options is not optional --- lib/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers.ts b/lib/helpers.ts index ee8adf9..4d68f69 100644 --- a/lib/helpers.ts +++ b/lib/helpers.ts @@ -22,7 +22,7 @@ export class ParserHelpers { let squareSyntax: SyntaxOptions['square']; - if (typeof options?.syntax?.square === 'function') { + if (typeof options.syntax?.square === 'function') { squareSyntax = options.syntax.square; } else if (options?.syntax?.square === 'node') { squareSyntax = arrayLiteralNode;