mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[REF] compiler: rename qweb/ into compiler/
This commit is contained in:
committed by
Aaron Bohy
parent
7513b1e507
commit
1700a6fba3
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,356 @@
|
||||
/**
|
||||
* Owl QWeb Expression Parser
|
||||
*
|
||||
* Owl needs in various contexts to be able to understand the structure of a
|
||||
* string representing a javascript expression. The usual goal is to be able
|
||||
* to rewrite some variables. For example, if a template has
|
||||
*
|
||||
* ```xml
|
||||
* <t t-if="computeSomething({val: state.val})">...</t>
|
||||
* ```
|
||||
*
|
||||
* this needs to be translated in something like this:
|
||||
*
|
||||
* ```js
|
||||
* if (context["computeSomething"]({val: context["state"].val})) { ... }
|
||||
* ```
|
||||
*
|
||||
* This file contains the implementation of an extremely naive tokenizer/parser
|
||||
* and evaluator for javascript expressions. The supported grammar is basically
|
||||
* only expressive enough to understand the shape of objects, of arrays, and
|
||||
* various operators.
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Misc types, constants and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const RESERVED_WORDS =
|
||||
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
|
||||
","
|
||||
);
|
||||
|
||||
const WORD_REPLACEMENT: { [key: string]: string } = Object.assign(Object.create(null), {
|
||||
and: "&&",
|
||||
or: "||",
|
||||
gt: ">",
|
||||
gte: ">=",
|
||||
lt: "<",
|
||||
lte: "<=",
|
||||
});
|
||||
|
||||
export interface QWebVar {
|
||||
id: string; // foo
|
||||
expr: string; // scope.foo (local variables => only foo)
|
||||
value?: string; // 1 + 3
|
||||
hasBody?: boolean;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Tokenizer
|
||||
//------------------------------------------------------------------------------
|
||||
type TKind =
|
||||
| "LEFT_BRACE"
|
||||
| "RIGHT_BRACE"
|
||||
| "LEFT_BRACKET"
|
||||
| "RIGHT_BRACKET"
|
||||
| "LEFT_PAREN"
|
||||
| "RIGHT_PAREN"
|
||||
| "COMMA"
|
||||
| "VALUE"
|
||||
| "TEMPLATE_STRING"
|
||||
| "SYMBOL"
|
||||
| "OPERATOR"
|
||||
| "COLON";
|
||||
|
||||
interface Token {
|
||||
type: TKind;
|
||||
value: string;
|
||||
originalValue?: string;
|
||||
size?: number;
|
||||
varName?: string;
|
||||
replace?: Function;
|
||||
isLocal?: boolean;
|
||||
}
|
||||
|
||||
const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(null), {
|
||||
"{": "LEFT_BRACE",
|
||||
"}": "RIGHT_BRACE",
|
||||
"[": "LEFT_BRACKET",
|
||||
"]": "RIGHT_BRACKET",
|
||||
":": "COLON",
|
||||
",": "COMMA",
|
||||
"(": "LEFT_PAREN",
|
||||
")": "RIGHT_PAREN",
|
||||
});
|
||||
|
||||
// note that the space after typeof is relevant. It makes sure that the formatted
|
||||
// expression has a space after typeof
|
||||
const OPERATORS = "...,.,===,==,+,!==,!=,!,||,&&,>=,>,<=,<,?,-,*,/,%,typeof ,=>,=,;,in ".split(",");
|
||||
|
||||
type Tokenizer = (expr: string) => Token | false;
|
||||
|
||||
let tokenizeString: Tokenizer = function (expr) {
|
||||
let s = expr[0];
|
||||
let start = s;
|
||||
if (s !== "'" && s !== '"' && s !== "`") {
|
||||
return false;
|
||||
}
|
||||
let i = 1;
|
||||
let cur;
|
||||
while (expr[i] && expr[i] !== start) {
|
||||
cur = expr[i];
|
||||
s += cur;
|
||||
if (cur === "\\") {
|
||||
i++;
|
||||
cur = expr[i];
|
||||
if (!cur) {
|
||||
throw new Error("Invalid expression");
|
||||
}
|
||||
s += cur;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (expr[i] !== start) {
|
||||
throw new Error("Invalid expression");
|
||||
}
|
||||
s += start;
|
||||
if (start === "`") {
|
||||
return {
|
||||
type: "TEMPLATE_STRING",
|
||||
value: s,
|
||||
replace(replacer: any) {
|
||||
return s.replace(/\$\{(.*?)\}/g, (match, group) => {
|
||||
return "${" + replacer(group) + "}";
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
return { type: "VALUE", value: s };
|
||||
};
|
||||
|
||||
let tokenizeNumber: Tokenizer = function (expr) {
|
||||
let s = expr[0];
|
||||
if (s && s.match(/[0-9]/)) {
|
||||
let i = 1;
|
||||
while (expr[i] && expr[i].match(/[0-9]|\./)) {
|
||||
s += expr[i];
|
||||
i++;
|
||||
}
|
||||
return { type: "VALUE", value: s };
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let tokenizeSymbol: Tokenizer = function (expr) {
|
||||
let s = expr[0];
|
||||
if (s && s.match(/[a-zA-Z_\$]/)) {
|
||||
let i = 1;
|
||||
while (expr[i] && expr[i].match(/\w/)) {
|
||||
s += expr[i];
|
||||
i++;
|
||||
}
|
||||
if (s in WORD_REPLACEMENT) {
|
||||
return { type: "OPERATOR", value: WORD_REPLACEMENT[s], size: s.length };
|
||||
}
|
||||
return { type: "SYMBOL", value: s };
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const tokenizeStatic: Tokenizer = function (expr) {
|
||||
const char = expr[0];
|
||||
if (char && char in STATIC_TOKEN_MAP) {
|
||||
return { type: STATIC_TOKEN_MAP[char], value: char };
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const tokenizeOperator: Tokenizer = function (expr) {
|
||||
for (let op of OPERATORS) {
|
||||
if (expr.startsWith(op)) {
|
||||
return { type: "OPERATOR", value: op };
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const TOKENIZERS = [
|
||||
tokenizeString,
|
||||
tokenizeNumber,
|
||||
tokenizeOperator,
|
||||
tokenizeSymbol,
|
||||
tokenizeStatic,
|
||||
];
|
||||
|
||||
/**
|
||||
* Convert a javascript expression (as a string) into a list of tokens. For
|
||||
* example: `tokenize("1 + b")` will return:
|
||||
* ```js
|
||||
* [
|
||||
* {type: "VALUE", value: "1"},
|
||||
* {type: "OPERATOR", value: "+"},
|
||||
* {type: "SYMBOL", value: "b"}
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
export function tokenize(expr: string): Token[] {
|
||||
const result: Token[] = [];
|
||||
let token: boolean | Token = true;
|
||||
|
||||
while (token) {
|
||||
expr = expr.trim();
|
||||
if (expr) {
|
||||
for (let tokenizer of TOKENIZERS) {
|
||||
token = tokenizer(expr);
|
||||
if (token) {
|
||||
result.push(token);
|
||||
expr = expr.slice(token.size || token.value.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
token = false;
|
||||
}
|
||||
}
|
||||
if (expr.length) {
|
||||
throw new Error(`Tokenizer error: could not tokenize "${expr}"`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Expression "evaluator"
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const isLeftSeparator = (token: Token) =>
|
||||
token && (token.type === "LEFT_BRACE" || token.type === "COMMA");
|
||||
const isRightSeparator = (token: Token) =>
|
||||
token && (token.type === "RIGHT_BRACE" || token.type === "COMMA");
|
||||
|
||||
/**
|
||||
* This is the main function exported by this file. This is the code that will
|
||||
* process an expression (given as a string) and returns another expression with
|
||||
* proper lookups in the context.
|
||||
*
|
||||
* Usually, this kind of code would be very simple to do if we had an AST (so,
|
||||
* if we had a javascript parser), since then, we would only need to find the
|
||||
* variables and replace them. However, a parser is more complicated, and there
|
||||
* are no standard builtin parser API.
|
||||
*
|
||||
* Since this method is applied to simple javasript expressions, and the work to
|
||||
* be done is actually quite simple, we actually can get away with not using a
|
||||
* parser, which helps with the code size.
|
||||
*
|
||||
* Here is the heuristic used by this method to determine if a token is a
|
||||
* variable:
|
||||
* - by default, all symbols are considered a variable
|
||||
* - unless the previous token is a dot (in that case, this is a property: `a.b`)
|
||||
* - or if the previous token is a left brace or a comma, and the next token is
|
||||
* a colon (in that case, this is an object key: `{a: b}`)
|
||||
*
|
||||
* Some specific code is also required to support arrow functions. If we detect
|
||||
* the arrow operator, then we add the current (or some previous tokens) token to
|
||||
* the list of variables so it does not get replaced by a lookup in the context
|
||||
*/
|
||||
export function compileExprToArray(expr: string): Token[] {
|
||||
const localVars = new Set<string>();
|
||||
const tokens = tokenize(expr);
|
||||
let i = 0;
|
||||
let stack = []; // to track last opening [ or {
|
||||
|
||||
while (i < tokens.length) {
|
||||
let token = tokens[i];
|
||||
let prevToken = tokens[i - 1];
|
||||
let nextToken = tokens[i + 1];
|
||||
let groupType = stack[stack.length - 1];
|
||||
|
||||
switch (token.type) {
|
||||
case "LEFT_BRACE":
|
||||
case "LEFT_BRACKET":
|
||||
stack.push(token.type);
|
||||
break;
|
||||
case "RIGHT_BRACE":
|
||||
case "RIGHT_BRACKET":
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
let isVar = token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value);
|
||||
if (token.type === "SYMBOL" && !RESERVED_WORDS.includes(token.value)) {
|
||||
if (prevToken) {
|
||||
// normalize missing tokens: {a} should be equivalent to {a:a}
|
||||
if (
|
||||
groupType === "LEFT_BRACE" &&
|
||||
isLeftSeparator(prevToken) &&
|
||||
isRightSeparator(nextToken)
|
||||
) {
|
||||
tokens.splice(i + 1, 0, { type: "COLON", value: ":" }, { ...token });
|
||||
nextToken = tokens[i + 1];
|
||||
}
|
||||
|
||||
if (prevToken.type === "OPERATOR" && prevToken.value === ".") {
|
||||
isVar = false;
|
||||
} else if (prevToken.type === "LEFT_BRACE" || prevToken.type === "COMMA") {
|
||||
if (nextToken && nextToken.type === "COLON") {
|
||||
isVar = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (token.type === "TEMPLATE_STRING") {
|
||||
token.value = token.replace!((expr: any) => compileExpr(expr));
|
||||
}
|
||||
if (nextToken && nextToken.type === "OPERATOR" && nextToken.value === "=>") {
|
||||
if (token.type === "RIGHT_PAREN") {
|
||||
let j = i - 1;
|
||||
while (j > 0 && tokens[j].type !== "LEFT_PAREN") {
|
||||
if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) {
|
||||
tokens[j].value = tokens[j].originalValue!;
|
||||
localVars.add(tokens[j].value); //] = { id: tokens[j].value, expr: tokens[j].value };
|
||||
}
|
||||
j--;
|
||||
}
|
||||
} else {
|
||||
localVars.add(token.value); //] = { id: token.value, expr: token.value };
|
||||
}
|
||||
}
|
||||
|
||||
if (isVar) {
|
||||
token.varName = token.value;
|
||||
if (!localVars.has(token.value)) {
|
||||
token.originalValue = token.value;
|
||||
token.value = `ctx['${token.value}']`;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
// Mark all variables that have been used locally.
|
||||
// This assumes the expression has only one scope (incorrect but "good enough for now")
|
||||
for (const token of tokens) {
|
||||
if (token.type === "SYMBOL" && localVars.has(token.value)) {
|
||||
token.isLocal = true;
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export function compileExpr(expr: string): string {
|
||||
return compileExprToArray(expr)
|
||||
.map((t) => t.value)
|
||||
.join("");
|
||||
}
|
||||
|
||||
export const INTERP_REGEXP = /\{\{.*?\}\}/g;
|
||||
const INTERP_GROUP_REGEXP = /\{\{.*?\}\}/g;
|
||||
|
||||
export function interpolate(s: string): string {
|
||||
let matches = s.match(INTERP_REGEXP);
|
||||
if (matches && matches[0].length === s.length) {
|
||||
return `(${compileExpr(s.slice(2, -2))})`;
|
||||
}
|
||||
|
||||
let r = s.replace(INTERP_GROUP_REGEXP, (s) => "${" + compileExpr(s.slice(2, -2)) + "}");
|
||||
return "`" + r + "`";
|
||||
}
|
||||
@@ -0,0 +1,883 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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 = `<t>${xml}</t>`;
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// <t /> 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 <input>, <textarea> and <select>");
|
||||
}
|
||||
|
||||
let baseExpr, expr;
|
||||
if (hasDotAtTheEnd.test(value)) {
|
||||
const index = value.lastIndexOf(".");
|
||||
baseExpr = value.slice(0, index);
|
||||
expr = `'${value.slice(index + 1)}'`;
|
||||
} else if (hasBracketsAtTheEnd.test(value)) {
|
||||
const index = value.lastIndexOf("[");
|
||||
baseExpr = value.slice(0, index);
|
||||
expr = value.slice(index + 1, -1);
|
||||
} else {
|
||||
throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
|
||||
}
|
||||
|
||||
const typeAttr = node.getAttribute("type");
|
||||
const isInput = tagName === "input";
|
||||
const isSelect = tagName === "select";
|
||||
const isTextarea = tagName === "textarea";
|
||||
const isCheckboxInput = isInput && typeAttr === "checkbox";
|
||||
const isRadioInput = isInput && typeAttr === "radio";
|
||||
const isOtherInput = isInput && !isCheckboxInput && !isRadioInput;
|
||||
const hasLazyMod = attr.includes(".lazy");
|
||||
const hasNumberMod = attr.includes(".number");
|
||||
const hasTrimMod = attr.includes(".trim");
|
||||
const eventType = isRadioInput ? "click" : isSelect || hasLazyMod ? "change" : "input";
|
||||
|
||||
model = {
|
||||
baseExpr,
|
||||
expr,
|
||||
targetAttr: isCheckboxInput ? "checked" : "value",
|
||||
specialInitTargetAttr: isRadioInput ? "checked" : null,
|
||||
eventType,
|
||||
shouldTrim: hasTrimMod && (isOtherInput || isTextarea),
|
||||
shouldNumberize: hasNumberMod && (isOtherInput || isTextarea),
|
||||
};
|
||||
} else {
|
||||
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||||
throw new Error(`Unknown QWeb directive: '${attr}'`);
|
||||
}
|
||||
attrs[attr] = value;
|
||||
}
|
||||
}
|
||||
if (children.length === 1 && children[0].type === ASTType.TForEach) {
|
||||
children[0].isOnlyChild = true;
|
||||
}
|
||||
return {
|
||||
type: ASTType.DomNode,
|
||||
tag: tagName,
|
||||
dynamicTag,
|
||||
attrs,
|
||||
on,
|
||||
ref,
|
||||
content: children,
|
||||
model,
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-esc
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTEscNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-esc")) {
|
||||
return null;
|
||||
}
|
||||
const escValue = node.getAttribute("t-esc")!;
|
||||
node.removeAttribute("t-esc");
|
||||
const tesc: AST = {
|
||||
type: ASTType.TEsc,
|
||||
expr: escValue,
|
||||
defaultValue: node.textContent || "",
|
||||
};
|
||||
let ref = node.getAttribute("t-ref");
|
||||
node.removeAttribute("t-ref");
|
||||
const ast = parseNode(node, ctx);
|
||||
if (!ast) {
|
||||
return tesc;
|
||||
}
|
||||
if (ast && ast.type === ASTType.DomNode) {
|
||||
return {
|
||||
...ast,
|
||||
ref,
|
||||
content: [tesc],
|
||||
};
|
||||
}
|
||||
if (ast && ast.type === ASTType.TComponent) {
|
||||
return {
|
||||
...ast,
|
||||
slots: { default: tesc },
|
||||
};
|
||||
}
|
||||
return tesc;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-raw
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTRawNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-raw")) {
|
||||
return null;
|
||||
}
|
||||
const expr = node.getAttribute("t-raw")!;
|
||||
node.removeAttribute("t-raw");
|
||||
|
||||
const tRaw: AST = { type: ASTType.TRaw, expr, body: null };
|
||||
const ref = node.getAttribute("t-ref");
|
||||
node.removeAttribute("t-ref");
|
||||
const ast = parseNode(node, ctx);
|
||||
if (!ast) {
|
||||
return tRaw;
|
||||
}
|
||||
if (ast && ast.type === ASTType.DomNode) {
|
||||
tRaw.body = ast.content.length ? ast.content : null;
|
||||
return {
|
||||
...ast,
|
||||
ref,
|
||||
content: [tRaw],
|
||||
};
|
||||
}
|
||||
|
||||
return tRaw;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-foreach and t-key
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-foreach")) {
|
||||
return null;
|
||||
}
|
||||
const html = node.outerHTML;
|
||||
const collection = node.getAttribute("t-foreach")!;
|
||||
node.removeAttribute("t-foreach");
|
||||
const elem = node.getAttribute("t-as") || "";
|
||||
node.removeAttribute("t-as");
|
||||
const key = node.getAttribute("t-key");
|
||||
if (!key) {
|
||||
throw new Error(
|
||||
`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`
|
||||
);
|
||||
}
|
||||
node.removeAttribute("t-key");
|
||||
const memo = node.getAttribute("t-memo") || "";
|
||||
node.removeAttribute("t-memo");
|
||||
const body = parseNode(node, ctx);
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasNoTCall = !html.includes("t-call");
|
||||
const hasNoFirst = hasNoTCall && !html.includes(`${elem}_first`);
|
||||
const hasNoLast = hasNoTCall && !html.includes(`${elem}_last`);
|
||||
const hasNoIndex = hasNoTCall && !html.includes(`${elem}_index`);
|
||||
const hasNoValue = hasNoTCall && !html.includes(`${elem}_value`);
|
||||
|
||||
return {
|
||||
type: ASTType.TForEach,
|
||||
collection,
|
||||
elem,
|
||||
body,
|
||||
memo,
|
||||
key,
|
||||
isOnlyChild: false,
|
||||
hasNoComponent: hasNoComponent(body),
|
||||
hasNoFirst,
|
||||
hasNoLast,
|
||||
hasNoIndex,
|
||||
hasNoValue,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns true if we are sure the ast does not contain any component
|
||||
*/
|
||||
function hasNoComponent(ast: AST): boolean {
|
||||
switch (ast.type) {
|
||||
case ASTType.TComponent:
|
||||
case ASTType.TRaw:
|
||||
case ASTType.TCall:
|
||||
case ASTType.TCallBlock:
|
||||
case ASTType.TSlot:
|
||||
return false;
|
||||
case ASTType.TSet:
|
||||
case ASTType.Text:
|
||||
case ASTType.Comment:
|
||||
case ASTType.TEsc:
|
||||
return true;
|
||||
case ASTType.TKey:
|
||||
return hasNoComponent(ast.content);
|
||||
case ASTType.TDebug:
|
||||
case ASTType.TLog:
|
||||
case ASTType.TTranslation:
|
||||
return ast.content ? hasNoComponent(ast.content) : true;
|
||||
case ASTType.TForEach:
|
||||
return ast.hasNoComponent;
|
||||
case ASTType.Multi:
|
||||
case ASTType.DomNode: {
|
||||
for (let elem of ast.content) {
|
||||
if (!hasNoComponent(elem)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case ASTType.TIf: {
|
||||
if (!hasNoComponent(ast.content)) {
|
||||
return false;
|
||||
}
|
||||
if (ast.tElif) {
|
||||
for (let elem of ast.tElif) {
|
||||
if (!hasNoComponent(elem.content)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ast.tElse && !hasNoComponent(ast.tElse)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseTKey(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-key")) {
|
||||
return null;
|
||||
}
|
||||
const key = node.getAttribute("t-key")!;
|
||||
node.removeAttribute("t-key");
|
||||
const body = parseNode(node, ctx);
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
return { type: ASTType.TKey, expr: key, content: body };
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-call
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTCall(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-call")) {
|
||||
return null;
|
||||
}
|
||||
const subTemplate = node.getAttribute("t-call")!;
|
||||
|
||||
node.removeAttribute("t-call");
|
||||
if (node.tagName !== "t") {
|
||||
const ast = parseNode(node, ctx);
|
||||
const tcall: AST = { type: ASTType.TCall, name: subTemplate, body: null };
|
||||
if (ast && ast.type === ASTType.DomNode) {
|
||||
ast.content = [tcall];
|
||||
return ast;
|
||||
}
|
||||
if (ast && ast.type === ASTType.TComponent) {
|
||||
return {
|
||||
...ast,
|
||||
slots: { default: tcall },
|
||||
};
|
||||
}
|
||||
}
|
||||
const body: AST[] = [];
|
||||
for (let child of node.childNodes) {
|
||||
const ast = parseNode(child, ctx);
|
||||
if (ast) {
|
||||
body.push(ast);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: ASTType.TCall,
|
||||
name: subTemplate,
|
||||
body: body.length ? body : null,
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-call-block
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTCallBlock(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-call-block")) {
|
||||
return null;
|
||||
}
|
||||
const name = node.getAttribute("t-call-block")!;
|
||||
return {
|
||||
type: ASTType.TCallBlock,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-if
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTIf(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-if")) {
|
||||
return null;
|
||||
}
|
||||
const condition = node.getAttribute("t-if")!;
|
||||
node.removeAttribute("t-if");
|
||||
const content = parseNode(node, ctx);
|
||||
if (!content) {
|
||||
throw new Error("hmmm");
|
||||
}
|
||||
|
||||
let nextElement = node.nextElementSibling;
|
||||
// t-elifs
|
||||
const tElifs: any[] = [];
|
||||
while (nextElement && nextElement.hasAttribute("t-elif")) {
|
||||
const condition = nextElement.getAttribute("t-elif");
|
||||
nextElement.removeAttribute("t-elif");
|
||||
const tElif = parseNode(nextElement, ctx);
|
||||
const next = nextElement.nextElementSibling;
|
||||
nextElement.remove();
|
||||
nextElement = next;
|
||||
if (tElif) {
|
||||
tElifs.push({ condition, content: tElif });
|
||||
}
|
||||
}
|
||||
|
||||
// t-else
|
||||
let tElse: AST | null = null;
|
||||
if (nextElement && nextElement.hasAttribute("t-else")) {
|
||||
nextElement.removeAttribute("t-else");
|
||||
tElse = parseNode(nextElement, ctx);
|
||||
nextElement.remove();
|
||||
}
|
||||
|
||||
return {
|
||||
type: ASTType.TIf,
|
||||
condition,
|
||||
content,
|
||||
tElif: tElifs.length ? tElifs : null,
|
||||
tElse,
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// t-set directive
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTSetNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-set")) {
|
||||
return null;
|
||||
}
|
||||
const name = node.getAttribute("t-set")!;
|
||||
const value = node.getAttribute("t-value") || null;
|
||||
const defaultValue = node.innerHTML === node.textContent ? node.textContent || null : null;
|
||||
let body: AST[] | null = null;
|
||||
if (node.textContent !== node.innerHTML) {
|
||||
body = [];
|
||||
for (let child of node.childNodes) {
|
||||
let childAst = parseNode(child, ctx);
|
||||
if (childAst) {
|
||||
body.push(childAst);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { type: ASTType.TSet, name, value, defaultValue, body };
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Components
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
let name = node.tagName;
|
||||
const firstLetter = name[0];
|
||||
let isDynamic = node.hasAttribute("t-component");
|
||||
|
||||
if (isDynamic && name !== "t") {
|
||||
throw new Error(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);
|
||||
}
|
||||
|
||||
if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
|
||||
return null;
|
||||
}
|
||||
if (isDynamic) {
|
||||
name = node.getAttribute("t-component")!;
|
||||
node.removeAttribute("t-component");
|
||||
}
|
||||
|
||||
const dynamicProps = node.getAttribute("t-props");
|
||||
node.removeAttribute("t-props");
|
||||
|
||||
const props: ASTComponent["props"] = {};
|
||||
for (let name of node.getAttributeNames()) {
|
||||
const value = node.getAttribute(name)!;
|
||||
if (name.startsWith("t-on-")) {
|
||||
throw new Error(
|
||||
"t-on is no longer supported on Component node. Consider passing a callback in props."
|
||||
);
|
||||
} else {
|
||||
props[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const slots: ASTComponent["slots"] = {};
|
||||
if (node.hasChildNodes()) {
|
||||
const clone = <Element>node.cloneNode(true);
|
||||
|
||||
// named slots
|
||||
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
|
||||
for (let slotNode of slotNodes) {
|
||||
const name = slotNode.getAttribute("t-set-slot")!;
|
||||
|
||||
// check if this is defined in a sub component (in which case it should
|
||||
// be ignored)
|
||||
let el = slotNode.parentElement!;
|
||||
let isInSubComponent = false;
|
||||
while (el !== clone) {
|
||||
if (el!.hasAttribute("t-component") || el!.tagName[0] === el!.tagName[0].toUpperCase()) {
|
||||
isInSubComponent = true;
|
||||
break;
|
||||
}
|
||||
el = el.parentElement!;
|
||||
}
|
||||
if (isInSubComponent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
slotNode.removeAttribute("t-set-slot");
|
||||
slotNode.remove();
|
||||
const slotAst = parseNode(slotNode, ctx);
|
||||
if (slotAst) {
|
||||
slots[name] = slotAst;
|
||||
}
|
||||
}
|
||||
|
||||
// default slot
|
||||
const defaultContent = parseChildNodes(clone, ctx);
|
||||
if (defaultContent) {
|
||||
slots.default = defaultContent;
|
||||
}
|
||||
}
|
||||
return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots };
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Slots
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (!node.hasAttribute("t-slot")) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: ASTType.TSlot,
|
||||
name: node.getAttribute("t-slot")!,
|
||||
defaultContent: parseChildNodes(node, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (node.getAttribute("t-translation") !== "off") {
|
||||
return null;
|
||||
}
|
||||
node.removeAttribute("t-translation");
|
||||
return {
|
||||
type: ASTType.TTranslation,
|
||||
content: parseNode(node, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseChildNodes(node: Element, ctx: ParsingContext): AST | null {
|
||||
const children: AST[] = [];
|
||||
for (let child of node.childNodes) {
|
||||
const childAst = parseNode(child, ctx);
|
||||
if (childAst) {
|
||||
children.push(childAst);
|
||||
}
|
||||
}
|
||||
switch (children.length) {
|
||||
case 0:
|
||||
return null;
|
||||
case 1:
|
||||
return children[0];
|
||||
default:
|
||||
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]");
|
||||
for (let i = 0, ilen = tbranch.length; i < ilen; i++) {
|
||||
let node = tbranch[i];
|
||||
let prevElem = node.previousElementSibling!;
|
||||
let pattr = (name: string) => prevElem.getAttribute(name);
|
||||
let nattr = (name: string) => +!!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 (with only spaces) and comment nodes (nodeType 8) between
|
||||
// branch nodes are removed
|
||||
let textNode;
|
||||
while ((textNode = node.previousSibling) !== prevElem) {
|
||||
if (textNode!.nodeValue!.trim().length && textNode!.nodeType !== 8) {
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import { BDom, createBlock, html, list, multi, text, toggler } from "../blockdom";
|
||||
import { compileTemplate, Template } from "./code_generator";
|
||||
import { component } from "../component/component_node";
|
||||
import { validateProps } from "../component/props_validation";
|
||||
|
||||
const bdom = { text, createBlock, list, multi, html, toggler, component };
|
||||
|
||||
export const globalTemplates: { [key: string]: string } = {};
|
||||
|
||||
function withDefault(value: any, defaultValue: any): any {
|
||||
return value === undefined || value === null || value === false ? defaultValue : value;
|
||||
}
|
||||
|
||||
function callSlot(
|
||||
ctx: any,
|
||||
parent: any,
|
||||
key: string,
|
||||
name: string,
|
||||
defaultSlot?: (ctx: any, key: string) => BDom,
|
||||
dynamic?: boolean
|
||||
): BDom {
|
||||
const slots = ctx.__owl__.slots;
|
||||
const slotFn = slots[name];
|
||||
const slotBDom = slotFn ? slotFn(parent, key) : null;
|
||||
if (defaultSlot) {
|
||||
let child1: BDom | undefined = undefined;
|
||||
let child2: BDom | undefined = undefined;
|
||||
// const result = new BMulti(2);
|
||||
if (slotBDom) {
|
||||
child1 = dynamic ? toggler(name, slotBDom) : slotBDom;
|
||||
} else {
|
||||
child2 = defaultSlot(parent, key);
|
||||
}
|
||||
return multi([child1, child2]);
|
||||
}
|
||||
return slotBDom || text("");
|
||||
}
|
||||
|
||||
function capture(ctx: any): any {
|
||||
const component = ctx.__owl__.component;
|
||||
const result = Object.create(component);
|
||||
for (let k in ctx) {
|
||||
result[k] = ctx[k];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function withKey(elem: any, k: string) {
|
||||
elem.key = k;
|
||||
return elem;
|
||||
}
|
||||
|
||||
function prepareList(collection: any): [any[], any[], number, any[]] {
|
||||
let keys: any[];
|
||||
let values: any[];
|
||||
|
||||
if (Array.isArray(collection)) {
|
||||
keys = collection;
|
||||
values = collection;
|
||||
} else if (collection) {
|
||||
values = Object.keys(collection);
|
||||
keys = Object.values(collection);
|
||||
} else {
|
||||
throw new Error("Invalid loop expression");
|
||||
}
|
||||
const n = values.length;
|
||||
return [keys, values, n, new Array(n)];
|
||||
}
|
||||
|
||||
const isBoundary = Symbol("isBoundary");
|
||||
|
||||
function setContextValue(ctx: { [key: string]: any }, key: string, value: any): void {
|
||||
const ctx0 = ctx;
|
||||
while (!ctx.hasOwnProperty(key) && !ctx.hasOwnProperty(isBoundary)) {
|
||||
const newCtx = ctx.__proto__;
|
||||
if (!newCtx) {
|
||||
ctx = ctx0;
|
||||
break;
|
||||
}
|
||||
ctx = newCtx;
|
||||
}
|
||||
ctx[key] = value;
|
||||
}
|
||||
|
||||
export const UTILS = {
|
||||
// elem,
|
||||
// setText,
|
||||
withDefault,
|
||||
zero: Symbol("zero"),
|
||||
isBoundary,
|
||||
callSlot,
|
||||
capture,
|
||||
// toClassObj,
|
||||
withKey,
|
||||
prepareList,
|
||||
setContextValue,
|
||||
shallowEqual,
|
||||
toNumber,
|
||||
validateProps,
|
||||
};
|
||||
|
||||
export class TemplateSet {
|
||||
rawTemplates: { [name: string]: string } = Object.create(globalTemplates);
|
||||
templates: { [name: string]: Template } = {};
|
||||
translateFn?: (s: string) => string;
|
||||
translatableAttributes?: string[];
|
||||
utils: typeof UTILS;
|
||||
dev?: boolean;
|
||||
|
||||
constructor() {
|
||||
const call = (subTemplate: string, ctx: any, parent: any) => {
|
||||
const template = this.getTemplate(subTemplate);
|
||||
return toggler(subTemplate, template(ctx, parent));
|
||||
};
|
||||
|
||||
const getTemplate = (name: string) => this.getTemplate(name);
|
||||
this.utils = Object.assign({}, UTILS, { getTemplate, call });
|
||||
}
|
||||
|
||||
addTemplate(name: string, template: string, options: { allowDuplicate?: boolean } = {}) {
|
||||
if (name in this.rawTemplates && !options.allowDuplicate) {
|
||||
throw new Error(`Template ${name} already defined`);
|
||||
}
|
||||
this.rawTemplates[name] = template;
|
||||
}
|
||||
|
||||
getTemplate(name: string): Template {
|
||||
if (!(name in this.templates)) {
|
||||
const rawTemplate = this.rawTemplates[name];
|
||||
if (rawTemplate === undefined) {
|
||||
throw new Error(`Missing template: "${name}"`);
|
||||
}
|
||||
const templateFn = compileTemplate(rawTemplate, {
|
||||
name,
|
||||
dev: this.dev,
|
||||
translateFn: this.translateFn,
|
||||
translatableAttributes: this.translatableAttributes,
|
||||
});
|
||||
|
||||
// first add a function to lazily get the template, in case there is a
|
||||
// recursive call to the template name
|
||||
this.templates[name] = (context, parent) => this.templates[name](context, parent);
|
||||
const template = templateFn(bdom, this.utils);
|
||||
this.templates[name] = template;
|
||||
}
|
||||
return this.templates[name];
|
||||
}
|
||||
}
|
||||
|
||||
function toNumber(val: string): number | string {
|
||||
const n = parseFloat(val);
|
||||
return isNaN(n) ? val : n;
|
||||
}
|
||||
|
||||
function shallowEqual(l1: any[], l2: any[]): boolean {
|
||||
for (let i = 0, l = l1.length; i < l; i++) {
|
||||
if (l1[i] !== l2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user