« get me outta code hell

content, data: experimental art tag sidebar - 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-10-05 23:22:43 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-10-05 23:22:43 -0300
commit62d7e0146d471e7623e4ec384a928a828b8780e9 (patch)
tree790fb2246ccc9a6dbf64db6d21c6fa4886aac79c
parentabb73f5e8a2361d133736a6cc5bed5c98206a814 (diff)
content, data: experimental art tag sidebar
-rw-r--r--src/content/dependencies/generateArtTagAncestorDescendantMapList.js129
-rw-r--r--src/content/dependencies/generateArtTagAncestorSidebarBox.js27
-rw-r--r--src/content/dependencies/generateArtTagInfoPage.js9
-rw-r--r--src/content/dependencies/generateArtTagSidebar.js31
-rw-r--r--src/data/composite/things/art-tag/index.js1
-rw-r--r--src/data/composite/things/art-tag/withAncestorArtTagBaobabTree.js46
-rw-r--r--src/data/things/art-tag.js8
-rw-r--r--src/data/things/language.js1
-rw-r--r--src/static/site4.css9
-rw-r--r--src/strings-default.json8
-rw-r--r--src/util/wiki-data.js32
11 files changed, 296 insertions, 5 deletions
diff --git a/src/content/dependencies/generateArtTagAncestorDescendantMapList.js b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js
new file mode 100644
index 0000000..6b4e52d
--- /dev/null
+++ b/src/content/dependencies/generateArtTagAncestorDescendantMapList.js
@@ -0,0 +1,129 @@
+import {stitchArrays} from '#sugar';
+import {filterMultipleArrays, sortMultipleArrays} from '#wiki-data';
+
+export default {
+  contentDependencies: ['linkArtTagDynamically'],
+  extraDependencies: ['html', 'language'],
+
+  // Recursion ain't too pretty!
+
+  query(ancestorArtTag, targetArtTag) {
+    const recursive = artTag => {
+      const artTags =
+        artTag.directDescendantArtTags.slice();
+
+      const displayBriefly =
+        !artTags.includes(targetArtTag) &&
+        artTags.length > 3;
+
+      const artTagsIncludeTargetArtTag =
+        artTags.map(artTag => artTag.allDescendantArtTags.includes(targetArtTag));
+
+      const numExemptArtTags =
+        (displayBriefly
+          ? artTagsIncludeTargetArtTag
+              .filter(includesTargetArtTag => !includesTargetArtTag)
+              .length
+          : null);
+
+      const sublists =
+        stitchArrays({
+          artTag: artTags,
+          includesTargetArtTag: artTagsIncludeTargetArtTag,
+        }).map(({artTag, includesTargetArtTag}) =>
+            (includesTargetArtTag
+              ? recursive(artTag)
+              : null));
+
+      if (displayBriefly) {
+        filterMultipleArrays(artTags, sublists,
+          (artTag, sublist) =>
+            artTag === targetArtTag ||
+            sublist !== null);
+      } else {
+        sortMultipleArrays(artTags, sublists,
+          (artTagA, artTagB, sublistA, sublistB) =>
+            (sublistA && sublistB
+              ? 0
+           : !sublistA && !sublistB
+              ? 0
+           : sublistA
+              ? 1
+              : -1));
+      }
+
+      return {
+        displayBriefly,
+        numExemptArtTags,
+        artTags,
+        sublists,
+      };
+    };
+
+    return {root: recursive(ancestorArtTag)};
+  },
+
+  relations(relation, query, _ancestorArtTag, _targetArtTag) {
+    const recursive = ({artTags, sublists}) => ({
+      artTagLinks:
+        artTags
+          .map(artTag => relation('linkArtTagDynamically', artTag)),
+
+      sublists:
+        sublists
+          .map(sublist => (sublist ? recursive(sublist) : null)),
+    });
+
+    return {root: recursive(query.root)};
+  },
+
+  data(query, _ancestorArtTag, targetArtTag) {
+    const recursive = ({displayBriefly, numExemptArtTags, artTags, sublists}) => ({
+      displayBriefly,
+      numExemptArtTags,
+
+      artTagsAreTargetTag:
+        artTags
+          .map(artTag => artTag === targetArtTag),
+
+      sublists:
+        sublists
+          .map(sublist => (sublist ? recursive(sublist) : null)),
+    });
+
+    return {root: recursive(query.root)};
+  },
+
+  generate(data, relations, {html, language}) {
+    const recursive = (dataNode, relationsNode) =>
+      html.tag('dl', [
+        dataNode.displayBriefly &&
+          html.tag('dt',
+            language.$('artTagSidebar.otherTagsExempt', {
+              tags:
+                language.countArtTags(dataNode.numExemptArtTags, {unit: true}),
+            })),
+
+        stitchArrays({
+          isTargetTag: dataNode.artTagsAreTargetTag,
+          dataSublist: dataNode.sublists,
+
+          artTagLink: relationsNode.artTagLinks,
+          relationsSublist: relationsNode.sublists,
+        }).map(({
+            isTargetTag, dataSublist,
+            artTagLink, relationsSublist,
+          }) => [
+            html.tag('dt',
+              {class: (dataSublist || isTargetTag) && 'current'},
+              artTagLink),
+
+            dataSublist &&
+              html.tag('dd',
+                recursive(dataSublist, relationsSublist)),
+          ]),
+      ]);
+
+    return recursive(data.root, relations.root);
+  },
+};
diff --git a/src/content/dependencies/generateArtTagAncestorSidebarBox.js b/src/content/dependencies/generateArtTagAncestorSidebarBox.js
new file mode 100644
index 0000000..ea85c2b
--- /dev/null
+++ b/src/content/dependencies/generateArtTagAncestorSidebarBox.js
@@ -0,0 +1,27 @@
+export default {
+  contentDependencies: [
+    'generateArtTagAncestorDescendantMapList',
+    'linkArtTagDynamically',
+  ],
+
+  extraDependencies: ['html'],
+
+  relations: (relation, ancestorArtTag, descendantArtTag) => ({
+    ancestorArtTagLink:
+      relation('linkArtTagDynamically', ancestorArtTag),
+
+    ancestorArtTagMapList:
+      relation('generateArtTagAncestorDescendantMapList',
+        ancestorArtTag,
+        descendantArtTag),
+  }),
+
+  generate: (relations, {html}) => ({
+    content: html.tags([
+      html.tag('h2',
+        relations.ancestorArtTagLink),
+
+      relations.ancestorArtTagMapList,
+    ]),
+  }),
+};
diff --git a/src/content/dependencies/generateArtTagInfoPage.js b/src/content/dependencies/generateArtTagInfoPage.js
index 7b9d47b..be25cd9 100644
--- a/src/content/dependencies/generateArtTagInfoPage.js
+++ b/src/content/dependencies/generateArtTagInfoPage.js
@@ -3,6 +3,7 @@ import {empty, unique} from '#sugar';
 export default {
   contentDependencies: [
     'generateArtTagNavLinks',
+    'generateArtTagSidebar',
     'generateContentHeading',
     'generatePageLayout',
     'linkArtTagGallery',
@@ -37,6 +38,9 @@ export default {
     relations.navLinks =
       relation('generateArtTagNavLinks', artTag);
 
+    relations.sidebar =
+      relation('generateArtTagSidebar', artTag);
+
     const info = sec.info = {};
 
     if (artTag.description) {
@@ -180,10 +184,7 @@ export default {
         navLinkStyle: 'hierarchical',
         navLinks: relations.navLinks.content,
 
-        leftSidebarMultiple: [
-          {content: `I'm a sidebar.`},
-          {content: `I am another sidebar.`},
-        ],
+        ...relations.sidebar,
       });
   },
 };
diff --git a/src/content/dependencies/generateArtTagSidebar.js b/src/content/dependencies/generateArtTagSidebar.js
new file mode 100644
index 0000000..51e53d2
--- /dev/null
+++ b/src/content/dependencies/generateArtTagSidebar.js
@@ -0,0 +1,31 @@
+import {collectTreeLeaves} from '#wiki-data';
+
+export default {
+  contentDependencies: ['generateArtTagAncestorSidebarBox'],
+  extraDependencies: ['wikiData'],
+
+  sprawl: ({artTagData}) =>
+    ({artTagData}),
+
+  query(sprawl, artTag) {
+    const baobab = artTag.ancestorArtTagBaobabTree;
+    const uniqueLeaves = new Set(collectTreeLeaves(baobab));
+
+    // Just match the order in tag data.
+    const furthestAncestorArtTags =
+      sprawl.artTagData
+        .filter(artTag => uniqueLeaves.has(artTag));
+
+    return {furthestAncestorArtTags};
+  },
+
+  relations: (relation, query, sprawl, artTag) => ({
+    ancestorBoxes:
+      query.furthestAncestorArtTags
+        .map(ancestorArtTag =>
+          relation('generateArtTagAncestorSidebarBox', ancestorArtTag, artTag)),
+  }),
+
+  generate: (relations) =>
+    ({leftSidebarMultiple: relations.ancestorBoxes}),
+};
diff --git a/src/data/composite/things/art-tag/index.js b/src/data/composite/things/art-tag/index.js
index 0c365ce..bbd3829 100644
--- a/src/data/composite/things/art-tag/index.js
+++ b/src/data/composite/things/art-tag/index.js
@@ -1 +1,2 @@
 export {default as withAllDescendantArtTags} from './withAllDescendantArtTags.js';
+export {default as withAncestorArtTagBaobabTree} from './withAncestorArtTagBaobabTree.js';
diff --git a/src/data/composite/things/art-tag/withAncestorArtTagBaobabTree.js b/src/data/composite/things/art-tag/withAncestorArtTagBaobabTree.js
new file mode 100644
index 0000000..d5caa99
--- /dev/null
+++ b/src/data/composite/things/art-tag/withAncestorArtTagBaobabTree.js
@@ -0,0 +1,46 @@
+// Gets all the art tags which are ancestors of this one as a "baobab tree" -
+// what you'd typically think of as roots are all up in the air! Since this
+// really is backwards from the way that the art tag tree is written in data,
+// chances are pretty good that there will be many of the exact same "leaf"
+// nodes - art tags which don't themselves have any ancestors. In the actual
+// data structure, each node is a Map, with keys for each ancestor and values
+// for each ancestor's own baobab (thus a branching structure, just like normal
+// trees in this regard).
+
+import {input, templateCompositeFrom} from '#composite';
+
+import {raiseOutputWithoutDependency} from '#composite/control-flow';
+import {withReverseReferenceList} from '#composite/wiki-data';
+
+export default templateCompositeFrom({
+  annotation: `withAncestorArtTagBaobabTree`,
+
+  outputs: ['#ancestorArtTagBaobabTree'],
+
+  steps: () => [
+    withReverseReferenceList({
+      data: 'artTagData',
+      list: input.value('directDescendantArtTags'),
+    }).outputs({
+      ['#reverseReferenceList']: '#directAncestorArtTags',
+    }),
+
+    raiseOutputWithoutDependency({
+      dependency: '#directAncestorArtTags',
+      mode: input.value('empty'),
+      output: input.value({'#ancestorArtTagBaobabTree': {}})
+    }),
+
+    {
+      dependencies: ['#directAncestorArtTags'],
+      compute: (continuation, {
+        ['#directAncestorArtTags']: directAncestorArtTags,
+      }) => continuation({
+        ['#ancestorArtTagBaobabTree']:
+          new Map(
+            directAncestorArtTags
+              .map(artTag => [artTag, artTag.ancestorArtTagBaobabTree])),
+      }),
+    },
+  ],
+});
diff --git a/src/data/things/art-tag.js b/src/data/things/art-tag.js
index 707b3c9..a530ba8 100644
--- a/src/data/things/art-tag.js
+++ b/src/data/things/art-tag.js
@@ -19,7 +19,8 @@ import {
   wikiData,
 } from '#composite/wiki-properties';
 
-import {withAllDescendantArtTags} from '#composite/things/art-tag';
+import {withAllDescendantArtTags, withAncestorArtTagBaobabTree}
+  from '#composite/things/art-tag';
 
 import Thing from './thing.js';
 
@@ -110,5 +111,10 @@ export class ArtTag extends Thing {
       data: 'artTagData',
       list: input.value('directDescendantArtTags'),
     }),
+
+    ancestorArtTagBaobabTree: [
+      withAncestorArtTagBaobabTree(),
+      exposeDependency({dependency: '#ancestorArtTagBaobabTree'}),
+    ],
   });
 }
diff --git a/src/data/things/language.js b/src/data/things/language.js
index fe74f7b..cd719d0 100644
--- a/src/data/things/language.js
+++ b/src/data/things/language.js
@@ -376,6 +376,7 @@ Object.assign(Language.prototype, {
   countAdditionalFiles: countHelper('additionalFiles', 'files'),
   countAlbums: countHelper('albums'),
   countArtworks: countHelper('artworks'),
+  countArtTags: countHelper('artTags', 'tags'),
   countFlashes: countHelper('flashes'),
   countCommentaryEntries: countHelper('commentaryEntries', 'entries'),
   countContributions: countHelper('contributions'),
diff --git a/src/static/site4.css b/src/static/site4.css
index 6265951..6dd19d8 100644
--- a/src/static/site4.css
+++ b/src/static/site4.css
@@ -296,6 +296,11 @@ body::before {
   margin: 0;
 }
 
+.sidebar h2:first-child {
+  margin-top: 0.5em;
+  margin-bottom: 0.5em;
+}
+
 .sidebar h3 {
   font-size: 1.1em;
   font-style: oblique;
@@ -343,6 +348,10 @@ body::before {
   margin-left: 0;
 }
 
+.sidebar dl > dd > dl {
+  padding-left: 15px;
+}
+
 .sidebar > dl .side {
   padding-left: 10px;
 }
diff --git a/src/strings-default.json b/src/strings-default.json
index 7f7a364..be8939b 100644
--- a/src/strings-default.json
+++ b/src/strings-default.json
@@ -29,6 +29,13 @@
   "count.artworks.withUnit.few": "",
   "count.artworks.withUnit.many": "",
   "count.artworks.withUnit.other": "{ARTWORKS} artworks",
+  "count.artTags": "{TAGS}",
+  "count.artTags.withUnit.zero": "",
+  "count.artTags.withUnit.one": "{TAGS} tag",
+  "count.artTags.withUnit.two": "",
+  "count.artTags.withUnit.few": "",
+  "count.artTags.withUnit.many": "",
+  "count.artTags.withUnit.other": "{TAGS} tags",
   "count.commentaryEntries": "{ENTRIES}",
   "count.commentaryEntries.withUnit.zero": "",
   "count.commentaryEntries.withUnit.one": "{ENTRIES} entry",
@@ -335,6 +342,7 @@
   "artTagGalleryPage.infoLine": "Appears in {COVER_ARTS}.",
   "artTagGalleryPage.descendsFrom": "Descends from {TAGS}.",
   "artTagGalleryPage.desendants": "Direct descendants: {TAGS}.",
+  "artTagSidebar.otherTagsExempt": "(…another {TAGS}…)",
   "commentaryIndex.title": "Commentary",
   "commentaryIndex.infoLine": "{WORDS} across {ENTRIES}, in all.",
   "commentaryIndex.albumList.title": "Choose an album:",
diff --git a/src/util/wiki-data.js b/src/util/wiki-data.js
index fecf94e..a85dd9a 100644
--- a/src/util/wiki-data.js
+++ b/src/util/wiki-data.js
@@ -97,6 +97,38 @@ export function chunkMultipleArrays(...args) {
   return results;
 }
 
+// This (or its helper function) should probably be a generator, but generators
+// are scary... Note that the root node is never considered a leaf, even if it
+// doesn't have any branches. It does NOT pay attention to the *values* of the
+// leaf nodes - it's suited to handle this kind of form:
+//
+//   {
+//     foo: {
+//       bar: {},
+//       baz: {},
+//       qux: {
+//         woz: {},
+//       },
+//     },
+//   }
+//
+// for which it outputs ['bar', 'baz', 'woz'].
+//
+export function collectTreeLeaves(tree) {
+  const recursive = ([key, value]) =>
+    (value instanceof Map
+      ? (value.size === 0
+          ? [key]
+          : Array.from(value.entries()).flatMap(recursive))
+      : (empty(Object.keys(value))
+          ? [key]
+          : Object.entries(value).flatMap(recursive)));
+
+  const root = Symbol();
+  const leaves = recursive([root, tree]);
+  return (leaves[0] === root ? [] : leaves);
+}
+
 // Sorting functions - all utils here are mutating, so make sure to initially
 // slice/filter/somehow generate a new array from input data if retaining the
 // initial sort matters! (Spoilers: If what you're doing involves any kind of