import { LRLanguage } from "@codemirror/language"; import { parse } from "../markdown_parser/parse_tree.ts"; import { parser as templateParser } from "./parse-template.js"; import { parser as expressionParser } from "../markdown_parser/parse-expression.js"; import { parser as queryParser } from "../markdown_parser/parse-query.js"; import { AST } from "$sb/lib/tree.ts"; import { parseTreeToAST } from "$sb/lib/tree.ts"; import { deepEqual } from "$sb/lib/json.ts"; export const templateLanguage = LRLanguage.define({ name: "template", parser: templateParser, }); export const expressionLanguage = LRLanguage.define({ name: "expression", parser: expressionParser, }); export const queryLanguage = LRLanguage.define({ name: "query", parser: queryParser, }); export function parseTemplate(text: string) { // Remove a newline after a singleton (only thing on the line) block open or close tag // text = text.replaceAll(/(^|\n)(\{\{[#\/][^}]+\}\})(\n)/g, "$1$2"); const tree = parse(templateLanguage, text); const ast = processTree(parseTreeToAST(tree, false)); // console.log("AST", JSON.stringify(ast, null, 2)); return ast; } function processTree(tree: AST): AST { switch (tree[0]) { case "Template": return [ "Template", ...stripInitialNewline((tree.slice(1) as AST[]).map(processTree)), ]; case "TemplateElement": return ["TemplateElement", ...(tree.slice(1) as AST[]).map(processTree)]; case "ExpressionDirective": { let exprString = tree[2][1] as string; const legacyCallSyntax = /^([A-Za-z]+)\s+([^(]+$)/.exec(exprString); if (legacyCallSyntax) { // Translates "escapeRegex @page.name" -> "escapeRegex(@page.name)" const [_, fn, args] = legacyCallSyntax; exprString = `${fn}(${args})`; console.warn( "Translated legacy function call to new syntax", exprString, ); } const expressionTree = parseTreeToAST(parse( expressionLanguage, exprString, )); return ["ExpressionDirective", expressionTree[1]]; } case "BlockDirective": { const blockType = tree[2][1] as string; const blockTextContent = tree[3][1] as string; const bodyElements = (tree as any[]).filter((n) => n[0] === "TemplateElement" ); const closingBlockName = tree[tree.length - 2][1]; if (closingBlockName !== blockType) { throw new Error( `Block #${blockType} is not properly closed, saw /${closingBlockName} instead`, ); } // const body = stripInitialNewline(bodyElements.map(processTree)); const body = bodyElements.map(processTree); switch (blockType) { case "each": { const eachExpr = blockTextContent.trim(); const eachVarMatch = eachExpr.match(/^@(\w+)\s+in\s+(.+)$/s); if (!eachVarMatch) { // Not a each var declaration, just an expression const expressionTree = parseTreeToAST(parse( expressionLanguage, blockTextContent.trim(), )); // console.log("Each body", bodyElements); return ["EachDirective", expressionTree[1], [ "Template", ...stripInitialNewline(body), ]]; } // This is a #each @p = version const expressionTree = parseTreeToAST(parse( expressionLanguage, eachVarMatch[2], )); return [ "EachVarDirective", eachVarMatch[1], expressionTree[1], ["Template", ...stripInitialNewline(body)], ]; } case "let": { const letExpr = blockTextContent.trim(); const letMatch = letExpr.match(/^@(\w+)\s*=\s*(.+)$/s); if (!letMatch) { throw new Error( `A #let directive should be of the shape {{#let @var = expression}}, got instead: ${blockTextContent}`, ); } const expressionTree = parseTreeToAST(parse( expressionLanguage, letMatch[2], )); return [ "LetDirective", letMatch[1], expressionTree[1], ["Template", ...stripInitialNewline(body)], ]; } case "if": { const expressionTree = parseTreeToAST(parse( expressionLanguage, blockTextContent.trim(), )); const elseIndex = body.findIndex((n) => deepEqual(n, ["TemplateElement", ["ExpressionDirective", [ "Expression", ["Identifier", "else"], ]]]) ); if (elseIndex !== -1) { return [ "IfDirective", expressionTree[1], ["Template", ...stripInitialNewline(body.slice(0, elseIndex))], ["Template", ...stripInitialNewline(body.slice(elseIndex + 1))], ]; } else { return ["IfDirective", expressionTree[1], [ "Template", ...stripInitialNewline(body), ]]; } } default: { throw new Error(`Unknown block type: ${blockType}`); } } } case "Text": return tree; default: console.log("tree", tree); throw new Error(`Unknown node type: ${tree[0]}`); } } function stripInitialNewline(body: any[]) { // body = [["TemplateElement", ["Text", "\n..."], ...]] let first = true; let stripNext = false; for (const el of body) { // Strip initial newline if (first && el[1][0] === "Text" && el[1][1].startsWith("\n")) { // Remove initial newline el[1][1] = el[1][1].slice(1); } first = false; // After each block directive, strip the next newline if ( ["IfDirective", "EachDirective", "EachVarDirective", "LetDirective"] .includes(el[1][0]) ) { // console.log("Got a block directive, consider stripping the next one", el); stripNext = true; continue; } if ( el[1][0] === "Text" && el[1][1].startsWith("\n") && stripNext ) { // console.log("Stripping initial newline from", el); el[1][1] = el[1][1].slice(1); } stripNext = false; } return body; }