« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/static/site6.css4
-rw-r--r--src/util/html.js58
2 files changed, 61 insertions, 1 deletions
diff --git a/src/static/site6.css b/src/static/site6.css
index c7587971..bf9bc2cc 100644
--- a/src/static/site6.css
+++ b/src/static/site6.css
@@ -473,6 +473,10 @@ a:not([href]):hover {
   white-space: nowrap;
 }
 
+.blockwrap {
+  display: inline-block;
+}
+
 .contribution.has-tooltip,
 .datetimestamp.has-tooltip {
   position: relative;
diff --git a/src/util/html.js b/src/util/html.js
index 7a73e567..08d03bc7 100644
--- a/src/util/html.js
+++ b/src/util/html.js
@@ -44,6 +44,15 @@ export const joinChildren = Symbol();
 // or when there are multiple children.
 export const noEdgeWhitespace = 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
+// metatag all stay together, line-breaking only if needed, and following
+// text is displayed immediately after the last character of the last line of
+// the metatag (provided there's room on that line for the following word or
+// character).
+export const blockwrap = Symbol();
+
 // Note: This is only guaranteed to return true for blanks (as returned by
 // html.blank()) and false for Tags and Templates (regardless of contents or
 // other properties). Don't depend on this to match any other values.
@@ -182,6 +191,31 @@ export function tags(content, attributes = null) {
   return new Tag(null, attributes, content);
 }
 
+export function metatag(identifier, ...args) {
+  let content;
+  let opts = {};
+
+  if (
+    typeof args[0] === 'object' &&
+    !(Array.isArray(args[0]) ||
+      args[0] instanceof Tag ||
+      args[0] instanceof Template)
+  ) {
+    opts = args[0];
+    content = args[1];
+  } else {
+    content = args[0];
+  }
+
+  switch (identifier) {
+    case 'blockwrap':
+      return new Tag(null, {[blockwrap]: true}, content);
+
+    default:
+      throw new Error(`Unknown metatag "${identifier}"`);
+  }
+}
+
 export function normalize(content) {
   return Tag.normalize(content);
 }
@@ -285,7 +319,10 @@ export class Tag {
   }
 
   get contentOnly() {
-    return this.tagName === '' && this.attributes.blank;
+    if (this.tagName !== '') return false;
+    if (!this.attributes.blank) return false;
+    if (this.blockwrap) return false;
+    return true;
   }
 
   #setAttributeFlag(attribute, value) {
@@ -346,6 +383,14 @@ export class Tag {
     return this.#getAttributeFlag(noEdgeWhitespace);
   }
 
+  set blockwrap(value) {
+    this.#setAttributeFlag(blockwrap, value);
+  }
+
+  get blockwrap() {
+    return this.#getAttributeFlag(blockwrap);
+  }
+
   toString() {
     const attributesString = this.attributes.toString();
     const contentString = this.content.toString();
@@ -407,6 +452,7 @@ export class Tag {
         : `\n${this.joinChildren}\n`);
 
     let content = '';
+    let blockwrapClosers = '';
 
     for (const [index, item] of this.content.entries()) {
       let itemContent;
@@ -429,9 +475,19 @@ export class Tag {
         content += joiner;
       }
 
+      // Blockwraps only apply if they actually contain some content whose
+      // words should be kept together, so it's okay to put them beneath the
+      // itemContent check.
+      if (item instanceof Tag && item.blockwrap) {
+        content += `<span class="blockwrap">`;
+        blockwrapClosers += `</span>`;
+      }
+
       content += itemContent;
     }
 
+    content += blockwrapClosers;
+
     return content;
   }