2024-02-03 02:19:07 +08:00
|
|
|
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";
|
2024-07-30 23:33:33 +08:00
|
|
|
import { type AST, parseTreeToAST } from "../../plug-api/lib/tree.ts";
|
2024-02-29 22:23:05 +08:00
|
|
|
import { deepEqual } from "../../plug-api/lib/json.ts";
|
2024-02-03 02:19:07 +08:00
|
|
|
|
|
|
|
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": {
|
2024-07-03 01:12:40 +08:00
|
|
|
const exprString = tree[2][1] as string;
|
|
|
|
|
2024-02-03 02:19:07 +08:00
|
|
|
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": {
|
2024-02-03 22:28:24 +08:00
|
|
|
const eachExpr = blockTextContent.trim();
|
2024-02-03 23:14:48 +08:00
|
|
|
const eachVarMatch = eachExpr.match(/^@(\w+)\s+in\s+(.+)$/s);
|
2024-02-03 22:28:24 +08:00
|
|
|
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
|
2024-02-03 02:19:07 +08:00
|
|
|
const expressionTree = parseTreeToAST(parse(
|
|
|
|
expressionLanguage,
|
2024-02-03 22:28:24 +08:00
|
|
|
eachVarMatch[2],
|
2024-02-03 02:19:07 +08:00
|
|
|
));
|
2024-02-03 22:28:24 +08:00
|
|
|
return [
|
|
|
|
"EachVarDirective",
|
|
|
|
eachVarMatch[1],
|
|
|
|
expressionTree[1],
|
|
|
|
["Template", ...stripInitialNewline(body)],
|
|
|
|
];
|
2024-02-03 02:19:07 +08:00
|
|
|
}
|
|
|
|
case "let": {
|
|
|
|
const letExpr = blockTextContent.trim();
|
2024-02-03 22:55:39 +08:00
|
|
|
const letMatch = letExpr.match(/^@(\w+)\s*=\s*(.+)$/s);
|
2024-02-03 02:19:07 +08:00
|
|
|
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 (
|
2024-02-03 22:28:24 +08:00
|
|
|
["IfDirective", "EachDirective", "EachVarDirective", "LetDirective"]
|
|
|
|
.includes(el[1][0])
|
2024-02-03 02:19:07 +08:00
|
|
|
) {
|
|
|
|
// 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;
|
|
|
|
}
|