# HG changeset patch # User cin # Date 2018-12-28 23:00:59 # Node ID 224ffacdbef2aeeec087ffca2679ff946a146a0e # Parent 4eaaaa5c1cf3adc6e0db8952179734488c405498 text/template-compile ported to typescript diff --git a/src/amd/js/text/template-compile.js b/src/amd/js/text/template-compile.js deleted file mode 100644 --- a/src/amd/js/text/template-compile.js +++ /dev/null @@ -1,134 +0,0 @@ -define( - ["dojo/request", "./format", "../log/trace!"], - function (request, format, trace) { - - // разбивает строку шаблона на токены, возвращает контекст для - // дальнейшей обработки в visitTemplate - var parseTemplate = function (str) { - var tokens = str.split(/(<%=|\[%=|<%|\[%|%\]|%>)/); - var pos = -1; - var data = [], - code = []; - - return { - next: function () { - pos++; - return pos < tokens.length; - }, - token: function () { - return tokens[pos]; - }, - pushData: function () { - var i = data.length; - data.push.apply(data, arguments); - return i; - }, - pushCode : function() { - var i = code.length; - code.push.apply(code, arguments); - return i; - }, - compile: function () { - var text = "var $p = [];\n" + - "var print = function(){\n" + - " $p.push(format.apply(null,arguments));\n" + - "};\n" + - // Introduce the data as local variables using with(){} - "with(obj){\n" + - code.join("\n") + - "}\n" + - "return $p.join('');"; - - try { - var compiled = new Function("obj, format, $data", text); - /** - * Функция форматирования по шаблону - * - * @type{Function} - * @param{Object} obj объект с параметрами для подстановки - */ - return function (obj) { - return compiled(obj || {}, format, data); - }; - } catch (e) { - trace.error([e]); - trace.log([text, data]); - throw e; - } - } - } - }; - - function visitTemplate(context) { - while (context.next()) { - switch (context.token()) { - case "<%": - case "[%": - visitCode(context); - break; - case "<%=": - case "[%=": - visitInline(context); - break; - default: - visitTextFragment(context); - break; - } - } - } - - function visitInline(context) { - var code = ["$p.push("]; - while (context.next()) { - if (context.token() == "%>" || context.token() == "%]") - break; - code.push(context.token()); - } - code.push(");"); - context.pushCode(code.join('')); - } - - function visitCode(context) { - var code = []; - while (context.next()) { - if (context.token() == "%>" || context.token() == "%]") - break; - code.push(context.token()); - } - context.pushCode(code.join('')); - } - - function visitTextFragment(context) { - var i = context.pushData(context.token()); - context.pushCode("$p.push($data["+i+"]);"); - } - - var compile = function (str) { - if (!str) - return function() { return "";}; - - var ctx = parseTemplate(str); - visitTemplate(ctx); - return ctx.compile(); - }; - - var cache = {}; - - compile.load = function (id, require, callback) { - var url = require.toUrl(id); - if (url in cache) { - callback(cache[url]); - } else { - request(url).then(compile).then(function (tc) { - callback(cache[url] = tc); - }, function (err) { - require.signal("error", [{ - error: err, - src: 'implab/text/template-compile' - }]); - }); - } - }; - - return compile; - }); \ No newline at end of file diff --git a/src/amd/ts/text/template-compile.ts b/src/amd/ts/text/template-compile.ts new file mode 100644 --- /dev/null +++ b/src/amd/ts/text/template-compile.ts @@ -0,0 +1,49 @@ +import request = require("dojo/request"); +import m = require("module"); +import { TraceSource } from "../log/TraceSource"; +import { TemplateCompiler } from "./TemplateCompiler"; +import { TemplateParser } from "./TemplateParser"; +import { isNullOrEmptyString } from "../safe"; +import { MapOf } from "../interfaces"; + +type TemplateFn = (obj: object) => string; + +const trace = TraceSource.get(m.id); + +function compile(str: string) { + if (isNullOrEmptyString(str)) + return () => ""; + + const parser = new TemplateParser(str); + const compiler = new TemplateCompiler(); + + return compiler.compile(parser); +} + +const cache: MapOf = {}; + +interface OnLoadFn { + (res: T): void; + error(e: any): void; +} + +compile.load = (id: string, require: Require, callback: OnLoadFn) => { + const url = require.toUrl(id); + if (url in cache) { + trace.debug("{0} -> {1}: cached", id, url); + callback(cache[url]); + } else { + trace.debug("{0} -> {1}: load", id, url); + request(url).then(compile).then((tc: TemplateFn) => { + trace.debug("{0}: compiled", url); + callback(cache[url] = tc); + }, (err: any) => { + callback.error({ + inner: err, + src: "@implab/core/text/template-compile" + }); + }); + } +}; + +export = compile; diff --git a/src/main/ts/text/TemplateCompiler.ts b/src/main/ts/text/TemplateCompiler.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/text/TemplateCompiler.ts @@ -0,0 +1,102 @@ +import { format } from "./StringFormat"; +import { TraceSource, DebugLevel } from "../log/TraceSource"; +import { ITemplateParser, TokenType } from "./TemplateParser"; + +const trace = TraceSource.get("@implab/text/TemplateCompiler"); + +type TemplateFn = (obj: object) => string; + +export class TemplateCompiler { + + _data: string[]; + _code: string[]; + _wrapWith = true; + + constructor() { + this._code = []; + this._data = []; + } + + compile(parser: ITemplateParser): TemplateFn { + this.preamble(); + this.visitTemplate(parser); + this.postamble(); + + const text = this._code.join("\n"); + + try { + const compiled = new Function("obj, format, $data", text); + /** + * Функция форматирования по шаблону + * + * @type{Function} + * @param{Object} obj объект с параметрами для подстановки + */ + return (obj: object) => compiled(obj || {}, format, this._data); + } catch (e) { + trace.traceEvent(DebugLevel, [e, text, this._data]); + throw e; + } + } + + preamble() { + this._code.push( + "var $p = [];", + "var print = function(){", + " $p.push(format.apply(null,arguments));", + "};" + ); + + if (this._wrapWith) + this._code.push("with(obj){"); + } + + postamble() { + if (this._wrapWith) + this._code.push("}"); + + this._code.push("return $p.join('');"); + } + + visitTemplate(parser: ITemplateParser) { + while (parser.next()) { + switch (parser.token()) { + case TokenType.OpenBlock: + this.visitCode(parser); + break; + case TokenType.OpenInlineBlock: + this.visitInline(parser); + break; + default: + this.visitTextFragment(parser); + break; + } + } + } + + visitInline(parser: ITemplateParser) { + const code = ["$p.push("]; + while (parser.next()) { + if (parser.token() === TokenType.CloseBlock) + break; + code.push(parser.value()); + } + code.push(");"); + this._code.push(code.join("")); + } + + visitCode(parser: ITemplateParser) { + const code = []; + while (parser.next()) { + if (parser.token() === TokenType.CloseBlock) + break; + code.push(parser.value()); + } + this._code.push(code.join("")); + } + + visitTextFragment(parser: ITemplateParser) { + const i = this._data.push(parser.value()); + this._code.push("$p.push($data[" + i + "]);"); + } +} diff --git a/src/main/ts/text/TemplateParser.ts b/src/main/ts/text/TemplateParser.ts new file mode 100644 --- /dev/null +++ b/src/main/ts/text/TemplateParser.ts @@ -0,0 +1,64 @@ +import { argumentNotEmptyString } from "../safe"; +import { MapOf } from "../interfaces"; + +const splitRx = /(<%=|\[%=|<%|\[%|%\]|%>)/; + +export enum TokenType { + None, + Text, + OpenInlineBlock, + OpenBlock, + CloseBlock +} + +const tokenMap: MapOf = { + "<%": TokenType.OpenBlock, + "[%": TokenType.OpenBlock, + "<%=": TokenType.OpenInlineBlock, + "[%=": TokenType.OpenInlineBlock, + "%>": TokenType.CloseBlock, + "%]": TokenType.CloseBlock +}; + +export interface ITemplateParser { + next(): boolean; + token(): TokenType; + value(): string; +} + +export class TemplateParser implements ITemplateParser { + + _tokens: string[]; + _pos = -1; + _type: TokenType; + _value: string; + + constructor(text: string) { + argumentNotEmptyString(text, "text"); + + this._tokens = text.split(splitRx); + this._type = TokenType.None; + } + + next() { + this._pos++; + if (this._pos < this._tokens.length) { + this._value = this._tokens[this._pos]; + this._type = tokenMap[this._value] || TokenType.Text; + return true; + } else { + this._type = TokenType.None; + this._value = undefined; + return false; + } + } + + token() { + return this._type; + } + + value() { + return this._value; + } + +}