mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
16bbb8bc9f
- fix issue with t-debug on a t-set (defining a slot) - put t-debug and t-log at a very low (high) priority to make sure they are executed before other directives closes #201
761 lines
22 KiB
TypeScript
761 lines
22 KiB
TypeScript
import { VNode, h } from "./vdom";
|
|
import { QWebVar, compileExpr } from "./qweb_expressions";
|
|
import { EventBus } from "./event_bus";
|
|
|
|
/**
|
|
* Owl QWeb Engine
|
|
*
|
|
* In this file, you will find a QWeb engine/template compiler. It is the core
|
|
* of how Owl component works.
|
|
*
|
|
* Briefly, Owl QWeb compiles XML templates into functions that output a virtual
|
|
* DOM representation.
|
|
*
|
|
* We have here:
|
|
* - a CompilationContext class, which is an internal object that contains all
|
|
* compilation specific information, while a template is being compiled.
|
|
* - a QWeb class: this is the code of the QWeb compiler.
|
|
*
|
|
* Note that this file does not contain the implementation of the QWeb
|
|
* directives (see qweb_directives.ts and qweb_extensions.ts).
|
|
*/
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Types
|
|
//------------------------------------------------------------------------------
|
|
export type EvalContext = { [key: string]: any };
|
|
export type CompiledTemplate = (context: EvalContext, extra: any) => VNode;
|
|
|
|
interface Template {
|
|
elem: Element;
|
|
fn: CompiledTemplate;
|
|
}
|
|
|
|
interface CompilationInfo {
|
|
node: Element;
|
|
qweb: QWeb;
|
|
ctx: Context;
|
|
fullName: string;
|
|
value: string;
|
|
}
|
|
|
|
interface NodeCreationCompilationInfo extends CompilationInfo {
|
|
nodeID: number;
|
|
addNodeHook: Function;
|
|
}
|
|
|
|
export interface Directive {
|
|
name: string;
|
|
extraNames?: string[];
|
|
priority: number;
|
|
// if return true, then directive is fully applied and there is no need to
|
|
// keep processing node. Otherwise, we keep going.
|
|
atNodeEncounter?(info: CompilationInfo): boolean | void;
|
|
atNodeCreation?(info: NodeCreationCompilationInfo): void;
|
|
finalize?(info: CompilationInfo): void;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Const/global stuff/helpers
|
|
//------------------------------------------------------------------------------
|
|
|
|
const DISABLED_TAGS = [
|
|
"input",
|
|
"textarea",
|
|
"button",
|
|
"select",
|
|
"option",
|
|
"optgroup"
|
|
];
|
|
|
|
const lineBreakRE = /[\r\n]/;
|
|
const whitespaceRE = /\s+/g;
|
|
|
|
const DIRECTIVE_NAMES = {
|
|
name: 1,
|
|
att: 1,
|
|
attf: 1,
|
|
key: 1
|
|
};
|
|
|
|
const DIRECTIVES: Directive[] = [];
|
|
|
|
const NODE_HOOKS_PARAMS = {
|
|
create: "(_, n)",
|
|
insert: "vn",
|
|
remove: "(vn, rm)"
|
|
};
|
|
|
|
interface Utils {
|
|
h: typeof h;
|
|
objectToAttrString(obj: Object): string;
|
|
shallowEqual(p1: Object, p2: Object): boolean;
|
|
[key: string]: any;
|
|
}
|
|
|
|
export const UTILS: Utils = {
|
|
h: h,
|
|
objectToAttrString(obj: Object): string {
|
|
let classes: string[] = [];
|
|
for (let k in obj) {
|
|
if (obj[k]) {
|
|
classes.push(k);
|
|
}
|
|
}
|
|
return classes.join(" ");
|
|
},
|
|
shallowEqual(p1, p2) {
|
|
for (let k in p1) {
|
|
if (p1[k] !== p2[k]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
function parseXML(xml: string): Document {
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(xml, "text/xml");
|
|
if (doc.getElementsByTagName("parsererror").length) {
|
|
throw new Error("Invalid XML in template");
|
|
}
|
|
return doc;
|
|
}
|
|
|
|
let nextID = 1;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// QWeb rendering engine
|
|
//------------------------------------------------------------------------------
|
|
export class QWeb extends EventBus {
|
|
templates: { [name: string]: Template } = {};
|
|
utils = UTILS;
|
|
static components = Object.create(null);
|
|
|
|
// dev mode enables better error messages or more costly validations
|
|
static dev: boolean = false;
|
|
|
|
// the id field is useful to be able to hash qweb instances. The current
|
|
// use case is that component's templates are qweb dependant, and need to be
|
|
// able to map a qweb instance to a template name.
|
|
id = nextID++;
|
|
|
|
// slots contains sub templates defined with t-set inside t-component nodes, and
|
|
// are meant to be used by the t-slot directive.
|
|
slots = {};
|
|
nextSlotId = 1;
|
|
|
|
constructor(data?: string) {
|
|
super();
|
|
if (data) {
|
|
this.addTemplates(data);
|
|
}
|
|
}
|
|
|
|
static addDirective(directive: Directive) {
|
|
DIRECTIVES.push(directive);
|
|
DIRECTIVE_NAMES[directive.name] = 1;
|
|
DIRECTIVES.sort((d1, d2) => d1.priority - d2.priority);
|
|
if (directive.extraNames) {
|
|
directive.extraNames.forEach(n => (DIRECTIVE_NAMES[n] = 1));
|
|
}
|
|
}
|
|
|
|
static register(name: string, Component: any) {
|
|
if (QWeb.components[name]) {
|
|
throw new Error(`Component '${name}' has already been registered`);
|
|
}
|
|
QWeb.components[name] = Component;
|
|
}
|
|
|
|
/**
|
|
* Add a template to the internal template map. Note that it is not
|
|
* immediately compiled.
|
|
*/
|
|
addTemplate(name: string, xmlString: string, allowDuplicate?: boolean) {
|
|
if (allowDuplicate && name in this.templates) {
|
|
return;
|
|
}
|
|
const doc = parseXML(xmlString);
|
|
if (!doc.firstChild) {
|
|
throw new Error("Invalid template (should not be empty)");
|
|
}
|
|
this._addTemplate(name, <Element>doc.firstChild);
|
|
}
|
|
|
|
/**
|
|
* Load templates from a xml (as a string). This will look up for the first
|
|
* <templates> tag, and will consider each child of this as a template, with
|
|
* the name given by the t-name attribute.
|
|
*/
|
|
addTemplates(xmlstr: string) {
|
|
const doc = parseXML(xmlstr);
|
|
const templates = doc.getElementsByTagName("templates")[0];
|
|
if (!templates) {
|
|
return;
|
|
}
|
|
for (let elem of <any>templates.children) {
|
|
const name = elem.getAttribute("t-name");
|
|
this._addTemplate(name, elem);
|
|
}
|
|
}
|
|
|
|
_addTemplate(name: string, elem: Element) {
|
|
if (name in this.templates) {
|
|
throw new Error(`Template ${name} already defined`);
|
|
}
|
|
this._processTemplate(elem);
|
|
const template = {
|
|
elem,
|
|
fn: (context, extra) => {
|
|
const compiledFunction = this._compile(name, elem);
|
|
template.fn = compiledFunction;
|
|
return compiledFunction.call(this, context, extra);
|
|
}
|
|
};
|
|
this.templates[name] = template;
|
|
}
|
|
|
|
_processTemplate(elem: Element) {
|
|
let tbranch = elem.querySelectorAll("[t-elif], [t-else]");
|
|
for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
|
|
let node = tbranch[i];
|
|
let prevElem = node.previousElementSibling!;
|
|
let pattr = function(name) {
|
|
return prevElem.getAttribute(name);
|
|
};
|
|
let nattr = function(name) {
|
|
return +!!node.getAttribute(name);
|
|
};
|
|
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
|
|
if (pattr("t-foreach")) {
|
|
throw new Error(
|
|
"t-if cannot stay at the same level as t-foreach when using t-elif or t-else"
|
|
);
|
|
}
|
|
if (
|
|
["t-if", "t-elif", "t-else"].map(nattr).reduce(function(a, b) {
|
|
return a + b;
|
|
}) > 1
|
|
) {
|
|
throw new Error(
|
|
"Only one conditional branching directive is allowed per node"
|
|
);
|
|
}
|
|
// All text nodes between branch nodes are removed
|
|
let textNode;
|
|
while ((textNode = node.previousSibling) !== prevElem) {
|
|
if (textNode.nodeValue.trim().length) {
|
|
throw new Error("text is not allowed between branching directives");
|
|
}
|
|
textNode.remove();
|
|
}
|
|
} else {
|
|
throw new Error(
|
|
"t-elif and t-else directives must be preceded by a t-if or t-elif directive"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Render a template
|
|
*
|
|
* @param {string} name the template should already have been added
|
|
*/
|
|
render(name: string, context: EvalContext = {}, extra: any = null): VNode {
|
|
const template = this.templates[name];
|
|
if (!template) {
|
|
throw new Error(`Template ${name} does not exist`);
|
|
}
|
|
return template.fn.call(this, context, extra);
|
|
}
|
|
|
|
_compile(name: string, elem: Element, parentNode?: number): CompiledTemplate {
|
|
const isDebug = elem.attributes.hasOwnProperty("t-debug");
|
|
const ctx = new Context(name);
|
|
if (parentNode) {
|
|
ctx.nextID = parentNode + 1;
|
|
ctx.parentNode = parentNode;
|
|
ctx.allowMultipleRoots = true;
|
|
ctx.addLine(`let c${parentNode} = extra.parentNode;`);
|
|
}
|
|
this._compileNode(elem, ctx);
|
|
|
|
if (ctx.shouldProtectContext) {
|
|
ctx.code.unshift(" context = Object.create(context);");
|
|
}
|
|
if (ctx.shouldDefineOwner) {
|
|
// this is necessary to prevent some directives (t-forach for ex) to
|
|
// pollute the rendering context by adding some keys in it.
|
|
ctx.code.unshift(" let owner = context;");
|
|
}
|
|
if (ctx.shouldDefineQWeb) {
|
|
ctx.code.unshift(" let QWeb = this.constructor;");
|
|
}
|
|
if (ctx.shouldDefineUtils) {
|
|
ctx.code.unshift(" let utils = this.utils;");
|
|
}
|
|
|
|
if (!parentNode) {
|
|
if (!ctx.rootNode) {
|
|
throw new Error("A template should have one root node");
|
|
}
|
|
ctx.addLine(`return vn${ctx.rootNode};`);
|
|
}
|
|
let template;
|
|
try {
|
|
template = new Function(
|
|
"context",
|
|
"extra",
|
|
ctx.code.join("\n")
|
|
) as CompiledTemplate;
|
|
} catch (e) {
|
|
const templateName = ctx.templateName.replace(/`/g, "'");
|
|
console.groupCollapsed(`Invalid Code generated by ${templateName}`);
|
|
console.warn(ctx.code.join("\n"));
|
|
console.groupEnd();
|
|
throw new Error(
|
|
`Invalid generated code while compiling template '${templateName}': ${
|
|
e.message
|
|
}`
|
|
);
|
|
}
|
|
if (isDebug) {
|
|
const tpl = this.templates[name];
|
|
if (tpl) {
|
|
const msg = `Template: ${
|
|
tpl.elem.outerHTML
|
|
}\nCompiled code:\n${template.toString()}`;
|
|
console.log(msg);
|
|
}
|
|
}
|
|
return template;
|
|
}
|
|
|
|
/**
|
|
* Generate code from an xml node
|
|
*
|
|
*/
|
|
_compileNode(node: ChildNode, ctx: Context) {
|
|
if (!(node instanceof Element)) {
|
|
// this is a text node, there are no directive to apply
|
|
let text = node.textContent!;
|
|
if (!ctx.inPreTag) {
|
|
if (lineBreakRE.test(text) && !text.trim()) {
|
|
return;
|
|
}
|
|
text = text.replace(whitespaceRE, " ");
|
|
}
|
|
if (ctx.parentNode) {
|
|
ctx.addLine(`c${ctx.parentNode}.push({text: \`${text}\`});`);
|
|
} else if (ctx.parentTextNode) {
|
|
ctx.addLine(`vn${ctx.parentTextNode}.text += \`${text}\`;`);
|
|
} else {
|
|
// this is an unusual situation: this text node is the result of the
|
|
// template rendering.
|
|
let nodeID = ctx.generateID();
|
|
ctx.addLine(`var vn${nodeID} = {text: \`${text}\`};`);
|
|
ctx.rootContext.rootNode = nodeID;
|
|
ctx.rootContext.parentTextNode = nodeID;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const firstLetter = node.tagName[0];
|
|
if (firstLetter === firstLetter.toUpperCase()) {
|
|
// this is a component, we modify in place the xml document to change
|
|
// <SomeComponent ... /> to <t t-component="SomeComponent" ... />
|
|
node.setAttribute("t-component", node.tagName);
|
|
}
|
|
const attributes = (<Element>node).attributes;
|
|
|
|
const validDirectives: {
|
|
directive: Directive;
|
|
value: string;
|
|
fullName: string;
|
|
}[] = [];
|
|
|
|
let withHandlers = false;
|
|
|
|
// maybe this is not optimal: we iterate on all attributes here, and again
|
|
// just after for each directive.
|
|
for (let i = 0; i < attributes.length; i++) {
|
|
let attrName = attributes[i].name;
|
|
if (attrName.startsWith("t-")) {
|
|
let dName = attrName.slice(2).split(/-|\./)[0];
|
|
if (!(dName in DIRECTIVE_NAMES)) {
|
|
throw new Error(`Unknown QWeb directive: '${attrName}'`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const DIR_N = DIRECTIVES.length;
|
|
const ATTR_N = attributes.length;
|
|
for (let i = 0; i < DIR_N; i++) {
|
|
let directive = DIRECTIVES[i];
|
|
let fullName;
|
|
let value;
|
|
for (let j = 0; j < ATTR_N; j++) {
|
|
const name = attributes[j].name;
|
|
if (
|
|
name === "t-" + directive.name ||
|
|
name.startsWith("t-" + directive.name + "-") ||
|
|
name.startsWith("t-" + directive.name + ".")
|
|
) {
|
|
fullName = name;
|
|
value = attributes[j].textContent;
|
|
validDirectives.push({ directive, value, fullName });
|
|
if (directive.name === "on" || directive.name === "model") {
|
|
withHandlers = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (let { directive, value, fullName } of validDirectives) {
|
|
if (directive.atNodeEncounter) {
|
|
const isDone = directive.atNodeEncounter({
|
|
node,
|
|
qweb: this,
|
|
ctx,
|
|
fullName,
|
|
value
|
|
});
|
|
if (isDone) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (node.nodeName !== "t") {
|
|
let nodeID = this._compileGenericNode(node, ctx, withHandlers);
|
|
ctx = ctx.withParent(nodeID);
|
|
let nodeHooks = {};
|
|
let addNodeHook = function(hook, handler) {
|
|
nodeHooks[hook] = nodeHooks[hook] || [];
|
|
nodeHooks[hook].push(handler);
|
|
};
|
|
|
|
for (let { directive, value, fullName } of validDirectives) {
|
|
if (directive.atNodeCreation) {
|
|
directive.atNodeCreation({
|
|
node,
|
|
qweb: this,
|
|
ctx,
|
|
fullName,
|
|
value,
|
|
nodeID,
|
|
addNodeHook
|
|
});
|
|
}
|
|
}
|
|
|
|
if (Object.keys(nodeHooks).length) {
|
|
ctx.addLine(`p${nodeID}.hook = {`);
|
|
for (let hook in nodeHooks) {
|
|
ctx.addLine(` ${hook}: ${NODE_HOOKS_PARAMS[hook]} => {`);
|
|
for (let handler of nodeHooks[hook]) {
|
|
ctx.addLine(` ${handler}`);
|
|
}
|
|
ctx.addLine(` },`);
|
|
}
|
|
ctx.addLine(`};`);
|
|
}
|
|
}
|
|
if (node.nodeName === "pre") {
|
|
ctx = ctx.subContext("inPreTag", true);
|
|
}
|
|
|
|
this._compileChildren(node, ctx);
|
|
|
|
for (let { directive, value, fullName } of validDirectives) {
|
|
if (directive.finalize) {
|
|
directive.finalize({ node, qweb: this, ctx, fullName, value });
|
|
}
|
|
}
|
|
}
|
|
|
|
_compileGenericNode(
|
|
node: ChildNode,
|
|
ctx: Context,
|
|
withHandlers: boolean = true
|
|
): number {
|
|
// nodeType 1 is generic tag
|
|
if (node.nodeType !== 1) {
|
|
throw new Error("unsupported node type");
|
|
}
|
|
const attributes = (<Element>node).attributes;
|
|
const attrs: string[] = [];
|
|
const props: string[] = [];
|
|
const tattrs: number[] = [];
|
|
|
|
function handleBooleanProps(key, val) {
|
|
let isProp = false;
|
|
if (node.nodeName === "input" && key === "checked") {
|
|
let type = (<Element>node).getAttribute("type");
|
|
if (type === "checkbox" || type === "radio") {
|
|
isProp = true;
|
|
}
|
|
}
|
|
if (node.nodeName === "option" && key === "selected") {
|
|
isProp = true;
|
|
}
|
|
if (key === "disabled" && DISABLED_TAGS.indexOf(node.nodeName) > -1) {
|
|
isProp = true;
|
|
}
|
|
if (
|
|
(key === "readonly" && node.nodeName === "input") ||
|
|
node.nodeName === "textarea"
|
|
) {
|
|
isProp = true;
|
|
}
|
|
if (isProp) {
|
|
props.push(`${key}: _${val}`);
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < attributes.length; i++) {
|
|
let name = attributes[i].name;
|
|
const value = attributes[i].textContent!;
|
|
|
|
// regular attributes
|
|
if (
|
|
!name.startsWith("t-") &&
|
|
!(<Element>node).getAttribute("t-attf-" + name)
|
|
) {
|
|
const attID = ctx.generateID();
|
|
ctx.addLine(`var _${attID} = '${value}';`);
|
|
if (!name.match(/^[a-zA-Z]+$/)) {
|
|
// attribute contains 'non letters' => we want to quote it
|
|
name = '"' + name + '"';
|
|
}
|
|
attrs.push(`${name}: _${attID}`);
|
|
handleBooleanProps(name, attID);
|
|
}
|
|
|
|
// dynamic attributes
|
|
if (name.startsWith("t-att-")) {
|
|
let attName = name.slice(6);
|
|
const v = ctx.getValue(value);
|
|
let formattedValue = v.id || ctx.formatExpression(v);
|
|
if (
|
|
formattedValue[0] === "{" &&
|
|
formattedValue[formattedValue.length - 1] === "}"
|
|
) {
|
|
formattedValue = `this.utils.objectToAttrString(${formattedValue})`;
|
|
}
|
|
const attID = ctx.generateID();
|
|
if (!attName.match(/^[a-zA-Z]+$/)) {
|
|
// attribute contains 'non letters' => we want to quote it
|
|
attName = '"' + attName + '"';
|
|
}
|
|
// we need to combine dynamic with non dynamic attributes:
|
|
// class="a" t-att-class="'yop'" should be rendered as class="a yop"
|
|
const attValue = (<Element>node).getAttribute(attName);
|
|
if (attValue) {
|
|
const attValueID = ctx.generateID();
|
|
ctx.addLine(`var _${attValueID} = ${formattedValue};`);
|
|
formattedValue = `'${attValue}' + (_${attValueID} ? ' ' + _${attValueID} : '')`;
|
|
const attrIndex = attrs.findIndex(att =>
|
|
att.startsWith(attName + ":")
|
|
);
|
|
attrs.splice(attrIndex, 1);
|
|
}
|
|
ctx.addLine(`var _${attID} = ${formattedValue};`);
|
|
attrs.push(`${attName}: _${attID}`);
|
|
handleBooleanProps(attName, attID);
|
|
}
|
|
|
|
if (name.startsWith("t-attf-")) {
|
|
let attName = name.slice(7);
|
|
if (!attName.match(/^[a-zA-Z]+$/)) {
|
|
// attribute contains 'non letters' => we want to quote it
|
|
attName = '"' + attName + '"';
|
|
}
|
|
const formattedExpr = ctx.interpolate(value);
|
|
const attID = ctx.generateID();
|
|
let staticVal = (<Element>node).getAttribute(attName);
|
|
if (staticVal) {
|
|
ctx.addLine(`var _${attID} = '${staticVal} ' + ${formattedExpr};`);
|
|
} else {
|
|
ctx.addLine(`var _${attID} = ${formattedExpr};`);
|
|
}
|
|
attrs.push(`${attName}: _${attID}`);
|
|
}
|
|
|
|
// t-att= attributes
|
|
if (name === "t-att") {
|
|
let id = ctx.generateID();
|
|
ctx.addLine(`var _${id} = ${ctx.formatExpression(value!)};`);
|
|
tattrs.push(id);
|
|
}
|
|
}
|
|
let nodeID = ctx.generateID();
|
|
let nodeKey: any = (<Element>node).getAttribute("t-key");
|
|
if (nodeKey) {
|
|
nodeKey = ctx.formatExpression(nodeKey);
|
|
} else {
|
|
nodeKey = nodeID;
|
|
}
|
|
const parts = [`key:${nodeKey}`];
|
|
if (attrs.length + tattrs.length > 0) {
|
|
parts.push(`attrs:{${attrs.join(",")}}`);
|
|
}
|
|
if (props.length > 0) {
|
|
parts.push(`props:{${props.join(",")}}`);
|
|
}
|
|
if (withHandlers) {
|
|
parts.push(`on:{}`);
|
|
}
|
|
|
|
ctx.addLine(`let c${nodeID} = [], p${nodeID} = {${parts.join(",")}};`);
|
|
for (let id of tattrs) {
|
|
ctx.addIf(`_${id} instanceof Array`);
|
|
ctx.addLine(`p${nodeID}.attrs[_${id}[0]] = _${id}[1];`);
|
|
ctx.addElse();
|
|
ctx.addLine(`for (let key in _${id}) {`);
|
|
ctx.indent();
|
|
ctx.addLine(`p${nodeID}.attrs[key] = _${id}[key];`);
|
|
ctx.dedent();
|
|
ctx.addLine(`}`);
|
|
ctx.closeIf();
|
|
}
|
|
ctx.addLine(
|
|
`var vn${nodeID} = h('${node.nodeName}', p${nodeID}, c${nodeID});`
|
|
);
|
|
if (ctx.parentNode) {
|
|
ctx.addLine(`c${ctx.parentNode}.push(vn${nodeID});`);
|
|
}
|
|
|
|
return nodeID;
|
|
}
|
|
|
|
_compileChildren(node: ChildNode, ctx: Context) {
|
|
if (node.childNodes.length > 0) {
|
|
for (let child of Array.from(node.childNodes)) {
|
|
this._compileNode(child, ctx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Compilation Context
|
|
//------------------------------------------------------------------------------
|
|
|
|
export class Context {
|
|
nextID: number = 1;
|
|
code: string[] = [];
|
|
variables: { [key: string]: QWebVar } = {};
|
|
escaping: boolean = false;
|
|
parentNode: number | null = null;
|
|
parentTextNode: number | null = null;
|
|
rootNode: number | null = null;
|
|
indentLevel: number = 0;
|
|
rootContext: Context;
|
|
caller: Element | undefined;
|
|
shouldDefineOwner: boolean = false;
|
|
shouldDefineQWeb: boolean = false;
|
|
shouldDefineUtils: boolean = false;
|
|
shouldProtectContext: boolean = false;
|
|
inLoop: boolean = false;
|
|
inPreTag: boolean = false;
|
|
templateName: string;
|
|
allowMultipleRoots: boolean = false;
|
|
|
|
constructor(name?: string) {
|
|
this.rootContext = this;
|
|
this.templateName = name || "noname";
|
|
this.addLine("var h = this.utils.h;");
|
|
}
|
|
|
|
generateID(): number {
|
|
const id = this.rootContext.nextID++;
|
|
return id;
|
|
}
|
|
|
|
withParent(node: number): Context {
|
|
if (
|
|
!this.allowMultipleRoots &&
|
|
this === this.rootContext &&
|
|
(this.parentNode || this.parentTextNode)
|
|
) {
|
|
throw new Error("A template should not have more than one root node");
|
|
}
|
|
if (!this.rootContext.rootNode) {
|
|
this.rootContext.rootNode = node;
|
|
}
|
|
return this.subContext("parentNode", node);
|
|
}
|
|
|
|
subContext(key: keyof Context, value: any): Context {
|
|
const newContext = Object.create(this);
|
|
newContext[key] = value;
|
|
return newContext;
|
|
}
|
|
|
|
indent() {
|
|
this.indentLevel++;
|
|
}
|
|
|
|
dedent() {
|
|
this.indentLevel--;
|
|
}
|
|
|
|
addLine(line: string) {
|
|
const prefix = new Array(this.indentLevel + 2).join(" ");
|
|
this.code.push(prefix + line);
|
|
}
|
|
|
|
addIf(condition: string) {
|
|
this.addLine(`if (${condition}) {`);
|
|
this.indent();
|
|
}
|
|
|
|
addElse() {
|
|
this.dedent();
|
|
this.addLine("} else {");
|
|
this.indent();
|
|
}
|
|
|
|
closeIf() {
|
|
this.dedent();
|
|
this.addLine("}");
|
|
}
|
|
|
|
getValue(val: any): any {
|
|
return val in this.variables ? this.getValue(this.variables[val]) : val;
|
|
}
|
|
|
|
/**
|
|
* Prepare an expression for being consumed at render time. Its main job
|
|
* is to
|
|
* - replace unknown variables by a lookup in the context
|
|
* - replace already defined variables by their internal name
|
|
*/
|
|
formatExpression(expr: string): string {
|
|
return compileExpr(expr, this.variables);
|
|
}
|
|
|
|
/**
|
|
* Perform string interpolation on the given string. Note that if the whole
|
|
* string is an expression, it simply returns it (formatted and enclosed in
|
|
* parentheses).
|
|
* For instance:
|
|
* 'Hello {{x}}!' -> `Hello ${x}`
|
|
* '{{x ? 'a': 'b'}}' -> (x ? 'a' : 'b')
|
|
*/
|
|
interpolate(s: string): string {
|
|
let matches = s.match(/\{\{.*?\}\}/g);
|
|
if (matches && matches[0].length === s.length) {
|
|
return `(${this.formatExpression(s.slice(2, -2))})`;
|
|
}
|
|
|
|
let r = s.replace(
|
|
/\{\{.*?\}\}/g,
|
|
s => "${" + this.formatExpression(s.slice(2, -2)) + "}"
|
|
);
|
|
return "`" + r + "`";
|
|
}
|
|
}
|