import { OwlError } from "../runtime/error_handling"; // ----------------------------------------------------------------------------- // AST Type definition // ----------------------------------------------------------------------------- export type EventHandlers = { [eventName: string]: string }; export type Attrs = { [attrs: string]: string }; export const enum ASTType { Text, Comment, DomNode, Multi, TEsc, TIf, TSet, TCall, TOut, TForEach, TKey, TComponent, TDebug, TLog, TSlot, TSetSlot, TCallBlock, TTranslation, TPortal, } export interface ASTText { type: ASTType.Text; value: string; } export interface ASTComment { type: ASTType.Comment; value: string; } interface TModelInfo { baseExpr: string; expr: string; targetAttr: string; eventType: "change" | "click" | "input"; shouldTrim: boolean; shouldNumberize: boolean; hasDynamicChildren: boolean; specialInitTargetAttr: string | null; } export interface ASTDomNode { type: ASTType.DomNode; tag: string; content: AST[]; attrs: Attrs | null; ref: string | null; on: EventHandlers | null; model: TModelInfo | null; dynamicTag: string | null; ns: string | null; } export interface ASTMulti { type: ASTType.Multi; content: AST[]; } export interface ASTTEsc { type: ASTType.TEsc; expr: string; defaultValue: string; } export interface ASTTOut { type: ASTType.TOut; 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; body: AST; memo: string; hasNoFirst: boolean; hasNoLast: boolean; hasNoIndex: boolean; hasNoValue: boolean; key: string | null; } export interface ASTTKey { type: ASTType.TKey; expr: string; content: AST; } export interface ASTTCall { type: ASTType.TCall; name: string; body: AST[] | null; context: string | null; } export interface ASTSlotDefinition { type: ASTType.TSetSlot; name: string; content: AST | null; scope: string | null; on: EventHandlers | null; attrs: Attrs | null; } export interface ASTComponent { type: ASTType.TComponent; name: string; isDynamic: boolean; dynamicProps: string | null; on: EventHandlers | null; props: { [name: string]: string } | null; body: AST | null; // slots: { [name: string]: ASTSlotDefinition } | null; } export interface ASTSlot { type: ASTType.TSlot; name: string; attrs: Attrs | null; on: EventHandlers | null; 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 interface ASTTPortal { type: ASTType.TPortal; target: string; content: AST; } export type AST = | ASTText | ASTComment | ASTDomNode | ASTMulti | ASTTEsc | ASTTif | ASTTSet | ASTTCall | ASTTOut | ASTTForEach | ASTTKey | ASTComponent | ASTSlot | ASTSlotDefinition | ASTTCallBlock | ASTLog | ASTDebug | ASTTranslation | ASTTPortal; // ----------------------------------------------------------------------------- // Parser // ----------------------------------------------------------------------------- const cache: WeakMap = new WeakMap(); export function parse(xml: string | Element): AST { if (typeof xml === "string") { const elem = parseXML(`${xml}`).firstChild as Element; return _parse(elem); } let ast = cache.get(xml); if (!ast) { // we clone here the xml to prevent modifying it in place ast = _parse(xml.cloneNode(true) as Element); cache.set(xml, ast); } return ast; } function _parse(xml: Element): AST { normalizeXML(xml); const ctx = { inPreTag: false, inSVG: false }; return parseNode(xml, ctx) || { type: ASTType.Text, value: "" }; } interface ParsingContext { tModelInfo?: TModelInfo | null; inPreTag: boolean; inSVG: boolean; } function parseNode(node: Node, ctx: ParsingContext): AST | null { if (!(node instanceof Element)) { return parseTextCommentNode(node, ctx); } return ( parseTDebugLog(node, ctx) || parseTForEach(node, ctx) || parseTIf(node, ctx) || parseTPortal(node, ctx) || parseTCall(node, ctx) || parseTCallBlock(node, ctx) || parseTEscNode(node, ctx) || parseTKey(node, ctx) || parseTTranslation(node, ctx) || parseTSlot(node, ctx) || parseTSetSlot(node, ctx) || parseTOutNode(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; } return parseChildNodes(node, ctx); } // ----------------------------------------------------------------------------- // Text and Comment Nodes // ----------------------------------------------------------------------------- const lineBreakRE = /[\r\n]/; const whitespaceRE = /\s+/g; function parseTextCommentNode(node: Node, ctx: ParsingContext): AST | null { if (node.nodeType === Node.TEXT_NODE) { 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 === Node.COMMENT_NODE) { 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*$/; const ROOT_SVG_TAGS = new Set(["svg", "g", "path"]); function parseDOMNode(node: Element, ctx: ParsingContext): AST | null { const { tagName } = node; const dynamicTag = node.getAttribute("t-tag"); node.removeAttribute("t-tag"); if (tagName === "t" && !dynamicTag) { return null; } if (tagName.startsWith("block-")) { throw new OwlError(`Invalid tag name: '${tagName}'`); } ctx = Object.assign({}, ctx); if (tagName === "pre") { ctx.inPreTag = true; } const shouldAddSVGNS = ROOT_SVG_TAGS.has(tagName) && !ctx.inSVG; ctx.inSVG = ctx.inSVG || shouldAddSVGNS; const ns = shouldAddSVGNS ? "http://www.w3.org/2000/svg" : null; const ref = node.getAttribute("t-ref"); node.removeAttribute("t-ref"); const nodeAttrsNames = node.getAttributeNames(); let attrs: ASTDomNode["attrs"] = null; let on: EventHandlers | null = null; let model: TModelInfo | null = null; for (let attr of nodeAttrsNames) { const value = node.getAttribute(attr)!; if (attr.startsWith("t-on")) { if (attr === "t-on") { throw new OwlError("Missing event name with t-on directive"); } on = on || {}; on[attr.slice(5)] = value; } else if (attr.startsWith("t-model")) { if (!["input", "select", "textarea"].includes(tagName)) { throw new OwlError( "The t-model directive only works with ,