Remove the circular dependency between Core/Language and Core/Template
authorTim Düsterhus <duesterhus@woltlab.com>
Fri, 5 Feb 2021 11:19:57 +0000 (12:19 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Fri, 5 Feb 2021 11:19:57 +0000 (12:19 +0100)
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
ts/WoltLabSuite/Core/Language/Store.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Template.ts
ts/WoltLabSuite/Core/Template/Compiler.ts [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Language.js
wcfsetup/install/files/js/WoltLabSuite/Core/Language/Store.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Template.js
wcfsetup/install/files/js/WoltLabSuite/Core/Template/Compiler.js [new file with mode: 0644]

index 05dc5539a222cc82a51cd28e5f7ee4982cd16485..8287e8b41c263dd808525a48bae3369bd3b83954 100644 (file)
@@ -10,7 +10,9 @@
 
 import Template from "./Template";
 
-const _languageItems = new Map<string, string | Template>();
+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 (file)
index 0000000..a269c9f
--- /dev/null
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Language/Store
+ */
+
+const languageItems = new Map<string, Phrase>();
+
+/**
+ * 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;
index b43694b70443498680f088a4f649919c176f5c85..204ac79b2045e654260137d21923ac43b3a58756 100644 (file)
@@ -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
  */
 
 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 (file)
index 0000000..6d72466
--- /dev/null
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
+ * @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;
+}
index 0c3c01a5d5c57b8f1edab107b6bbe8ceb0e03800..559835e40ae9c06e8358398ed397cea04ebb4cd6 100644 (file)
@@ -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 (file)
index 0000000..4379489
--- /dev/null
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
+ * @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;
+});
index 4498de44e0aeaf0161996af91a7b927c0618b799..813fdd393071b9fc1ca2b7b7c5a4330e9ed65b46 100644 (file)
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
  * @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 (file)
index 0000000..1e9a2fd
--- /dev/null
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
+ * @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;
+});