diff --git a/src/app/template_set.ts b/src/app/template_set.ts index a4ec1db4..d39b6973 100644 --- a/src/app/template_set.ts +++ b/src/app/template_set.ts @@ -5,7 +5,7 @@ import { UTILS } from "./template_helpers"; const bdom = { text, createBlock, list, multi, html, toggler, component, comment }; -export const globalTemplates: { [key: string]: string | Node } = {}; +export const globalTemplates: { [key: string]: string | Element } = {}; function parseXML(xml: string): Document { const parser = new DOMParser(); @@ -67,7 +67,11 @@ export class TemplateSet { } } - addTemplate(name: string, template: string | Node, options: { allowDuplicate?: boolean } = {}) { + addTemplate( + name: string, + template: string | Element, + options: { allowDuplicate?: boolean } = {} + ) { if (name in this.rawTemplates && !options.allowDuplicate) { throw new Error(`Template ${name} already defined`); } @@ -82,7 +86,6 @@ export class TemplateSet { xml = xml instanceof Document ? xml : parseXML(xml); for (const template of xml.querySelectorAll("[t-name]")) { const name = template.getAttribute("t-name")!; - template.removeAttribute("t-name"); this.addTemplate(name, template, options); } } @@ -106,7 +109,7 @@ export class TemplateSet { return this.templates[name]; } - _compileTemplate(name: string, template: string | Node) { + _compileTemplate(name: string, template: string | Element) { return compile(template, { name, dev: this.dev, diff --git a/src/compiler/index.ts b/src/compiler/index.ts index 547c6322..a3f5d84d 100644 --- a/src/compiler/index.ts +++ b/src/compiler/index.ts @@ -9,7 +9,10 @@ export type TemplateFunction = (blocks: any, utils: any) => Template; interface CompileOptions extends Config { name?: string; } -export function compile(template: string | Node, options: CompileOptions = {}): TemplateFunction { +export function compile( + template: string | Element, + options: CompileOptions = {} +): TemplateFunction { // parsing const ast = parse(template); diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 61f2531e..121d4380 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -180,24 +180,35 @@ export type AST = // ----------------------------------------------------------------------------- // 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; } -export function parse(xml: string | Node): AST { - 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) { - return { type: ASTType.Text, value: "" }; - } - return ast; -} - -function parseNode(node: ChildNode, ctx: ParsingContext): AST | null { +function parseNode(node: Node, ctx: ParsingContext): AST | null { if (!(node instanceof Element)) { return parseTextCommentNode(node, ctx); } @@ -237,7 +248,7 @@ function parseTNode(node: Element, ctx: ParsingContext): AST | null { const lineBreakRE = /[\r\n]/; const whitespaceRE = /\s+/g; -function parseTextCommentNode(node: ChildNode, ctx: ParsingContext): AST | null { +function parseTextCommentNode(node: Node, ctx: ParsingContext): AST | null { if (node.nodeType === Node.TEXT_NODE) { let value = node.textContent || ""; if (!ctx.inPreTag) { @@ -360,7 +371,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null { ctx = Object.assign({}, ctx); ctx.tModelInfo = model; } - } else { + } else if (attr !== "t-name") { if (attr.startsWith("t-") && !attr.startsWith("t-att")) { throw new Error(`Unknown QWeb directive: '${attr}'`); } @@ -801,7 +812,7 @@ function parseTPortal(node: Element, ctx: ParsingContext): AST | null { /** * Parse all the child nodes of a given node and return a list of ast elements */ -function parseChildren(node: Node, ctx: ParsingContext): AST[] { +function parseChildren(node: Element, ctx: ParsingContext): AST[] { const children: AST[] = []; for (let child of node.childNodes) { const childAst = parseNode(child, ctx); @@ -820,7 +831,7 @@ function parseChildren(node: Node, ctx: ParsingContext): AST[] { * Parse all the child nodes of a given node and return an ast if possible. * In the case there are multiple children, they are wrapped in a astmulti. */ -function parseChildNodes(node: Node, ctx: ParsingContext): AST | null { +function parseChildNodes(node: Element, ctx: ParsingContext): AST | null { const children = parseChildren(node, ctx); switch (children.length) { case 0: diff --git a/tests/compiler/__snapshots__/template_set.test.ts.snap b/tests/compiler/__snapshots__/template_set.test.ts.snap index 89977ba7..f20fa6a7 100644 --- a/tests/compiler/__snapshots__/template_set.test.ts.snap +++ b/tests/compiler/__snapshots__/template_set.test.ts.snap @@ -1,5 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`loading templates addTemplates does not modify its xml document in place 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let txt1 = ctx['value']; + return block1([txt1]); + } +}" +`; + exports[`loading templates can initialize qweb with a string 1`] = ` "function anonymous(bdom, helpers ) { diff --git a/tests/compiler/template_set.test.ts b/tests/compiler/template_set.test.ts index 46f36d9d..731cb7f3 100644 --- a/tests/compiler/template_set.test.ts +++ b/tests/compiler/template_set.test.ts @@ -24,6 +24,17 @@ describe("loading templates", () => { expect(context.renderToString("hey")).toBe("
jupiler
"); }); + test("addTemplates does not modify its xml document in place", () => { + const data = ` +
`; + const xml = new DOMParser().parseFromString(data, "text/xml"); + const context = new TestContext(); + expect(xml.firstElementChild!.innerHTML).toBe(`
`); + context.addTemplates(xml); + expect(context.renderToString("hey", { value: 123 })).toBe("
123
"); + expect(xml.firstElementChild!.innerHTML).toBe(`
`); + }); + test("can load a few templates from a xml string", () => { const data = ` diff --git a/tests/helpers.ts b/tests/helpers.ts index 9865c1af..9e17f1e2 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -123,7 +123,7 @@ export function snapshotEverything() { }); const originalCompileTemplate = TemplateSet.prototype._compileTemplate; - TemplateSet.prototype._compileTemplate = function (name: string, template: string | Node) { + TemplateSet.prototype._compileTemplate = function (name: string, template: string | Element) { const fn = originalCompileTemplate.call(this, "", template); if (!globalTemplateNames.has(name)) { expect(fn.toString()).toMatchSnapshot();