« get me outta code hell

data: language: compare missing/misplaced options more rigorously - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data/things/language.js
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-12-29 13:31:39 -0400
committer(quasar) nebula <qznebula@protonmail.com>2023-12-30 13:23:07 -0400
commit3ec9473bf3fb6438a331890138c0f81264495c63 (patch)
tree3c0f4fe5765247285dc44eff84360138c6d6b9c6 /src/data/things/language.js
parent2ea4de9ad3a7187dea5cab3ef1bb33f9a158819f (diff)
data: language: compare missing/misplaced options more rigorously
Diffstat (limited to 'src/data/things/language.js')
-rw-r--r--src/data/things/language.js40
1 files changed, 32 insertions, 8 deletions
diff --git a/src/data/things/language.js b/src/data/things/language.js
index 6c15e28..60d089b 100644
--- a/src/data/things/language.js
+++ b/src/data/things/language.js
@@ -2,6 +2,7 @@ import { Temporal, toTemporalInstant } from '@js-temporal/polyfill';
 
 import {isLanguageCode} from '#validators';
 import {Tag} from '#html';
+import {empty, withAggregate} from '#sugar';
 
 import {
   getExternalLinkStringOfStyleFromDescriptors,
@@ -188,13 +189,42 @@ export class Language extends Thing {
 
     const template = this.strings[key];
 
+    const providedOptionNames =
+      (hasOptions
+        ? Object.keys(options)
+            .map(name => name.replace(/[A-Z]/g, '_$&'))
+            .map(name => name.toUpperCase())
+        : []);
+
+    const expectedOptionNames =
+      Array.from(template.matchAll(/{(?<name>[A-Z0-9_]+)}/g))
+        .map(({groups}) => groups.name);
+
+    const missingOptionNames =
+      expectedOptionNames.filter(name => !providedOptionNames.includes(name));
+
+    const misplacedOptionNames =
+      providedOptionNames.filter(name => !expectedOptionNames.includes(name));
+
+    withAggregate({message: `Errors in options for string "${key}"`}, ({push}) => {
+      if (!empty(missingOptionNames)) {
+        const names = missingOptionNames.join(`, `);
+        push(new Error(`Missing options: ${names}`));
+      }
+
+      if (!empty(misplacedOptionNames)) {
+        const names = misplacedOptionNames.join(`, `);
+        push(new Error(`Unexpected options: ${names}`));
+      }
+    });
+
     let output;
 
     if (hasOptions) {
       // Convert the keys on the options dict from camelCase to CONSTANT_CASE.
       // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut
       // like, who cares, dude?) Also, this is an array, 8ecause it's handy
-      // for the iterating we're a8out to do. Also strip HTML from arguments
+      // for the iterating we're a8out to do. Also strip HTML from options
       // that are literal strings - real HTML content should always be proper
       // HTML objects (see html.js).
       const processedOptions =
@@ -210,17 +240,11 @@ export class Language extends Thing {
           template);
     } else {
       // Without any options provided, just use the template as-is. This will
-      // still error if the template expected arguments, and otherwise will be
+      // have errored if the template expected options, and otherwise will be
       // the right value.
       output = template;
     }
 
-    // Post-processing: if any expected arguments *weren't* replaced, that
-    // is almost definitely an error.
-    if (output.match(/\{[A-Z][A-Z0-9_]*\}/)) {
-      throw new Error(`Args in ${key} were missing - output: ${output}`);
-    }
-
     // Last caveat: Wrap the output in an HTML tag so that it doesn't get
     // treated as unsanitized HTML if *it* gets passed as an argument to
     // *another* formatString call.