// src/lexer.ts var TOKEN_TYPES = Object.freeze({ Text: "Text", // The text between Jinja statements or expressions NumericLiteral: "NumericLiteral", // e.g., 123, 1.0 StringLiteral: "StringLiteral", // 'string' Identifier: "Identifier", // Variables, functions, statements, booleans, etc. Equals: "Equals", // = OpenParen: "OpenParen", // ( CloseParen: "CloseParen", // ) OpenStatement: "OpenStatement", // {% CloseStatement: "CloseStatement", // %} OpenExpression: "OpenExpression", // {{ CloseExpression: "CloseExpression", // }} OpenSquareBracket: "OpenSquareBracket", // [ CloseSquareBracket: "CloseSquareBracket", // ] OpenCurlyBracket: "OpenCurlyBracket", // { CloseCurlyBracket: "CloseCurlyBracket", // } Comma: "Comma", // , Dot: "Dot", // . Colon: "Colon", // : Pipe: "Pipe", // | CallOperator: "CallOperator", // () AdditiveBinaryOperator: "AdditiveBinaryOperator", // + - ~ MultiplicativeBinaryOperator: "MultiplicativeBinaryOperator", // * / % ComparisonBinaryOperator: "ComparisonBinaryOperator", // < > <= >= == != UnaryOperator: "UnaryOperator", // ! - + Comment: "Comment" // {# ... #} }); var Token = class { /** * Constructs a new Token. * @param {string} value The raw value as seen inside the source code. * @param {TokenType} type The type of token. */ constructor(value, type) { this.value = value; this.type = type; } }; function isWord(char) { return /\w/.test(char); } function isInteger(char) { return /[0-9]/.test(char); } function isWhitespace(char) { return /\s/.test(char); } var ORDERED_MAPPING_TABLE = [ // Control sequences ["{%", TOKEN_TYPES.OpenStatement], ["%}", TOKEN_TYPES.CloseStatement], ["{{", TOKEN_TYPES.OpenExpression], ["}}", TOKEN_TYPES.CloseExpression], // Single character tokens ["(", TOKEN_TYPES.OpenParen], [")", TOKEN_TYPES.CloseParen], ["{", TOKEN_TYPES.OpenCurlyBracket], ["}", TOKEN_TYPES.CloseCurlyBracket], ["[", TOKEN_TYPES.OpenSquareBracket], ["]", TOKEN_TYPES.CloseSquareBracket], [",", TOKEN_TYPES.Comma], [".", TOKEN_TYPES.Dot], [":", TOKEN_TYPES.Colon], ["|", TOKEN_TYPES.Pipe], // Comparison operators ["<=", TOKEN_TYPES.ComparisonBinaryOperator], [">=", TOKEN_TYPES.ComparisonBinaryOperator], ["==", TOKEN_TYPES.ComparisonBinaryOperator], ["!=", TOKEN_TYPES.ComparisonBinaryOperator], ["<", TOKEN_TYPES.ComparisonBinaryOperator], [">", TOKEN_TYPES.ComparisonBinaryOperator], // Arithmetic operators ["+", TOKEN_TYPES.AdditiveBinaryOperator], ["-", TOKEN_TYPES.AdditiveBinaryOperator], ["~", TOKEN_TYPES.AdditiveBinaryOperator], ["*", TOKEN_TYPES.MultiplicativeBinaryOperator], ["/", TOKEN_TYPES.MultiplicativeBinaryOperator], ["%", TOKEN_TYPES.MultiplicativeBinaryOperator], // Assignment operator ["=", TOKEN_TYPES.Equals] ]; var ESCAPE_CHARACTERS = /* @__PURE__ */ new Map([ ["n", "\n"], // New line ["t", " "], // Horizontal tab ["r", "\r"], // Carriage return ["b", "\b"], // Backspace ["f", "\f"], // Form feed ["v", "\v"], // Vertical tab ["'", "'"], // Single quote ['"', '"'], // Double quote ["\\", "\\"] // Backslash ]); function preprocess(template, options = {}) { if (template.endsWith("\n")) { template = template.slice(0, -1); } if (options.lstrip_blocks) { template = template.replace(/^[ \t]*({[#%-])/gm, "$1"); } if (options.trim_blocks) { template = template.replace(/([#%-]})\n/g, "$1"); } return template.replace(/{%\s*(end)?generation\s*%}/gs, ""); } function tokenize(source, options = {}) { const tokens = []; const src = preprocess(source, options); let cursorPosition = 0; let curlyBracketDepth = 0; const consumeWhile = (predicate) => { let str = ""; while (predicate(src[cursorPosition])) { if (src[cursorPosition] === "\\") { ++cursorPosition; if (cursorPosition >= src.length) throw new SyntaxError("Unexpected end of input"); const escaped = src[cursorPosition++]; const unescaped = ESCAPE_CHARACTERS.get(escaped); if (unescaped === void 0) { throw new SyntaxError(`Unexpected escaped character: ${escaped}`); } str += unescaped; continue; } str += src[cursorPosition++]; if (cursorPosition >= src.length) throw new SyntaxError("Unexpected end of input"); } return str; }; const stripTrailingWhitespace = () => { const lastToken = tokens.at(-1); if (lastToken && lastToken.type === TOKEN_TYPES.Text) { lastToken.value = lastToken.value.trimEnd(); if (lastToken.value === "") { tokens.pop(); } } }; const skipLeadingWhitespace = () => { while (cursorPosition < src.length && isWhitespace(src[cursorPosition])) { ++cursorPosition; } }; main: while (cursorPosition < src.length) { const lastTokenType = tokens.at(-1)?.type; if (lastTokenType === void 0 || lastTokenType === TOKEN_TYPES.CloseStatement || lastTokenType === TOKEN_TYPES.CloseExpression || lastTokenType === TOKEN_TYPES.Comment) { let text = ""; while (cursorPosition < src.length && // Keep going until we hit the next Jinja statement or expression !(src[cursorPosition] === "{" && (src[cursorPosition + 1] === "%" || src[cursorPosition + 1] === "{" || src[cursorPosition + 1] === "#"))) { text += src[cursorPosition++]; } if (text.length > 0) { tokens.push(new Token(text, TOKEN_TYPES.Text)); continue; } } if (src[cursorPosition] === "{" && src[cursorPosition + 1] === "#") { cursorPosition += 2; const stripBefore = src[cursorPosition] === "-"; if (stripBefore) { ++cursorPosition; } let comment = ""; while (src[cursorPosition] !== "#" || src[cursorPosition + 1] !== "}") { if (cursorPosition + 2 >= src.length) { throw new SyntaxError("Missing end of comment tag"); } comment += src[cursorPosition++]; } const stripAfter = comment.endsWith("-"); if (stripAfter) { comment = comment.slice(0, -1); } if (stripBefore) { stripTrailingWhitespace(); } tokens.push(new Token(comment, TOKEN_TYPES.Comment)); cursorPosition += 2; if (stripAfter) { skipLeadingWhitespace(); } continue; } if (src.slice(cursorPosition, cursorPosition + 3) === "{%-") { stripTrailingWhitespace(); tokens.push(new Token("{%", TOKEN_TYPES.OpenStatement)); cursorPosition += 3; continue; } if (src.slice(cursorPosition, cursorPosition + 3) === "{{-") { stripTrailingWhitespace(); tokens.push(new Token("{{", TOKEN_TYPES.OpenExpression)); curlyBracketDepth = 0; cursorPosition += 3; continue; } consumeWhile(isWhitespace); if (src.slice(cursorPosition, cursorPosition + 3) === "-%}") { tokens.push(new Token("%}", TOKEN_TYPES.CloseStatement)); cursorPosition += 3; skipLeadingWhitespace(); continue; } if (src.slice(cursorPosition, cursorPosition + 3) === "-}}") { tokens.push(new Token("}}", TOKEN_TYPES.CloseExpression)); cursorPosition += 3; skipLeadingWhitespace(); continue; } const char = src[cursorPosition]; if (char === "-" || char === "+") { const lastTokenType2 = tokens.at(-1)?.type; if (lastTokenType2 === TOKEN_TYPES.Text || lastTokenType2 === void 0) { throw new SyntaxError(`Unexpected character: ${char}`); } switch (lastTokenType2) { case TOKEN_TYPES.Identifier: case TOKEN_TYPES.NumericLiteral: case TOKEN_TYPES.StringLiteral: case TOKEN_TYPES.CloseParen: case TOKEN_TYPES.CloseSquareBracket: break; default: { ++cursorPosition; const num = consumeWhile(isInteger); tokens.push( new Token(`${char}${num}`, num.length > 0 ? TOKEN_TYPES.NumericLiteral : TOKEN_TYPES.UnaryOperator) ); continue; } } } for (const [seq, type] of ORDERED_MAPPING_TABLE) { if (seq === "}}" && curlyBracketDepth > 0) { continue; } const slice2 = src.slice(cursorPosition, cursorPosition + seq.length); if (slice2 === seq) { tokens.push(new Token(seq, type)); if (type === TOKEN_TYPES.OpenExpression) { curlyBracketDepth = 0; } else if (type === TOKEN_TYPES.OpenCurlyBracket) { ++curlyBracketDepth; } else if (type === TOKEN_TYPES.CloseCurlyBracket) { --curlyBracketDepth; } cursorPosition += seq.length; continue main; } } if (char === "'" || char === '"') { ++cursorPosition; const str = consumeWhile((c) => c !== char); tokens.push(new Token(str, TOKEN_TYPES.StringLiteral)); ++cursorPosition; continue; } if (isInteger(char)) { let num = consumeWhile(isInteger); if (src[cursorPosition] === "." && isInteger(src[cursorPosition + 1])) { ++cursorPosition; const frac = consumeWhile(isInteger); num = `${num}.${frac}`; } tokens.push(new Token(num, TOKEN_TYPES.NumericLiteral)); continue; } if (isWord(char)) { const word = consumeWhile(isWord); tokens.push(new Token(word, TOKEN_TYPES.Identifier)); continue; } throw new SyntaxError(`Unexpected character: ${char}`); } return tokens; } // src/ast.ts var Statement = class { type = "Statement"; }; var Program = class extends Statement { constructor(body) { super(); this.body = body; } type = "Program"; }; var If = class extends Statement { constructor(test, body, alternate) { super(); this.test = test; this.body = body; this.alternate = alternate; } type = "If"; }; var For = class extends Statement { constructor(loopvar, iterable, body, defaultBlock) { super(); this.loopvar = loopvar; this.iterable = iterable; this.body = body; this.defaultBlock = defaultBlock; } type = "For"; }; var Break = class extends Statement { type = "Break"; }; var Continue = class extends Statement { type = "Continue"; }; var SetStatement = class extends Statement { constructor(assignee, value, body) { super(); this.assignee = assignee; this.value = value; this.body = body; } type = "Set"; }; var Macro = class extends Statement { constructor(name, args, body) { super(); this.name = name; this.args = args; this.body = body; } type = "Macro"; }; var Comment = class extends Statement { constructor(value) { super(); this.value = value; } type = "Comment"; }; var Expression = class extends Statement { type = "Expression"; }; var MemberExpression = class extends Expression { constructor(object, property, computed) { super(); this.object = object; this.property = property; this.computed = computed; } type = "MemberExpression"; }; var CallExpression = class extends Expression { constructor(callee, args) { super(); this.callee = callee; this.args = args; } type = "CallExpression"; }; var Identifier = class extends Expression { /** * @param {string} value The name of the identifier */ constructor(value) { super(); this.value = value; } type = "Identifier"; }; var Literal = class extends Expression { constructor(value) { super(); this.value = value; } type = "Literal"; }; var IntegerLiteral = class extends Literal { type = "IntegerLiteral"; }; var FloatLiteral = class extends Literal { type = "FloatLiteral"; }; var StringLiteral = class extends Literal { type = "StringLiteral"; }; var ArrayLiteral = class extends Literal { type = "ArrayLiteral"; }; var TupleLiteral = class extends Literal { type = "TupleLiteral"; }; var ObjectLiteral = class extends Literal { type = "ObjectLiteral"; }; var BinaryExpression = class extends Expression { constructor(operator, left, right) { super(); this.operator = operator; this.left = left; this.right = right; } type = "BinaryExpression"; }; var FilterExpression = class extends Expression { constructor(operand, filter) { super(); this.operand = operand; this.filter = filter; } type = "FilterExpression"; }; var FilterStatement = class extends Statement { constructor(filter, body) { super(); this.filter = filter; this.body = body; } type = "FilterStatement"; }; var SelectExpression = class extends Expression { constructor(lhs, test) { super(); this.lhs = lhs; this.test = test; } type = "SelectExpression"; }; var TestExpression = class extends Expression { constructor(operand, negate, test) { super(); this.operand = operand; this.negate = negate; this.test = test; } type = "TestExpression"; }; var UnaryExpression = class extends Expression { constructor(operator, argument) { super(); this.operator = operator; this.argument = argument; } type = "UnaryExpression"; }; var SliceExpression = class extends Expression { constructor(start = void 0, stop = void 0, step = void 0) { super(); this.start = start; this.stop = stop; this.step = step; } type = "SliceExpression"; }; var KeywordArgumentExpression = class extends Expression { constructor(key, value) { super(); this.key = key; this.value = value; } type = "KeywordArgumentExpression"; }; var SpreadExpression = class extends Expression { constructor(argument) { super(); this.argument = argument; } type = "SpreadExpression"; }; var CallStatement = class extends Statement { constructor(call, callerArgs, body) { super(); this.call = call; this.callerArgs = callerArgs; this.body = body; } type = "CallStatement"; }; var Ternary = class extends Expression { constructor(condition, trueExpr, falseExpr) { super(); this.condition = condition; this.trueExpr = trueExpr; this.falseExpr = falseExpr; } type = "Ternary"; }; // src/parser.ts function parse(tokens) { const program = new Program([]); let current = 0; function expect(type, error) { const prev = tokens[current++]; if (!prev || prev.type !== type) { throw new Error(`Parser Error: ${error}. ${prev.type} !== ${type}.`); } return prev; } function expectIdentifier(name) { if (!isIdentifier(name)) { throw new SyntaxError(`Expected ${name}`); } ++current; } function parseAny() { switch (tokens[current].type) { case TOKEN_TYPES.Comment: return new Comment(tokens[current++].value); case TOKEN_TYPES.Text: return parseText(); case TOKEN_TYPES.OpenStatement: return parseJinjaStatement(); case TOKEN_TYPES.OpenExpression: return parseJinjaExpression(); default: throw new SyntaxError(`Unexpected token type: ${tokens[current].type}`); } } function is(...types) { return current + types.length <= tokens.length && types.every((type, i) => type === tokens[current + i].type); } function isStatement(...names) { return tokens[current]?.type === TOKEN_TYPES.OpenStatement && tokens[current + 1]?.type === TOKEN_TYPES.Identifier && names.includes(tokens[current + 1]?.value); } function isIdentifier(...names) { return current + names.length <= tokens.length && names.every((name, i) => tokens[current + i].type === "Identifier" && name === tokens[current + i].value); } function parseText() { return new StringLiteral(expect(TOKEN_TYPES.Text, "Expected text token").value); } function parseJinjaStatement() { expect(TOKEN_TYPES.OpenStatement, "Expected opening statement token"); if (tokens[current].type !== TOKEN_TYPES.Identifier) { throw new SyntaxError(`Unknown statement, got ${tokens[current].type}`); } const name = tokens[current].value; let result; switch (name) { case "set": ++current; result = parseSetStatement(); break; case "if": ++current; result = parseIfStatement(); expect(TOKEN_TYPES.OpenStatement, "Expected {% token"); expectIdentifier("endif"); expect(TOKEN_TYPES.CloseStatement, "Expected %} token"); break; case "macro": ++current; result = parseMacroStatement(); expect(TOKEN_TYPES.OpenStatement, "Expected {% token"); expectIdentifier("endmacro"); expect(TOKEN_TYPES.CloseStatement, "Expected %} token"); break; case "for": ++current; result = parseForStatement(); expect(TOKEN_TYPES.OpenStatement, "Expected {% token"); expectIdentifier("endfor"); expect(TOKEN_TYPES.CloseStatement, "Expected %} token"); break; case "call": { ++current; let callerArgs = null; if (is(TOKEN_TYPES.OpenParen)) { callerArgs = parseArgs(); } const callee = parsePrimaryExpression(); if (callee.type !== "Identifier") { throw new SyntaxError(`Expected identifier following call statement`); } const callArgs = parseArgs(); expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token"); const body = []; while (!isStatement("endcall")) { body.push(parseAny()); } expect(TOKEN_TYPES.OpenStatement, "Expected '{%'"); expectIdentifier("endcall"); expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token"); const callExpr = new CallExpression(callee, callArgs); result = new CallStatement(callExpr, callerArgs, body); break; } case "break": ++current; expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token"); result = new Break(); break; case "continue": ++current; expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token"); result = new Continue(); break; case "filter": { ++current; let filterNode = parsePrimaryExpression(); if (filterNode instanceof Identifier && is(TOKEN_TYPES.OpenParen)) { filterNode = parseCallExpression(filterNode); } expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token"); const filterBody = []; while (!isStatement("endfilter")) { filterBody.push(parseAny()); } expect(TOKEN_TYPES.OpenStatement, "Expected '{%'"); expectIdentifier("endfilter"); expect(TOKEN_TYPES.CloseStatement, "Expected '%}'"); result = new FilterStatement(filterNode, filterBody); break; } default: throw new SyntaxError(`Unknown statement type: ${name}`); } return result; } function parseJinjaExpression() { expect(TOKEN_TYPES.OpenExpression, "Expected opening expression token"); const result = parseExpression(); expect(TOKEN_TYPES.CloseExpression, "Expected closing expression token"); return result; } function parseSetStatement() { const left = parseExpressionSequence(); let value = null; const body = []; if (is(TOKEN_TYPES.Equals)) { ++current; value = parseExpressionSequence(); } else { expect(TOKEN_TYPES.CloseStatement, "Expected %} token"); while (!isStatement("endset")) { body.push(parseAny()); } expect(TOKEN_TYPES.OpenStatement, "Expected {% token"); expectIdentifier("endset"); } expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token"); return new SetStatement(left, value, body); } function parseIfStatement() { const test = parseExpression(); expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token"); const body = []; const alternate = []; while (!isStatement("elif", "else", "endif")) { body.push(parseAny()); } if (isStatement("elif")) { ++current; ++current; const result = parseIfStatement(); alternate.push(result); } else if (isStatement("else")) { ++current; ++current; expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token"); while (!isStatement("endif")) { alternate.push(parseAny()); } } return new If(test, body, alternate); } function parseMacroStatement() { const name = parsePrimaryExpression(); if (name.type !== "Identifier") { throw new SyntaxError(`Expected identifier following macro statement`); } const args = parseArgs(); expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token"); const body = []; while (!isStatement("endmacro")) { body.push(parseAny()); } return new Macro(name, args, body); } function parseExpressionSequence(primary = false) { const fn = primary ? parsePrimaryExpression : parseExpression; const expressions = [fn()]; const isTuple = is(TOKEN_TYPES.Comma); while (isTuple) { ++current; expressions.push(fn()); if (!is(TOKEN_TYPES.Comma)) { break; } } return isTuple ? new TupleLiteral(expressions) : expressions[0]; } function parseForStatement() { const loopVariable = parseExpressionSequence(true); if (!(loopVariable instanceof Identifier || loopVariable instanceof TupleLiteral)) { throw new SyntaxError(`Expected identifier/tuple for the loop variable, got ${loopVariable.type} instead`); } if (!isIdentifier("in")) { throw new SyntaxError("Expected `in` keyword following loop variable"); } ++current; const iterable = parseExpression(); expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token"); const body = []; while (!isStatement("endfor", "else")) { body.push(parseAny()); } const alternative = []; if (isStatement("else")) { ++current; ++current; expect(TOKEN_TYPES.CloseStatement, "Expected closing statement token"); while (!isStatement("endfor")) { alternative.push(parseAny()); } } return new For(loopVariable, iterable, body, alternative); } function parseExpression() { return parseIfExpression(); } function parseIfExpression() { const a = parseLogicalOrExpression(); if (isIdentifier("if")) { ++current; const test = parseLogicalOrExpression(); if (isIdentifier("else")) { ++current; const falseExpr = parseIfExpression(); return new Ternary(test, a, falseExpr); } else { return new SelectExpression(a, test); } } return a; } function parseLogicalOrExpression() { let left = parseLogicalAndExpression(); while (isIdentifier("or")) { const operator = tokens[current]; ++current; const right = parseLogicalAndExpression(); left = new BinaryExpression(operator, left, right); } return left; } function parseLogicalAndExpression() { let left = parseLogicalNegationExpression(); while (isIdentifier("and")) { const operator = tokens[current]; ++current; const right = parseLogicalNegationExpression(); left = new BinaryExpression(operator, left, right); } return left; } function parseLogicalNegationExpression() { let right; while (isIdentifier("not")) { const operator = tokens[current]; ++current; const arg = parseLogicalNegationExpression(); right = new UnaryExpression(operator, arg); } return right ?? parseComparisonExpression(); } function parseComparisonExpression() { let left = parseAdditiveExpression(); while (true) { let operator; if (isIdentifier("not", "in")) { operator = new Token("not in", TOKEN_TYPES.Identifier); current += 2; } else if (isIdentifier("in")) { operator = tokens[current++]; } else if (is(TOKEN_TYPES.ComparisonBinaryOperator)) { operator = tokens[current++]; } else { break; } const right = parseAdditiveExpression(); left = new BinaryExpression(operator, left, right); } return left; } function parseAdditiveExpression() { let left = parseMultiplicativeExpression(); while (is(TOKEN_TYPES.AdditiveBinaryOperator)) { const operator = tokens[current]; ++current; const right = parseMultiplicativeExpression(); left = new BinaryExpression(operator, left, right); } return left; } function parseCallMemberExpression() { const member = parseMemberExpression(parsePrimaryExpression()); if (is(TOKEN_TYPES.OpenParen)) { return parseCallExpression(member); } return member; } function parseCallExpression(callee) { let expression = new CallExpression(callee, parseArgs()); expression = parseMemberExpression(expression); if (is(TOKEN_TYPES.OpenParen)) { expression = parseCallExpression(expression); } return expression; } function parseArgs() { expect(TOKEN_TYPES.OpenParen, "Expected opening parenthesis for arguments list"); const args = parseArgumentsList(); expect(TOKEN_TYPES.CloseParen, "Expected closing parenthesis for arguments list"); return args; } function parseArgumentsList() { const args = []; while (!is(TOKEN_TYPES.CloseParen)) { let argument; if (tokens[current].type === TOKEN_TYPES.MultiplicativeBinaryOperator && tokens[current].value === "*") { ++current; const expr = parseExpression(); argument = new SpreadExpression(expr); } else { argument = parseExpression(); if (is(TOKEN_TYPES.Equals)) { ++current; if (!(argument instanceof Identifier)) { throw new SyntaxError(`Expected identifier for keyword argument`); } const value = parseExpression(); argument = new KeywordArgumentExpression(argument, value); } } args.push(argument); if (is(TOKEN_TYPES.Comma)) { ++current; } } return args; } function parseMemberExpressionArgumentsList() { const slices = []; let isSlice = false; while (!is(TOKEN_TYPES.CloseSquareBracket)) { if (is(TOKEN_TYPES.Colon)) { slices.push(void 0); ++current; isSlice = true; } else { slices.push(parseExpression()); if (is(TOKEN_TYPES.Colon)) { ++current; isSlice = true; } } } if (slices.length === 0) { throw new SyntaxError(`Expected at least one argument for member/slice expression`); } if (isSlice) { if (slices.length > 3) { throw new SyntaxError(`Expected 0-3 arguments for slice expression`); } return new SliceExpression(...slices); } return slices[0]; } function parseMemberExpression(object) { while (is(TOKEN_TYPES.Dot) || is(TOKEN_TYPES.OpenSquareBracket)) { const operator = tokens[current]; ++current; let property; const computed = operator.type === TOKEN_TYPES.OpenSquareBracket; if (computed) { property = parseMemberExpressionArgumentsList(); expect(TOKEN_TYPES.CloseSquareBracket, "Expected closing square bracket"); } else { property = parsePrimaryExpression(); if (property.type !== "Identifier") { throw new SyntaxError(`Expected identifier following dot operator`); } } object = new MemberExpression(object, property, computed); } return object; } function parseMultiplicativeExpression() { let left = parseTestExpression(); while (is(TOKEN_TYPES.MultiplicativeBinaryOperator)) { const operator = tokens[current++]; const right = parseTestExpression(); left = new BinaryExpression(operator, left, right); } return left; } function parseTestExpression() { let operand = parseFilterExpression(); while (isIdentifier("is")) { ++current; const negate = isIdentifier("not"); if (negate) { ++current; } const filter = parsePrimaryExpression(); if (!(filter instanceof Identifier)) { throw new SyntaxError(`Expected identifier for the test`); } operand = new TestExpression(operand, negate, filter); } return operand; } function parseFilterExpression() { let operand = parseCallMemberExpression(); while (is(TOKEN_TYPES.Pipe)) { ++current; let filter = parsePrimaryExpression(); if (!(filter instanceof Identifier)) { throw new SyntaxError(`Expected identifier for the filter`); } if (is(TOKEN_TYPES.OpenParen)) { filter = parseCallExpression(filter); } operand = new FilterExpression(operand, filter); } return operand; } function parsePrimaryExpression() { const token = tokens[current++]; switch (token.type) { case TOKEN_TYPES.NumericLiteral: { const num = token.value; return num.includes(".") ? new FloatLiteral(Number(num)) : new IntegerLiteral(Number(num)); } case TOKEN_TYPES.StringLiteral: { let value = token.value; while (is(TOKEN_TYPES.StringLiteral)) { value += tokens[current++].value; } return new StringLiteral(value); } case TOKEN_TYPES.Identifier: return new Identifier(token.value); case TOKEN_TYPES.OpenParen: { const expression = parseExpressionSequence(); expect(TOKEN_TYPES.CloseParen, "Expected closing parenthesis, got ${tokens[current].type} instead."); return expression; } case TOKEN_TYPES.OpenSquareBracket: { const values = []; while (!is(TOKEN_TYPES.CloseSquareBracket)) { values.push(parseExpression()); if (is(TOKEN_TYPES.Comma)) { ++current; } } ++current; return new ArrayLiteral(values); } case TOKEN_TYPES.OpenCurlyBracket: { const values = /* @__PURE__ */ new Map(); while (!is(TOKEN_TYPES.CloseCurlyBracket)) { const key = parseExpression(); expect(TOKEN_TYPES.Colon, "Expected colon between key and value in object literal"); const value = parseExpression(); values.set(key, value); if (is(TOKEN_TYPES.Comma)) { ++current; } } ++current; return new ObjectLiteral(values); } default: throw new SyntaxError(`Unexpected token: ${token.type}`); } } while (current < tokens.length) { program.body.push(parseAny()); } return program; } // src/utils.ts function range(start, stop, step = 1) { if (stop === void 0) { stop = start; start = 0; } if (step === 0) { throw new Error("range() step must not be zero"); } const result = []; if (step > 0) { for (let i = start; i < stop; i += step) { result.push(i); } } else { for (let i = start; i > stop; i += step) { result.push(i); } } return result; } function slice(array, start, stop, step = 1) { const direction = Math.sign(step); if (direction >= 0) { start = (start ??= 0) < 0 ? Math.max(array.length + start, 0) : Math.min(start, array.length); stop = (stop ??= array.length) < 0 ? Math.max(array.length + stop, 0) : Math.min(stop, array.length); } else { start = (start ??= array.length - 1) < 0 ? Math.max(array.length + start, -1) : Math.min(start, array.length - 1); stop = (stop ??= -1) < -1 ? Math.max(array.length + stop, -1) : Math.min(stop, array.length - 1); } const result = []; for (let i = start; direction * i < direction * stop; i += step) { result.push(array[i]); } return result; } function titleCase(value) { return value.replace(/\b\w/g, (c) => c.toUpperCase()); } function strftime_now(format2) { return strftime(/* @__PURE__ */ new Date(), format2); } function strftime(date, format2) { const monthFormatterLong = new Intl.DateTimeFormat(void 0, { month: "long" }); const monthFormatterShort = new Intl.DateTimeFormat(void 0, { month: "short" }); const pad2 = (n) => n < 10 ? "0" + n : n.toString(); return format2.replace(/%[YmdbBHM%]/g, (token) => { switch (token) { case "%Y": return date.getFullYear().toString(); case "%m": return pad2(date.getMonth() + 1); case "%d": return pad2(date.getDate()); case "%b": return monthFormatterShort.format(date); case "%B": return monthFormatterLong.format(date); case "%H": return pad2(date.getHours()); case "%M": return pad2(date.getMinutes()); case "%%": return "%"; default: return token; } }); } function escapeRegExp(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function replace(str, oldvalue, newvalue, count) { if (count === 0) return str; let remaining = count == null || count < 0 ? Infinity : count; const pattern = oldvalue.length === 0 ? new RegExp("(?=)", "gu") : new RegExp(escapeRegExp(oldvalue), "gu"); return str.replaceAll(pattern, (match) => { if (remaining > 0) { --remaining; return newvalue; } return match; }); } // src/runtime.ts var BreakControl = class extends Error { }; var ContinueControl = class extends Error { }; var RuntimeValue = class { type = "RuntimeValue"; value; /** * A collection of built-in functions for this type. */ builtins = /* @__PURE__ */ new Map(); /** * Creates a new RuntimeValue. */ constructor(value = void 0) { this.value = value; } /** * Determines truthiness or falsiness of the runtime value. * This function should be overridden by subclasses if it has custom truthiness criteria. * @returns {BooleanValue} BooleanValue(true) if the value is truthy, BooleanValue(false) otherwise. */ __bool__() { return new BooleanValue(!!this.value); } toString() { return String(this.value); } }; var IntegerValue = class extends RuntimeValue { type = "IntegerValue"; }; var FloatValue = class extends RuntimeValue { type = "FloatValue"; toString() { return this.value % 1 === 0 ? this.value.toFixed(1) : this.value.toString(); } }; var StringValue = class extends RuntimeValue { type = "StringValue"; builtins = /* @__PURE__ */ new Map([ [ "upper", new FunctionValue(() => { return new StringValue(this.value.toUpperCase()); }) ], [ "lower", new FunctionValue(() => { return new StringValue(this.value.toLowerCase()); }) ], [ "strip", new FunctionValue(() => { return new StringValue(this.value.trim()); }) ], [ "title", new FunctionValue(() => { return new StringValue(titleCase(this.value)); }) ], [ "capitalize", new FunctionValue(() => { return new StringValue(this.value.charAt(0).toUpperCase() + this.value.slice(1)); }) ], ["length", new IntegerValue(this.value.length)], [ "rstrip", new FunctionValue(() => { return new StringValue(this.value.trimEnd()); }) ], [ "lstrip", new FunctionValue(() => { return new StringValue(this.value.trimStart()); }) ], [ "startswith", new FunctionValue((args) => { if (args.length === 0) { throw new Error("startswith() requires at least one argument"); } const pattern = args[0]; if (pattern instanceof StringValue) { return new BooleanValue(this.value.startsWith(pattern.value)); } else if (pattern instanceof ArrayValue) { for (const item of pattern.value) { if (!(item instanceof StringValue)) { throw new Error("startswith() tuple elements must be strings"); } if (this.value.startsWith(item.value)) { return new BooleanValue(true); } } return new BooleanValue(false); } throw new Error("startswith() argument must be a string or tuple of strings"); }) ], [ "endswith", new FunctionValue((args) => { if (args.length === 0) { throw new Error("endswith() requires at least one argument"); } const pattern = args[0]; if (pattern instanceof StringValue) { return new BooleanValue(this.value.endsWith(pattern.value)); } else if (pattern instanceof ArrayValue) { for (const item of pattern.value) { if (!(item instanceof StringValue)) { throw new Error("endswith() tuple elements must be strings"); } if (this.value.endsWith(item.value)) { return new BooleanValue(true); } } return new BooleanValue(false); } throw new Error("endswith() argument must be a string or tuple of strings"); }) ], [ "split", // follows Python's `str.split(sep=None, maxsplit=-1)` function behavior // https://docs.python.org/3.13/library/stdtypes.html#str.split new FunctionValue((args) => { const sep = args[0] ?? new NullValue(); if (!(sep instanceof StringValue || sep instanceof NullValue)) { throw new Error("sep argument must be a string or null"); } const maxsplit = args[1] ?? new IntegerValue(-1); if (!(maxsplit instanceof IntegerValue)) { throw new Error("maxsplit argument must be a number"); } let result = []; if (sep instanceof NullValue) { const text = this.value.trimStart(); for (const { 0: match, index } of text.matchAll(/\S+/g)) { if (maxsplit.value !== -1 && result.length >= maxsplit.value && index !== void 0) { result.push(match + text.slice(index + match.length)); break; } result.push(match); } } else { if (sep.value === "") { throw new Error("empty separator"); } result = this.value.split(sep.value); if (maxsplit.value !== -1 && result.length > maxsplit.value) { result.push(result.splice(maxsplit.value).join(sep.value)); } } return new ArrayValue(result.map((part) => new StringValue(part))); }) ], [ "replace", new FunctionValue((args) => { if (args.length < 2) { throw new Error("replace() requires at least two arguments"); } const oldValue = args[0]; const newValue = args[1]; if (!(oldValue instanceof StringValue && newValue instanceof StringValue)) { throw new Error("replace() arguments must be strings"); } let count; if (args.length > 2) { if (args[2].type === "KeywordArgumentsValue") { count = args[2].value.get("count") ?? new NullValue(); } else { count = args[2]; } } else { count = new NullValue(); } if (!(count instanceof IntegerValue || count instanceof NullValue)) { throw new Error("replace() count argument must be a number or null"); } return new StringValue(replace(this.value, oldValue.value, newValue.value, count.value)); }) ] ]); }; var BooleanValue = class extends RuntimeValue { type = "BooleanValue"; }; var NON_ASCII_CHARS = /[\x7f-\uffff]/g; function makeAsciiSafe(str) { return str.replace(NON_ASCII_CHARS, (char) => "\\u" + char.charCodeAt(0).toString(16).padStart(4, "0")); } function toJSON(input, options = {}, depth = 0, convertUndefinedToNull = true) { const { indent = null, ensureAscii = false, separators = null, sortKeys = false } = options; let itemSeparator; let keySeparator; if (separators) { [itemSeparator, keySeparator] = separators; } else if (indent) { itemSeparator = ","; keySeparator = ": "; } else { itemSeparator = ", "; keySeparator = ": "; } switch (input.type) { case "NullValue": return "null"; case "UndefinedValue": return convertUndefinedToNull ? "null" : "undefined"; case "IntegerValue": case "FloatValue": case "BooleanValue": return JSON.stringify(input.value); case "StringValue": { let result = JSON.stringify(input.value); if (ensureAscii) { result = makeAsciiSafe(result); } return result; } case "ArrayValue": case "ObjectValue": { const indentValue = indent ? " ".repeat(indent) : ""; const basePadding = "\n" + indentValue.repeat(depth); const childrenPadding = basePadding + indentValue; if (input.type === "ArrayValue") { const core = input.value.map((x) => toJSON(x, options, depth + 1, convertUndefinedToNull)); return indent ? `[${childrenPadding}${core.join(`${itemSeparator}${childrenPadding}`)}${basePadding}]` : `[${core.join(itemSeparator)}]`; } else { let entries = Array.from(input.value.entries()); if (sortKeys) { entries = entries.sort(([a], [b]) => a.localeCompare(b)); } const core = entries.map(([key, value]) => { let keyStr = JSON.stringify(key); if (ensureAscii) { keyStr = makeAsciiSafe(keyStr); } const v = `${keyStr}${keySeparator}${toJSON(value, options, depth + 1, convertUndefinedToNull)}`; return indent ? `${childrenPadding}${v}` : v; }); return indent ? `{${core.join(itemSeparator)}${basePadding}}` : `{${core.join(itemSeparator)}}`; } } default: throw new Error(`Cannot convert to JSON: ${input.type}`); } } var ObjectValue = class extends RuntimeValue { type = "ObjectValue"; /** * NOTE: necessary to override since all JavaScript arrays are considered truthy, * while only non-empty Python arrays are consider truthy. * * e.g., * - JavaScript: {} && 5 -> 5 * - Python: {} and 5 -> {} */ __bool__() { return new BooleanValue(this.value.size > 0); } builtins = /* @__PURE__ */ new Map([ [ "get", new FunctionValue(([key, defaultValue]) => { if (!(key instanceof StringValue)) { throw new Error(`Object key must be a string: got ${key.type}`); } return this.value.get(key.value) ?? defaultValue ?? new NullValue(); }) ], ["items", new FunctionValue(() => this.items())], ["keys", new FunctionValue(() => this.keys())], ["values", new FunctionValue(() => this.values())], [ "dictsort", new FunctionValue((args) => { let kwargs = /* @__PURE__ */ new Map(); const positionalArgs = args.filter((arg) => { if (arg instanceof KeywordArgumentsValue) { kwargs = arg.value; return false; } return true; }); const caseSensitive = positionalArgs.at(0) ?? kwargs.get("case_sensitive") ?? new BooleanValue(false); if (!(caseSensitive instanceof BooleanValue)) { throw new Error("case_sensitive must be a boolean"); } const by = positionalArgs.at(1) ?? kwargs.get("by") ?? new StringValue("key"); if (!(by instanceof StringValue)) { throw new Error("by must be a string"); } if (!["key", "value"].includes(by.value)) { throw new Error("by must be either 'key' or 'value'"); } const reverse = positionalArgs.at(2) ?? kwargs.get("reverse") ?? new BooleanValue(false); if (!(reverse instanceof BooleanValue)) { throw new Error("reverse must be a boolean"); } const items = Array.from(this.value.entries()).map(([key, value]) => new ArrayValue([new StringValue(key), value])).sort((a, b) => { const index = by.value === "key" ? 0 : 1; const aVal = a.value[index]; const bVal = b.value[index]; const result = compareRuntimeValues(aVal, bVal, caseSensitive.value); return reverse.value ? -result : result; }); return new ArrayValue(items); }) ] ]); items() { return new ArrayValue( Array.from(this.value.entries()).map(([key, value]) => new ArrayValue([new StringValue(key), value])) ); } keys() { return new ArrayValue(Array.from(this.value.keys()).map((key) => new StringValue(key))); } values() { return new ArrayValue(Array.from(this.value.values())); } toString() { return toJSON(this, {}, 0, false); } }; var KeywordArgumentsValue = class extends ObjectValue { type = "KeywordArgumentsValue"; }; var ArrayValue = class extends RuntimeValue { type = "ArrayValue"; builtins = /* @__PURE__ */ new Map([["length", new IntegerValue(this.value.length)]]); /** * NOTE: necessary to override since all JavaScript arrays are considered truthy, * while only non-empty Python arrays are consider truthy. * * e.g., * - JavaScript: [] && 5 -> 5 * - Python: [] and 5 -> [] */ __bool__() { return new BooleanValue(this.value.length > 0); } toString() { return toJSON(this, {}, 0, false); } }; var TupleValue = class extends ArrayValue { type = "TupleValue"; }; var FunctionValue = class extends RuntimeValue { type = "FunctionValue"; }; var NullValue = class extends RuntimeValue { type = "NullValue"; }; var UndefinedValue = class extends RuntimeValue { type = "UndefinedValue"; }; var Environment = class { constructor(parent) { this.parent = parent; } /** * The variables declared in this environment. */ variables = /* @__PURE__ */ new Map([ [ "namespace", new FunctionValue((args) => { if (args.length === 0) { return new ObjectValue(/* @__PURE__ */ new Map()); } if (args.length !== 1 || !(args[0] instanceof ObjectValue)) { throw new Error("`namespace` expects either zero arguments or a single object argument"); } return args[0]; }) ] ]); /** * The tests available in this environment. */ tests = /* @__PURE__ */ new Map([ ["boolean", (operand) => operand.type === "BooleanValue"], ["callable", (operand) => operand instanceof FunctionValue], [ "odd", (operand) => { if (!(operand instanceof IntegerValue)) { throw new Error(`cannot odd on ${operand.type}`); } return operand.value % 2 !== 0; } ], [ "even", (operand) => { if (!(operand instanceof IntegerValue)) { throw new Error(`cannot even on ${operand.type}`); } return operand.value % 2 === 0; } ], ["false", (operand) => operand.type === "BooleanValue" && !operand.value], ["true", (operand) => operand.type === "BooleanValue" && operand.value], ["none", (operand) => operand.type === "NullValue"], ["string", (operand) => operand.type === "StringValue"], ["number", (operand) => operand instanceof IntegerValue || operand instanceof FloatValue], ["integer", (operand) => operand instanceof IntegerValue], ["iterable", (operand) => operand.type === "ArrayValue" || operand.type === "StringValue"], ["mapping", (operand) => operand.type === "ObjectValue"], [ "lower", (operand) => { const str = operand.value; return operand.type === "StringValue" && str === str.toLowerCase(); } ], [ "upper", (operand) => { const str = operand.value; return operand.type === "StringValue" && str === str.toUpperCase(); } ], ["none", (operand) => operand.type === "NullValue"], ["defined", (operand) => operand.type !== "UndefinedValue"], ["undefined", (operand) => operand.type === "UndefinedValue"], ["equalto", (a, b) => a.value === b.value], ["eq", (a, b) => a.value === b.value] ]); /** * Set the value of a variable in the current environment. */ set(name, value) { return this.declareVariable(name, convertToRuntimeValues(value)); } declareVariable(name, value) { if (this.variables.has(name)) { throw new SyntaxError(`Variable already declared: ${name}`); } this.variables.set(name, value); return value; } // private assignVariable(name: string, value: AnyRuntimeValue): AnyRuntimeValue { // const env = this.resolve(name); // env.variables.set(name, value); // return value; // } /** * Set variable in the current scope. * See https://jinja.palletsprojects.com/en/3.0.x/templates/#assignments for more information. */ setVariable(name, value) { this.variables.set(name, value); return value; } /** * Resolve the environment in which the variable is declared. * @param {string} name The name of the variable. * @returns {Environment} The environment in which the variable is declared. */ resolve(name) { if (this.variables.has(name)) { return this; } if (this.parent) { return this.parent.resolve(name); } throw new Error(`Unknown variable: ${name}`); } lookupVariable(name) { try { return this.resolve(name).variables.get(name) ?? new UndefinedValue(); } catch { return new UndefinedValue(); } } }; function setupGlobals(env) { env.set("false", false); env.set("true", true); env.set("none", null); env.set("raise_exception", (args) => { throw new Error(args); }); env.set("range", range); env.set("strftime_now", strftime_now); env.set("True", true); env.set("False", false); env.set("None", null); } function getAttributeValue(item, attributePath) { const parts = attributePath.split("."); let value = item; for (const part of parts) { if (value instanceof ObjectValue) { value = value.value.get(part) ?? new UndefinedValue(); } else if (value instanceof ArrayValue) { const index = parseInt(part, 10); if (!isNaN(index) && index >= 0 && index < value.value.length) { value = value.value[index]; } else { return new UndefinedValue(); } } else { return new UndefinedValue(); } } return value; } function compareRuntimeValues(a, b, caseSensitive = false) { if (a instanceof NullValue && b instanceof NullValue) { return 0; } if (a instanceof NullValue || b instanceof NullValue) { throw new Error(`Cannot compare ${a.type} with ${b.type}`); } if (a instanceof UndefinedValue && b instanceof UndefinedValue) { return 0; } if (a instanceof UndefinedValue || b instanceof UndefinedValue) { throw new Error(`Cannot compare ${a.type} with ${b.type}`); } const isNumericLike = (v) => v instanceof IntegerValue || v instanceof FloatValue || v instanceof BooleanValue; const getNumericValue = (v) => { if (v instanceof BooleanValue) { return v.value ? 1 : 0; } return v.value; }; if (isNumericLike(a) && isNumericLike(b)) { const aNum = getNumericValue(a); const bNum = getNumericValue(b); return aNum < bNum ? -1 : aNum > bNum ? 1 : 0; } if (a.type !== b.type) { throw new Error(`Cannot compare different types: ${a.type} and ${b.type}`); } switch (a.type) { case "StringValue": { let aStr = a.value; let bStr = b.value; if (!caseSensitive) { aStr = aStr.toLowerCase(); bStr = bStr.toLowerCase(); } return aStr < bStr ? -1 : aStr > bStr ? 1 : 0; } default: throw new Error(`Cannot compare type: ${a.type}`); } } var Interpreter = class { global; constructor(env) { this.global = env ?? new Environment(); } /** * Run the program. */ run(program) { return this.evaluate(program, this.global); } /** * Evaluates expressions following the binary operation type. */ evaluateBinaryExpression(node, environment) { const left = this.evaluate(node.left, environment); switch (node.operator.value) { case "and": return left.__bool__().value ? this.evaluate(node.right, environment) : left; case "or": return left.__bool__().value ? left : this.evaluate(node.right, environment); } const right = this.evaluate(node.right, environment); switch (node.operator.value) { case "==": return new BooleanValue(left.value == right.value); case "!=": return new BooleanValue(left.value != right.value); } if (left instanceof UndefinedValue || right instanceof UndefinedValue) { if (right instanceof UndefinedValue && ["in", "not in"].includes(node.operator.value)) { return new BooleanValue(node.operator.value === "not in"); } throw new Error(`Cannot perform operation ${node.operator.value} on undefined values`); } else if (left instanceof NullValue || right instanceof NullValue) { throw new Error("Cannot perform operation on null values"); } else if (node.operator.value === "~") { return new StringValue(left.value.toString() + right.value.toString()); } else if ((left instanceof IntegerValue || left instanceof FloatValue) && (right instanceof IntegerValue || right instanceof FloatValue)) { const a = left.value, b = right.value; switch (node.operator.value) { case "+": case "-": case "*": { const res = node.operator.value === "+" ? a + b : node.operator.value === "-" ? a - b : a * b; const isFloat = left instanceof FloatValue || right instanceof FloatValue; return isFloat ? new FloatValue(res) : new IntegerValue(res); } case "/": return new FloatValue(a / b); case "%": { const rem = a % b; const isFloat = left instanceof FloatValue || right instanceof FloatValue; return isFloat ? new FloatValue(rem) : new IntegerValue(rem); } case "<": return new BooleanValue(a < b); case ">": return new BooleanValue(a > b); case ">=": return new BooleanValue(a >= b); case "<=": return new BooleanValue(a <= b); } } else if (left instanceof ArrayValue && right instanceof ArrayValue) { switch (node.operator.value) { case "+": return new ArrayValue(left.value.concat(right.value)); } } else if (right instanceof ArrayValue) { const member = right.value.find((x) => x.value === left.value) !== void 0; switch (node.operator.value) { case "in": return new BooleanValue(member); case "not in": return new BooleanValue(!member); } } if (left instanceof StringValue || right instanceof StringValue) { switch (node.operator.value) { case "+": return new StringValue(left.value.toString() + right.value.toString()); } } if (left instanceof StringValue && right instanceof StringValue) { switch (node.operator.value) { case "in": return new BooleanValue(right.value.includes(left.value)); case "not in": return new BooleanValue(!right.value.includes(left.value)); } } if (left instanceof StringValue && right instanceof ObjectValue) { switch (node.operator.value) { case "in": return new BooleanValue(right.value.has(left.value)); case "not in": return new BooleanValue(!right.value.has(left.value)); } } throw new SyntaxError(`Unknown operator "${node.operator.value}" between ${left.type} and ${right.type}`); } evaluateArguments(args, environment) { const positionalArguments = []; const keywordArguments = /* @__PURE__ */ new Map(); for (const argument of args) { if (argument.type === "SpreadExpression") { const spreadNode = argument; const val = this.evaluate(spreadNode.argument, environment); if (!(val instanceof ArrayValue)) { throw new Error(`Cannot unpack non-iterable type: ${val.type}`); } for (const item of val.value) { positionalArguments.push(item); } } else if (argument.type === "KeywordArgumentExpression") { const kwarg = argument; keywordArguments.set(kwarg.key.value, this.evaluate(kwarg.value, environment)); } else { if (keywordArguments.size > 0) { throw new Error("Positional arguments must come before keyword arguments"); } positionalArguments.push(this.evaluate(argument, environment)); } } return [positionalArguments, keywordArguments]; } applyFilter(operand, filterNode, environment) { if (filterNode.type === "Identifier") { const filter = filterNode; if (filter.value === "tojson") { return new StringValue(toJSON(operand, {})); } if (operand instanceof ArrayValue) { switch (filter.value) { case "list": return operand; case "first": return operand.value[0]; case "last": return operand.value[operand.value.length - 1]; case "length": return new IntegerValue(operand.value.length); case "reverse": return new ArrayValue(operand.value.slice().reverse()); case "sort": { return new ArrayValue(operand.value.slice().sort((a, b) => compareRuntimeValues(a, b, false))); } case "join": return new StringValue(operand.value.map((x) => x.value).join("")); case "string": return new StringValue(toJSON(operand, {}, 0, false)); case "unique": { const seen = /* @__PURE__ */ new Set(); const output = []; for (const item of operand.value) { if (!seen.has(item.value)) { seen.add(item.value); output.push(item); } } return new ArrayValue(output); } default: throw new Error(`Unknown ArrayValue filter: ${filter.value}`); } } else if (operand instanceof StringValue) { switch (filter.value) { case "length": case "upper": case "lower": case "title": case "capitalize": { const builtin = operand.builtins.get(filter.value); if (builtin instanceof FunctionValue) { return builtin.value( /* no arguments */ [], environment ); } else if (builtin instanceof IntegerValue) { return builtin; } else { throw new Error(`Unknown StringValue filter: ${filter.value}`); } } case "trim": return new StringValue(operand.value.trim()); case "indent": return new StringValue( operand.value.split("\n").map( (x, i) => ( // By default, don't indent the first line or empty lines i === 0 || x.length === 0 ? x : " " + x ) ).join("\n") ); case "join": case "string": return operand; case "int": { const val = parseInt(operand.value, 10); return new IntegerValue(isNaN(val) ? 0 : val); } case "float": { const val = parseFloat(operand.value); return new FloatValue(isNaN(val) ? 0 : val); } default: throw new Error(`Unknown StringValue filter: ${filter.value}`); } } else if (operand instanceof IntegerValue || operand instanceof FloatValue) { switch (filter.value) { case "abs": return operand instanceof IntegerValue ? new IntegerValue(Math.abs(operand.value)) : new FloatValue(Math.abs(operand.value)); case "int": return new IntegerValue(Math.floor(operand.value)); case "float": return new FloatValue(operand.value); default: throw new Error(`Unknown NumericValue filter: ${filter.value}`); } } else if (operand instanceof ObjectValue) { switch (filter.value) { case "items": return new ArrayValue( Array.from(operand.value.entries()).map(([key, value]) => new ArrayValue([new StringValue(key), value])) ); case "length": return new IntegerValue(operand.value.size); default: { const builtin = operand.builtins.get(filter.value); if (builtin) { if (builtin instanceof FunctionValue) { return builtin.value([], environment); } return builtin; } throw new Error(`Unknown ObjectValue filter: ${filter.value}`); } } } else if (operand instanceof BooleanValue) { switch (filter.value) { case "bool": return new BooleanValue(operand.value); case "int": return new IntegerValue(operand.value ? 1 : 0); case "float": return new FloatValue(operand.value ? 1 : 0); case "string": return new StringValue(operand.value ? "true" : "false"); default: throw new Error(`Unknown BooleanValue filter: ${filter.value}`); } } throw new Error(`Cannot apply filter "${filter.value}" to type: ${operand.type}`); } else if (filterNode.type === "CallExpression") { const filter = filterNode; if (filter.callee.type !== "Identifier") { throw new Error(`Unknown filter: ${filter.callee.type}`); } const filterName = filter.callee.value; if (filterName === "tojson") { const [, kwargs] = this.evaluateArguments(filter.args, environment); const indent = kwargs.get("indent") ?? new NullValue(); if (!(indent instanceof IntegerValue || indent instanceof NullValue)) { throw new Error("If set, indent must be a number"); } const ensureAscii = kwargs.get("ensure_ascii") ?? new BooleanValue(false); if (!(ensureAscii instanceof BooleanValue)) { throw new Error("If set, ensure_ascii must be a boolean"); } const sortKeys = kwargs.get("sort_keys") ?? new BooleanValue(false); if (!(sortKeys instanceof BooleanValue)) { throw new Error("If set, sort_keys must be a boolean"); } const separatorsArg = kwargs.get("separators") ?? new NullValue(); let separators = null; if (separatorsArg instanceof ArrayValue || separatorsArg instanceof TupleValue) { if (separatorsArg.value.length !== 2) { throw new Error("separators must be a tuple of two strings"); } const [itemSep, keySep] = separatorsArg.value; if (!(itemSep instanceof StringValue) || !(keySep instanceof StringValue)) { throw new Error("separators must be a tuple of two strings"); } separators = [itemSep.value, keySep.value]; } else if (!(separatorsArg instanceof NullValue)) { throw new Error("If set, separators must be a tuple of two strings"); } return new StringValue( toJSON(operand, { indent: indent.value, ensureAscii: ensureAscii.value, sortKeys: sortKeys.value, separators }) ); } else if (filterName === "join") { let value; if (operand instanceof StringValue) { value = Array.from(operand.value); } else if (operand instanceof ArrayValue) { value = operand.value.map((x) => x.value); } else { throw new Error(`Cannot apply filter "${filterName}" to type: ${operand.type}`); } const [args, kwargs] = this.evaluateArguments(filter.args, environment); const separator = args.at(0) ?? kwargs.get("separator") ?? new StringValue(""); if (!(separator instanceof StringValue)) { throw new Error("separator must be a string"); } return new StringValue(value.join(separator.value)); } else if (filterName === "int" || filterName === "float") { const [args, kwargs] = this.evaluateArguments(filter.args, environment); const defaultValue = args.at(0) ?? kwargs.get("default") ?? (filterName === "int" ? new IntegerValue(0) : new FloatValue(0)); if (operand instanceof StringValue) { const val = filterName === "int" ? parseInt(operand.value, 10) : parseFloat(operand.value); return isNaN(val) ? defaultValue : filterName === "int" ? new IntegerValue(val) : new FloatValue(val); } else if (operand instanceof IntegerValue || operand instanceof FloatValue) { return operand; } else if (operand instanceof BooleanValue) { return filterName === "int" ? new IntegerValue(operand.value ? 1 : 0) : new FloatValue(operand.value ? 1 : 0); } else { throw new Error(`Cannot apply filter "${filterName}" to type: ${operand.type}`); } } else if (filterName === "default") { const [args, kwargs] = this.evaluateArguments(filter.args, environment); const defaultValue = args[0] ?? new StringValue(""); const booleanValue = args[1] ?? kwargs.get("boolean") ?? new BooleanValue(false); if (!(booleanValue instanceof BooleanValue)) { throw new Error("`default` filter flag must be a boolean"); } if (operand instanceof UndefinedValue || booleanValue.value && !operand.__bool__().value) { return defaultValue; } return operand; } if (operand instanceof ArrayValue) { switch (filterName) { case "sort": { const [args, kwargs] = this.evaluateArguments(filter.args, environment); const reverse = args.at(0) ?? kwargs.get("reverse") ?? new BooleanValue(false); if (!(reverse instanceof BooleanValue)) { throw new Error("reverse must be a boolean"); } const caseSensitive = args.at(1) ?? kwargs.get("case_sensitive") ?? new BooleanValue(false); if (!(caseSensitive instanceof BooleanValue)) { throw new Error("case_sensitive must be a boolean"); } const attribute = args.at(2) ?? kwargs.get("attribute") ?? new NullValue(); if (!(attribute instanceof StringValue || attribute instanceof IntegerValue || attribute instanceof NullValue)) { throw new Error("attribute must be a string, integer, or null"); } const getSortValue = (item) => { if (attribute instanceof NullValue) { return item; } const attrPath = attribute instanceof IntegerValue ? String(attribute.value) : attribute.value; return getAttributeValue(item, attrPath); }; return new ArrayValue( operand.value.slice().sort((a, b) => { const aVal = getSortValue(a); const bVal = getSortValue(b); const result = compareRuntimeValues(aVal, bVal, caseSensitive.value); return reverse.value ? -result : result; }) ); } case "selectattr": case "rejectattr": { const select = filterName === "selectattr"; if (operand.value.some((x) => !(x instanceof ObjectValue))) { throw new Error(`\`${filterName}\` can only be applied to array of objects`); } if (filter.args.some((x) => x.type !== "StringLiteral")) { throw new Error(`arguments of \`${filterName}\` must be strings`); } const [attr, testName, value] = filter.args.map((x) => this.evaluate(x, environment)); let testFunction; if (testName) { const test = environment.tests.get(testName.value); if (!test) { throw new Error(`Unknown test: ${testName.value}`); } testFunction = test; } else { testFunction = (...x) => x[0].__bool__().value; } const filtered = operand.value.filter((item) => { const a = item.value.get(attr.value); const result = a ? testFunction(a, value) : false; return select ? result : !result; }); return new ArrayValue(filtered); } case "map": { const [, kwargs] = this.evaluateArguments(filter.args, environment); if (kwargs.has("attribute")) { const attr = kwargs.get("attribute"); if (!(attr instanceof StringValue)) { throw new Error("attribute must be a string"); } const defaultValue = kwargs.get("default"); const mapped = operand.value.map((item) => { if (!(item instanceof ObjectValue)) { throw new Error("items in map must be an object"); } const value = getAttributeValue(item, attr.value); return value instanceof UndefinedValue ? defaultValue ?? new UndefinedValue() : value; }); return new ArrayValue(mapped); } else { throw new Error("`map` expressions without `attribute` set are not currently supported."); } } } throw new Error(`Unknown ArrayValue filter: ${filterName}`); } else if (operand instanceof StringValue) { switch (filterName) { case "indent": { const [args, kwargs] = this.evaluateArguments(filter.args, environment); const width = args.at(0) ?? kwargs.get("width") ?? new IntegerValue(4); if (!(width instanceof IntegerValue)) { throw new Error("width must be a number"); } const first = args.at(1) ?? kwargs.get("first") ?? new BooleanValue(false); const blank = args.at(2) ?? kwargs.get("blank") ?? new BooleanValue(false); const lines = operand.value.split("\n"); const indent = " ".repeat(width.value); const indented = lines.map( (x, i) => !first.value && i === 0 || !blank.value && x.length === 0 ? x : indent + x ); return new StringValue(indented.join("\n")); } case "replace": { const replaceFn = operand.builtins.get("replace"); if (!(replaceFn instanceof FunctionValue)) { throw new Error("replace filter not available"); } const [args, kwargs] = this.evaluateArguments(filter.args, environment); return replaceFn.value([...args, new KeywordArgumentsValue(kwargs)], environment); } } throw new Error(`Unknown StringValue filter: ${filterName}`); } else if (operand instanceof ObjectValue) { const builtin = operand.builtins.get(filterName); if (builtin && builtin instanceof FunctionValue) { const [args, kwargs] = this.evaluateArguments(filter.args, environment); if (kwargs.size > 0) { args.push(new KeywordArgumentsValue(kwargs)); } return builtin.value(args, environment); } throw new Error(`Unknown ObjectValue filter: ${filterName}`); } else { throw new Error(`Cannot apply filter "${filterName}" to type: ${operand.type}`); } } throw new Error(`Unknown filter: ${filterNode.type}`); } /** * Evaluates expressions following the filter operation type. */ evaluateFilterExpression(node, environment) { const operand = this.evaluate(node.operand, environment); return this.applyFilter(operand, node.filter, environment); } /** * Evaluates expressions following the test operation type. */ evaluateTestExpression(node, environment) { const operand = this.evaluate(node.operand, environment); const test = environment.tests.get(node.test.value); if (!test) { throw new Error(`Unknown test: ${node.test.value}`); } const result = test(operand); return new BooleanValue(node.negate ? !result : result); } /** * Evaluates expressions following the select operation type. */ evaluateSelectExpression(node, environment) { const predicate = this.evaluate(node.test, environment); if (!predicate.__bool__().value) { return new UndefinedValue(); } return this.evaluate(node.lhs, environment); } /** * Evaluates expressions following the unary operation type. */ evaluateUnaryExpression(node, environment) { const argument = this.evaluate(node.argument, environment); switch (node.operator.value) { case "not": return new BooleanValue(!argument.value); default: throw new SyntaxError(`Unknown operator: ${node.operator.value}`); } } evaluateTernaryExpression(node, environment) { const cond = this.evaluate(node.condition, environment); return cond.__bool__().value ? this.evaluate(node.trueExpr, environment) : this.evaluate(node.falseExpr, environment); } evalProgram(program, environment) { return this.evaluateBlock(program.body, environment); } evaluateBlock(statements, environment) { let result = ""; for (const statement of statements) { const lastEvaluated = this.evaluate(statement, environment); if (lastEvaluated.type !== "NullValue" && lastEvaluated.type !== "UndefinedValue") { result += lastEvaluated.toString(); } } return new StringValue(result); } evaluateIdentifier(node, environment) { return environment.lookupVariable(node.value); } evaluateCallExpression(expr, environment) { const [args, kwargs] = this.evaluateArguments(expr.args, environment); if (kwargs.size > 0) { args.push(new KeywordArgumentsValue(kwargs)); } const fn = this.evaluate(expr.callee, environment); if (fn.type !== "FunctionValue") { throw new Error(`Cannot call something that is not a function: got ${fn.type}`); } return fn.value(args, environment); } evaluateSliceExpression(object, expr, environment) { if (!(object instanceof ArrayValue || object instanceof StringValue)) { throw new Error("Slice object must be an array or string"); } const start = this.evaluate(expr.start, environment); const stop = this.evaluate(expr.stop, environment); const step = this.evaluate(expr.step, environment); if (!(start instanceof IntegerValue || start instanceof UndefinedValue)) { throw new Error("Slice start must be numeric or undefined"); } if (!(stop instanceof IntegerValue || stop instanceof UndefinedValue)) { throw new Error("Slice stop must be numeric or undefined"); } if (!(step instanceof IntegerValue || step instanceof UndefinedValue)) { throw new Error("Slice step must be numeric or undefined"); } if (object instanceof ArrayValue) { return new ArrayValue(slice(object.value, start.value, stop.value, step.value)); } else { return new StringValue(slice(Array.from(object.value), start.value, stop.value, step.value).join("")); } } evaluateMemberExpression(expr, environment) { const object = this.evaluate(expr.object, environment); let property; if (expr.computed) { if (expr.property.type === "SliceExpression") { return this.evaluateSliceExpression(object, expr.property, environment); } else { property = this.evaluate(expr.property, environment); } } else { property = new StringValue(expr.property.value); } let value; if (object instanceof ObjectValue) { if (!(property instanceof StringValue)) { throw new Error(`Cannot access property with non-string: got ${property.type}`); } value = object.value.get(property.value) ?? object.builtins.get(property.value); } else if (object instanceof ArrayValue || object instanceof StringValue) { if (property instanceof IntegerValue) { value = object.value.at(property.value); if (object instanceof StringValue) { value = new StringValue(object.value.at(property.value)); } } else if (property instanceof StringValue) { value = object.builtins.get(property.value); } else { throw new Error(`Cannot access property with non-string/non-number: got ${property.type}`); } } else { if (!(property instanceof StringValue)) { throw new Error(`Cannot access property with non-string: got ${property.type}`); } value = object.builtins.get(property.value); } return value instanceof RuntimeValue ? value : new UndefinedValue(); } evaluateSet(node, environment) { const rhs = node.value ? this.evaluate(node.value, environment) : this.evaluateBlock(node.body, environment); if (node.assignee.type === "Identifier") { const variableName = node.assignee.value; environment.setVariable(variableName, rhs); } else if (node.assignee.type === "TupleLiteral") { const tuple = node.assignee; if (!(rhs instanceof ArrayValue)) { throw new Error(`Cannot unpack non-iterable type in set: ${rhs.type}`); } const arr = rhs.value; if (arr.length !== tuple.value.length) { throw new Error(`Too ${tuple.value.length > arr.length ? "few" : "many"} items to unpack in set`); } for (let i = 0; i < tuple.value.length; ++i) { const elem = tuple.value[i]; if (elem.type !== "Identifier") { throw new Error(`Cannot unpack to non-identifier in set: ${elem.type}`); } environment.setVariable(elem.value, arr[i]); } } else if (node.assignee.type === "MemberExpression") { const member = node.assignee; const object = this.evaluate(member.object, environment); if (!(object instanceof ObjectValue)) { throw new Error("Cannot assign to member of non-object"); } if (member.property.type !== "Identifier") { throw new Error("Cannot assign to member with non-identifier property"); } object.value.set(member.property.value, rhs); } else { throw new Error(`Invalid LHS inside assignment expression: ${JSON.stringify(node.assignee)}`); } return new NullValue(); } evaluateIf(node, environment) { const test = this.evaluate(node.test, environment); return this.evaluateBlock(test.__bool__().value ? node.body : node.alternate, environment); } evaluateFor(node, environment) { const scope = new Environment(environment); let test, iterable; if (node.iterable.type === "SelectExpression") { const select = node.iterable; iterable = this.evaluate(select.lhs, scope); test = select.test; } else { iterable = this.evaluate(node.iterable, scope); } if (!(iterable instanceof ArrayValue || iterable instanceof ObjectValue)) { throw new Error(`Expected iterable or object type in for loop: got ${iterable.type}`); } if (iterable instanceof ObjectValue) { iterable = iterable.keys(); } const items = []; const scopeUpdateFunctions = []; for (let i = 0; i < iterable.value.length; ++i) { const loopScope = new Environment(scope); const current = iterable.value[i]; let scopeUpdateFunction; if (node.loopvar.type === "Identifier") { scopeUpdateFunction = (scope2) => scope2.setVariable(node.loopvar.value, current); } else if (node.loopvar.type === "TupleLiteral") { const loopvar = node.loopvar; if (current.type !== "ArrayValue") { throw new Error(`Cannot unpack non-iterable type: ${current.type}`); } const c = current; if (loopvar.value.length !== c.value.length) { throw new Error(`Too ${loopvar.value.length > c.value.length ? "few" : "many"} items to unpack`); } scopeUpdateFunction = (scope2) => { for (let j = 0; j < loopvar.value.length; ++j) { if (loopvar.value[j].type !== "Identifier") { throw new Error(`Cannot unpack non-identifier type: ${loopvar.value[j].type}`); } scope2.setVariable(loopvar.value[j].value, c.value[j]); } }; } else { throw new Error(`Invalid loop variable(s): ${node.loopvar.type}`); } if (test) { scopeUpdateFunction(loopScope); const testValue = this.evaluate(test, loopScope); if (!testValue.__bool__().value) { continue; } } items.push(current); scopeUpdateFunctions.push(scopeUpdateFunction); } let result = ""; let noIteration = true; for (let i = 0; i < items.length; ++i) { const loop = /* @__PURE__ */ new Map([ ["index", new IntegerValue(i + 1)], ["index0", new IntegerValue(i)], ["revindex", new IntegerValue(items.length - i)], ["revindex0", new IntegerValue(items.length - i - 1)], ["first", new BooleanValue(i === 0)], ["last", new BooleanValue(i === items.length - 1)], ["length", new IntegerValue(items.length)], ["previtem", i > 0 ? items[i - 1] : new UndefinedValue()], ["nextitem", i < items.length - 1 ? items[i + 1] : new UndefinedValue()] ]); scope.setVariable("loop", new ObjectValue(loop)); scopeUpdateFunctions[i](scope); try { const evaluated = this.evaluateBlock(node.body, scope); result += evaluated.value; } catch (err) { if (err instanceof ContinueControl) { continue; } if (err instanceof BreakControl) { break; } throw err; } noIteration = false; } if (noIteration) { const defaultEvaluated = this.evaluateBlock(node.defaultBlock, scope); result += defaultEvaluated.value; } return new StringValue(result); } /** * See https://jinja.palletsprojects.com/en/3.1.x/templates/#macros for more information. */ evaluateMacro(node, environment) { environment.setVariable( node.name.value, new FunctionValue((args, scope) => { const macroScope = new Environment(scope); args = args.slice(); let kwargs; if (args.at(-1)?.type === "KeywordArgumentsValue") { kwargs = args.pop(); } for (let i = 0; i < node.args.length; ++i) { const nodeArg = node.args[i]; const passedArg = args[i]; if (nodeArg.type === "Identifier") { const identifier = nodeArg; if (!passedArg) { throw new Error(`Missing positional argument: ${identifier.value}`); } macroScope.setVariable(identifier.value, passedArg); } else if (nodeArg.type === "KeywordArgumentExpression") { const kwarg = nodeArg; const value = passedArg ?? // Try positional arguments first kwargs?.value.get(kwarg.key.value) ?? // Look in user-passed kwargs this.evaluate(kwarg.value, macroScope); macroScope.setVariable(kwarg.key.value, value); } else { throw new Error(`Unknown argument type: ${nodeArg.type}`); } } return this.evaluateBlock(node.body, macroScope); }) ); return new NullValue(); } evaluateCallStatement(node, environment) { const callerFn = new FunctionValue((callerArgs, callerEnv) => { const callBlockEnv = new Environment(callerEnv); if (node.callerArgs) { for (let i = 0; i < node.callerArgs.length; ++i) { const param = node.callerArgs[i]; if (param.type !== "Identifier") { throw new Error(`Caller parameter must be an identifier, got ${param.type}`); } callBlockEnv.setVariable(param.value, callerArgs[i] ?? new UndefinedValue()); } } return this.evaluateBlock(node.body, callBlockEnv); }); const [macroArgs, macroKwargs] = this.evaluateArguments(node.call.args, environment); macroArgs.push(new KeywordArgumentsValue(macroKwargs)); const fn = this.evaluate(node.call.callee, environment); if (fn.type !== "FunctionValue") { throw new Error(`Cannot call something that is not a function: got ${fn.type}`); } const newEnv = new Environment(environment); newEnv.setVariable("caller", callerFn); return fn.value(macroArgs, newEnv); } evaluateFilterStatement(node, environment) { const rendered = this.evaluateBlock(node.body, environment); return this.applyFilter(rendered, node.filter, environment); } evaluate(statement, environment) { if (!statement) return new UndefinedValue(); switch (statement.type) { case "Program": return this.evalProgram(statement, environment); case "Set": return this.evaluateSet(statement, environment); case "If": return this.evaluateIf(statement, environment); case "For": return this.evaluateFor(statement, environment); case "Macro": return this.evaluateMacro(statement, environment); case "CallStatement": return this.evaluateCallStatement(statement, environment); case "Break": throw new BreakControl(); case "Continue": throw new ContinueControl(); case "IntegerLiteral": return new IntegerValue(statement.value); case "FloatLiteral": return new FloatValue(statement.value); case "StringLiteral": return new StringValue(statement.value); case "ArrayLiteral": return new ArrayValue(statement.value.map((x) => this.evaluate(x, environment))); case "TupleLiteral": return new TupleValue(statement.value.map((x) => this.evaluate(x, environment))); case "ObjectLiteral": { const mapping = /* @__PURE__ */ new Map(); for (const [key, value] of statement.value) { const evaluatedKey = this.evaluate(key, environment); if (!(evaluatedKey instanceof StringValue)) { throw new Error(`Object keys must be strings: got ${evaluatedKey.type}`); } mapping.set(evaluatedKey.value, this.evaluate(value, environment)); } return new ObjectValue(mapping); } case "Identifier": return this.evaluateIdentifier(statement, environment); case "CallExpression": return this.evaluateCallExpression(statement, environment); case "MemberExpression": return this.evaluateMemberExpression(statement, environment); case "UnaryExpression": return this.evaluateUnaryExpression(statement, environment); case "BinaryExpression": return this.evaluateBinaryExpression(statement, environment); case "FilterExpression": return this.evaluateFilterExpression(statement, environment); case "FilterStatement": return this.evaluateFilterStatement(statement, environment); case "TestExpression": return this.evaluateTestExpression(statement, environment); case "SelectExpression": return this.evaluateSelectExpression(statement, environment); case "Ternary": return this.evaluateTernaryExpression(statement, environment); case "Comment": return new NullValue(); default: throw new SyntaxError(`Unknown node type: ${statement.type}`); } } }; function convertToRuntimeValues(input) { switch (typeof input) { case "number": return Number.isInteger(input) ? new IntegerValue(input) : new FloatValue(input); case "string": return new StringValue(input); case "boolean": return new BooleanValue(input); case "undefined": return new UndefinedValue(); case "object": if (input === null) { return new NullValue(); } else if (Array.isArray(input)) { return new ArrayValue(input.map(convertToRuntimeValues)); } else { return new ObjectValue( new Map(Object.entries(input).map(([key, value]) => [key, convertToRuntimeValues(value)])) ); } case "function": return new FunctionValue((args, _scope) => { const result = input(...args.map((x) => x.value)) ?? null; return convertToRuntimeValues(result); }); default: throw new Error(`Cannot convert to runtime value: ${input}`); } } // src/format.ts var NEWLINE = "\n"; var OPEN_STATEMENT = "{%- "; var CLOSE_STATEMENT = " -%}"; function getBinaryOperatorPrecedence(expr) { switch (expr.operator.type) { case "MultiplicativeBinaryOperator": return 4; case "AdditiveBinaryOperator": return 3; case "ComparisonBinaryOperator": return 2; case "Identifier": if (expr.operator.value === "and") return 1; if (expr.operator.value === "in" || expr.operator.value === "not in") return 2; return 0; } return 0; } function format(program, indent = " ") { const indentStr = typeof indent === "number" ? " ".repeat(indent) : indent; const body = formatStatements(program.body, 0, indentStr); return body.replace(/\n$/, ""); } function createStatement(...text) { return OPEN_STATEMENT + text.join(" ") + CLOSE_STATEMENT; } function formatStatements(stmts, depth, indentStr) { return stmts.map((stmt) => formatStatement(stmt, depth, indentStr)).join(NEWLINE); } function formatStatement(node, depth, indentStr) { const pad = indentStr.repeat(depth); switch (node.type) { case "Program": return formatStatements(node.body, depth, indentStr); case "If": return formatIf(node, depth, indentStr); case "For": return formatFor(node, depth, indentStr); case "Set": return formatSet(node, depth, indentStr); case "Macro": return formatMacro(node, depth, indentStr); case "Break": return pad + createStatement("break"); case "Continue": return pad + createStatement("continue"); case "CallStatement": return formatCallStatement(node, depth, indentStr); case "FilterStatement": return formatFilterStatement(node, depth, indentStr); case "Comment": return pad + "{# " + node.value + " #}"; default: return pad + "{{- " + formatExpression(node) + " -}}"; } } function formatIf(node, depth, indentStr) { const pad = indentStr.repeat(depth); const clauses = []; let current = node; while (current) { clauses.push({ test: current.test, body: current.body }); if (current.alternate.length === 1 && current.alternate[0].type === "If") { current = current.alternate[0]; } else { break; } } let out = pad + createStatement("if", formatExpression(clauses[0].test)) + NEWLINE + formatStatements(clauses[0].body, depth + 1, indentStr); for (let i = 1; i < clauses.length; ++i) { out += NEWLINE + pad + createStatement("elif", formatExpression(clauses[i].test)) + NEWLINE + formatStatements(clauses[i].body, depth + 1, indentStr); } if (current && current.alternate.length > 0) { out += NEWLINE + pad + createStatement("else") + NEWLINE + formatStatements(current.alternate, depth + 1, indentStr); } out += NEWLINE + pad + createStatement("endif"); return out; } function formatFor(node, depth, indentStr) { const pad = indentStr.repeat(depth); let formattedIterable = ""; if (node.iterable.type === "SelectExpression") { const n = node.iterable; formattedIterable = `${formatExpression(n.lhs)} if ${formatExpression(n.test)}`; } else { formattedIterable = formatExpression(node.iterable); } let out = pad + createStatement("for", formatExpression(node.loopvar), "in", formattedIterable) + NEWLINE + formatStatements(node.body, depth + 1, indentStr); if (node.defaultBlock.length > 0) { out += NEWLINE + pad + createStatement("else") + NEWLINE + formatStatements(node.defaultBlock, depth + 1, indentStr); } out += NEWLINE + pad + createStatement("endfor"); return out; } function formatSet(node, depth, indentStr) { const pad = indentStr.repeat(depth); const left = formatExpression(node.assignee); const right = node.value ? formatExpression(node.value) : ""; const value = pad + createStatement("set", `${left}${node.value ? " = " + right : ""}`); if (node.body.length === 0) { return value; } return value + NEWLINE + formatStatements(node.body, depth + 1, indentStr) + NEWLINE + pad + createStatement("endset"); } function formatMacro(node, depth, indentStr) { const pad = indentStr.repeat(depth); const args = node.args.map(formatExpression).join(", "); return pad + createStatement("macro", `${node.name.value}(${args})`) + NEWLINE + formatStatements(node.body, depth + 1, indentStr) + NEWLINE + pad + createStatement("endmacro"); } function formatCallStatement(node, depth, indentStr) { const pad = indentStr.repeat(depth); const params = node.callerArgs && node.callerArgs.length > 0 ? `(${node.callerArgs.map(formatExpression).join(", ")})` : ""; const callExpr = formatExpression(node.call); let out = pad + createStatement(`call${params}`, callExpr) + NEWLINE; out += formatStatements(node.body, depth + 1, indentStr) + NEWLINE; out += pad + createStatement("endcall"); return out; } function formatFilterStatement(node, depth, indentStr) { const pad = indentStr.repeat(depth); const spec = node.filter.type === "Identifier" ? node.filter.value : formatExpression(node.filter); let out = pad + createStatement("filter", spec) + NEWLINE; out += formatStatements(node.body, depth + 1, indentStr) + NEWLINE; out += pad + createStatement("endfilter"); return out; } function formatExpression(node, parentPrec = -1) { switch (node.type) { case "SpreadExpression": { const n = node; return `*${formatExpression(n.argument)}`; } case "Identifier": return node.value; case "IntegerLiteral": return `${node.value}`; case "FloatLiteral": return `${node.value}`; case "StringLiteral": return JSON.stringify(node.value); case "BinaryExpression": { const n = node; const thisPrecedence = getBinaryOperatorPrecedence(n); const left = formatExpression(n.left, thisPrecedence); const right = formatExpression(n.right, thisPrecedence + 1); const expr = `${left} ${n.operator.value} ${right}`; return thisPrecedence < parentPrec ? `(${expr})` : expr; } case "UnaryExpression": { const n = node; const val = n.operator.value + (n.operator.value === "not" ? " " : "") + formatExpression(n.argument, Infinity); return val; } case "CallExpression": { const n = node; const args = n.args.map(formatExpression).join(", "); return `${formatExpression(n.callee)}(${args})`; } case "MemberExpression": { const n = node; let obj = formatExpression(n.object); if (![ "Identifier", "MemberExpression", "CallExpression", "StringLiteral", "IntegerLiteral", "FloatLiteral", "ArrayLiteral", "TupleLiteral", "ObjectLiteral" ].includes(n.object.type)) { obj = `(${obj})`; } let prop = formatExpression(n.property); if (!n.computed && n.property.type !== "Identifier") { prop = `(${prop})`; } return n.computed ? `${obj}[${prop}]` : `${obj}.${prop}`; } case "FilterExpression": { const n = node; const operand = formatExpression(n.operand, Infinity); if (n.filter.type === "CallExpression") { return `${operand} | ${formatExpression(n.filter)}`; } return `${operand} | ${n.filter.value}`; } case "SelectExpression": { const n = node; return `${formatExpression(n.lhs)} if ${formatExpression(n.test)}`; } case "TestExpression": { const n = node; return `${formatExpression(n.operand)} is${n.negate ? " not" : ""} ${n.test.value}`; } case "ArrayLiteral": case "TupleLiteral": { const elems = node.value.map(formatExpression); const brackets = node.type === "ArrayLiteral" ? "[]" : "()"; return `${brackets[0]}${elems.join(", ")}${brackets[1]}`; } case "ObjectLiteral": { const entries = Array.from(node.value.entries()).map( ([k, v]) => `${formatExpression(k)}: ${formatExpression(v)}` ); return `{${entries.join(", ")}}`; } case "SliceExpression": { const n = node; const s = n.start ? formatExpression(n.start) : ""; const t = n.stop ? formatExpression(n.stop) : ""; const st = n.step ? `:${formatExpression(n.step)}` : ""; return `${s}:${t}${st}`; } case "KeywordArgumentExpression": { const n = node; return `${n.key.value}=${formatExpression(n.value)}`; } case "Ternary": { const n = node; const expr = `${formatExpression(n.trueExpr)} if ${formatExpression(n.condition, 0)} else ${formatExpression( n.falseExpr )}`; return parentPrec > -1 ? `(${expr})` : expr; } default: throw new Error(`Unknown expression type: ${node.type}`); } } // src/index.ts var Template = class { parsed; /** * @param {string} template The template string */ constructor(template) { const tokens = tokenize(template, { lstrip_blocks: true, trim_blocks: true }); this.parsed = parse(tokens); } render(items) { const env = new Environment(); setupGlobals(env); if (items) { for (const [key, value] of Object.entries(items)) { env.set(key, value); } } const interpreter = new Interpreter(env); const result = interpreter.run(this.parsed); return result.value; } format(options) { return format(this.parsed, options?.indent || " "); } }; export { Environment, Interpreter, Template, parse, tokenize };