« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/util')
-rw-r--r--src/util/html.js139
-rw-r--r--src/util/sugar.js36
2 files changed, 112 insertions, 63 deletions
diff --git a/src/util/html.js b/src/util/html.js
index b5930d06..b75820e8 100644
--- a/src/util/html.js
+++ b/src/util/html.js
@@ -489,7 +489,10 @@ export class Template {
   #slotValues = {};
 
   constructor(description) {
-    Template.validateDescription(description);
+    if (!description[Stationery.validated]) {
+      Template.validateDescription(description);
+    }
+
     this.#description = description;
   }
 
@@ -528,69 +531,79 @@ export class Template {
         break validateSlots;
       }
 
-      const slotErrors = [];
+      try {
+        this.validateSlotsDescription(description.slots);
+      } catch (slotError) {
+        topErrors.push(slotError);
+      }
+    }
 
-      for (const [slotName, slotDescription] of Object.entries(description.slots)) {
-        if (typeof slotDescription !== 'object' || slotDescription === null) {
-          slotErrors.push(new TypeError(`(${slotName}) Expected slot description to be object`));
-          continue;
-        }
+    if (!empty(topErrors)) {
+      throw new AggregateError(topErrors,
+        (typeof description.annotation === 'string'
+          ? `Errors validating template "${description.annotation}" description`
+          : `Errors validating template description`));
+    }
 
-        if ('default' in slotDescription) validateDefault: {
-          if (
-            slotDescription.default === undefined ||
-            slotDescription.default === null
-          ) {
-            slotErrors.push(new TypeError(`(${slotName}) Leave slot default unspecified instead of undefined or null`));
-            break validateDefault;
-          }
+    return true;
+  }
 
-          try {
-            Template.validateSlotValueAgainstDescription(slotDescription.default, slotDescription);
-          } catch (error) {
-            error.message = `(${slotName}) Error validating slot default value: ${error.message}`;
-            slotErrors.push(error);
-          }
+  static validateSlotsDescription(slots) {
+    const slotErrors = [];
+
+    for (const [slotName, slotDescription] of Object.entries(slots)) {
+      if (typeof slotDescription !== 'object' || slotDescription === null) {
+        slotErrors.push(new TypeError(`(${slotName}) Expected slot description to be object`));
+        continue;
+      }
+
+      if ('default' in slotDescription) validateDefault: {
+        if (
+          slotDescription.default === undefined ||
+          slotDescription.default === null
+        ) {
+          slotErrors.push(new TypeError(`(${slotName}) Leave slot default unspecified instead of undefined or null`));
+          break validateDefault;
         }
 
-        if ('validate' in slotDescription && 'type' in slotDescription) {
-          slotErrors.push(new TypeError(`(${slotName}) Don't specify both slot validate and type`));
-        } else if (!('validate' in slotDescription || 'type' in slotDescription)) {
-          slotErrors.push(new TypeError(`(${slotName}) Expected either slot validate or type`));
-        } else if ('validate' in slotDescription) {
-          if (typeof slotDescription.validate !== 'function') {
-            slotErrors.push(new TypeError(`(${slotName}) Expected slot validate to be function`));
-          }
-        } else if ('type' in slotDescription) {
-          const acceptableSlotTypes = [
-            'string',
-            'number',
-            'bigint',
-            'boolean',
-            'symbol',
-            'html',
-          ];
-
-          if (slotDescription.type === 'function') {
-            slotErrors.push(new TypeError(`(${slotName}) Functions shouldn't be provided to slots`));
-          } else if (slotDescription.type === 'object') {
-            slotErrors.push(new TypeError(`(${slotName}) Provide validate function instead of type: object`));
-          } else if (!acceptableSlotTypes.includes(slotDescription.type)) {
-            slotErrors.push(new TypeError(`(${slotName}) Expected slot type to be one of ${acceptableSlotTypes.join(', ')}`));
-          }
+        try {
+          Template.validateSlotValueAgainstDescription(slotDescription.default, slotDescription);
+        } catch (error) {
+          error.message = `(${slotName}) Error validating slot default value: ${error.message}`;
+          slotErrors.push(error);
         }
       }
 
-      if (!empty(slotErrors)) {
-        topErrors.push(new AggregateError(slotErrors, `Errors in slot descriptions`));
+      if ('validate' in slotDescription && 'type' in slotDescription) {
+        slotErrors.push(new TypeError(`(${slotName}) Don't specify both slot validate and type`));
+      } else if (!('validate' in slotDescription || 'type' in slotDescription)) {
+        slotErrors.push(new TypeError(`(${slotName}) Expected either slot validate or type`));
+      } else if ('validate' in slotDescription) {
+        if (typeof slotDescription.validate !== 'function') {
+          slotErrors.push(new TypeError(`(${slotName}) Expected slot validate to be function`));
+        }
+      } else if ('type' in slotDescription) {
+        const acceptableSlotTypes = [
+          'string',
+          'number',
+          'bigint',
+          'boolean',
+          'symbol',
+          'html',
+        ];
+
+        if (slotDescription.type === 'function') {
+          slotErrors.push(new TypeError(`(${slotName}) Functions shouldn't be provided to slots`));
+        } else if (slotDescription.type === 'object') {
+          slotErrors.push(new TypeError(`(${slotName}) Provide validate function instead of type: object`));
+        } else if (!acceptableSlotTypes.includes(slotDescription.type)) {
+          slotErrors.push(new TypeError(`(${slotName}) Expected slot type to be one of ${acceptableSlotTypes.join(', ')}`));
+        }
       }
     }
 
-    if (!empty(topErrors)) {
-      throw new AggregateError(topErrors,
-        (typeof description.annotation === 'string'
-          ? `Errors validating template "${description.annotation}" description`
-          : `Errors validating template description`));
+    if (!empty(slotErrors)) {
+      throw new AggregateError(slotErrors, `Errors in slot descriptions`);
     }
 
     return true;
@@ -769,3 +782,23 @@ export class Template {
     return this.content.toString();
   }
 }
+
+export function stationery(description) {
+  return new Stationery(description);
+}
+
+export class Stationery {
+  #templateDescription = null;
+
+  static validated = Symbol('Stationery.validated');
+
+  constructor(templateDescription) {
+    Template.validateDescription(templateDescription);
+    templateDescription[Stationery.validated] = true;
+    this.#templateDescription = templateDescription;
+  }
+
+  template() {
+    return new Template(this.#templateDescription);
+  }
+}
diff --git a/src/util/sugar.js b/src/util/sugar.js
index 6ab70bc6..3a7e6f82 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -26,18 +26,24 @@ export function* splitArray(array, fn) {
   }
 }
 
-// Null-accepting function to check if an array is empty. Accepts null (and
-// treats as empty) as a shorthand for "hey, check if this property is an array
-// with/without stuff in it" for objects where properties that are PRESENT but
-// don't currently have a VALUE are null (instead of undefined).
-export function empty(arrayOrNull) {
-  if (arrayOrNull === null) {
+// Null-accepting function to check if an array or set is empty. Accepts null
+// (which is treated as empty) as a shorthand for "hey, check if this property
+// is an array with/without stuff in it" for objects where properties that are
+// PRESENT but don't currently have a VALUE are null (rather than undefined).
+export function empty(value) {
+  if (value === null) {
     return true;
-  } else if (Array.isArray(arrayOrNull)) {
-    return arrayOrNull.length === 0;
-  } else {
-    throw new Error(`Expected array or null`);
   }
+
+  if (Array.isArray(value)) {
+    return value.length === 0;
+  }
+
+  if (value instanceof Set) {
+    return value.size === 0;
+  }
+
+  throw new Error(`Expected array, set, or null`);
 }
 
 // Repeats all the items of an array a number of times.
@@ -82,6 +88,16 @@ export const compareArrays = (arr1, arr2, {checkOrder = true} = {}) =>
 export const withEntries = (obj, fn) =>
   Object.fromEntries(fn(Object.entries(obj)));
 
+export function setIntersection(set1, set2) {
+  const intersection = new Set();
+  for (const item of set1) {
+    if (set2.has(item)) {
+      intersection.add(item);
+    }
+  }
+  return intersection;
+}
+
 export function filterProperties(obj, properties) {
   const set = new Set(properties);
   return Object.fromEntries(