From 4cb0e576949b0223279e32bc26533cf9607fab23 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Tim=20D=C3=BCsterhus?= Date: Fri, 5 Feb 2021 12:19:57 +0100 Subject: [PATCH] Remove the circular dependency between Core/Language and Core/Template The old work-around for this issue was no longer working well since the migration to TypeScript. This commit untangles both modules, by each splitting them into a low level and a high level interface. The largest change is that language items are compiled when add()ing them to the language store instead of when get()ting the contents. This might slightly reduce the initialization performance on pages with a large number of unused language items and it also might increase memory usage, due to needing to store functions instead of strings. It however improves the readability of the code and of course it also fixes this breakage introduced by TypeScript. If it turns out that the change actually *is* an issue then the logic can be optimized, e.g. by skipping the template compiler if no `{` can be found within the phrase that is being add()ed to the language store. --- ts/WoltLabSuite/Core/Language.ts | 52 +++++-------------- ts/WoltLabSuite/Core/Language/Store.ts | 36 +++++++++++++ ts/WoltLabSuite/Core/Template.ts | 44 ++++------------ ts/WoltLabSuite/Core/Template/Compiler.ts | 47 +++++++++++++++++ .../files/js/WoltLabSuite/Core/Language.js | 46 +++++----------- .../js/WoltLabSuite/Core/Language/Store.js | 34 ++++++++++++ .../files/js/WoltLabSuite/Core/Template.js | 36 +++---------- .../js/WoltLabSuite/Core/Template/Compiler.js | 32 ++++++++++++ 8 files changed, 191 insertions(+), 136 deletions(-) create mode 100644 ts/WoltLabSuite/Core/Language/Store.ts create mode 100644 ts/WoltLabSuite/Core/Template/Compiler.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Language/Store.js create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Template/Compiler.js diff --git a/ts/WoltLabSuite/Core/Language.ts b/ts/WoltLabSuite/Core/Language.ts index 05dc5539a2..8287e8b41c 100644 --- a/ts/WoltLabSuite/Core/Language.ts +++ b/ts/WoltLabSuite/Core/Language.ts @@ -10,7 +10,9 @@ import Template from "./Template"; -const _languageItems = new Map(); +import { add as addToStore, Phrase } from "./Language/Store"; + +export { get } from "./Language/Store"; /** * Adds all the language items in the given object to the store. @@ -25,49 +27,21 @@ export function addObject(object: LanguageItems): void { * Adds a single language item to the store. */ export function add(key: string, value: string): void { - _languageItems.set(key, value); + addToStore(key, compile(value)); } /** - * Fetches the language item specified by the given key. - * If the language item is a string it will be evaluated as - * WoltLabSuite/Core/Template with the given parameters. - * - * @param {string} key Language item to return. - * @param {Object=} parameters Parameters to provide to WoltLabSuite/Core/Template. - * @return {string} + * Compiles the given value into a phrase. */ -export function get(key: string, parameters?: object): string { - let value = _languageItems.get(key); - if (value === undefined) { - return key; +function compile(value: string): Phrase { + try { + const template = new Template(value); + return template.fetch.bind(template); + } catch (e) { + return function () { + return value; + }; } - - if (Template === undefined) { - // @ts-expect-error: This is required due to a circular dependency. - Template = require("./Template"); - } - - if (typeof value === "string") { - // lazily convert to WCF.Template - try { - _languageItems.set(key, new Template(value)); - } catch (e) { - _languageItems.set( - key, - new Template( - "{literal}" + value.replace(/{\/literal}/g, "{/literal}{ldelim}/literal}{literal}") + "{/literal}", - ), - ); - } - value = _languageItems.get(key); - } - - if (value instanceof Template) { - value = value.fetch(parameters || {}); - } - - return value as string; } interface LanguageItems { diff --git a/ts/WoltLabSuite/Core/Language/Store.ts b/ts/WoltLabSuite/Core/Language/Store.ts new file mode 100644 index 0000000000..a269c9ff44 --- /dev/null +++ b/ts/WoltLabSuite/Core/Language/Store.ts @@ -0,0 +1,36 @@ +/** + * Handles the low level management of language items. + * + * @author Tim Duesterhus + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Language/Store + */ + +const languageItems = new Map(); + +/** + * Fetches the language item specified by the given key. + * + * The given parameters are passed to the compiled Phrase. + */ +export function get(key: string, parameters: object = {}): string { + const value = languageItems.get(key); + if (value === undefined) { + return key; + } + + return value(parameters); +} + +/** + * Adds a single language item to the store. + */ +export function add(key: string, value: Phrase): void { + languageItems.set(key, value); +} + +/** + * Represents a compiled phrase. + */ +export type Phrase = (parameters: object) => string; diff --git a/ts/WoltLabSuite/Core/Template.ts b/ts/WoltLabSuite/Core/Template.ts index b43694b704..204ac79b20 100644 --- a/ts/WoltLabSuite/Core/Template.ts +++ b/ts/WoltLabSuite/Core/Template.ts @@ -1,8 +1,5 @@ /** - * WoltLabSuite/Core/Template provides a template scripting compiler similar - * to the PHP one of WoltLab Suite Core. It supports a limited - * set of useful commands and compiles templates down to a pure - * JavaScript Function. + * Provides a high level wrapper around the Template/Compiler. * * @author Tim Duesterhus * @copyright 2001-2019 WoltLab GmbH @@ -11,10 +8,10 @@ */ import * as Core from "./Core"; -import * as parser from "./Template.grammar"; -import * as StringUtil from "./StringUtil"; -import * as Language from "./Language"; import * as I18nPlural from "./I18n/Plural"; +import * as LanguageStore from "./Language/Store"; +import * as StringUtil from "./StringUtil"; +import { compile, CompiledTemplate } from "./Template/Compiler"; // @todo: still required? // work around bug in AMD module generation of Jison @@ -27,33 +24,11 @@ parser.Parser = Parser; parser = new Parser();*/ class Template { - constructor(template: string) { - if (Language === undefined) { - // @ts-expect-error: This is required due to a circular dependency. - Language = require("./Language"); - } - if (StringUtil === undefined) { - // @ts-expect-error: This is required due to a circular dependency. - StringUtil = require("./StringUtil"); - } + private compiled: CompiledTemplate; + constructor(template: string) { try { - template = parser.parse(template) as string; - template = - "var tmp = {};\n" + - "for (var key in v) tmp[key] = v[key];\n" + - "v = tmp;\n" + - "v.__wcf = window.WCF; v.__window = window;\n" + - "return " + - template; - - // eslint-disable-next-line @typescript-eslint/no-implied-eval - this.fetch = new Function("StringUtil", "Language", "I18nPlural", "v", template).bind( - undefined, - StringUtil, - Language, - I18nPlural, - ); + this.compiled = compile(template); } catch (e) { console.debug(e.message); throw e; @@ -63,9 +38,8 @@ class Template { /** * Evaluates the Template using the given parameters. */ - fetch(_v: object): string { - // this will be replaced in the init function - throw new Error("This Template is not initialized."); + fetch(v: object): string { + return this.compiled(StringUtil, LanguageStore, I18nPlural, v); } } diff --git a/ts/WoltLabSuite/Core/Template/Compiler.ts b/ts/WoltLabSuite/Core/Template/Compiler.ts new file mode 100644 index 0000000000..6d7246656b --- /dev/null +++ b/ts/WoltLabSuite/Core/Template/Compiler.ts @@ -0,0 +1,47 @@ +/** + * WoltLabSuite/Core/Template/Compiler provides a template scripting compiler + * similar to the PHP one of WoltLab Suite Core. It supports a limited set of + * useful commands and compiles templates down to a pure JavaScript Function. + * + * @author Tim Duesterhus + * @copyright 2001-2021 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Template/Compiler + */ + +import * as parser from "../Template.grammar"; +import type { escapeHTML, formatNumeric } from "../StringUtil"; +import type { get as getLanguage } from "../Language"; +import type { getCategoryFromTemplateParameters } from "../I18n/Plural"; + +interface TemplateLanguage { + get: typeof getLanguage; +} + +interface TemplateStringUtil { + escapeHTML: typeof escapeHTML; + formatNumeric: typeof formatNumeric; +} + +interface TemplatePlural { + getCategoryFromTemplateParameters: typeof getCategoryFromTemplateParameters; +} + +export type CompiledTemplate = (S: TemplateStringUtil, L: TemplateLanguage, P: TemplatePlural, v: object) => string; + +/** + * Compiles the given template. + */ +export function compile(template: string): CompiledTemplate { + template = parser.parse(template) as string; + template = + "var tmp = {};\n" + + "for (var key in v) tmp[key] = v[key];\n" + + "v = tmp;\n" + + "v.__wcf = window.WCF; v.__window = window;\n" + + "return " + + template; + + // eslint-disable-next-line @typescript-eslint/no-implied-eval + return new Function("StringUtil", "Language", "I18nPlural", "v", template) as CompiledTemplate; +} diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Language.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Language.js index 0c3c01a5d5..559835e40a 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Language.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Language.js @@ -7,12 +7,12 @@ * @module Language (alias) * @module WoltLabSuite/Core/Language */ -define(["require", "exports", "tslib", "./Template"], function (require, exports, tslib_1, Template_1) { +define(["require", "exports", "tslib", "./Template", "./Language/Store", "./Language/Store"], function (require, exports, tslib_1, Template_1, Store_1, Store_2) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); - exports.get = exports.add = exports.addObject = void 0; + exports.add = exports.addObject = exports.get = void 0; Template_1 = tslib_1.__importDefault(Template_1); - const _languageItems = new Map(); + Object.defineProperty(exports, "get", { enumerable: true, get: function () { return Store_2.get; } }); /** * Adds all the language items in the given object to the store. */ @@ -26,41 +26,21 @@ define(["require", "exports", "tslib", "./Template"], function (require, exports * Adds a single language item to the store. */ function add(key, value) { - _languageItems.set(key, value); + Store_1.add(key, compile(value)); } exports.add = add; /** - * Fetches the language item specified by the given key. - * If the language item is a string it will be evaluated as - * WoltLabSuite/Core/Template with the given parameters. - * - * @param {string} key Language item to return. - * @param {Object=} parameters Parameters to provide to WoltLabSuite/Core/Template. - * @return {string} + * Compiles the given value into a phrase. */ - function get(key, parameters) { - let value = _languageItems.get(key); - if (value === undefined) { - return key; + function compile(value) { + try { + const template = new Template_1.default(value); + return template.fetch.bind(template); } - if (Template_1.default === undefined) { - // @ts-expect-error: This is required due to a circular dependency. - Template_1.default = require("./Template"); + catch (e) { + return function () { + return value; + }; } - if (typeof value === "string") { - // lazily convert to WCF.Template - try { - _languageItems.set(key, new Template_1.default(value)); - } - catch (e) { - _languageItems.set(key, new Template_1.default("{literal}" + value.replace(/{\/literal}/g, "{/literal}{ldelim}/literal}{literal}") + "{/literal}")); - } - value = _languageItems.get(key); - } - if (value instanceof Template_1.default) { - value = value.fetch(parameters || {}); - } - return value; } - exports.get = get; }); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Store.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Store.js new file mode 100644 index 0000000000..43794897ce --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Store.js @@ -0,0 +1,34 @@ +/** + * Handles the low level management of language items. + * + * @author Tim Duesterhus + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Language/Store + */ +define(["require", "exports"], function (require, exports) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.add = exports.get = void 0; + const languageItems = new Map(); + /** + * Fetches the language item specified by the given key. + * + * The given parameters are passed to the compiled Phrase. + */ + function get(key, parameters = {}) { + const value = languageItems.get(key); + if (value === undefined) { + return key; + } + return value(parameters); + } + exports.get = get; + /** + * Adds a single language item to the store. + */ + function add(key, value) { + languageItems.set(key, value); + } + exports.add = add; +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Template.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Template.js index 4498de44e0..813fdd3930 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Template.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Template.js @@ -1,21 +1,17 @@ /** - * WoltLabSuite/Core/Template provides a template scripting compiler similar - * to the PHP one of WoltLab Suite Core. It supports a limited - * set of useful commands and compiles templates down to a pure - * JavaScript Function. + * Provides a high level wrapper around the Template/Compiler. * * @author Tim Duesterhus * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License * @module WoltLabSuite/Core/Template */ -define(["require", "exports", "tslib", "./Core", "./Template.grammar", "./StringUtil", "./Language", "./I18n/Plural"], function (require, exports, tslib_1, Core, parser, StringUtil, Language, I18nPlural) { +define(["require", "exports", "tslib", "./Core", "./I18n/Plural", "./Language/Store", "./StringUtil", "./Template/Compiler"], function (require, exports, tslib_1, Core, I18nPlural, LanguageStore, StringUtil, Compiler_1) { "use strict"; Core = tslib_1.__importStar(Core); - parser = tslib_1.__importStar(parser); - StringUtil = tslib_1.__importStar(StringUtil); - Language = tslib_1.__importStar(Language); I18nPlural = tslib_1.__importStar(I18nPlural); + LanguageStore = tslib_1.__importStar(LanguageStore); + StringUtil = tslib_1.__importStar(StringUtil); // @todo: still required? // work around bug in AMD module generation of Jison /*function Parser() { @@ -27,25 +23,8 @@ define(["require", "exports", "tslib", "./Core", "./Template.grammar", "./String parser = new Parser();*/ class Template { constructor(template) { - if (Language === undefined) { - // @ts-expect-error: This is required due to a circular dependency. - Language = require("./Language"); - } - if (StringUtil === undefined) { - // @ts-expect-error: This is required due to a circular dependency. - StringUtil = require("./StringUtil"); - } try { - template = parser.parse(template); - template = - "var tmp = {};\n" + - "for (var key in v) tmp[key] = v[key];\n" + - "v = tmp;\n" + - "v.__wcf = window.WCF; v.__window = window;\n" + - "return " + - template; - // eslint-disable-next-line @typescript-eslint/no-implied-eval - this.fetch = new Function("StringUtil", "Language", "I18nPlural", "v", template).bind(undefined, StringUtil, Language, I18nPlural); + this.compiled = Compiler_1.compile(template); } catch (e) { console.debug(e.message); @@ -55,9 +34,8 @@ define(["require", "exports", "tslib", "./Core", "./Template.grammar", "./String /** * Evaluates the Template using the given parameters. */ - fetch(_v) { - // this will be replaced in the init function - throw new Error("This Template is not initialized."); + fetch(v) { + return this.compiled(StringUtil, LanguageStore, I18nPlural, v); } } Object.defineProperty(Template, "callbacks", { diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Template/Compiler.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Template/Compiler.js new file mode 100644 index 0000000000..1e9a2fde7b --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Template/Compiler.js @@ -0,0 +1,32 @@ +/** + * WoltLabSuite/Core/Template/Compiler provides a template scripting compiler + * similar to the PHP one of WoltLab Suite Core. It supports a limited set of + * useful commands and compiles templates down to a pure JavaScript Function. + * + * @author Tim Duesterhus + * @copyright 2001-2021 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Template/Compiler + */ +define(["require", "exports", "tslib", "../Template.grammar"], function (require, exports, tslib_1, parser) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.compile = void 0; + parser = tslib_1.__importStar(parser); + /** + * Compiles the given template. + */ + function compile(template) { + template = parser.parse(template); + template = + "var tmp = {};\n" + + "for (var key in v) tmp[key] = v[key];\n" + + "v = tmp;\n" + + "v.__wcf = window.WCF; v.__window = window;\n" + + "return " + + template; + // eslint-disable-next-line @typescript-eslint/no-implied-eval + return new Function("StringUtil", "Language", "I18nPlural", "v", template); + } + exports.compile = compile; +}); -- 2.20.1