« 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/aggregate.js2
-rw-r--r--src/content/dependencies/transformContent.js89
-rw-r--r--src/find.js8
-rw-r--r--src/replacer.js170
-rw-r--r--src/static/css/site.css23
-rwxr-xr-xsrc/upd8.js1
-rw-r--r--src/urls-default.yaml2
-rw-r--r--src/write/bind-utilities.js2
8 files changed, 220 insertions, 77 deletions
diff --git a/src/aggregate.js b/src/aggregate.js
index cb806e89..d5ea2d73 100644
--- a/src/aggregate.js
+++ b/src/aggregate.js
@@ -604,7 +604,7 @@ export function showAggregate(topError, {
       headerPart += ` ${colors.dim(tracePart)}`;
     }
 
-    const head1 = level % 2 === 0 ? '\u21aa' : colors.dim('\u21aa');
+    const head1 = '\u21aa';
     const bar1 = ' ';
 
     const causePart =
diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js
index 69ecf5a4..8ba052fd 100644
--- a/src/content/dependencies/transformContent.js
+++ b/src/content/dependencies/transformContent.js
@@ -1,5 +1,6 @@
 import {basename} from 'node:path';
 
+import {logWarn} from '#cli';
 import {bindFind} from '#find';
 import {replacerSpec, parseContentNodes} from '#replacer';
 
@@ -62,20 +63,30 @@ export default {
       Object.values(replacerSpec)
         .map(description => description.link)
         .filter(Boolean)),
+
     'image',
     'generateTextWithTooltip',
     'generateTooltip',
     'linkExternal',
   ],
 
-  extraDependencies: ['html', 'language', 'to', 'wikiData'],
+  extraDependencies: [
+    'html',
+    'language',
+    'niceShowAggregate',
+    'to',
+    'wikiData',
+  ],
 
   sprawl(wikiData, content) {
     const find = bindFind(wikiData);
 
-    const parsedNodes = parseContentNodes(content ?? '');
+    const {result: parsedNodes, error} =
+      parseContentNodes(content ?? '', {errorMode: 'return'});
 
     return {
+      error,
+
       nodes: parsedNodes
         .map(node => {
           if (node.type !== 'tag') {
@@ -189,6 +200,9 @@ export default {
     return {
       content,
 
+      error:
+        sprawl.error,
+
       nodes:
         sprawl.nodes
           .map(node => {
@@ -301,7 +315,12 @@ export default {
     },
   },
 
-  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;
@@ -360,9 +379,8 @@ export default {
                   height && {height},
                   style && {style},
 
-                  align === 'center' &&
-                  !link &&
-                    {class: 'align-center'},
+                  align && !link &&
+                    {class: 'align-' + align},
 
                   pixelate &&
                     {class: 'pixelate'});
@@ -373,8 +391,8 @@ export default {
                     {href: link},
                     {target: '_blank'},
 
-                    align === 'center' &&
-                      {class: 'align-center'},
+                    align &&
+                      {class: 'align-' + align},
 
                     {title:
                       language.encapsulate('misc.external.opensInNewTab', capsule =>
@@ -424,8 +442,8 @@ export default {
               inline: false,
               data:
                 html.tag('div', {class: 'content-image-container'},
-                  align === 'center' &&
-                    {class: 'align-center'},
+                  align &&
+                    {class: 'align-' + align},
 
                   image),
             };
@@ -437,22 +455,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',
@@ -466,15 +493,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});
 
@@ -482,13 +508,14 @@ export default {
               (inline
                 ? audio
                 : html.tag('div', {class: 'content-audio-container'},
-                    align === 'center' &&
-                      {class: 'align-center'},
+                    align &&
+                      {class: 'align-' + align},
 
                     [
-                      html.tag('a', {class: 'filename'},
-                        src && {href: src},
-                        language.sanitize(basename(node.src))),
+                      !nameless &&
+                        html.tag('a', {class: 'filename'},
+                          src && {href: src},
+                          language.sanitize(basename(node.src))),
 
                       audio,
                     ]));
diff --git a/src/find.js b/src/find.js
index d5ef400d..8f2170d4 100644
--- a/src/find.js
+++ b/src/find.js
@@ -391,7 +391,13 @@ function findMixedHelper(config) {
           });
         }
 
-        return byDirectory[referenceType][directory];
+        const match = byDirectory[referenceType][directory];
+
+        if (match) {
+          return match.thing;
+        } else {
+          return null;
+        }
       },
 
       matchByName:
diff --git a/src/replacer.js b/src/replacer.js
index 0698eced..bf28061c 100644
--- a/src/replacer.js
+++ b/src/replacer.js
@@ -8,7 +8,7 @@
 import * as marked from 'marked';
 
 import * as html from '#html';
-import {escapeRegex, typeAppearance} from '#sugar';
+import {empty, escapeRegex, typeAppearance} from '#sugar';
 import {matchMarkdownLinks} from '#wiki-data';
 
 export const replacerSpec = {
@@ -526,6 +526,7 @@ export function postprocessComments(inputNodes) {
 
 function postprocessHTMLTags(inputNodes, tagName, callback) {
   const outputNodes = [];
+  const errors = [];
 
   const lastNode = inputNodes.at(-1);
 
@@ -593,10 +594,16 @@ function postprocessHTMLTags(inputNodes, tagName, callback) {
           return false;
         })();
 
-        outputNodes.push(
-          callback(attributes, {
-            inline,
-          }));
+        try {
+          outputNodes.push(
+            callback(attributes, {
+              inline,
+            }));
+        } catch (caughtError) {
+          errors.push(new Error(
+            `Failed to process ${match[0]}`,
+            {cause: caughtError}));
+        }
 
         // No longer at the start of a line after the tag - there will at
         // least be text with only '\n' before the next of this tag that's
@@ -619,15 +626,33 @@ function postprocessHTMLTags(inputNodes, tagName, callback) {
     outputNodes.push(node);
   }
 
+  if (!empty(errors)) {
+    throw new AggregateError(
+      errors,
+    `Errors postprocessing <${tagName}> tags`);
+  }
+
   return outputNodes;
 }
 
+function complainAboutMediaSrc(src) {
+  if (!src) {
+    throw new Error(`Missing "src" attribute`);
+  }
+
+  if (src.startsWith('/media/')) {
+    throw new Error(`Start "src" with "media/", not "/media/"`);
+  }
+}
+
 export function postprocessImages(inputNodes) {
   return postprocessHTMLTags(inputNodes, 'img',
     (attributes, {inline}) => {
       const node = {type: 'image'};
 
       node.src = attributes.get('src');
+      complainAboutMediaSrc(node.src);
+
       node.inline = attributes.get('inline') ?? inline;
 
       if (attributes.get('link')) node.link = attributes.get('link');
@@ -648,10 +673,13 @@ export function postprocessImages(inputNodes) {
 
 export function postprocessVideos(inputNodes) {
   return postprocessHTMLTags(inputNodes, 'video',
-    attributes => {
+    (attributes, {inline}) => {
       const node = {type: 'video'};
 
       node.src = attributes.get('src');
+      complainAboutMediaSrc(node.src);
+
+      node.inline = attributes.get('inline') ?? inline;
 
       if (attributes.get('width')) node.width = parseInt(attributes.get('width'));
       if (attributes.get('height')) node.height = parseInt(attributes.get('height'));
@@ -668,8 +696,12 @@ export function postprocessAudios(inputNodes) {
       const node = {type: 'audio'};
 
       node.src = attributes.get('src');
+      complainAboutMediaSrc(node.src);
+
       node.inline = attributes.get('inline') ?? inline;
+
       if (attributes.get('align')) node.align = attributes.get('align');
+      if (attributes.get('nameless')) node.nameless = true;
 
       return node;
     });
@@ -821,54 +853,108 @@ export function postprocessExternalLinks(inputNodes) {
   return outputNodes;
 }
 
-export function parseContentNodes(input) {
+export function parseContentNodes(input, {
+  errorMode = 'throw',
+} = {}) {
   if (typeof input !== 'string') {
     throw new TypeError(`Expected input to be string, got ${typeAppearance(input)}`);
   }
 
-  try {
-    let output = parseNodes(input, 0);
-    output = postprocessComments(output);
-    output = postprocessImages(output);
-    output = postprocessVideos(output);
-    output = postprocessAudios(output);
-    output = postprocessHeadings(output);
-    output = postprocessSummaries(output);
-    output = postprocessExternalLinks(output);
-    return output;
-  } catch (errorNode) {
-    if (errorNode.type !== 'error') {
-      throw errorNode;
-    }
+  let result = null, error = null;
 
-    const {
-      i,
-      data: {message},
-    } = errorNode;
+  process: {
+    try {
+      result = parseNodes(input, 0);
+    } catch (caughtError) {
+      if (caughtError.type === 'error') {
+        const {i, data: {message}} = caughtError;
 
-    let lineStart = input.slice(0, i).lastIndexOf('\n');
-    if (lineStart >= 0) {
-      lineStart += 1;
-    } else {
-      lineStart = 0;
+        let lineStart = input.slice(0, i).lastIndexOf('\n');
+        if (lineStart >= 0) {
+          lineStart += 1;
+        } else {
+          lineStart = 0;
+        }
+
+        let lineEnd = input.slice(i).indexOf('\n');
+        if (lineEnd >= 0) {
+          lineEnd += i;
+        } else {
+          lineEnd = input.length;
+        }
+
+        const line = input.slice(lineStart, lineEnd);
+
+        const cursor = i - lineStart;
+
+        error =
+          new SyntaxError(
+            `Parse error (at pos ${i}): ${message}\n` +
+            line + `\n` +
+            '-'.repeat(cursor) + '^');
+      } else {
+        error = caughtError;
+      }
+
+      // A parse error means there's no output to continue with at all,
+      // so stop here.
+      break process;
     }
 
-    let lineEnd = input.slice(i).indexOf('\n');
-    if (lineEnd >= 0) {
-      lineEnd += i;
-    } else {
-      lineEnd = input.length;
+    const postprocessErrors = [];
+
+    for (const postprocess of [
+      postprocessComments,
+      postprocessImages,
+      postprocessVideos,
+      postprocessAudios,
+      postprocessHeadings,
+      postprocessSummaries,
+      postprocessExternalLinks,
+    ]) {
+      try {
+        result = postprocess(result);
+      } catch (caughtError) {
+        const error =
+          new Error(
+            `Error in step ${`"${postprocess.name}"`}`,
+            {cause: caughtError});
+
+        error[Symbol.for('hsmusic.aggregate.translucent')] = true;
+
+        postprocessErrors.push(error);
+      }
     }
 
-    const line = input.slice(lineStart, lineEnd);
+    if (!empty(postprocessErrors)) {
+      error =
+        new AggregateError(
+          postprocessErrors,
+        `Errors postprocessing content text`);
 
-    const cursor = i - lineStart;
+      error[Symbol.for('hsmusic.aggregate.translucent')] = 'single';
+    }
+  }
+
+  if (errorMode === 'throw') {
+    if (error) {
+      throw error;
+    } else {
+      return result;
+    }
+  } else if (errorMode === 'return') {
+    if (!result) {
+      result = [{
+        i: 0,
+        iEnd: input.length,
+        type: 'text',
+        data: input,
+      }];
+    }
 
-    throw new SyntaxError([
-      `Parse error (at pos ${i}): ${message}`,
-      line,
-      '-'.repeat(cursor) + '^',
-    ].join('\n'));
+    return {error, result};
+  } else {
+    throw new Error(`Unknown errorMode ${errorMode}`);
   }
 }
 
diff --git a/src/static/css/site.css b/src/static/css/site.css
index fe16f5d2..f3e0cb2c 100644
--- a/src/static/css/site.css
+++ b/src/static/css/site.css
@@ -1840,12 +1840,20 @@ p.image-details.origin-details .origin-details {
   margin-bottom: 1.5em;
 }
 
-a.align-center, img.align-center, audio.align-center {
+.content-image-container.align-full {
+  width: 100%;
+}
+
+a.align-center, img.align-center, audio.align-center, video.align-center {
   display: block;
   margin-left: auto;
   margin-right: auto;
 }
 
+a.align-full, img.align-full, video.align-full {
+  width: 100%;
+}
+
 center {
   margin-top: 1em;
   margin-bottom: 1em;
@@ -2650,6 +2658,7 @@ html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:las
 .content-video-container,
 .content-audio-container {
   width: fit-content;
+  max-width: 100%;
   background-color: var(--dark-color);
   border: 2px solid var(--primary-color);
   border-radius: 2.5px 2.5px 3px 3px;
@@ -2659,6 +2668,7 @@ html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:las
 .content-video-container video,
 .content-audio-container audio {
   display: block;
+  max-width: 100%;
 }
 
 .content-video-container.align-center,
@@ -2667,6 +2677,11 @@ html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:las
   margin-right: auto;
 }
 
+.content-video-container.align-full,
+.content-audio-container.align-full {
+  width: 100%;
+}
+
 .content-audio-container .filename {
   color: white;
   font-family: monospace;
@@ -2733,6 +2748,12 @@ img {
   object-fit: cover;
 }
 
+p > img {
+  max-width: 100%;
+  object-fit: contain;
+  height: auto;
+}
+
 .image-inner-area::after {
   content: "";
   display: block;
diff --git a/src/upd8.js b/src/upd8.js
index 40a25dfb..145a4f43 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -3207,6 +3207,7 @@ async function main() {
     developersComment,
     languages,
     missingImagePaths,
+    niceShowAggregate,
     thumbsCache,
     urlSpec,
     urls,
diff --git a/src/urls-default.yaml b/src/urls-default.yaml
index 74225efd..cbdd8a23 100644
--- a/src/urls-default.yaml
+++ b/src/urls-default.yaml
@@ -11,7 +11,7 @@ yamlAliases:
   # part of a build. This is so that multiple builds of a wiki can coexist
   # served from the same server / file system root: older builds' HTML files
   # refer to earlier values of STATIC_VERSION, avoiding name collisions.
-  - &staticVersion 5p1
+  - &staticVersion 5p2
 
 data:
   prefix: 'data/'
diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js
index d55ab215..afbf8b2f 100644
--- a/src/write/bind-utilities.js
+++ b/src/write/bind-utilities.js
@@ -24,6 +24,7 @@ export function bindUtilities({
   language,
   languages,
   missingImagePaths,
+  niceShowAggregate,
   pagePath,
   pagePathStringFromRoot,
   thumbsCache,
@@ -42,6 +43,7 @@ export function bindUtilities({
     language,
     languages,
     missingImagePaths,
+    niceShowAggregate,
     pagePath,
     pagePathStringFromRoot,
     thumb,