« get me outta code hell

language: compositional withStrings, update strings_htmlEscaped - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2025-10-20 13:07:21 -0300
committer(quasar) nebula <qznebula@protonmail.com>2025-10-20 13:07:21 -0300
commit2374124f0b9f758021648e8bd3d99c205b2e3aea (patch)
tree367285e847721d86e71310798ff835a7caaf371f /src
parent4f69806230d69f215a873479f1728cc2fe5ebb46 (diff)
language: compositional withStrings, update strings_htmlEscaped
Diffstat (limited to 'src')
-rw-r--r--src/common-util/wiki-data.js2
-rw-r--r--src/data/composite/things/language/index.js1
-rw-r--r--src/data/composite/things/language/withStrings.js111
-rw-r--r--src/data/things/language.js89
4 files changed, 142 insertions, 61 deletions
diff --git a/src/common-util/wiki-data.js b/src/common-util/wiki-data.js
index 3fde2495..6089b8fc 100644
--- a/src/common-util/wiki-data.js
+++ b/src/common-util/wiki-data.js
@@ -106,6 +106,8 @@ export const commentaryRegexCaseSensitive =
 export const commentaryRegexCaseSensitiveOneShot =
   new RegExp(commentaryRegexRaw);
 
+export const languageOptionRegex = /{(?<name>[A-Z0-9_]+)}/g;
+
 // The #validators function isOldStyleLyrics() describes
 // what this regular expression detects against.
 export const multipleLyricsDetectionRegex =
diff --git a/src/data/composite/things/language/index.js b/src/data/composite/things/language/index.js
new file mode 100644
index 00000000..f22cdaf6
--- /dev/null
+++ b/src/data/composite/things/language/index.js
@@ -0,0 +1 @@
+export {default as withStrings} from './withStrings.js';
diff --git a/src/data/composite/things/language/withStrings.js b/src/data/composite/things/language/withStrings.js
new file mode 100644
index 00000000..3b8d46b3
--- /dev/null
+++ b/src/data/composite/things/language/withStrings.js
@@ -0,0 +1,111 @@
+import {logWarn} from '#cli';
+import {input, templateCompositeFrom} from '#composite';
+import {empty, withEntries} from '#sugar';
+import {languageOptionRegex} from '#wiki-data';
+
+import {withResultOfAvailabilityCheck} from '#composite/control-flow';
+
+export default templateCompositeFrom({
+  annotation: `withStrings`,
+
+  inputs: {
+    from: input({defaultDependency: 'strings'}),
+  },
+
+  outputs: ['#strings'],
+
+  steps: () => [
+    withResultOfAvailabilityCheck({
+      from: input('from'),
+    }).outputs({
+      '#availability': '#stringsAvailability',
+    }),
+
+    withResultOfAvailabilityCheck({
+      from: 'inheritedStrings',
+    }).outputs({
+      '#availability': '#inheritedStringsAvailability',
+    }),
+
+    {
+      dependencies: [
+        '#stringsAvailability',
+        '#inheritedStringsAvailability',
+      ],
+
+      compute: (continuation, {
+        ['#stringsAvailability']: stringsAvailability,
+        ['#inheritedStringsAvailability']: inheritedStringsAvailability,
+      }) =>
+        (stringsAvailability || inheritedStringsAvailability
+          ? continuation()
+          : continuation.raiseOutput({'#strings': null})),
+    },
+
+    {
+      dependencies: [input('from'), '#inheritedStringsAvailability'],
+      compute: (continuation, {
+        [input('from')]: strings,
+        ['#inheritedStringsAvailability']: inheritedStringsAvailability,
+      }) =>
+        (inheritedStringsAvailability
+          ? continuation()
+          : continuation.raiseOutput({'#strings': strings})),
+    },
+
+    {
+      dependencies: ['inheritedStrings', '#stringsAvailability'],
+      compute: (continuation, {
+        ['inheritedStrings']: inheritedStrings,
+        ['#stringsAvailability']: stringsAvailability,
+      }) =>
+        (stringsAvailability
+          ? continuation()
+          : continuation.raiseOutput({'#strings': inheritedStrings})),
+    },
+
+    {
+      dependencies: [input('from'), 'inheritedStrings', 'code'],
+      compute(continuation, {
+        [input('from')]: strings,
+        ['inheritedStrings']: inheritedStrings,
+        ['code']: code,
+      }) {
+        const validStrings = {
+          ...inheritedStrings,
+          ...strings,
+        };
+
+        const optionsFromTemplate = template =>
+          Array.from(template.matchAll(languageOptionRegex))
+            .map(({groups}) => groups.name);
+
+        for (const [key, providedTemplate] of Object.entries(strings)) {
+          const inheritedTemplate = inheritedStrings[key];
+          if (!inheritedTemplate) continue;
+
+          const providedOptions = optionsFromTemplate(providedTemplate);
+          const inheritedOptions = optionsFromTemplate(inheritedTemplate);
+
+          const missingOptionNames =
+            inheritedOptions.filter(name => !providedOptions.includes(name));
+
+          const misplacedOptionNames =
+            providedOptions.filter(name => !inheritedOptions.includes(name));
+
+          if (!empty(missingOptionNames) || !empty(misplacedOptionNames)) {
+            logWarn`Not using ${code ?? '(no code)'} string ${key}:`;
+            if (!empty(missingOptionNames))
+              logWarn`- Missing options: ${missingOptionNames.join(', ')}`;
+            if (!empty(misplacedOptionNames))
+              logWarn`- Unexpected options: ${misplacedOptionNames.join(', ')}`;
+
+            validStrings[key] = inheritedStrings[key];
+          }
+        }
+
+        return continuation({'#strings': validStrings});
+      },
+    },
+  ],
+});
diff --git a/src/data/things/language.js b/src/data/things/language.js
index 46cff26a..997cf31e 100644
--- a/src/data/things/language.js
+++ b/src/data/things/language.js
@@ -2,12 +2,12 @@ import {Temporal, toTemporalInstant} from '@js-temporal/polyfill';
 
 import {withAggregate} from '#aggregate';
 import CacheableObject from '#cacheable-object';
-import {logWarn} from '#cli';
 import {input} from '#composite';
 import * as html from '#html';
-import {empty} from '#sugar';
+import {empty, withEntries} from '#sugar';
 import {isLanguageCode} from '#validators';
 import Thing from '#thing';
+import {languageOptionRegex} from '#wiki-data';
 
 import {
   getExternalLinkStringOfStyleFromDescriptors,
@@ -17,10 +17,11 @@ import {
   isExternalLinkStyle,
 } from '#external-links';
 
-import {exposeConstant} from '#composite/control-flow';
+import {exitWithoutDependency, exposeConstant, exposeDependency}
+  from '#composite/control-flow';
 import {externalFunction, flag, name} from '#composite/wiki-properties';
 
-export const languageOptionRegex = /{(?<name>[A-Z0-9_]+)}/g;
+import {withStrings} from '#composite/things/language';
 
 export class Language extends Thing {
   static [Thing.getPropertyDescriptors] = () => ({
@@ -62,52 +63,17 @@ export class Language extends Thing {
 
     // Mapping of translation keys to values (strings). Generally, don't
     // access this object directly - use methods instead.
-    strings: {
-      flags: {update: true, expose: true},
-      update: {validate: (t) => typeof t === 'object'},
-
-      expose: {
-        dependencies: ['inheritedStrings', 'code'],
-        transform(strings, {inheritedStrings, code}) {
-          if (!strings && !inheritedStrings) return null;
-          if (!inheritedStrings) return strings;
-
-          const validStrings = {
-            ...inheritedStrings,
-            ...strings,
-          };
-
-          const optionsFromTemplate = template =>
-            Array.from(template.matchAll(languageOptionRegex))
-              .map(({groups}) => groups.name);
-
-          for (const [key, providedTemplate] of Object.entries(strings)) {
-            const inheritedTemplate = inheritedStrings[key];
-            if (!inheritedTemplate) continue;
-
-            const providedOptions = optionsFromTemplate(providedTemplate);
-            const inheritedOptions = optionsFromTemplate(inheritedTemplate);
-
-            const missingOptionNames =
-              inheritedOptions.filter(name => !providedOptions.includes(name));
-
-            const misplacedOptionNames =
-              providedOptions.filter(name => !inheritedOptions.includes(name));
-
-            if (!empty(missingOptionNames) || !empty(misplacedOptionNames)) {
-              logWarn`Not using ${code ?? '(no code)'} string ${key}:`;
-              if (!empty(missingOptionNames))
-                logWarn`- Missing options: ${missingOptionNames.join(', ')}`;
-              if (!empty(misplacedOptionNames))
-                logWarn`- Unexpected options: ${misplacedOptionNames.join(', ')}`;
-              validStrings[key] = inheritedStrings[key];
-            }
-          }
+    strings: [
+      withStrings({
+        from: input.updateValue({
+          validate: t => typeof t === 'object',
+        }),
+      }),
 
-          return validStrings;
-        },
-      },
-    },
+      exposeDependency({
+        dependency: '#strings',
+      }),
+    ],
 
     // May be provided to specify "default" strings, generally (but not
     // necessarily) inherited from another Language object.
@@ -163,19 +129,20 @@ export class Language extends Thing {
     },
 
     // TODO: This currently isn't used. Is it still needed?
-    strings_htmlEscaped: {
-      flags: {expose: true},
-      expose: {
-        dependencies: ['strings', 'inheritedStrings'],
-        compute({strings, inheritedStrings}) {
-          if (!(strings || inheritedStrings)) return null;
-          const allStrings = {...inheritedStrings, ...strings};
-          return Object.fromEntries(
-            Object.entries(allStrings).map(([k, v]) => [k, html.escape(v)])
-          );
-        },
+    strings_htmlEscaped: [
+      withStrings(),
+
+      exitWithoutDependency({
+        dependency: '#strings',
+      }),
+
+      {
+        dependencies: ['#strings'],
+        compute: ({'#strings': strings}) =>
+          withEntries(strings, entries => entries
+            .map(([key, value]) => [key, html.escape(value)])),
       },
-    },
+    ],
   });
 
   static #intlHelper (constructor, opts) {