« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/content/dependencies/transformContent.js58
-rw-r--r--src/util/replacer.js146
2 files changed, 194 insertions, 10 deletions
diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js
index 75cb4847..4e25e18a 100644
--- a/src/content/dependencies/transformContent.js
+++ b/src/content/dependencies/transformContent.js
@@ -43,9 +43,10 @@ export default {
     ...Object.values(linkThingRelationMap),
     ...Object.values(linkValueRelationMap),
     ...Object.values(linkIndexRelationMap),
+    'image',
   ],
 
-  extraDependencies: ['html', 'language', 'wikiData'],
+  extraDependencies: ['html', 'language', 'to', 'wikiData'],
 
   sprawl(wikiData, content) {
     const find = bindFind(wikiData);
@@ -140,14 +141,16 @@ export default {
       nodes:
         sprawl.nodes
           .map(node => {
-            // Replace link nodes with a stub. It'll be replaced (by position)
-            // with an item from relations.
-            if (node.type === 'link') {
-              return {type: 'link'};
+            switch (node.type) {
+              // Replace link nodes with a stub. It'll be replaced (by position)
+              // with an item from relations.
+              case 'link':
+                return {type: 'link'};
+
+              // Other nodes will get processed in generate.
+              default:
+                return node;
             }
-
-            // Other nodes will get processed in generate.
-            return node;
           }),
     };
   },
@@ -181,6 +184,12 @@ export default {
               return relationOrPlaceholder(node, linkIndexRelationMap[key]);
             }
           }),
+
+      images:
+        nodes
+          .filter(({type}) => type === 'image')
+          .filter(({inline}) => !inline)
+          .map(() => relation('image')),
     };
   },
 
@@ -200,8 +209,9 @@ export default {
     },
   },
 
-  generate(data, relations, slots, {html, language}) {
+  generate(data, relations, slots, {html, language, to}) {
     let linkIndex = 0;
+    let imageIndex = 0;
 
     // This array contains only straight text and link nodes, which are directly
     // representable in html (so no further processing is needed on the level of
@@ -212,6 +222,36 @@ export default {
           case 'text':
             return {type: 'text', data: node.data};
 
+          case 'image': {
+            const src =
+              (node.src.startsWith('media/')
+                ? to('media.path', node.src.slice('media/'.length))
+                : node.src);
+
+            const {width, height} = node;
+
+            if (node.inline) {
+              return {
+                type: 'image',
+                data:
+                  html.tag('img', {src, width, height}),
+              };
+            }
+
+            const image = relations.images[imageIndex++];
+
+            return {
+              type: 'image',
+              data:
+                image.slots({
+                  src,
+                  link: true,
+                  width: width ?? null,
+                  height: height ?? null,
+                }),
+            };
+          }
+
           case 'link': {
             const linkNode = relations.links[linkIndex++];
             if (linkNode.type === 'text') {
diff --git a/src/util/replacer.js b/src/util/replacer.js
index 50a90004..8ebd3b6e 100644
--- a/src/util/replacer.js
+++ b/src/util/replacer.js
@@ -292,13 +292,157 @@ function parseNodes(input, i, stopAt, textOnly) {
   return nodes;
 }
 
+function parseAttributes(string) {
+  const attributes = Object.create(null);
+
+  const skipWhitespace = i => {
+    if (!/\s/.test(string[i])) {
+      return i;
+    }
+
+    const match = string.slice(i).match(/[^\s]/);
+    if (match) {
+      return i + match.index;
+    }
+
+    return string.length;
+  };
+
+  for (let i = 0; i < string.length; ) {
+    i = skipWhitespace(i);
+    const aStart = i;
+    const aEnd = i + string.slice(i).match(/[\s=]|$/).index;
+    const attribute = string.slice(aStart, aEnd);
+    i = skipWhitespace(aEnd);
+    if (string[i] === '=') {
+      i = skipWhitespace(i + 1);
+      let end, endOffset;
+      if (string[i] === '"' || string[i] === "'") {
+        end = string[i];
+        endOffset = 1;
+        i++;
+      } else {
+        end = '\\s';
+        endOffset = 0;
+      }
+      const vStart = i;
+      const vEnd = i + string.slice(i).match(new RegExp(`${end}|$`)).index;
+      const value = string.slice(vStart, vEnd);
+      i = vEnd + endOffset;
+      attributes[attribute] = value;
+    } else {
+      attributes[attribute] = attribute;
+    }
+  }
+
+  return (
+    Object.fromEntries(
+      Object.entries(attributes)
+        .map(([key, val]) => [
+          key,
+          val === 'true'
+            ? true
+            : val === 'false'
+            ? false
+            : val === key
+            ? true
+            : val,
+        ])));
+}
+
+export function postprocessImages(inputNodes) {
+  const outputNodes = [];
+
+  let atStartOfLine = true;
+
+  const lastNode = inputNodes[inputNodes.length - 1];
+
+  for (const node of inputNodes) {
+    if (node.type === 'tag') {
+      atStartOfLine = false;
+    }
+
+    if (node.type === 'text') {
+      const imageRegexp = /<img (.*?)>/g;
+
+      let match = null, parseFrom = 0;
+      while (match = imageRegexp.exec(node.data)) {
+        const previousText = node.data.slice(parseFrom, match.index);
+        outputNodes.push({type: 'text', data: previousText});
+        parseFrom = match.index + match[0].length;
+
+        const imageNode = {type: 'image'};
+        const attributes = parseAttributes(match[1]);
+
+        imageNode.src = attributes.src;
+
+        if (previousText.endsWith('\n')) {
+          atStartOfLine = true;
+        }
+
+        imageNode.inline = (() => {
+          // If we've already determined we're in the middle of a line,
+          // we're inline. (Of course!)
+          if (!atStartOfLine) {
+            return true;
+          }
+
+          // If there's more text to go in this text node, and what's
+          // remaining doesn't start with a line break, we're inline.
+          if (
+            parseFrom !== node.data.length &&
+            node.data[parseFrom] !== '\n'
+          ) {
+            return true;
+          }
+
+          // If we're at the end of this text node, but this text node
+          // isn't the last node overall, we're inline.
+          if (
+            parseFrom === node.data.length &&
+            node !== lastNode
+          ) {
+            return true;
+          }
+
+          // If no other condition matches, this image is on its own line.
+          return false;
+        })();
+
+        if (attributes.width) imageNode.width = parseInt(attributes.width);
+        if (attributes.height) imageNode.height = parseInt(attributes.height);
+
+        outputNodes.push(imageNode);
+
+        // No longer at the start of a line after an image - there will at
+        // least be a text node with only '\n' before the next image that's
+        // on its own line.
+        atStartOfLine = false;
+      }
+
+      if (parseFrom !== node.data.length) {
+        outputNodes.push({
+          type: 'text',
+          data: node.data.slice(parseFrom),
+        });
+      }
+
+      continue;
+    }
+
+    outputNodes.push(node);
+  }
+
+  return outputNodes;
+}
+
 export function parseInput(input) {
   if (typeof input !== 'string') {
     throw new TypeError(`Expected input to be string, got ${input}`);
   }
 
   try {
-    return parseNodes(input, 0);
+    return postprocessImages(parseNodes(input, 0));
   } catch (errorNode) {
     if (errorNode.type !== 'error') {
       throw errorNode;