« 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/misc-templates.js61
-rw-r--r--src/page/album.js184
-rw-r--r--src/page/index.js70
-rw-r--r--src/util/link.js168
-rw-r--r--src/util/sugar.js9
5 files changed, 362 insertions, 130 deletions
diff --git a/src/misc-templates.js b/src/misc-templates.js
index 867193c7..0d749d1d 100644
--- a/src/misc-templates.js
+++ b/src/misc-templates.js
@@ -18,6 +18,8 @@ import {
   sortChronologically,
 } from './util/wiki-data.js';
 
+import u_link from './util/link.js';
+
 const BANDCAMP_DOMAINS = ['bc.s3m.us', 'music.solatrux.com'];
 
 const MASTODON_DOMAINS = ['types.pl'];
@@ -78,45 +80,60 @@ function unbound_generateAdditionalFilesList(additionalFiles, {
 
 // Artist strings
 
-function unbound_getArtistString(artists, {
+unbound_generateContributionLinks.data = (contributions, {
+  showContribution = false,
+  showIcons = false,
+}) => {
+  return {
+    showContribution,
+    showIcons,
+
+    contributionData:
+      contributions.map(({who, what}) => ({
+        artistLinkData: u_link.artist.data(who),
+
+        hasContributionPart: !!(showContribution && what),
+        hasExternalPart: !!(showIcons && !empty(who.urls)),
+
+        artistUrls: who.urls,
+        contribution: showContribution && what,
+      })),
+  };
+};
+
+function unbound_generateContributionLinks(data, {
   html,
+  iconifyURL,
   language,
   link,
-
-  iconifyURL,
-
-  showIcons = false,
-  showContrib = false,
 }) {
   return language.formatConjunctionList(
-    artists.map(({who, what}) => {
-      const {urls} = who;
-
-      const hasContribPart = !!(showContrib && what);
-      const hasExternalPart = !!(showIcons && !empty(urls));
-
-      const artistLink = link.artist(who);
+    data.contributionData.map(({
+      artistLinkData,
+      hasContributionPart,
+      hasExternalPart,
+      artistUrls,
+      contribution,
+    }) => {
+      const artistLink = link.artist(artistLinkData);
 
       const externalLinks = hasExternalPart &&
         html.tag('span',
-          {
-            [html.noEdgeWhitespace]: true,
-            class: 'icons'
-          },
+          {[html.noEdgeWhitespace]: true, class: 'icons'},
           language.formatUnitList(
-            urls.map(url => iconifyURL(url, {language}))));
+            artistUrls.map(url => iconifyURL(url, {language}))));
 
       return (
-        (hasContribPart
+        (hasContributionPart
           ? (hasExternalPart
               ? language.$('misc.artistLink.withContribution.withExternalLinks', {
                   artist: artistLink,
-                  contrib: what,
+                  contrib: contribution,
                   links: externalLinks,
                 })
               : language.$('misc.artistLink.withContribution', {
                   artist: artistLink,
-                  contrib: what,
+                  contrib: contribution,
                 }))
           : (hasExternalPart
               ? language.$('misc.artistLink.withExternalLinks', {
@@ -1040,7 +1057,7 @@ export {
   unbound_generateAdditionalFilesList as generateAdditionalFilesList,
   unbound_generateAdditionalFilesShortcut as generateAdditionalFilesShortcut,
 
-  unbound_getArtistString as getArtistString,
+  unbound_generateContributionLinks as generateContributionLinks,
 
   unbound_generateChronologyLinks as generateChronologyLinks,
 
diff --git a/src/page/album.js b/src/page/album.js
index 9ee57c09..a266b911 100644
--- a/src/page/album.js
+++ b/src/page/album.js
@@ -12,55 +12,148 @@ import {
   getTotalDuration,
 } from '../util/wiki-data.js';
 
+import {
+  generateContributionLinks as u_generateContributionLinks,
+} from '../misc-templates.js';
+
+import u_link from '../util/link.js';
+
 export const description = `per-album info & track artwork gallery pages`;
 
 export function targets({wikiData}) {
   return wikiData.albumData;
 }
 
-export function write(album, {wikiData}) {
-  const unbound_trackToListItem = (track, {
-    getArtistString,
-    getLinkThemeString,
-    html,
-    language,
-    link,
-  }) => {
-    const itemOpts = {
-      duration: language.formatDuration(track.duration ?? 0),
-      track: link.track(track),
-    };
+export const dataSteps = {
+  computePathsForTarget(data, album) {
+    data.hasGalleryPage = album.tracks.some(t => t.hasUniqueCoverArt);
+    data.hasCommentaryPage = !!album.commentary || album.tracks.some(t => t.commentary);;
 
-    return html.tag('li',
-      {style: getLinkThemeString(track.color)},
-      compareArrays(
-        track.artistContribs.map((c) => c.who),
-        album.artistContribs.map((c) => c.who),
-        {checkOrder: false}
-      )
-        ? language.$('trackList.item.withDuration', itemOpts)
-        : language.$('trackList.item.withDuration.withArtists', {
-            ...itemOpts,
-            by: html.tag('span',
-              {class: 'by'},
-              language.$('trackList.item.withArtists.by', {
-                artists: getArtistString(track.artistContribs),
-              })),
-          }));
+    return [
+      {
+        type: 'page',
+        path: ['album', album.directory],
+      },
+
+      data.hasGalleryPage && {
+        type: 'page',
+        path: ['albumGallery', album.directory],
+      },
+
+      data.hasCommentaryPage && {
+        type: 'page',
+        path: ['albumCommentary', album.directory],
+      },
+
+      {
+        type: 'data',
+        path: ['album', album.directory],
+      },
+    ];
+  },
+
+  computeDataCommonAcrossMixedWrites(data, album) {
+    data.albumDuration = getTotalDuration(album.tracks);
+  },
+
+  computeDataCommonAcrossPageWrites(data, album) {
+    data.listTag = getAlbumListTag(album);
+  },
+
+  computeDataForPageWrite: {
+    album(data, album, _pathArgs) {
+      data.hasAdditionalFiles = !empty(album.additionalFiles);
+      data.numAdditionalFiles = album.additionalFiles.flatMap((g) => g.files).length;
+
+      data.displayTrackSections =
+        album.trackSections &&
+          (album.trackSections.length > 1 ||
+            !album.trackSections[0]?.isDefaultTrackSection);
+    },
+  },
+
+  computeContentForPageWrite: {
+    album(data, {
+      absoluteTo,
+      fancifyURL,
+      generateAdditionalFilesShortcut,
+      generateAdditionalFilesList,
+      generateChronologyLinks,
+      generateContributionLinks,
+      generateContentHeading,
+      generateNavigationLinks,
+      getAlbumCover,
+      getAlbumStylesheet,
+      getLinkThemeString,
+      getSizeOfAdditionalFile,
+      getThemeString,
+      html,
+      link,
+      language,
+      transformMultiline,
+      urls,
+    }) {
+      const generateTrackListItem = bindOpts(u_generateTrackListItem, {
+        generateContributionLinks,
+        getLinkThemeString,
+        html,
+        language,
+        link,
+      });
+
+      void generateTrackListItem;
+    },
+  },
+};
+
+function u_generateTrackListItem(data, {
+  generateContributionLinks,
+  getLinkThemeString,
+  html,
+  language,
+  link,
+}) {
+  const stringOpts = {
+    duration: language.formatDuration(data.duration),
+    track: link.track(data.linkData),
   };
 
-  const hasAdditionalFiles = !empty(album.additionalFiles);
-  const numAdditionalFiles = album.additionalFiles.flatMap((g) => g.files).length;
+  return html.tag('li',
+    {style: getLinkThemeString(data.color)},
+    (!data.showArtists
+      ? language.$('trackList.item.withDuration', stringOpts)
+      : language.$('trackList.item.withDuration.withArtists', {
+          ...stringOpts,
+          by:
+            html.tag('span', {class: 'by'},
+              language.$('trackList.item.withArtists.by', {
+                artists: generateContributionLinks(data.contributionLinksData),
+              })),
+        })));
+}
 
-  const albumDuration = getTotalDuration(album.tracks);
+u_generateTrackListItem.data = track => {
+  return {
+    color: track.color,
+    duration: track.duration ?? 0,
+    linkData: u_link.track.data(track),
 
-  const displayTrackSections =
-    album.trackSections &&
-      (album.trackSections.length > 1 ||
-        !album.trackSections[0]?.isDefaultTrackSection);
+    showArtists:
+      !compareArrays(
+        track.artistContribs.map((c) => c.who),
+        track.album.artistContribs.map((c) => c.who),
+        {checkOrder: false}),
 
-  const listTag = getAlbumListTag(album);
+    contributionLinksData:
+      u_generateContributionLinks.data(track.artistContribs, {
+        showContribution: false,
+        showIcons: false,
+      }),
+  };
+};
 
+/*
+export function write(album, {wikiData}) {
   const getSocialEmbedDescription = ({
     getArtistString: _getArtistString,
     language,
@@ -127,24 +220,6 @@ export function write(album, {wikiData}) {
     type: 'page',
     path: ['album', album.directory],
     page: ({
-      absoluteTo,
-      fancifyURL,
-      generateAdditionalFilesShortcut,
-      generateAdditionalFilesList,
-      generateChronologyLinks,
-      generateContentHeading,
-      generateNavigationLinks,
-      getAlbumCover,
-      getAlbumStylesheet,
-      getArtistString,
-      getLinkThemeString,
-      getSizeOfAdditionalFile,
-      getThemeString,
-      html,
-      link,
-      language,
-      transformMultiline,
-      urls,
     }) => {
       const trackToListItem = bindOpts(unbound_trackToListItem, {
         getArtistString,
@@ -867,3 +942,4 @@ export function generateAlbumAdditionalFilesList(album, additionalFiles, {
       link.albumAdditionalFile({album, file}),
   });
 }
+*/
diff --git a/src/page/index.js b/src/page/index.js
index f580cbea..fc0c646c 100644
--- a/src/page/index.js
+++ b/src/page/index.js
@@ -17,6 +17,76 @@
 //     Usually this will simply mean returning the appropriate thingData array,
 //     but it may also apply filter/map/etc if useful.
 //
+// dataSteps {...}
+//     Object with key-to-functions matching a variety of steps described next.
+//     In general, the use of dataSteps is to separate data computations from
+//     actual page content generation, making explicit what data is carried
+//     from one step to the next, and letting the build/serve mode have a
+//     standardized guideline for deciding when to compute data at each step.
+//
+//     Important notes on the usage of dataSteps:
+//
+//     - Every dataStep function is provided a `data` object which stores
+//       values passed through to that step. To save data for a coming step,
+//       just mutate this object (set a key and value on it).
+//
+//     - Some dataStep functions return values, but not all do. Some are just
+//       for computing data used by following steps.
+//
+//     - Do not set any data properties to live wiki objects or arrays/objects
+//       including live wiki objects. All data passed between each step should
+//       be fully serializable in JSON or otherwise plain-text format.
+//
+// **NB: DATA WRITES ARE CURRENTLY DISABLED. All steps exclusively applicable
+//   to data writes will currently be skipped.**
+//
+// dataSteps.computePathsForTargets(data, target)
+//     Compute paths at which pages or files will be generated for the given
+//     target wiki object, returning {type, path} pairs. Data applied here,
+//     such as flags indicating which pages have content, will automatically
+//     be passed onto all further steps.
+//
+// dataSteps.computeDataCommonAcrossMixedWrites(data, target)
+//     Compute data which is useful in a mixed list of any path writes.
+//     This function should only be used when data is pertinent to more than
+//     one kind of write, ex. a variable which is useful for page writes but
+//     also exposed through a data write. Data applied here is passed onto
+//     all further steps.
+//
+// dataSteps.computeDataCommonAcrossPageWrites(data, target)
+//     Compute data which is useful across more than one page write.
+//     Use this function when data is pertinent to more than one page write,
+//     but isn't relevant outside of page writes. Data applied here is passed
+//     onto further steps for page writes.
+//
+// dataSteps.computeDataCommonAcrossDataWrites(data, target)
+//     Analagous to computeDataAcrossPages; for data writes.
+//
+// dataSteps.computeDataForPageWrite.[pathKey](data, target, pathArgs)
+//     Compute data which is useful for a single given page write.
+//     Note that dataSteps.computeDataForPage is an object; its keys are the
+//     possible path keys from computePathsForTargets() for page writes.
+//     Data applied here is passed onto the final write call for this page.
+//
+// dataSteps.computeDataForDataWrite.[pathKey](data, target, pathArgs)
+//     Analogous to computeDataForPageWrite; for data writes.
+//
+// dataSteps.computeContentForPageWrite.[pathKey](data, utils)
+//     Use data prepared in previous steps to compute and return the actual
+//     content for a given page write. The target wiki object is no longer
+//     accessible at this step, so all required data must be computed ahead.
+//
+//     - The returned page object will be destructured for
+//       usage in generateDocumentHTML(), `src/write/page-template.js`.
+//
+//     - The utils object is a set of bound functions handy for any page
+//       content. It is described in `src/write/bind-utilities.js`.
+//
+// dataSteps.compteContentForDataWrite.[pathKey](data, utils)
+//     Analogous to computeContentForDataWrite; for data writes.
+//     NB: When data writes are enabled, the utils object will be uniquely
+//     defined separate from what's provided to page writes.
+//
 // write(thing, {wikiData})
 //     Provides descriptors for any page and data writes associated with the
 //     given thing (which will be a value from the targets() array). This
diff --git a/src/util/link.js b/src/util/link.js
index 62106345..00abc69e 100644
--- a/src/util/link.js
+++ b/src/util/link.js
@@ -24,23 +24,29 @@ export function unbound_getLinkThemeString(color, {
 
 const appendIndexHTMLRegex = /^(?!https?:\/\/).+\/$/;
 
-const linkHelper =
-  (hrefFn, {
-    color = true,
-    attr = null,
-  } = {}) =>
-  (thing, {
+function linkHelper({
+  path: pathOption,
+
+  expectThing = true,
+  color: colorOption = true,
+
+  attr: attrOption = null,
+  data: dataOption = null,
+  text: textOption = null,
+}) {
+  const generateLink = (data, {
     getLinkThemeString,
     to,
 
     text = '',
     attributes = null,
     class: className = '',
-    color: color2 = true,
+    color = true,
     hash = '',
     preferShortName = false,
   }) => {
-    let href = hrefFn(thing, {to});
+    const path = (expectThing ? pathOption(data) : pathOption());
+    let href = to(...path);
 
     if (link.globalOptions.appendIndexHTML) {
       if (appendIndexHTMLRegex.test(href)) {
@@ -52,41 +58,100 @@ const linkHelper =
       href += (hash.startsWith('#') ? '' : '#') + hash;
     }
 
-    return html.tag(
-      'a',
+    return html.tag('a',
       {
-        ...(attr ? attr(thing) : {}),
+        ...(attrOption ? attrOption(data) : {}),
         ...(attributes ? attributes : {}),
         href,
         style:
-          typeof color2 === 'string'
-            ? getLinkThemeString(color2)
-            : color2 && color
-            ? getLinkThemeString(thing.color)
+          typeof color === 'string'
+            ? getLinkThemeString(color)
+            : color && colorOption
+            ? getLinkThemeString(data.color)
             : '',
         class: className,
       },
+
       (text ||
-        (preferShortName
-          ? thing.nameShort ?? thing.name
-          : thing.name))
-    );
+        (textOption
+          ? textOption(data)
+          : (preferShortName
+              ? data.nameShort ?? data.name
+              : data.name))));
   };
 
-const linkDirectory = (key, {expose = null, attr = null, ...conf} = {}) =>
-  linkHelper((thing, {to}) => to('localized.' + key, thing.directory), {
-    attr: (thing) => ({
-      ...(attr ? attr(thing) : {}),
-      ...(expose ? {[expose]: thing.directory} : {}),
+  generateLink.data = thing => {
+    if (!expectThing) {
+      throw new Error(`This kind of link doesn't need any data serialized`);
+    }
+
+    const data = (dataOption ? dataOption(thing) : {});
+
+    if (colorOption) {
+      data.color = thing.color;
+    }
+
+    if (!textOption) {
+      data.name = thing.name;
+      data.nameShort = thing.nameShort ?? thing.name;
+    }
+
+    return data;
+  };
+
+  return generateLink;
+}
+
+function linkDirectory(key, {
+  exposeDirectory = null,
+  prependLocalized = true,
+
+  data = null,
+  attr = null,
+  ...conf
+}) {
+  return linkHelper({
+    data: thing => ({
+      ...(data ? data(thing) : {}),
+      directory: thing.directory,
     }),
+
+    path: data =>
+      (prependLocalized
+        ? ['localized.' + key, data.directory]
+        : [key, data.directory]),
+
+    attr: (data) => ({
+      ...(attr ? attr(data) : {}),
+      ...(exposeDirectory ? {[exposeDirectory]: data.directory} : {}),
+    }),
+
+    ...conf,
+  });
+}
+
+function linkIndex(key, conf) {
+  return linkHelper({
+    path: () => [key],
+
+    expectThing: false,
     ...conf,
   });
+}
 
-const linkPathname = (key, conf) =>
-  linkHelper(({directory: pathname}, {to}) => to(key, pathname), conf);
+function linkAdditionalFile(key, conf) {
+  return linkHelper({
+    data: ({file, album}) => ({
+      directory: album.directory,
+      file,
+    }),
 
-const linkIndex = (key, conf) =>
-  linkHelper((_, {to}) => to('localized.' + key), conf);
+    path: data => ['media.albumAdditionalFile', data.directory, data.file],
+
+    color: false,
+    ...conf,
+  });
+}
 
 // Mapping of Thing constructor classes to the key for a link.x() function.
 // These represent a sensible "default" link, i.e. to the primary page for
@@ -114,6 +179,7 @@ const link = {
   },
 
   album: linkDirectory('album'),
+  albumAdditionalFile: linkAdditionalFile('albumAdditionalFile'),
   albumGallery: linkDirectory('albumGallery'),
   albumCommentary: linkDirectory('albumCommentary'),
   artist: linkDirectory('artist', {color: false}),
@@ -130,32 +196,26 @@ const link = {
   newsEntry: linkDirectory('newsEntry', {color: false}),
   staticPage: linkDirectory('staticPage', {color: false}),
   tag: linkDirectory('tag'),
-  track: linkDirectory('track', {expose: 'data-track'}),
-
-  // TODO: This is a bit hacky. Files are just strings (not objects), so we
-  // have to manually provide the album alongside the file. They also don't
-  // follow the usual {name: whatever} type shape, so we have to provide that
-  // ourselves.
-  _albumAdditionalFileHelper: linkHelper(
-    (fakeFileObject, {to}) =>
-      to(
-        'media.albumAdditionalFile',
-        fakeFileObject.album.directory,
-        fakeFileObject.name),
-    {color: false}),
-
-  albumAdditionalFile: ({file, album}, {to, ...opts}) =>
-    link._albumAdditionalFileHelper(
-      {
-        name: file,
-        album,
-      },
-      {to, ...opts}),
-
-  media: linkPathname('media.path', {color: false}),
-  root: linkPathname('shared.path', {color: false}),
-  data: linkPathname('data.path', {color: false}),
-  site: linkPathname('localized.path', {color: false}),
+  track: linkDirectory('track', {exposeDirectory: 'data-track'}),
+
+  media: linkDirectory('media.path', {
+    prependLocalized: false,
+    color: false,
+  }),
+
+  root: linkDirectory('shared.path', {
+    prependLocalized: false,
+    color: false,
+  }),
+  data: linkDirectory('data.path', {
+    prependLocalized: false,
+    color: false,
+  }),
+
+  site: linkDirectory('localized.path', {
+    prependLocalized: false,
+    color: false,
+  }),
 
   // This is NOT an arrow functions because it should be callable for other
   // "this" objects - i.e, if we bind arguments in other functions on the same
diff --git a/src/util/sugar.js b/src/util/sugar.js
index 0813c1d4..c60bddb6 100644
--- a/src/util/sugar.js
+++ b/src/util/sugar.js
@@ -150,6 +150,15 @@ export function bindOpts(fn, bind) {
     value: fn.name ? `(options-bound) ${fn.name}` : `(options-bound)`,
   });
 
+  for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(fn))) {
+    if (key === 'length') continue;
+    if (key === 'name') continue;
+    if (key === 'arguments') continue;
+    if (key === 'caller') continue;
+    if (key === 'prototype') continue;
+    Object.defineProperty(bound, key, descriptor);
+  }
+
   return bound;
 }