// ----------------------------------------------------------------------------- // AST Type definition // ----------------------------------------------------------------------------- export const enum ASTType { Text, Comment, DomNode, Multi, TEsc, TIf, TSet, TCall, TRaw, TForEach, TKey, TComponent, TDebug, TLog, TSlot, TCallBlock, TTranslation, } export interface ASTText { type: ASTType.Text; value: string; } export interface ASTComment { type: ASTType.Comment; value: string; } export interface ASTDomNode { type: ASTType.DomNode; tag: string; dynamicTag: string | null; attrs: { [key: string]: string }; content: AST[]; ref: string | null; on: { [key: string]: string }; model: { baseExpr: string; expr: string; targetAttr: string; specialInitTargetAttr: string | null; eventType: "change" | "click" | "input"; shouldTrim: boolean; shouldNumberize: boolean; } | null; } export interface ASTMulti { type: ASTType.Multi; content: AST[]; } export interface ASTTEsc { type: ASTType.TEsc; expr: string; defaultValue: string; } export interface ASTTRaw { type: ASTType.TRaw; expr: string; body: AST[] | null; } export interface ASTTif { type: ASTType.TIf; condition: string; content: AST; tElif: { condition: string; content: AST }[] | null; tElse: AST | null; } export interface ASTTSet { type: ASTType.TSet; name: string; value: string | null; // value defined in attribute defaultValue: string | null; // value defined in body, if text body: AST[] | null; // content of body if not text } export interface ASTTForEach { type: ASTType.TForEach; collection: string; elem: string; key: string | null; body: AST; memo: string; isOnlyChild: boolean; hasNoComponent: boolean; hasNoFirst: boolean; hasNoLast: boolean; hasNoIndex: boolean; hasNoValue: boolean; } export interface ASTTKey { type: ASTType.TKey; expr: string; content: AST; } export interface ASTTCall { type: ASTType.TCall; name: string; body: AST[] | null; } export interface ASTComponent { type: ASTType.TComponent; name: string; isDynamic: boolean; dynamicProps: string | null; props: { [name: string]: string }; slots: { [name: string]: AST }; } export interface ASTSlot { type: ASTType.TSlot; name: string; defaultContent: AST | null; } export interface ASTTCallBlock { type: ASTType.TCallBlock; name: string; } export interface ASTDebug { type: ASTType.TDebug; content: AST | null; } export interface ASTLog { type: ASTType.TLog; expr: string; content: AST | null; } export interface ASTTranslation { type: ASTType.TTranslation; content: AST | null; } export type AST = | ASTText | ASTComment | ASTDomNode | ASTMulti | ASTTEsc | ASTTif | ASTTSet | ASTTCall | ASTTRaw | ASTTForEach | ASTTKey | ASTComponent | ASTSlot | ASTTCallBlock | ASTLog | ASTDebug | ASTTranslation; // ----------------------------------------------------------------------------- // Parser // ----------------------------------------------------------------------------- interface ParsingContext { inPreTag: boolean; } export function parse(xml: string): AST { const template = `${xml}`; const doc = parseXML(template); const ctx = { inPreTag: false }; const ast = parseNode(doc.firstChild!, ctx); if (!ast) { return { type: ASTType.Text, value: "" }; } return ast; } function parseNode(node: ChildNode, ctx: ParsingContext): AST | null { if (!(node instanceof Element)) { return parseTextCommentNode(node, ctx); } return ( parseTDebugLog(node, ctx) || parseTForEach(node, ctx) || parseTIf(node, ctx) || parseTCall(node, ctx) || parseTCallBlock(node, ctx) || parseTEscNode(node, ctx) || parseTKey(node, ctx) || parseTTranslation(node, ctx) || parseTSlot(node, ctx) || parseTRawNode(node, ctx) || parseComponent(node, ctx) || parseDOMNode(node, ctx) || parseTSetNode(node, ctx) || parseTNode(node, ctx) ); } // ----------------------------------------------------------------------------- // tag // ----------------------------------------------------------------------------- function parseTNode(node: Element, ctx: ParsingContext): AST | null { if (node.tagName !== "t") { return null; } const children: AST[] = []; for (let child of node.childNodes) { const ast = parseNode(child, ctx); if (ast) { children.push(ast); } } switch (children.length) { case 0: return null; case 1: return children[0]; default: return { type: ASTType.Multi, content: children, }; } } // ----------------------------------------------------------------------------- // Text and Comment Nodes // ----------------------------------------------------------------------------- const lineBreakRE = /[\r\n]/; const whitespaceRE = /\s+/g; function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null { if (node.nodeType === 3) { let value = node.textContent || ""; if (!ctx.inPreTag) { if (lineBreakRE.test(value) && !value.trim()) { return null; } value = value.replace(whitespaceRE, " "); } return { type: ASTType.Text, value }; } else if (node.nodeType === 8) { return { type: ASTType.Comment, value: node.textContent || "" }; } return null; } // ----------------------------------------------------------------------------- // debugging // ----------------------------------------------------------------------------- function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null { if (node.hasAttribute("t-debug")) { node.removeAttribute("t-debug"); return { type: ASTType.TDebug, content: parseNode(node, ctx), }; } if (node.hasAttribute("t-log")) { const expr = node.getAttribute("t-log")!; node.removeAttribute("t-log"); return { type: ASTType.TLog, expr, content: parseNode(node, ctx), }; } return null; } // ----------------------------------------------------------------------------- // Regular dom node // ----------------------------------------------------------------------------- const hasDotAtTheEnd = /\.[\w_]+\s*$/; const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/; function parseDOMNode(node: Element, ctx: ParsingContext): AST | null { const { tagName } = node; let dynamicTag = null; if (node.hasAttribute("t-tag")) { dynamicTag = node.getAttribute("t-tag"); node.removeAttribute("t-tag"); } if (tagName === "t" && !dynamicTag) { return null; } const children: AST[] = []; if (tagName === "pre") { ctx = { inPreTag: true }; } const ref = node.getAttribute("t-ref"); node.removeAttribute("t-ref"); for (let child of node.childNodes) { const ast = parseNode(child, ctx); if (ast) { children.push(ast); } } const nodeAttrsNames = node.getAttributeNames(); const attrs: ASTDomNode["attrs"] = {}; const on: ASTDomNode["on"] = {}; let model: ASTDomNode["model"] = null; for (let attr of nodeAttrsNames) { const value = node.getAttribute(attr)!; if (attr.startsWith("t-on")) { if (attr === "t-on") { throw new Error("Missing event name with t-on directive"); } on[attr.slice(5)] = value; } else if (attr.startsWith("t-model")) { if (!["input", "select", "textarea"].includes(tagName)) { throw new Error("The t-model directive only works with ,