« 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-function.js221
-rw-r--r--src/content/dependencies/generateAdditionalFilesShortcut.js27
-rw-r--r--src/content/dependencies/generateAlbumInfoPageContent.js96
-rw-r--r--src/content/dependencies/linkAlbumAdditionalFile.js16
-rw-r--r--src/content/dependencies/linkAlbumCommentary.js8
-rw-r--r--src/content/dependencies/linkAlbumGallery.js8
-rw-r--r--src/content/dependencies/linkArtist.js11
-rw-r--r--src/content/dependencies/linkTemplate.js53
-rw-r--r--src/content/dependencies/linkThing.js51
9 files changed, 433 insertions, 58 deletions
diff --git a/src/content-function.js b/src/content-function.js
index 891a348f..dbac691b 100644
--- a/src/content-function.js
+++ b/src/content-function.js
@@ -200,3 +200,224 @@ export function fulfillDependencies({
 
   return newFulfilledDependencies;
 }
+
+export function getRelationsTree(dependencies, contentFunctionName, ...args) {
+  const relationIdentifier = Symbol('Relation');
+
+  function recursive(contentFunctionName, ...args) {
+    const contentFunction = dependencies[contentFunctionName];
+    if (!contentFunctionName) {
+      throw new Error(`Couldn't find dependency ${contentFunctionName}`);
+    }
+
+    if (!contentFunction?.relations) {
+      return null;
+    }
+
+    const relationSlots = {};
+
+    const relationSymbolMessage = (() => {
+      let num = 1;
+      return name => `#${num++} ${name}`;
+    })();
+
+    const relationFunction = (name, ...args) => {
+      const relationSymbol = Symbol(relationSymbolMessage(name));
+      relationSlots[relationSymbol] = {name, args};
+      return {[relationIdentifier]: relationSymbol};
+    };
+
+    const relationsLayout = contentFunction.relations(relationFunction, ...args);
+
+    const relationsTree = Object.fromEntries(
+      Object.getOwnPropertySymbols(relationSlots)
+        .map(symbol => [symbol, relationSlots[symbol]])
+        .map(([symbol, {name, args}]) => [
+          symbol,
+          recursive(name, ...args),
+        ]));
+
+    return {
+      layout: relationsLayout,
+      slots: relationSlots,
+      tree: relationsTree,
+    };
+  }
+
+  const relationsTree = recursive(contentFunctionName, ...args);
+
+  return {
+    root: {
+      name: contentFunctionName,
+      args,
+      relations: relationsTree?.layout,
+    },
+
+    relationIdentifier,
+    relationsTree,
+  };
+}
+
+export function flattenRelationsTree({
+  root,
+  relationIdentifier,
+  relationsTree,
+}) {
+  const flatRelationSlots = {};
+
+  function recursive({layout, slots, tree}) {
+    for (const slot of Object.getOwnPropertySymbols(slots)) {
+      if (tree[slot]) {
+        recursive(tree[slot]);
+      }
+
+      flatRelationSlots[slot] = {
+        name: slots[slot].name,
+        args: slots[slot].args,
+        relations: tree[slot]?.layout ?? null,
+      };
+    }
+  }
+
+  recursive(relationsTree);
+
+  return {
+    root,
+    relationIdentifier,
+    flatRelationSlots,
+  };
+}
+
+export function fillRelationsLayoutFromSlotResults(relationIdentifier, results, layout) {
+  function recursive(object) {
+    if (typeof object !== 'object' || object === null) {
+      return object;
+    }
+
+    if (Array.isArray(object)) {
+      return object.map(recursive);
+    }
+
+    if (relationIdentifier in object) {
+      return results[object[relationIdentifier]];
+    }
+
+    if (object.constructor !== Object) {
+      throw new Error(`Expected primitive, array, relation, or normal {key: value} style Object`);
+    }
+
+    return Object.fromEntries(
+      Object.entries(object)
+        .map(([key, value]) => [key, recursive(value)]));
+  }
+
+  return recursive(layout);
+}
+
+function getNeededContentDependencyNames(contentDependencies, name) {
+  const set = new Set();
+
+  function recursive(name) {
+    const contentFunction = contentDependencies[name];
+    for (const dependencyName of contentFunction?.contentDependencies ?? []) {
+      recursive(dependencyName);
+    }
+    set.add(name);
+  }
+
+  recursive(name);
+
+  return set;
+}
+
+export function quickEvaluate({
+  contentDependencies: allContentDependencies,
+  extraDependencies: allExtraDependencies,
+
+  name,
+  args,
+}) {
+  const treeInfo = getRelationsTree(allContentDependencies, name, ...args);
+  const flatTreeInfo = flattenRelationsTree(treeInfo);
+  const {root, relationIdentifier, flatRelationSlots} = flatTreeInfo;
+
+  const neededContentDependencyNames =
+    getNeededContentDependencyNames(allContentDependencies, name);
+
+  // Content functions aren't recursive, so by following the set above
+  // sequentually, we will always provide fulfilled content functions as the
+  // dependencies for later content functions.
+  const fulfilledContentDependencies = {};
+  for (const name of neededContentDependencyNames) {
+    const unfulfilledContentFunction = allContentDependencies[name];
+    if (!unfulfilledContentFunction) continue;
+
+    const {contentDependencies, extraDependencies} = unfulfilledContentFunction;
+
+    if (empty(contentDependencies) && empty(extraDependencies)) {
+      fulfilledContentDependencies[name] = unfulfilledContentFunction;
+      continue;
+    }
+
+    const fulfillments = {};
+
+    for (const dependencyName of contentDependencies ?? []) {
+      if (dependencyName in fulfilledContentDependencies) {
+        fulfillments[dependencyName] =
+          fulfilledContentDependencies[dependencyName];
+      }
+    }
+
+    for (const dependencyName of extraDependencies ?? []) {
+      if (dependencyName in allExtraDependencies) {
+        fulfillments[dependencyName] =
+          allExtraDependencies[dependencyName];
+      }
+    }
+
+    fulfilledContentDependencies[name] =
+      unfulfilledContentFunction.fulfill(fulfillments);
+  }
+
+  // There might still be unfulfilled content functions if dependencies weren't
+  // provided as part of allContentDependencies or allExtraDependencies.
+  // Catch and report these early, together in an aggregate error.
+  const unfulfilledErrors = [];
+  for (const name of neededContentDependencyNames) {
+    const contentFunction = fulfilledContentDependencies[name];
+    if (!contentFunction) continue;
+    if (!contentFunction.fulfilled) {
+      try {
+        contentFunction();
+      } catch (error) {
+        error.message = `(${name}) ${error.message}`;
+        unfulfilledErrors.push(error);
+      }
+    }
+  }
+
+  if (!empty(unfulfilledErrors)) {
+    throw new AggregateError(unfulfilledErrors, `Content functions unfulfilled`);
+  }
+
+  const slotResults = {};
+
+  function runContentFunction({name, args, relations}) {
+    const contentFunction = fulfilledContentDependencies[name];
+    const filledRelations =
+      fillRelationsLayoutFromSlotResults(relationIdentifier, slotResults, relations);
+
+    const generateArgs = [
+      contentFunction.data?.(...args),
+      filledRelations,
+    ].filter(Boolean);
+
+    return contentFunction(...generateArgs);
+  }
+
+  for (const slot of Object.getOwnPropertySymbols(flatRelationSlots)) {
+    slotResults[slot] = runContentFunction(flatRelationSlots[slot]);
+  }
+
+  return runContentFunction(root);
+}
diff --git a/src/content/dependencies/generateAdditionalFilesShortcut.js b/src/content/dependencies/generateAdditionalFilesShortcut.js
new file mode 100644
index 00000000..dd097e28
--- /dev/null
+++ b/src/content/dependencies/generateAdditionalFilesShortcut.js
@@ -0,0 +1,27 @@
+export default {
+  extraDependencies: [
+    'html',
+    'language',
+  ],
+
+  data(additionalFiles) {
+    return {
+      titles: additionalFiles.map(fileGroup => fileGroup.title),
+    };
+  },
+
+  generate(data, {
+    html,
+    language,
+  }) {
+    return language.$('releaseInfo.additionalFiles.shortcut', {
+      anchorLink:
+        html.tag('a',
+          {href: '#additional-files'},
+          language.$('releaseInfo.additionalFiles.shortcut.anchorLink')),
+
+      titles:
+        language.formatUnitList(data.titles),
+    });
+  },
+}
diff --git a/src/content/dependencies/generateAlbumInfoPageContent.js b/src/content/dependencies/generateAlbumInfoPageContent.js
index 013ab3f4..236e550e 100644
--- a/src/content/dependencies/generateAlbumInfoPageContent.js
+++ b/src/content/dependencies/generateAlbumInfoPageContent.js
@@ -2,9 +2,12 @@ import {accumulateSum, empty} from '../../util/sugar.js';
 
 export default {
   contentDependencies: [
+    'generateAdditionalFilesShortcut',
     'generateAlbumAdditionalFilesList',
     'generateContributionLinks',
     'generateContentHeading',
+    'linkAlbumCommentary',
+    'linkAlbumGallery',
   ],
 
   extraDependencies: [
@@ -18,9 +21,9 @@ export default {
 
     const contributionLinksRelation = contribs =>
       relation('generateContributionLinks', contribs, {
-        showContrib: true,
+        showContribution: true,
         showIcons: true,
-      })
+      });
 
     relations.artistLinks =
       contributionLinksRelation(album.artistContribs);
@@ -37,7 +40,20 @@ export default {
     const contentHeadingRelation = () =>
       relation('generateContentHeading');
 
+    if (album.tracks.some(t => t.hasUniqueCoverArt)) {
+      relations.galleryLink =
+        relation('linkAlbumGallery', album);
+    }
+
+    if (album.commentary || album.tracks.some(t => t.commentary)) {
+      relations.commentaryLink =
+        relation('linkAlbumCommentary', album);
+    }
+
     if (!empty(album.additionalFiles)) {
+      relations.additionalFilesShortcut =
+        relation('generateAdditionalFilesShortcut', album.additionalFiles);
+
       relations.additionalFilesHeading =
         contentHeadingRelation();
 
@@ -84,29 +100,29 @@ export default {
 
     content.main = {
       headingMode: 'sticky',
-      content: [
+      content: html.tag(null, [
         html.tag('p',
           {
             [html.onlyIfContent]: true,
             [html.joinChildren]: '<br>',
           },
           [
-            !empty(relations.artistLinks) &&
+            relations.artistLinks &&
               language.$('releaseInfo.by', {
                 artists: relations.artistLinks,
               }),
 
-            !empty(relations.coverArtistLinks) &&
+            relations.coverArtistLinks &&
               language.$('releaseInfo.coverArtBy', {
                 artists: relations.coverArtistLinks,
               }),
 
-            !empty(relations.wallpaperArtistLinks) &&
+            relations.wallpaperArtistLinks &&
               language.$('releaseInfo.wallpaperArtBy', {
                 artists: relations.wallpaperArtistLinks,
               }),
 
-            !empty(relations.bannerArtistLinks) &&
+            relations.bannerArtistLinks &&
               language.$('releaseInfo.bannerArtBy', {
                 artists: relations.bannerArtistLinks,
               }),
@@ -130,31 +146,30 @@ export default {
               }),
           ]),
 
-        /*
-          html.tag('p',
-            {
-              [html.onlyIfContent]: true,
-              [html.joinChildren]: '<br>',
-            },
-            [
-              hasAdditionalFiles &&
-                generateAdditionalFilesShortcut(album.additionalFiles),
+        html.tag('p',
+          {
+            [html.onlyIfContent]: true,
+            [html.joinChildren]: html.tag('br'),
+          },
+          [
+            relations.additionalFilesShortcut,
 
-              checkGalleryPage(album) &&
-                language.$('releaseInfo.viewGallery', {
-                  link: link.albumGallery(album, {
-                    text: language.$('releaseInfo.viewGallery.link'),
-                  }),
-                }),
+            relations.galleryLink &&
+              language.$('releaseInfo.viewGallery', {
+                link:
+                  relations.galleryLink
+                    .slot('text', language.$('releaseInfo.viewGallery.link')),
+              }),
 
-              checkCommentaryPage(album) &&
-                language.$('releaseInfo.viewCommentary', {
-                  link: link.albumCommentary(album, {
-                    text: language.$('releaseInfo.viewCommentary.link'),
-                  }),
-                }),
-            ]),
+            relations.commentaryLink &&
+              language.$('releaseInfo.viewCommentary', {
+                link:
+                  relations.commentaryLink
+                    .slot('text', language.$('releaseInfo.viewCommentary.link')),
+              }),
+          ]),
 
+        /*
           !empty(album.urls) &&
             html.tag('p',
               language.$('releaseInfo.listenOn', {
@@ -204,25 +219,6 @@ export default {
                   ),
                 })
             ]),
-
-          ...html.fragment(
-            hasAdditionalFiles && [
-              generateContentHeading({
-                id: 'additional-files',
-                title: language.$('releaseInfo.additionalFiles.heading', {
-                  additionalFiles: language.countAdditionalFiles(numAdditionalFiles, {
-                    unit: true,
-                  }),
-                }),
-              }),
-
-              generateAlbumAdditionalFilesList(album, album.additionalFiles, {
-                generateAdditionalFilesList,
-                getSizeOfAdditionalFile,
-                link,
-                urls,
-              }),
-            ]),
         */
 
         relations.additionalFilesList && [
@@ -240,12 +236,12 @@ export default {
         data.artistCommentary && [
           relations.artistCommentaryHeading
             .slot('id', 'artist-commentary')
-            .slot('title', language.$('releaseDate.artistCommentary')),
+            .slot('title', language.$('releaseInfo.artistCommentary')),
 
           html.tag('blockquote',
             transformMultiline(data.artistCommentary)),
         ],
-      ]
+      ]),
     };
 
     return content;
diff --git a/src/content/dependencies/linkAlbumAdditionalFile.js b/src/content/dependencies/linkAlbumAdditionalFile.js
index 6c47edca..d1cca914 100644
--- a/src/content/dependencies/linkAlbumAdditionalFile.js
+++ b/src/content/dependencies/linkAlbumAdditionalFile.js
@@ -1,4 +1,14 @@
 export default {
+  contentDependencies: [
+    'linkTemplate',
+  ],
+
+  relations(relation) {
+    return {
+      linkTemplate: relation('linkTemplate'),
+    };
+  },
+
   data(album, file) {
     return {
       albumDirectory: album.directory,
@@ -6,7 +16,9 @@ export default {
     };
   },
 
-  generate(data) {
-    return `(stub album additional file link: ${data.albumDirectory}/${data.file})`;
+  generate(data, relations) {
+    return relations.linkTemplate
+      .slot('path', ['media.albumAdditionalFile', data.albumDirectory, data.file])
+      .slot('content', data.file);
   },
 };
diff --git a/src/content/dependencies/linkAlbumCommentary.js b/src/content/dependencies/linkAlbumCommentary.js
new file mode 100644
index 00000000..ab519fd6
--- /dev/null
+++ b/src/content/dependencies/linkAlbumCommentary.js
@@ -0,0 +1,8 @@
+export default {
+  contentDependencies: ['linkThing'],
+
+  relations: (relation, album) =>
+    ({link: relation('linkThing', 'localized.albumCommentary', album)}),
+
+  generate: (relations) => relations.link,
+};
diff --git a/src/content/dependencies/linkAlbumGallery.js b/src/content/dependencies/linkAlbumGallery.js
new file mode 100644
index 00000000..e3f30a29
--- /dev/null
+++ b/src/content/dependencies/linkAlbumGallery.js
@@ -0,0 +1,8 @@
+export default {
+  contentDependencies: ['linkThing'],
+
+  relations: (relation, album) =>
+    ({link: relation('linkThing', 'localized.albumGallery', album)}),
+
+  generate: (relations) => relations.link,
+};
diff --git a/src/content/dependencies/linkArtist.js b/src/content/dependencies/linkArtist.js
index 396eca41..718ee6fa 100644
--- a/src/content/dependencies/linkArtist.js
+++ b/src/content/dependencies/linkArtist.js
@@ -1,9 +1,8 @@
 export default {
-  data(artist) {
-    return {directory: artist.directory};
-  },
+  contentDependencies: ['linkThing'],
 
-  generate(data) {
-    return `(stub artist link: "${data.directory}")`;
-  },
+  relations: (relation, artist) =>
+    ({link: relation('linkThing', 'localized.artist', artist)}),
+
+  generate: (relations) => relations.link,
 };
diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js
new file mode 100644
index 00000000..94b90652
--- /dev/null
+++ b/src/content/dependencies/linkTemplate.js
@@ -0,0 +1,53 @@
+import {empty} from '../../util/sugar.js';
+
+export default {
+  extraDependencies: [
+    'appendIndexHTML',
+    'getColors',
+    'html',
+    'to',
+  ],
+
+  generate({
+    appendIndexHTML,
+    getColors,
+    html,
+    to,
+  }) {
+    return html.template(slot =>
+      slot('color', ([color]) =>
+      slot('hash', ([hash]) =>
+      slot('href', ([href]) =>
+      slot('path', ([...path]) => {
+        let style;
+
+        if (!href && !empty(path)) {
+          href = to(...path);
+        }
+
+        if (appendIndexHTML) {
+          if (/^(?!https?:\/\/).+\/$/.test(href)) {
+            href += 'index.html';
+          }
+        }
+
+        if (hash) {
+          href += (hash.startsWith('#') ? '' : '#') + hash;
+        }
+
+        if (color) {
+          const {primary, dim} = getColors(color);
+          style = `--primary-color: ${primary}; --dim-color: ${dim}`;
+        }
+
+        return slot('attributes', ([attributes]) =>
+          html.tag('a',
+            {
+              ...attributes ?? {},
+              href,
+              style,
+            },
+            slot('content')));
+      })))));
+  },
+}
diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js
new file mode 100644
index 00000000..ebff6761
--- /dev/null
+++ b/src/content/dependencies/linkThing.js
@@ -0,0 +1,51 @@
+import {empty} from '../../util/sugar.js';
+
+export default {
+  contentDependencies: [
+    'linkTemplate',
+  ],
+
+  extraDependencies: [
+    'html',
+  ],
+
+  relations(relation) {
+    return {
+      linkTemplate: relation('linkTemplate'),
+    };
+  },
+
+  data(pathKey, thing) {
+    return {
+      pathKey,
+
+      color: thing.color,
+      directory: thing.directory,
+
+      name: thing.name,
+      nameShort: thing.nameShort,
+    };
+  },
+
+  generate(data, relations, {html}) {
+    const path = [data.pathKey, data.directory];
+
+    return html.template(slot =>
+      slot('content', ([...content]) =>
+      slot('preferShortName', ([preferShortName]) => {
+        if (empty(content)) {
+          content =
+            (preferShortName
+              ? data.nameShort ?? data.name
+              : data.name);
+        }
+
+        return relations.linkTemplate
+          .slot('path', path)
+          .slot('color', slot('color', data.color))
+          .slot('attributes', slot('attributes', {}))
+          .slot('hash', slot('hash'))
+          .slot('content', content);
+      })));
+  },
+}