« 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/html.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/util/html.js')
-rw-r--r--src/util/html.js100
1 files changed, 80 insertions, 20 deletions
diff --git a/src/util/html.js b/src/util/html.js
index b8dea51..9e07f9b 100644
--- a/src/util/html.js
+++ b/src/util/html.js
@@ -73,6 +73,15 @@ export const joinChildren = Symbol();
 // or when there are multiple children.
 export const noEdgeWhitespace = Symbol();
 
+// Pass as a value on an object-shaped set of attributes to indicate that it's
+// always, absolutely, no matter what, a valid attribute addition. It will be
+// completely exempt from validation, which may provide a significant speed
+// boost IF THIS OPERATION IS REPEATED MANY TENS OF THOUSANDS OF TIMES.
+// Basically, don't use this unless you're 1) providing a constant set of
+// attributes, and 2) writing a very basic building block which loads of other
+// content will build off of!
+export const blessAttributes = Symbol();
+
 // Don't pass this directly, use html.metatag('blockwrap') instead.
 // Causes *following* content (past the metatag) to be placed inside a span
 // which is styled 'inline-block', which ensures that the words inside the
@@ -373,13 +382,12 @@ export class Tag {
       throw new Error(`Tag <${this.tagName}> is self-closing but got content`);
     }
 
-    let contentArray;
-
-    if (Array.isArray(value)) {
-      contentArray = value;
-    } else {
-      contentArray = [value];
-    }
+    const contentArray =
+      (Array.isArray(value)
+        ? value.flat(Infinity).filter(Boolean)
+     : value
+        ? [value]
+        : []);
 
     if (this.chunkwrap) {
       if (contentArray.some(content => content?.blockwrap)) {
@@ -387,10 +395,7 @@ export class Tag {
       }
     }
 
-    this.#content = contentArray
-      .flat(Infinity)
-      .filter(Boolean);
-
+    this.#content = contentArray;
     this.#content.toString = () => this.#stringifyContent();
   }
 
@@ -697,7 +702,7 @@ export class Tag {
             if (index === 0) {
               content += chunk;
             } else {
-              const whitespace = chunk.match(/^\s+/);
+              const whitespace = chunk.match(/^\s+/) ?? '';
               content += chunkwrapSplitter;
               content += '</span>';
               content += whitespace;
@@ -994,6 +999,12 @@ export class Attributes {
     }
   }
 
+  with(...args) {
+    const clone = this.clone();
+    clone.add(...args);
+    return clone;
+  }
+
   #addMultipleAttributes(attributes) {
     const flatInputAttributes =
       [attributes].flat(Infinity).filter(Boolean);
@@ -1007,6 +1018,8 @@ export class Attributes {
       const setResults = {};
 
       for (const key of Reflect.ownKeys(set)) {
+        if (key === blessAttributes) continue;
+
         const value = set[key];
         setResults[key] = this.#addOneAttribute(key, value);
       }
@@ -1088,8 +1101,17 @@ export class Attributes {
     return this.#attributes[attribute];
   }
 
-  has(attribute) {
-    return attribute in this.#attributes;
+  has(attribute, pattern) {
+    if (typeof pattern === 'undefined') {
+      return attribute in this.#attributes;
+    } else if (this.has(attribute)) {
+      const value = this.get(attribute);
+      if (Array.isArray(value)) {
+        return value.includes(pattern);
+      } else {
+        return value === pattern;
+      }
+    }
   }
 
   remove(attribute) {
@@ -1325,6 +1347,22 @@ export function smush(smushee) {
   return smush(Tag.normalize(smushee));
 }
 
+// Much gentler version of smush - this only flattens nested html.tags(), and
+// guarantees the result is itself an html.tags(). It doesn't manipulate text
+// content, and it doesn't resolve templates.
+export function smooth(smoothie) {
+  // Helper function to avoid intermediate html.tags() calls.
+  function helper(tag) {
+    if (tag instanceof Tag && tag.contentOnly) {
+      return tag.content.flatMap(helper);
+    } else {
+      return tag;
+    }
+  }
+
+  return tags(helper(smoothie));
+}
+
 export function template(description) {
   return new Template(description);
 }
@@ -1546,14 +1584,16 @@ export class Template {
       return true;
     }
 
-    if ('validate' in description) {
+    if (Object.hasOwn(description, 'validate')) {
       description.validate({
         ...commonValidators,
         ...validators,
       })(value);
+
+      return true;
     }
 
-    if ('type' in description) {
+    if (Object.hasOwn(description, 'type')) {
       switch (description.type) {
         case 'html': {
           return isHTML(value);
@@ -1564,14 +1604,14 @@ export class Template {
         }
 
         case 'string': {
+          if (typeof value === 'string')
+            return true;
+
           // Tags and templates are valid in string arguments - they'll be
           // stringified when exposed to the description's .content() function.
           if (value instanceof Tag || value instanceof Template)
             return true;
 
-          if (typeof value !== 'string')
-            throw new TypeError(`Slot expects string, got ${typeof value}`);
-
           return true;
         }
 
@@ -1815,9 +1855,29 @@ export const isAttributesAdditionPair = pair => {
   return true;
 };
 
-export const isAttributesAdditionSinglet =
+const isAttributesAdditionSingletHelper =
   anyOf(
     validateInstanceOf(Template),
     validateInstanceOf(Attributes),
     validateAllPropertyValues(isAttributeValue),
     looseArrayOf(value => isAttributesAdditionSinglet(value)));
+
+export const isAttributesAdditionSinglet = (value) => {
+  if (typeof value === 'object' && value !== null) {
+    if (Object.hasOwn(value, blessAttributes)) {
+      return true;
+    }
+
+    if (
+      Array.isArray(value) &&
+      value.length === 1 &&
+      typeof value[0] === 'object' &&
+      value[0] !== null &&
+      Object.hasOwn(value[0], blessAttributes)
+    ) {
+      return true;
+    }
+  }
+
+  return isAttributesAdditionSingletHelper(value);
+};