From b988a68b6fdee6082498ebe2ce31f487e1ab7e6a Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Wed, 24 Nov 2021 10:56:21 +0100 Subject: [PATCH] [IMP] parser: normalize document before parsing --- src/compiler/parser.ts | 112 +++++++++++++++++++++++++--------- tests/compiler/parser.test.ts | 10 ++- 2 files changed, 92 insertions(+), 30 deletions(-) diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index ad3dc6de..443af533 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -177,7 +177,8 @@ interface ParsingContext { } export function parse(xml: string | Node): AST { - const node = xml instanceof Element ? xml : parseXML(`${xml}`).firstChild!; + const node = xml instanceof Element ? xml : (parseXML(`${xml}`).firstChild! as Element); + normalizeXML(node); const ctx = { inPreTag: false, inSVG: false }; const ast = parseNode(node, ctx); if (!ast) { @@ -862,34 +863,17 @@ function parseChildNodes(node: Element, ctx: ParsingContext): AST | null { return { type: ASTType.Multi, content: children }; } } -function parseXML(xml: string): Document { - const parser = new DOMParser(); - const doc = parser.parseFromString(xml, "text/xml"); - if (doc.getElementsByTagName("parsererror").length) { - let msg = "Invalid XML in template."; - const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent; - if (parsererrorText) { - msg += "\nThe parser has produced the following error message:\n" + parsererrorText; - const re = /\d+/g; - const firstMatch = re.exec(parsererrorText); - if (firstMatch) { - const lineNumber = Number(firstMatch[0]); - const line = xml.split("\n")[lineNumber - 1]; - const secondMatch = re.exec(parsererrorText); - if (line && secondMatch) { - const columnIndex = Number(secondMatch[0]) - 1; - if (line[columnIndex]) { - msg += - `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` + - `${line}\n${"-".repeat(columnIndex - 1)}^`; - } - } - } - } - throw new Error(msg); - } - let tbranch = doc.querySelectorAll("[t-elif], [t-else]"); +/** + * Normalizes the content of an Element so that t-if/t-elif/t-else directives + * immediately follow one another (by removing empty text nodes or comments). + * Throws an error when a conditional branching statement is malformed. This + * function modifies the Element in place. + * + * @param el the element containing the tree that should be normalized + */ +function normalizeTIf(el: Element) { + let tbranch = el.querySelectorAll("[t-elif], [t-else]"); for (let i = 0, ilen = tbranch.length; i < ilen; i++) { let node = tbranch[i]; let prevElem = node.previousElementSibling!; @@ -923,6 +907,78 @@ function parseXML(xml: string): Document { ); } } +} + +/** + * Normalizes the content of an Element so that t-esc directives on components + * are removed and instead places a as the default slot of the + * component. Also throws if the component already has content. This function + * modifies the Element in place. + * + * @param el the element containing the tree that should be normalized + */ +function normalizeTEsc(el: Element) { + const elements = [...el.querySelectorAll("[t-esc]")].filter( + (el) => el.tagName[0] === el.tagName[0].toUpperCase() || el.hasAttribute("t-component") + ); + for (const el of elements) { + if (el.childNodes.length) { + throw new Error("Cannot have t-esc on a component that already has content"); + } + const value = el.getAttribute("t-esc"); + el.removeAttribute("t-esc"); + const t = el.ownerDocument.createElement("t"); + if (value != null) { + t.setAttribute("t-esc", value); + } + el.appendChild(t); + } +} + +/** + * Normalizes the tree inside a given element and do some preliminary validation + * on it. This function modifies the Element in place. + * + * @param el the element containing the tree that should be normalized + */ +function normalizeXML(el: Element) { + normalizeTIf(el); + normalizeTEsc(el); +} + +/** + * Parses an XML string into an XML document, throwing errors on parser errors + * instead of returning an XML document containing the parseerror. + * + * @param xml the string to parse + * @returns an XML document corresponding to the content of the string + */ +function parseXML(xml: string): XMLDocument { + const parser = new DOMParser(); + const doc = parser.parseFromString(xml, "text/xml"); + if (doc.getElementsByTagName("parsererror").length) { + let msg = "Invalid XML in template."; + const parsererrorText = doc.getElementsByTagName("parsererror")[0].textContent; + if (parsererrorText) { + msg += "\nThe parser has produced the following error message:\n" + parsererrorText; + const re = /\d+/g; + const firstMatch = re.exec(parsererrorText); + if (firstMatch) { + const lineNumber = Number(firstMatch[0]); + const line = xml.split("\n")[lineNumber - 1]; + const secondMatch = re.exec(parsererrorText); + if (line && secondMatch) { + const columnIndex = Number(secondMatch[0]) - 1; + if (line[columnIndex]) { + msg += + `\nThe error might be located at xml line ${lineNumber} column ${columnIndex}\n` + + `${line}\n${"-".repeat(columnIndex - 1)}^`; + } + } + } + } + throw new Error(msg); + } return doc; } diff --git a/tests/compiler/parser.test.ts b/tests/compiler/parser.test.ts index 489e932d..39f6dc94 100644 --- a/tests/compiler/parser.test.ts +++ b/tests/compiler/parser.test.ts @@ -1259,8 +1259,14 @@ describe("qweb parser", () => { }); test("component with t-esc", async () => { - expect(() => parse(``)).toThrow( - "t-esc is not supported on Component nodes" + expect(parse(``)).toEqual( + parse(``) + ); + }); + + test("component with t-esc and content", async () => { + expect(() => parse(`Some content`)).toThrow( + "Cannot have t-esc on a component that already has content" ); });