« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/html.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/html.js')
-rw-r--r--src/html.js100
1 files changed, 85 insertions, 15 deletions
diff --git a/src/html.js b/src/html.js
index cde86a5c..7b8f327a 100644
--- a/src/html.js
+++ b/src/html.js
@@ -152,6 +152,14 @@ export const blockwrap = Symbol();
 // considered wrappable units, not the entire element!
 export const chunkwrap = Symbol();
 
+// Don't pass this directly, use html.metatag('breakout') instead.
+// Causes *contained* content to be excluded from any *containing* metatag
+// to do with special line breaking, i.e. blockwrap or chunkwrap, regardless
+// the hierarchical distance. Practically, that means momentarily concluding
+// the containing metatag's behavior wherever it operates, then starting more
+// or less anew, so the metatag still applies to any following content.
+export const breakout = Symbol();
+
 // Don't pass this directly, use html.metatag('imaginary-sibling') instead.
 // A tag without any content, which is completely ignored when serializing,
 // but makes siblings with [onlyIfSiblings] feel less shy and show up on
@@ -391,6 +399,9 @@ export function metatag(identifier, ...args) {
     case 'chunkwrap':
       return new Tag(null, {[chunkwrap]: true, ...opts}, content);
 
+    case 'breakout':
+      return new Tag(null, {[breakout]: true}, content);
+
     case 'imaginary-sibling':
       return new Tag(null, {[imaginarySibling]: true}, content);
 
@@ -691,6 +702,14 @@ export class Tag {
     return this.#getAttributeFlag(chunkwrap);
   }
 
+  get breakout() {
+    return this.#getAttributeFlag(breakout);
+  }
+
+  set breakout(value) {
+    this.#setAttributeFlag(breakout, value);
+  }
+
   set imaginarySibling(value) {
     this.#setAttributeFlag(imaginarySibling, value);
 
@@ -783,10 +802,7 @@ export class Tag {
         ? this.#getAttributeRaw('split')
         : null);
 
-    let seenChunkwrapSplitter =
-      (this.chunkwrap
-        ? false
-        : null);
+    let insideOpenChunkwrapChunk = false;
 
     const contentItems =
       (this.chunkwrap
@@ -800,6 +816,10 @@ export class Tag {
       getItemContent: item => item.toString(),
 
       appendItemContent(content, itemContent, item) {
+        // We can only place our own chunkwrap splits within text content -
+        // we can't open a span and then close that span inside a tag that's
+        // CONTAINED inside the very same span. So check for splits in text,
+        // not in tags.
         const chunkwrapChunks =
           (typeof item === 'string' && chunkwrapSplitter
             ? Array.from(getChunkwrapChunks(itemContent, chunkwrapSplitter))
@@ -810,16 +830,33 @@ export class Tag {
             ? chunkwrapChunks.length > 1
             : null);
 
+        // However, we can detect chunkwraps contained *within* non-text
+        // content, and adapt accordingly - by closing the current chunk and
+        // "deferring" to the chunks contained within this item.
+        const itemIncludesItsOwnChunkwrap =
+          typeof item !== 'string' &&
+          chunkwrapSplitter &&
+          itemContent.includes('<span class="chunkwrap"');
+
+        const itemIncludesBreakout =
+          typeof item !== 'string' &&
+          itemContent.includes(`<span class="breakout"`);
+
         if (content) {
-          if (itemIncludesChunkwrapSplit && !seenChunkwrapSplitter) {
+          if (
+            itemIncludesChunkwrapSplit && !insideOpenChunkwrapChunk ||
+            itemIncludesItsOwnChunkwrap && !insideOpenChunkwrapChunk ||
+            itemIncludesBreakout && chunkwrapSplitter && !insideOpenChunkwrapChunk
+          ) {
             // The first time we see a chunkwrap splitter, backtrack and wrap
             // the content *so far* in a chunk. This will be treated just like
             // any other open chunkwrap, and closed after the first chunk of
             // this item! (That means the existing content is part of the same
             // chunk as the first chunk included in this content, which makes
-            // sense, because that first chink is really just more text that
+            // sense, because that first chunk is really just more text that
             // precedes the first split.)
             content = `<span class="chunkwrap">` + content;
+            insideOpenChunkwrapChunk = true;
           }
 
           content += joiner;
@@ -830,10 +867,7 @@ export class Tag {
           // enter a chunkwrap wrapper *now*, so the first chunk of this
           // item will be properly wrapped.
           content = `<span class="chunkwrap">`;
-        }
-
-        if (itemIncludesChunkwrapSplit) {
-          seenChunkwrapSplitter = true;
+          insideOpenChunkwrapChunk = true;
         }
 
         // Blockwraps only apply if they actually contain some content whose
@@ -846,7 +880,41 @@ export class Tag {
           blockwrapClosers += `</span>`;
         }
 
-        if (itemIncludesChunkwrapSplit) {
+        if (itemIncludesItsOwnChunkwrap || itemIncludesBreakout) {
+          const trailingWhitespace = content.match(/\s*$/);
+          if (trailingWhitespace) {
+            content = content.slice(0, -trailingWhitespace.length);
+          }
+
+          if (insideOpenChunkwrapChunk) {
+            content += '</span>';
+          } else if (blockwrapClosers) {
+            content += blockwrapClosers;
+            blockwrapClosers = '';
+          }
+
+          content += trailingWhitespace;
+          content += itemContent;
+
+          if (chunkwrapSplitter) {
+            // Instate a new EVIL chunkwrap: it's not a chunkwrap chunk at all,
+            // because it's treated as display: inline, but will be closed the
+            // same as <span class="chunkwrap">, and can be detected in CSS.
+            content += `<span class="chunkwrap chunkwrapnt">`;
+            insideOpenChunkwrapChunk = true;
+          }
+
+          /* Bonus commentary re: above -
+           * An item that item includes its own chunkwrap purposely causes
+           * its last chunk to "grab" onto the following text out here.
+           * The in-between stuff (up to the chunk we start next - if any)
+           * isn't wrapped within a [display: inline-block] chunk at all,
+           * instead splitting by natural language rules... which probably
+           * isn't the right call, but it *is* the current approach without
+           * introducing new external-attribute logic to handle real world
+           * cases that just haven't come up yet. :pray:
+           */
+        } else if (itemIncludesChunkwrapSplit) {
           for (const [index, {chunk, following}] of chunkwrapChunks.entries()) {
             if (index === 0) {
               // The first chunk isn't actually a chunk all on its own, it's
@@ -878,11 +946,11 @@ export class Tag {
               }
             }
           }
-
-          return content;
+        } else {
+          content += itemContent;
         }
 
-        return content += itemContent;
+        return content;
       },
     });
 
@@ -891,7 +959,7 @@ export class Tag {
     }
 
     if (chunkwrapSplitter) {
-      if (seenChunkwrapSplitter) {
+      if (insideOpenChunkwrapChunk) {
         content += '</span>';
       } else {
         // Since chunkwraps take responsibility for wrapping *away* from the
@@ -900,6 +968,8 @@ export class Tag {
         // been seen at all, just wrap everything in one now.
         content = `<span class="chunkwrap">${content}</span>`;
       }
+    } else if (this.breakout) {
+      content = `<span class="breakout">${content}</span>`;
     }
 
     content += blockwrapClosers;