« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/content/dependencies/transformContent.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/content/dependencies/transformContent.js')
-rw-r--r--src/content/dependencies/transformContent.js290
1 files changed, 205 insertions, 85 deletions
diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js
index c1415474..8e902647 100644
--- a/src/content/dependencies/transformContent.js
+++ b/src/content/dependencies/transformContent.js
@@ -1,3 +1,6 @@
+import {basename} from 'node:path';
+
+import {logWarn} from '#cli';
 import {bindFind} from '#find';
 import {replacerSpec, parseContentNodes} from '#replacer';
 
@@ -26,14 +29,36 @@ const commonMarkedOptions = {
 
 const multilineMarked = new Marked({
   ...commonMarkedOptions,
+
+  renderer: {
+    code({text}) {
+      let lines = text
+        .replace(/^\n+/, '')
+        .replace(/\n+$/, '')
+        .split('\n');
+
+      lines = lines
+        .map(line => line
+          .replace(/^ +/, spaces => '&nbsp'.repeat(spaces.length))
+          .replaceAll(/ {2,}/g, spaces => '&nbsp'.repeat(spaces.length)));
+
+      return (
+        `<pre class="content-code"><span><code>` +
+        (lines.length > 1 ? '\n' : '') +
+        lines.join('<br>\n') +
+        (lines.length > 1 ? '\n' : '') +
+        `</pre></span></code>`
+      );
+    },
+  },
 });
 
 const inlineMarked = new Marked({
   ...commonMarkedOptions,
 
   renderer: {
-    paragraph(text) {
-      return text;
+    paragraph({tokens}) {
+      return this.parser.parseInline(tokens);
     },
   },
 });
@@ -55,27 +80,38 @@ function getArg(node, argKey) {
 }
 
 export default {
-  contentDependencies: [
-    ...(
-      Object.values(replacerSpec)
-        .map(description => description.link)
-        .filter(Boolean)),
-    'image',
-    'generateTextWithTooltip',
-    'generateTooltip',
-    'linkExternal',
-  ],
-
-  extraDependencies: ['html', 'language', 'to', 'wikiData'],
-
   sprawl(wikiData, content) {
-    const find = bindFind(wikiData);
+    const find =
+      bindFind(wikiData, {
+        mode: 'quiet',
+        fuzz: {
+          capitalization: true,
+          kebab: true,
+        },
+      });
 
-    const parsedNodes = parseContentNodes(content ?? '');
+    const {result: parsedNodes, error} =
+      parseContentNodes(content ?? '', {errorMode: 'return'});
 
     return {
+      error,
+
       nodes: parsedNodes
         .map(node => {
+          if (node.type === 'tooltip') {
+            return {
+              i: node.i,
+              iEnd: node.iEnd,
+              type: 'tooltip',
+              data: {
+                // No recursion yet. Sorry!
+                tooltip: node.data.content[0].data,
+                label: node.data.label[0].data,
+                link: null,
+              },
+            };
+          }
+
           if (node.type !== 'tag') {
             return node;
           }
@@ -96,7 +132,7 @@ export default {
           }
 
           if (spec.link) {
-            let data = {link: spec.link};
+            let data = {link: spec.link, replacerKey, replacerValue};
 
             determineData: {
               // No value at all: this is an index link.
@@ -135,9 +171,16 @@ export default {
 
             data.label =
               enteredLabel ??
-                (transformName && data.thing.name
-                  ? transformName(data.thing.name, node, content)
-                  : null);
+
+              (transformName && data.thing.name &&
+               replacerKeyImplied && replacerValue === data.thing.name
+
+                ? transformName(data.thing.name, node, content)
+                : null) ??
+
+              (replacerKeyImplied
+                ? replacerValue
+                : null);
 
             data.hash = enteredHash ?? null;
 
@@ -175,8 +218,8 @@ export default {
             ...node,
             data: {
               ...node.data,
-              replacerKey: node.data.replacerKey.data,
-              replacerValue: node.data.replacerValue[0].data,
+              replacerKey,
+              replacerValue,
             },
           };
         }),
@@ -187,25 +230,11 @@ export default {
     return {
       content,
 
+      error:
+        sprawl.error,
+
       nodes:
-        sprawl.nodes
-          .map(node => {
-            switch (node.type) {
-              // Replace internal link nodes with a stub. It'll be replaced
-              // (by position) with an item from relations.
-              //
-              // TODO: This should be where label and hash get passed through,
-              // rather than in relations... (in which case there's no need to
-              // handle it specially here, and we can really just return
-              // data.nodes = sprawl.nodes)
-              case 'internal-link':
-                return {type: 'internal-link'};
-
-              // Other nodes will get processed in generate.
-              default:
-                return node;
-            }
-          }),
+        sprawl.nodes,
     };
   },
 
@@ -297,9 +326,29 @@ export default {
       validate: v => v.is('small', 'medium', 'large'),
       default: 'large',
     },
+
+    substitute: {
+      validate: v =>
+        v.strictArrayOf(
+          v.validateProperties({
+            match: v.validateProperties({
+              replacerKey: v.isString,
+              replacerValue: v.isString,
+            }),
+
+            substitute: v.isHTML,
+
+            apply: v.optional(v.isFunction),
+          })),
+    },
   },
 
-  generate(data, relations, slots, {html, language, to}) {
+  generate(data, relations, slots, {html, language, niceShowAggregate, to}) {
+    if (data.error) {
+      logWarn`Error in content text.`;
+      niceShowAggregate(data.error);
+    }
+
     let imageIndex = 0;
     let internalLinkIndex = 0;
     let externalLinkIndex = 0;
@@ -307,6 +356,24 @@ export default {
 
     let offsetTextNode = 0;
 
+    const substitutions =
+      (slots.substitute
+        ? slots.substitute.slice()
+        : []);
+
+    const pickSubstitution = node => {
+      const index =
+        substitutions.findIndex(({match}) =>
+          match.replacerKey === node.data.replacerKey &&
+          match.replacerValue === node.data.replacerValue);
+
+      if (index === -1) {
+        return null;
+      }
+
+      return substitutions.splice(index, 1).at(0);
+    };
+
     const contentFromNodes =
       data.nodes.map((node, index) => {
         const nextNode = data.nodes[index + 1];
@@ -325,6 +392,25 @@ export default {
           }
         };
 
+        const substitution = pickSubstitution(node);
+
+        if (substitution) {
+          const source =
+            substitution.substitute;
+
+          let substitute = source;
+
+          if (substitution.apply) {
+            const result = substitution.apply(source, node);
+
+            if (result !== undefined) {
+              substitute = result;
+            }
+          }
+
+          return {type: 'substitution', data: substitute};
+        }
+
         switch (node.type) {
           case 'text': {
             const text = node.data.slice(offsetTextNode);
@@ -358,9 +444,8 @@ export default {
                   height && {height},
                   style && {style},
 
-                  align === 'center' &&
-                  !link &&
-                    {class: 'align-center'},
+                  align && !link &&
+                    {class: 'align-' + align},
 
                   pixelate &&
                     {class: 'pixelate'});
@@ -371,8 +456,8 @@ export default {
                     {href: link},
                     {target: '_blank'},
 
-                    align === 'center' &&
-                      {class: 'align-center'},
+                    align &&
+                      {class: 'align-' + align},
 
                     {title:
                       language.encapsulate('misc.external.opensInNewTab', capsule =>
@@ -422,8 +507,8 @@ export default {
               inline: false,
               data:
                 html.tag('div', {class: 'content-image-container'},
-                  align === 'center' &&
-                    {class: 'align-center'},
+                  align &&
+                    {class: 'align-' + align},
 
                   image),
             };
@@ -435,22 +520,31 @@ export default {
                 ? to('media.path', node.src.slice('media/'.length))
                 : node.src);
 
-            const {width, height, align, pixelate} = node;
+            const {width, height, align, inline, pixelate} = node;
 
-            const content =
-              html.tag('div', {class: 'content-video-container'},
-                align === 'center' &&
-                  {class: 'align-center'},
+            const video =
+              html.tag('video',
+                src && {src},
+                width && {width},
+                height && {height},
 
-                html.tag('video',
-                  src && {src},
-                  width && {width},
-                  height && {height},
+                {controls: true},
 
-                  {controls: true},
+                align && inline &&
+                  {class: 'align-' + align},
+
+                pixelate &&
+                  {class: 'pixelate'});
+
+            const content =
+              (inline
+                ? video
+                : html.tag('div', {class: 'content-video-container'},
+                    align &&
+                      {class: 'align-' + align},
+
+                    video));
 
-                  pixelate &&
-                    {class: 'pixelate'}));
 
             return {
               type: 'processed-video',
@@ -464,15 +558,14 @@ export default {
                 ? to('media.path', node.src.slice('media/'.length))
                 : node.src);
 
-            const {align, inline} = node;
+            const {align, inline, nameless} = node;
 
             const audio =
               html.tag('audio',
                 src && {src},
 
-                align === 'center' &&
-                inline &&
-                  {class: 'align-center'},
+                align && inline &&
+                  {class: 'align-' + align},
 
                 {controls: true});
 
@@ -480,10 +573,17 @@ export default {
               (inline
                 ? audio
                 : html.tag('div', {class: 'content-audio-container'},
-                    align === 'center' &&
-                      {class: 'align-center'},
+                    align &&
+                      {class: 'align-' + align},
 
-                    audio));
+                    [
+                      !nameless &&
+                        html.tag('a', {class: 'filename'},
+                          src && {href: src},
+                          language.sanitize(basename(node.src))),
+
+                      audio,
+                    ]));
 
             return {
               type: 'processed-audio',
@@ -529,7 +629,7 @@ export default {
             try {
               link.getSlotDescription('preferShortName');
               hasPreferShortNameSlot = true;
-            } catch (error) {
+            } catch {
               hasPreferShortNameSlot = false;
             }
 
@@ -542,7 +642,7 @@ export default {
             try {
               link.getSlotDescription('tooltipStyle');
               hasTooltipStyleSlot = true;
-            } catch (error) {
+            } catch {
               hasTooltipStyleSlot = false;
             }
 
@@ -566,9 +666,12 @@ export default {
           }
 
           case 'external-link': {
-            const {label} = node.data;
             const externalLink = relations.externalLinks[externalLinkIndex++];
 
+            const label =
+              node.data.label ??
+              node.data.href.replace(/^https?:\/\//, '');
+
             if (slots.textOnly) {
               return {type: 'text', data: label};
             }
@@ -786,20 +889,37 @@ export default {
     // This is separated into its own function just since we're gonna reuse
     // it in a minute if everything goes to heck in lyrics mode.
     const transformMultiline = () => {
-      const markedInput =
-        extractNonTextNodes()
-          // Compress multiple line breaks into single line breaks,
-          // except when they're preceding or following indented
-          // text (by at least two spaces).
-          .replace(/(?<!  .*)\n{2,}(?!^  )/gm, '\n') /* eslint-disable-line no-regex-spaces */
-          // Expand line breaks which don't follow a list, quote,
-          // or <br> / "  ", and which don't precede or follow
-          // indented text (by at least two spaces).
-          .replace(/(?<!^ *(?:-|\d+\.).*|^>.*|^  .*\n*|  $|<br>$)\n+(?!  |\n)/gm, '\n\n') /* eslint-disable-line no-regex-spaces */
-          // Expand line breaks which are at the end of a list.
-          .replace(/(?<=^ *(?:-|\d+\.).*)\n+(?!^ *(?:-|\d+\.))/gm, '\n\n')
-          // Expand line breaks which are at the end of a quote.
-          .replace(/(?<=^>.*)\n+(?!^>)/gm, '\n\n');
+      let fencedCode = [];
+
+      const fencedCodePlaceholder =
+        `<span class="INSERT-FENCED-CODE"></span>`;
+
+      let markedInput = extractNonTextNodes();
+
+      markedInput = markedInput
+        .replaceAll(/```(?:[\s\S](?!```))*\n```/g, (match) => {
+          fencedCode.push(match);
+          return fencedCodePlaceholder;
+        });
+
+      markedInput = markedInput
+        // Compress multiple line breaks into single line breaks,
+        // except when they're preceding or following indented
+        // text (by at least two spaces) or blockquotes.
+        .replace(/(?<!^  .*|^>.*)\n{2,}(?!^  |^>)/gm, '\n') /* eslint-disable-line no-regex-spaces */
+        // Expand line breaks which don't follow a list, quote,
+        // or <br> / "  ", and which don't precede or follow
+        // indented text (by at least two spaces).
+        .replace(/(?<!^ *(?:-|\d+\.).*|^>.*|^  .*\n*|  $|<br>$)\n+(?!  |\n)/gm, '\n\n') /* eslint-disable-line no-regex-spaces */
+        // Expand line breaks which are at the end of a list.
+        .replace(/(?<=^ *(?:-|\d+\.).*)\n+(?!^ *(?:-|\d+\.))/gm, '\n\n')
+        // Expand line breaks which are at the end of a quote.
+        .replace(/(?<=^>.*)\n+(?!^>)/gm, '\n\n');
+
+      fencedCode = fencedCode.reverse();
+
+      markedInput = markedInput
+        .replaceAll(fencedCodePlaceholder, () => fencedCode.pop());
 
       const markedOutput =
         multilineMarked.parse(markedInput);