« get me outta code hell

content: sprawl & transformContent - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-05-25 13:23:04 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-05-25 13:23:04 -0300
commit6d8fe82b5386af536ca96eb1d89150e201c603e9 (patch)
treec5d8cce46834facc82e66779e69e7cef67627d28
parentbd0741dcf0c23489bf710249ab8fd9ba647db843 (diff)
content: sprawl & transformContent
Sprawling basically means tying a component to objects which
aren't directly passed to it. This is necessary for functions
like transformContent, which was *mostly* implemented here
(the multiline/lyrics modes are stubs, and a number of links
haven't been implemented yet).
-rw-r--r--src/content-function.js78
-rw-r--r--src/content/dependencies/generatePageLayout.js34
-rw-r--r--src/content/dependencies/linkArtistGallery.js8
-rw-r--r--src/content/dependencies/linkFlash.js8
-rw-r--r--src/content/dependencies/linkGroupGallery.js8
-rw-r--r--src/content/dependencies/linkListing.js8
-rw-r--r--src/content/dependencies/linkNewsEntry.js8
-rw-r--r--src/content/dependencies/linkStaticPage.js8
-rw-r--r--src/content/dependencies/transformContent.js259
-rw-r--r--src/util/replacer.js11
-rw-r--r--src/util/transform-content.js3
-rw-r--r--src/write/bind-utilities.js1
-rw-r--r--src/write/build-modes/live-dev-server.js21
13 files changed, 405 insertions, 50 deletions
diff --git a/src/content-function.js b/src/content-function.js
index bdf9cd29..921b5bcd 100644
--- a/src/content-function.js
+++ b/src/content-function.js
@@ -4,14 +4,16 @@ export default function contentFunction({
   contentDependencies = [],
   extraDependencies = [],
 
+  sprawl,
+  relations,
   data,
   generate,
-  relations,
 }) {
   return expectDependencies({
+    sprawl,
+    relations,
     data,
     generate,
-    relations,
 
     expectedContentDependencyKeys: contentDependencies,
     expectedExtraDependencyKeys: extraDependencies,
@@ -22,9 +24,10 @@ export default function contentFunction({
 contentFunction.identifyingSymbol = Symbol(`Is a content function?`);
 
 export function expectDependencies({
+  sprawl,
+  relations,
   data,
   generate,
-  relations,
 
   expectedContentDependencyKeys,
   expectedExtraDependencyKeys,
@@ -34,8 +37,13 @@ export function expectDependencies({
     throw new Error(`Expected generate function`);
   }
 
-  const hasDataFunction = !!data;
+  const hasSprawlFunction = !!sprawl;
   const hasRelationsFunction = !!relations;
+  const hasDataFunction = !!data;
+
+  if (hasSprawlFunction && !expectedExtraDependencyKeys.includes('wikiData')) {
+    throw new Error(`Content functions which sprawl must specify wikiData in extraDependencies`);
+  }
 
   const fulfilledDependencyKeys = Object.keys(fulfilledDependencies);
 
@@ -98,27 +106,24 @@ export function expectDependencies({
 
   wrappedGenerate[contentFunction.identifyingSymbol] = true;
 
-  if (hasDataFunction) {
-    if (empty(missingContentDependencyKeys)) {
-      wrappedGenerate.data = data;
-    } else {
-      wrappedGenerate.data = function() {
-        throw new Error(`Dependencies still needed: ${missingContentDependencyKeys.join(', ')}`);
-      };
-
-      annotateFunction(wrappedGenerate.data, {name: data, trait: 'unfulfilled'});
-    }
+  if (hasSprawlFunction) {
+    wrappedGenerate.sprawl = sprawl;
   }
 
   if (hasRelationsFunction) {
     wrappedGenerate.relations = relations;
   }
 
+  if (hasDataFunction) {
+    wrappedGenerate.data = data;
+  }
+
   wrappedGenerate.fulfill ??= function fulfill(dependencies) {
     return expectDependencies({
+      sprawl,
+      relations,
       data,
       generate,
-      relations,
 
       expectedContentDependencyKeys,
       expectedExtraDependencyKeys,
@@ -201,19 +206,25 @@ export function fulfillDependencies({
   return newFulfilledDependencies;
 }
 
-export function getRelationsTree(dependencies, contentFunctionName, ...args) {
+export function getRelationsTree(dependencies, contentFunctionName, wikiData, ...args) {
   const relationIdentifier = Symbol('Relation');
 
   function recursive(contentFunctionName, ...args) {
     const contentFunction = dependencies[contentFunctionName];
-    if (!contentFunctionName) {
+    if (!contentFunction) {
       throw new Error(`Couldn't find dependency ${contentFunctionName}`);
     }
 
-    if (!contentFunction?.relations) {
+    if (!contentFunction.relations) {
       return null;
     }
 
+    // TODO: Evaluating a sprawl might belong somewhere better than here, lol...
+    const sprawl =
+      (contentFunction.sprawl
+        ? contentFunction.sprawl(wikiData, ...args)
+        : null)
+
     const relationSlots = {};
 
     const relationSymbolMessage = (() => {
@@ -227,7 +238,10 @@ export function getRelationsTree(dependencies, contentFunctionName, ...args) {
       return {[relationIdentifier]: relationSymbol};
     };
 
-    const relationsLayout = contentFunction.relations(relationFunction, ...args);
+    const relationsLayout =
+      (sprawl
+        ? contentFunction.relations(relationFunction, sprawl, ...args)
+        : contentFunction.relations(relationFunction, ...args))
 
     const relationsTree = Object.fromEntries(
       Object.getOwnPropertySymbols(relationSlots)
@@ -354,7 +368,7 @@ export function quickEvaluate({
       }));
   }
 
-  const treeInfo = getRelationsTree(allContentDependencies, name, ...args);
+  const treeInfo = getRelationsTree(allContentDependencies, name, allExtraDependencies.wikiData ?? {}, ...args);
   const flatTreeInfo = flattenRelationsTree(treeInfo);
   const {root, relationIdentifier, flatRelationSlots} = flatTreeInfo;
 
@@ -421,15 +435,25 @@ export function quickEvaluate({
 
   const slotResults = {};
 
-  function runContentFunction({name, args, relations}) {
+  function runContentFunction({name, args, relations: flatRelations}) {
     const contentFunction = fulfilledContentDependencies[name];
-    const filledRelations =
-      fillRelationsLayoutFromSlotResults(relationIdentifier, slotResults, relations);
 
-    const generateArgs = [
-      contentFunction.data?.(...args),
-      filledRelations,
-    ].filter(Boolean);
+    if (!contentFunction) {
+      throw new Error(`Content function ${name} unfulfilled or not listed`);
+    }
+
+    const sprawl =
+      contentFunction.sprawl?.(allExtraDependencies.wikiData, ...args);
+
+    const relations =
+      fillRelationsLayoutFromSlotResults(relationIdentifier, slotResults, flatRelations);
+
+    const data =
+      (sprawl
+        ? contentFunction.data?.(sprawl, ...args)
+        : contentFunction.data?.(...args));
+
+    const generateArgs = [data, relations].filter(Boolean);
 
     return contentFunction(...generateArgs);
   }
diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js
index e9de61df..be61a6cd 100644
--- a/src/content/dependencies/generatePageLayout.js
+++ b/src/content/dependencies/generatePageLayout.js
@@ -4,6 +4,7 @@ export default {
   contentDependencies: [
     'generateFooterLocalizationLinks',
     'generateStickyHeadingContainer',
+    'transformContent',
   ],
 
   extraDependencies: [
@@ -12,10 +13,23 @@ export default {
     'language',
     'to',
     'transformMultiline',
-    'wikiInfo',
+    'wikiData',
   ],
 
-  relations(relation) {
+  sprawl({wikiInfo}) {
+    return {
+      footerContent: wikiInfo.footerContent,
+      wikiName: wikiInfo.nameShort,
+    };
+  },
+
+  data({wikiName}) {
+    return {
+      wikiName,
+    };
+  },
+
+  relations(relation, sprawl) {
     const relations = {};
 
     relations.footerLocalizationLinks =
@@ -24,16 +38,17 @@ export default {
     relations.stickyHeadingContainer =
       relation('generateStickyHeadingContainer');
 
+    relations.defaultFooterContent =
+      relation('transformContent', sprawl.footerContent);
+
     return relations;
   },
 
-  generate(relations, {
+  generate(data, relations, {
     cachebust,
     html,
     language,
     to,
-    transformMultiline,
-    wikiInfo,
   }) {
     const sidebarSlots = side => ({
       // Content is a flat HTML array. It'll generate one sidebar section
@@ -186,8 +201,9 @@ export default {
 
         let footerContent = slots.footerContent;
 
-        if (html.isBlank(footerContent) && wikiInfo.footerContent) {
-          footerContent = transformMultiline(wikiInfo.footerContent);
+        if (html.isBlank(footerContent)) {
+          footerContent = relations.defaultFooterContent
+            .slot('mode', 'multiline');
         }
 
         const mainHTML =
@@ -251,7 +267,7 @@ export default {
 
                   switch (cur.auto) {
                     case 'home':
-                      title = wikiInfo.nameShort;
+                      title = data.wikiName;
                       href = to('localized.home');
                       break;
                     case 'current':
@@ -400,7 +416,7 @@ export default {
                   showWikiNameInTitle
                     ? language.formatString('misc.pageTitle.withWikiName', {
                         title,
-                        wikiName: wikiInfo.nameShort,
+                        wikiName: data.wikiName,
                       })
                     : language.formatString('misc.pageTitle', {title})),
                 */
diff --git a/src/content/dependencies/linkArtistGallery.js b/src/content/dependencies/linkArtistGallery.js
new file mode 100644
index 00000000..66dc172d
--- /dev/null
+++ b/src/content/dependencies/linkArtistGallery.js
@@ -0,0 +1,8 @@
+export default {
+  contentDependencies: ['linkThing'],
+
+  relations: (relation, artist) =>
+    ({link: relation('linkThing', 'localized.artistGallery', artist)}),
+
+  generate: (relations) => relations.link,
+};
diff --git a/src/content/dependencies/linkFlash.js b/src/content/dependencies/linkFlash.js
new file mode 100644
index 00000000..93dd5a28
--- /dev/null
+++ b/src/content/dependencies/linkFlash.js
@@ -0,0 +1,8 @@
+export default {
+  contentDependencies: ['linkThing'],
+
+  relations: (relation, flash) =>
+    ({link: relation('linkThing', 'localized.flash', flash)}),
+
+  generate: (relations) => relations.link,
+};
diff --git a/src/content/dependencies/linkGroupGallery.js b/src/content/dependencies/linkGroupGallery.js
new file mode 100644
index 00000000..86c4a0f3
--- /dev/null
+++ b/src/content/dependencies/linkGroupGallery.js
@@ -0,0 +1,8 @@
+export default {
+  contentDependencies: ['linkThing'],
+
+  relations: (relation, group) =>
+    ({link: relation('linkThing', 'localized.groupGallery', group)}),
+
+  generate: (relations) => relations.link,
+};
diff --git a/src/content/dependencies/linkListing.js b/src/content/dependencies/linkListing.js
new file mode 100644
index 00000000..f27d93ac
--- /dev/null
+++ b/src/content/dependencies/linkListing.js
@@ -0,0 +1,8 @@
+export default {
+  contentDependencies: ['linkThing'],
+
+  relations: (relation, listing) =>
+    ({link: relation('linkThing', 'localized.listing', listing)}),
+
+  generate: (relations) => relations.link,
+};
diff --git a/src/content/dependencies/linkNewsEntry.js b/src/content/dependencies/linkNewsEntry.js
new file mode 100644
index 00000000..1fb32dd9
--- /dev/null
+++ b/src/content/dependencies/linkNewsEntry.js
@@ -0,0 +1,8 @@
+export default {
+  contentDependencies: ['linkThing'],
+
+  relations: (relation, newsEntry) =>
+    ({link: relation('linkThing', 'localized.newsEntry', newsEntry)}),
+
+  generate: (relations) => relations.link,
+};
diff --git a/src/content/dependencies/linkStaticPage.js b/src/content/dependencies/linkStaticPage.js
new file mode 100644
index 00000000..032af6c9
--- /dev/null
+++ b/src/content/dependencies/linkStaticPage.js
@@ -0,0 +1,8 @@
+export default {
+  contentDependencies: ['linkThing'],
+
+  relations: (relation, staticPage) =>
+    ({link: relation('linkThing', 'localized.staticPage', staticPage)}),
+
+  generate: (relations) => relations.link,
+};
diff --git a/src/content/dependencies/transformContent.js b/src/content/dependencies/transformContent.js
new file mode 100644
index 00000000..262c2982
--- /dev/null
+++ b/src/content/dependencies/transformContent.js
@@ -0,0 +1,259 @@
+import {bindFind} from '../../util/find.js';
+import {parseInput} from '../../util/replacer.js';
+import {replacerSpec} from '../../util/transform-content.js';
+
+const linkThingRelationMap = {
+  album: 'linkAlbum',
+  albumCommentary: 'linkAlbumCommentary',
+  albumGallery: 'linkAlbumGallery',
+  artist: 'linkArtist',
+  artistGallery: 'linkArtistGallery',
+  flash: 'linkFlash',
+  group: 'linkGroup',
+  groupGallery: 'linkGroupGallery',
+  listing: 'linkListing',
+  newsEntry: 'linkNewsEntry',
+  staticPage: 'linkStaticPage',
+  tag: 'linkArtTag',
+  track: 'linkTrack',
+};
+
+const linkValueRelationMap = {
+  // media: 'linkPathFromMedia',
+  // root: 'linkPathFromRoot',
+  // site: 'linkPathFromSite',
+};
+
+const linkIndexRelationMap = {
+  // commentaryIndex: 'linkCommentaryIndex',
+  // flashIndex: 'linkFlashIndex',
+  // home: 'linkHome',
+  // listingIndex: 'linkListingIndex',
+  // newsIndex: 'linkNewsIndex',
+};
+
+function getPlaceholder(node, content) {
+  return {type: 'text', data: content.slice(node.i, node.iEnd)};
+}
+
+export default {
+  contentDependencies: [
+    ...Object.values(linkThingRelationMap),
+    ...Object.values(linkValueRelationMap),
+    ...Object.values(linkIndexRelationMap),
+  ],
+
+  extraDependencies: ['html', 'language', 'wikiData'],
+
+  sprawl(wikiData, content) {
+    const find = bindFind(wikiData);
+
+    const parsedNodes = parseInput(content);
+
+    return {
+      nodes: parsedNodes
+        .map(node => {
+          if (node.type !== 'tag') {
+            return node;
+          }
+
+          const placeholder = getPlaceholder(node, content);
+
+          const replacerKeyImplied = !node.data.replacerKey;
+          const replacerKey = replacerKeyImplied ? 'track' : node.data.replacerKey.data;
+
+          // TODO: We don't support recursive nodes like before, at the moment. Sorry!
+          // const replacerValue = transformNodes(node.data.replacerValue, opts);
+          const replacerValue = node.data.replacerValue[0].data;
+
+          const spec = replacerSpec[replacerKey];
+
+          if (!spec) {
+            return placeholder;
+          }
+
+          if (spec.link) {
+            let data = {key: spec.link};
+
+            determineData: {
+              // No value at all: this is an index link.
+              if (!replacerValue) {
+                break determineData;
+              }
+
+              // Nothing to find: the link operates on a path or string, not a data object.
+              if (!spec.find) {
+                data.value = replacerValue;
+                break determineData;
+              }
+
+              const thing =
+                find[spec.find](
+                  (replacerKeyImplied
+                    ? replacerValue
+                    : replacerKey + `:` + replacerValue),
+                  wikiData);
+
+              // Nothing was found: this is unexpected, so return placeholder.
+              if (!thing) {
+                return placeholder;
+              }
+
+              // Something was found: the link operates on that thing.
+              data.thing = thing;
+            }
+
+            const {transformName} = spec;
+
+            // TODO: Again, no recursive nodes. Sorry!
+            // const enteredLabel = node.data.label && transformNode(node.data.label, opts);
+            const enteredLabel = node.data.label?.data;
+            const enteredHash = node.data.hash?.data;
+
+            data.label =
+              enteredLabel ??
+                (transformName && data.thing.name
+                  ? transformName(data.thing.name)
+                  : null);
+
+            data.hash = enteredHash ?? null;
+
+            return {i: node.i, iEnd: node.iEnd, type: 'link', data};
+          }
+
+          // This will be another {type: 'tag'} node which gets processed in
+          // generate.
+          return node;
+        }),
+    };
+  },
+
+  data(sprawl, content) {
+    return {
+      content,
+
+      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'};
+            }
+
+            // Other nodes will get processed in generate.
+            return node;
+          }),
+    };
+  },
+
+  relations(relation, sprawl, content) {
+    const {nodes} = sprawl;
+
+    const relationOrPlaceholder =
+      (node, name, arg) =>
+        (name
+          ? {
+              link: relation(name, arg),
+              label: node.data.label,
+              hash: node.data.hash,
+            }
+          : getPlaceholder(node, content));
+
+    return {
+      links:
+        nodes
+          .filter(({type}) => type === 'link')
+          .map(node => {
+            const {key, thing, value} = node.data;
+
+            if (thing) {
+              return relationOrPlaceholder(node, linkThingRelationMap[key], thing);
+            } else if (value) {
+              return relationOrPlaceholder(node, linkValueRelationMap[key], value);
+            } else {
+              return relationOrPlaceholder(node, linkIndexRelationMap[key]);
+            }
+          }),
+    };
+  },
+
+  generate(data, relations, {html, language}) {
+    let linkIndex = 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
+    // individual nodes).
+    const contentFromNodes =
+      data.nodes.map(node => {
+        if (node.type === 'text') {
+          return {type: 'text', data: node.data};
+        }
+
+        if (node.type === 'link') {
+          const {link, label, hash} = relations.links[linkIndex++];
+          return {
+            type: 'link',
+            data: link.slots({content: label, hash}),
+          };
+        }
+
+        if (node.type === 'tag') {
+          const {replacerKey, replacerValue} = node.data;
+
+          const spec = replacerSpec[replacerKey];
+
+          if (!spec) {
+            return getPlaceholder(node, data.content);
+          }
+
+          const {value: valueFn, html: htmlFn} = spec;
+
+          const value =
+            (valueFn
+              ? valueFn(replacerValue)
+              : replacerValue);
+
+          const contents =
+            (htmlFn
+              ? htmlFn(value, {html, language})
+              : value);
+
+          return {type: 'text', data: contents};
+        }
+
+        return getPlaceholder(node, data.content);
+      });
+
+    return html.template({
+      annotation: `transformContent`,
+
+      slots: {
+        mode: {
+          validate: v => v.is('inline', 'multiline', 'lyrics'),
+          default: 'multiline',
+        },
+      },
+
+      content(slots) {
+        // In inline mode, no further processing is needed!
+
+        if (slots.mode === 'inline') {
+          return html.tags(contentFromNodes.map(node => node.data));
+        }
+
+        // In multiline mode...
+
+        if (slots.mode === 'multiline') {
+          return html.tags(contentFromNodes.map(node => node.data));
+        }
+
+        // In lyrics mode...
+
+        if (slots.mode === 'lyrics') {
+          return html.tags(contentFromNodes.map(node => node.data));
+        }
+      },
+    });
+  },
+}
diff --git a/src/util/replacer.js b/src/util/replacer.js
index ea957eda..50a90004 100644
--- a/src/util/replacer.js
+++ b/src/util/replacer.js
@@ -221,11 +221,10 @@ function parseNodes(input, i, stopAt, textOnly) {
       let hash;
 
       if (stop_literal === tagHash) {
-        N = parseNodes(input, i, [R_tagArgument, R_tagLabel, R_tagEnding]);
+        N = parseOneTextNode(input, i, [R_tagArgument, R_tagLabel, R_tagEnding]);
 
         if (!stopped) throw endOfInput(i, `reading hash`);
-
-        if (!N) throw makeError(i, `Expected content (hash).`);
+        if (!N) throw makeError(i, `Expected text (hash).`);
 
         hash = N;
         i = stop_iParse;
@@ -294,6 +293,10 @@ function parseNodes(input, i, stopAt, textOnly) {
 }
 
 export function parseInput(input) {
+  if (typeof input !== 'string') {
+    throw new TypeError(`Expected input to be string, got ${input}`);
+  }
+
   try {
     return parseNodes(input, 0);
   } catch (errorNode) {
@@ -378,7 +381,7 @@ function evaluateTag(node, opts) {
     (transformName && transformName(value.name, node, input)) ||
     null;
 
-  const hash = node.data.hash && transformNodes(node.data.hash, opts);
+  const hash = node.data.hash && transformNode(node.data.hash, opts);
 
   const args =
     node.data.args &&
diff --git a/src/util/transform-content.js b/src/util/transform-content.js
index d1d0f51a..454cb374 100644
--- a/src/util/transform-content.js
+++ b/src/util/transform-content.js
@@ -3,7 +3,6 @@
 // interfaces for converting various content found in wiki data to HTML for
 // display on the site.
 
-import * as html from './html.js';
 export {transformInline} from './replacer.js';
 
 export const replacerSpec = {
@@ -34,7 +33,7 @@ export const replacerSpec = {
   date: {
     find: null,
     value: (ref) => new Date(ref),
-    html: (date, {language}) =>
+    html: (date, {html, language}) =>
       html.tag('time',
         {datetime: date.toString()},
         language.formatDate(date)),
diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js
index be9ad66c..a31e02f7 100644
--- a/src/write/bind-utilities.js
+++ b/src/write/bind-utilities.js
@@ -49,6 +49,7 @@ export function bindUtilities({
     thumb,
     to,
     urls,
+    wikiData,
     wikiInfo: wikiData.wikiInfo,
   });
 
diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js
index c15fc465..d4b7472d 100644
--- a/src/write/build-modes/live-dev-server.js
+++ b/src/write/build-modes/live-dev-server.js
@@ -364,7 +364,7 @@ export async function go({
 
       // NOTE: ALL THIS STUFF IS PASTED, REVIEW AND INTEGRATE SOON(TM)
 
-      const treeInfo = getRelationsTree(allContentDependencies, name, ...args);
+      const treeInfo = getRelationsTree(allContentDependencies, name, wikiData, ...args);
       const flatTreeInfo = flattenRelationsTree(treeInfo);
       const {root, relationIdentifier, flatRelationSlots} = flatTreeInfo;
 
@@ -431,20 +431,25 @@ export async function go({
 
       const slotResults = {};
 
-      function runContentFunction({name, args, relations}) {
+      function runContentFunction({name, args, relations: flatRelations}) {
         const contentFunction = fulfilledContentDependencies[name];
 
         if (!contentFunction) {
           throw new Error(`Content function ${name} unfulfilled or not listed`);
         }
 
-        const filledRelations =
-          fillRelationsLayoutFromSlotResults(relationIdentifier, slotResults, relations);
+        const sprawl =
+          contentFunction.sprawl?.(allExtraDependencies.wikiData, ...args);
 
-        const generateArgs = [
-          contentFunction.data?.(...args),
-          filledRelations,
-        ].filter(Boolean);
+        const relations =
+          fillRelationsLayoutFromSlotResults(relationIdentifier, slotResults, flatRelations);
+
+        const data =
+          (sprawl
+            ? contentFunction.data?.(sprawl, ...args)
+            : contentFunction.data?.(...args));
+
+        const generateArgs = [data, relations].filter(Boolean);
 
         return contentFunction(...generateArgs);
       }