« get me outta code hell

html: drastically simplify template/slot system - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/content
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-04-08 16:54:39 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-04-08 16:54:39 -0300
commit776abf8d697716902692f357c6f179c1e681369f (patch)
treea75e7f63ffc13121c3c08f1f78c10bc883b4d4e1 /src/content
parent0cbfa8c1b70080c7ec4eb352902cf76f8ef30fcf (diff)
html: drastically simplify template/slot system
Diffstat (limited to 'src/content')
-rw-r--r--src/content/dependencies/generateAdditionalFilesList.js67
-rw-r--r--src/content/dependencies/generateAlbumAdditionalFilesList.js24
-rw-r--r--src/content/dependencies/generateAlbumInfoPage.js16
-rw-r--r--src/content/dependencies/generateAlbumInfoPageContent.js27
-rw-r--r--src/content/dependencies/generateContentHeading.js26
-rw-r--r--src/content/dependencies/generateCoverArtwork.js49
-rw-r--r--src/content/dependencies/generatePageLayout.js65
-rw-r--r--src/content/dependencies/image.js76
-rw-r--r--src/content/dependencies/index.js10
-rw-r--r--src/content/dependencies/linkAlbumAdditionalFile.js6
-rw-r--r--src/content/dependencies/linkTemplate.js50
-rw-r--r--src/content/dependencies/linkThing.js42
12 files changed, 310 insertions, 148 deletions
diff --git a/src/content/dependencies/generateAdditionalFilesList.js b/src/content/dependencies/generateAdditionalFilesList.js
index c51435a4..eb9fc8b0 100644
--- a/src/content/dependencies/generateAdditionalFilesList.js
+++ b/src/content/dependencies/generateAdditionalFilesList.js
@@ -18,15 +18,61 @@ export default {
     html,
     language,
   }) {
-    return html.template(slot =>
-      slot('additionalFileLinks', ([fileLinks]) =>
-      slot('additionalFileSizes', ([fileSizes]) => {
-        if (!fileSizes) {
+    const fileKeys = data.additionalFiles.flatMap(({files}) => files);
+    const validateFileMapping = (v, validateValue) => {
+      return value => {
+        v.isObject(value);
+
+        // It's OK to skip values for files, but if keys are provided for files
+        // which don't exist, that's an error.
+
+        const unexpectedKeys =
+          Object.keys(value).filter(key => !fileKeys.includes(key))
+
+        if (!empty(unexpectedKeys)) {
+          throw new TypeError(`Unexpected file keys: ${unexpectedKeys.join(', ')}`);
+        }
+
+        const valueErrors = [];
+        for (const [fileKey, fileValue] of Object.entries(value)) {
+          if (fileValue === null) {
+            continue;
+          }
+
+          try {
+            validateValue(fileValue);
+          } catch (error) {
+            error.message = `(${fileKey}) ` + error.message;
+            valueErrors.push(error);
+          }
+        }
+
+        if (!empty(valueErrors)) {
+          throw new AggregateError(valueErrors, `Errors validating values`);
+        }
+      };
+    };
+
+    return html.template({
+      annotation: 'generateAdditionalFilesList',
+
+      slots: {
+        fileLinks: {
+          validate: v => validateFileMapping(v, v.isHTML),
+        },
+
+        fileSizes: {
+          validate: v => validateFileMapping(v, v.isWholeNumber),
+        },
+      },
+
+      content(slots) {
+        if (!slots.fileSizes) {
           return html.blank();
         }
 
         const filesWithLinks = new Set(
-          Object.entries(fileLinks)
+          Object.entries(slots.fileLinks)
             .filter(([key, value]) => value)
             .map(([key]) => key));
 
@@ -60,15 +106,16 @@ export default {
               html.tag('ul',
                 files.map(file =>
                   html.tag('li',
-                    (fileSizes[file]
+                    (slots.fileSizes[file]
                       ? language.$('releaseInfo.additionalFiles.file.withSize', {
-                          file: fileLinks[file],
-                          size: language.formatFileSize(fileSizes[file]),
+                          file: slots.fileLinks[file],
+                          size: language.formatFileSize(slots.fileSizes[file]),
                         })
                       : language.$('releaseInfo.additionalFiles.file', {
-                          file: fileLinks[file],
+                          file: slots.fileLinks[file],
                         })))))),
           ]));
-      })));
+      },
+    });
   },
 };
diff --git a/src/content/dependencies/generateAlbumAdditionalFilesList.js b/src/content/dependencies/generateAlbumAdditionalFilesList.js
index d45fb583..04e6a5f1 100644
--- a/src/content/dependencies/generateAlbumAdditionalFilesList.js
+++ b/src/content/dependencies/generateAlbumAdditionalFilesList.js
@@ -40,16 +40,18 @@ export default {
     urls,
   }) {
     return relations.additionalFilesList
-      .slot('additionalFileLinks', relations.additionalFileLinks)
-      .slot('additionalFileSizes',
-        Object.fromEntries(data.fileLocations.map(file => [
-          file,
-          (data.showFileSizes
-            ? getSizeOfAdditionalFile(
-                urls
-                  .from('media.root')
-                  .to('media.albumAdditionalFile', data.albumDirectory, file))
-            : 0),
-        ])));
+      .slots({
+        additionalFileLinks: relations.additionalFileLinks,
+        additionalFileSizes:
+          Object.fromEntries(data.fileLocations.map(file => [
+            file,
+            (data.showFileSizes
+              ? getSizeOfAdditionalFile(
+                  urls
+                    .from('media.root')
+                    .to('media.albumAdditionalFile', data.albumDirectory, file))
+              : 0),
+          ])),
+      });
   },
 };
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js
index dcd8589c..f0a23259 100644
--- a/src/content/dependencies/generateAlbumInfoPage.js
+++ b/src/content/dependencies/generateAlbumInfoPage.js
@@ -36,8 +36,6 @@ export default {
   generate(data, relations, {
     language,
   }) {
-    // page.title = language.$('albumPage.title', {album: data.name});
-
     // page.themeColor = data.color;
 
     // page.styleRules = [
@@ -45,12 +43,14 @@ export default {
     //   relations.colorStyleRules,
     // ];
 
-    // page.socialEmbed = relations.socialEmbed;
-
     return relations.layout
-      .slot('title', language.$('albumPage.title', {album: data.name}))
-      .slot('cover', relations.content.cover)
-      .slot('mainContent', relations.content.main.content)
-      .slot('socialEmbed', relations.socialEmbed);
+      .slots({
+        title: language.$('albumPage.title', {album: data.name}),
+
+        cover: relations.content.cover,
+        mainContent: relations.content.main.content,
+
+        // socialEmbed: relations.socialEmbed,
+      });
   },
 };
diff --git a/src/content/dependencies/generateAlbumInfoPageContent.js b/src/content/dependencies/generateAlbumInfoPageContent.js
index a17a33f1..fd66f6b0 100644
--- a/src/content/dependencies/generateAlbumInfoPageContent.js
+++ b/src/content/dependencies/generateAlbumInfoPageContent.js
@@ -117,8 +117,10 @@ export default {
     const content = {};
 
     content.cover = relations.cover
-      .slot('path', ['media.albumCover', data.coverArtDirectory, data.coverArtFileExtension])
-      .slot('alt', language.$('misc.alt.trackCover'));
+      .slots({
+        path: ['media.albumCover', data.coverArtDirectory, data.coverArtFileExtension],
+        alt: language.$('misc.alt.trackCover')
+      });
 
     content.main = {
       headingMode: 'sticky',
@@ -213,20 +215,25 @@ export default {
 
         relations.additionalFilesList && [
           relations.additionalFilesHeading
-            .slot('id', 'additional-files')
-            .slot('title',
-              language.$('releaseInfo.additionalFiles.heading', {
-                additionalFiles:
-                  language.countAdditionalFiles(data.numAdditionalFiles, {unit: true}),
-              })),
+            .slots({
+              id: 'additional-files',
+
+              title:
+                language.$('releaseInfo.additionalFiles.heading', {
+                  additionalFiles:
+                    language.countAdditionalFiles(data.numAdditionalFiles, {unit: true}),
+                }),
+            }),
 
           relations.additionalFilesList,
         ],
 
         data.artistCommentary && [
           relations.artistCommentaryHeading
-            .slot('id', 'artist-commentary')
-            .slot('title', language.$('releaseInfo.artistCommentary')),
+            .slots({
+              id: 'artist-commentary',
+              title: language.$('releaseInfo.artistCommentary')
+            }),
 
           html.tag('blockquote',
             transformMultiline(data.artistCommentary)),
diff --git a/src/content/dependencies/generateContentHeading.js b/src/content/dependencies/generateContentHeading.js
index baa52080..f5e4bd00 100644
--- a/src/content/dependencies/generateContentHeading.js
+++ b/src/content/dependencies/generateContentHeading.js
@@ -4,13 +4,23 @@ export default {
   ],
 
   generate({html}) {
-    return html.template(slot =>
-      html.tag('p',
-        {
-          class: 'content-heading',
-          id: slot('id'),
-          tabindex: '0',
-        },
-        slot('title')));
+    return html.template({
+      annotation: 'generateContentHeading',
+
+      slots: {
+        title: {type: 'html'},
+        id: {type: 'string'},
+      },
+
+      content(slots) {
+        return html.tag('p',
+          {
+            class: 'content-heading',
+            id: slots.id,
+            tabindex: '0',
+          },
+          slots.content);
+      },
+    });
   }
 }
diff --git a/src/content/dependencies/generateCoverArtwork.js b/src/content/dependencies/generateCoverArtwork.js
index 62fc3566..2d18fed3 100644
--- a/src/content/dependencies/generateCoverArtwork.js
+++ b/src/content/dependencies/generateCoverArtwork.js
@@ -23,21 +23,38 @@ export default {
   },
 
   generate(relations, {html, language}) {
-    return html.template(slot =>
-      html.tag('div', {id: 'cover-art-container'}, [
-        relations.image
-          .slot('path', slot('path'))
-          .slot('alt', slot('alt'))
-          .slot('thumb', 'medium')
-          .slot('id', 'cover-art')
-          .slot('link', true)
-          .slot('square', true),
-
-        !empty(relations.tagLinks) &&
-          html.tag('p',
-            language.$('releaseInfo.artTags.inline', {
-              tags: language.formatUnitList(relations.tagLinks),
-            })),
-      ]));
+    return html.template({
+      annotation: 'generateCoverArtwork',
+
+      slots: {
+        path: {
+          validate: v => v.validateArrayItems(v.isString),
+        },
+
+        alt: {
+          type: 'string',
+        },
+      },
+
+      content(slots) {
+        return html.tag('div', {id: 'cover-art-container'}, [
+          relations.image
+            .slots({
+              path: slots.path,
+              alt: slots.alt,
+              thumb: 'medium',
+              id: 'cover-art',
+              link: true,
+              square: true,
+            }),
+
+          !empty(relations.tagLinks) &&
+            html.tag('p',
+              language.$('releaseInfo.artTags.inline', {
+                tags: language.formatUnitList(relations.tagLinks),
+              })),
+          ]);
+      },
+    });
   },
 };
diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js
index b27d487b..f36a7bb5 100644
--- a/src/content/dependencies/generatePageLayout.js
+++ b/src/content/dependencies/generatePageLayout.js
@@ -1,5 +1,3 @@
-import {empty} from '../../util/sugar.js';
-
 export default {
   extraDependencies: [
     'html',
@@ -13,41 +11,63 @@ export default {
     language,
     to,
   }) {
-    return html.template(slot =>
-      slot('title', ([...title]) =>
-      slot('headingMode', ([headingMode = 'static']) => {
+    return html.template({
+      annotation: 'generatePageLayout',
+
+      slots: {
+        title: {type: 'html'},
+        cover: {type: 'html'},
+
+        mainContent: {type: 'html'},
+        socialEmbed: {type: 'html'},
+
+        headingMode: {
+          validate: v => v.is('sticky', 'static'),
+          default: 'static',
+        },
+
+        mainClasses: {
+          validate: v => v.arrayOf(v.isString),
+          default: [],
+        },
+      },
+
+      content(slots) {
         let titleHTML = null;
 
-        if (!empty(title)) {
-          if (headingMode === 'sticky') {
-            /*
-              generateStickyHeadingContainer({
-                coverSrc: cover.src,
-                coverAlt: cover.alt,
-                coverArtTags: cover.artTags,
-                title,
-              })
-            */
-          } else if (headingMode === 'static') {
-            titleHTML = html.tag('h1', title);
+        if (!html.isBlank(slots.title)) {
+          switch (slots.headingMode) {
+            case 'sticky':
+              /*
+                generateStickyHeadingContainer({
+                  coverSrc: cover.src,
+                  coverAlt: cover.alt,
+                  coverArtTags: cover.artTags,
+                  title,
+                })
+              */
+              break;
+            case 'static':
+              titleHTML = html.tag('h1', slots.title);
+              break;
           }
         }
 
         const mainHTML =
           html.tag('main', {
             id: 'content',
-            class: slot('mainClass'),
+            class: slots.mainClasses,
           }, [
             titleHTML,
 
-            slot('cover'),
+            slots.cover,
 
             html.tag('div',
               {
                 [html.onlyIfContent]: true,
                 class: 'main-content-container',
               },
-              slot('mainContent')),
+              slots.mainContent),
           ]);
 
         const layoutHTML = [
@@ -135,7 +155,7 @@ export default {
 
                 */
 
-                // slot('socialEmbed'),
+                // slots.socialEmbed,
 
                 html.tag('link', {
                   rel: 'stylesheet',
@@ -176,6 +196,7 @@ export default {
         ]);
 
         return documentHTML;
-      })));
+      },
+    });
   },
 };
diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js
index 1f904377..1960fb0a 100644
--- a/src/content/dependencies/image.js
+++ b/src/content/dependencies/image.js
@@ -31,39 +31,60 @@ export default {
     thumb,
     to,
   }) {
-    return html.template(slot =>
-      slot('src', ([src]) =>
-      slot('path', ([...path]) =>
-      slot('thumb', ([thumbKey = '']) =>
-      slot('link', ([link = false]) =>
-      slot('lazy', ([lazy = false]) =>
-      slot('square', ([willSquare = false]) => {
+    return html.template({
+      annotation: 'image',
+
+      slots: {
+        src: {
+          type: 'string',
+        },
+
+        path: {
+          validate: v => v.validateArrayItems(v.isString),
+        },
+
+        thumb: {type: 'string'},
+
+        link: {type: 'boolean', default: false},
+        lazy: {type: 'boolean', default: false},
+        square: {type: 'boolean', default: false},
+
+        id: {type: 'string'},
+        alt: {type: 'string'},
+        width: {type: 'number'},
+        height: {type: 'number'},
+
+        missingSourceContent: {type: 'html'},
+      },
+
+      content(slots) {
         let originalSrc;
 
-        if (src) {
-          originalSrc = src;
-        } else if (!empty(path)) {
-          originalSrc = to(...path);
+        if (slots.src) {
+          originalSrc = slots.src;
+        } else if (!empty(slots.path)) {
+          originalSrc = to(...slots.path);
         } else {
           originalSrc = '';
         }
 
         const thumbSrc =
           originalSrc &&
-            (thumbKey
-              ? thumb[thumbKey](originalSrc)
+            (slots.thumb
+              ? thumb[slots.thumb](originalSrc)
               : originalSrc);
 
-        const willLink = typeof link === 'string' || link;
+        const willLink = typeof slots.link === 'string' || slots.link;
         const willReveal = originalSrc && !empty(data.contentWarnings);
+        const willSquare = slots.square;
 
-        const idOnImg = willLink ? null : slot('id');
-        const idOnLink = willLink ? slot('id') : null;
+        const idOnImg = willLink ? null : slots.id;
+        const idOnLink = willLink ? slots.id : null;
 
         if (!originalSrc) {
           return prepare(
             html.tag('div', {class: 'image-text-area'},
-              slot('missingSourceContent')));
+              slots.missingSourceContent));
         }
 
         let fileSize = null;
@@ -90,13 +111,11 @@ export default {
           ];
         }
 
-        const className = slot('class');
         const imgAttributes = {
           id: idOnImg,
-          class: className,
-          alt: slot('alt'),
-          width: slot('width'),
-          height: slot('height'),
+          alt: slots.alt,
+          width: slots.width,
+          height: slots.height,
           'data-original-size': fileSize,
         };
 
@@ -108,14 +127,14 @@ export default {
                 src: thumbSrc,
               }));
 
-        if (lazy) {
+        if (slots.lazy) {
           return html.tags([
             html.tag('noscript', nonlazyHTML),
             prepare(
               html.tag('img',
                 {
                   ...imgAttributes,
-                  class: [className, 'lazy'],
+                  class: 'lazy',
                   'data-original': thumbSrc,
                 }),
               true),
@@ -166,8 +185,8 @@ export default {
                 ],
 
                 href:
-                  (typeof link === 'string'
-                    ? link
+                  (typeof slots.link === 'string'
+                    ? slots.link
                     : originalSrc),
               },
               wrapped);
@@ -175,6 +194,7 @@ export default {
 
           return wrapped;
         }
-      })))))));
-    },
+      },
+    });
+  }
 };
diff --git a/src/content/dependencies/index.js b/src/content/dependencies/index.js
index c2d88f64..f82062f7 100644
--- a/src/content/dependencies/index.js
+++ b/src/content/dependencies/index.js
@@ -1,6 +1,7 @@
 import chokidar from 'chokidar';
 import EventEmitter from 'events';
 import * as path from 'path';
+import {ESLint} from 'eslint';
 import {fileURLToPath} from 'url';
 
 import contentFunction from '../../content-function.js';
@@ -35,6 +36,8 @@ export function watchContentDependencies({
     close,
   });
 
+  const eslint = new ESLint();
+
   // Watch adjacent files
   const metaPath = fileURLToPath(import.meta.url);
   const metaDirname = path.dirname(metaPath);
@@ -129,6 +132,13 @@ export function watchContentDependencies({
     let error = null;
 
     main: {
+      const eslintResults = await eslint.lintFiles([filePath]);
+      const eslintFormatter = await eslint.loadFormatter('stylish');
+      const eslintResultText = eslintFormatter.format(eslintResults);
+      if (eslintResultText.trim().length) {
+        console.log(eslintResultText);
+      }
+
       let spec;
       try {
         spec = (await import(cachebust(filePath))).default;
diff --git a/src/content/dependencies/linkAlbumAdditionalFile.js b/src/content/dependencies/linkAlbumAdditionalFile.js
index d1cca914..27c0ba9c 100644
--- a/src/content/dependencies/linkAlbumAdditionalFile.js
+++ b/src/content/dependencies/linkAlbumAdditionalFile.js
@@ -18,7 +18,9 @@ export default {
 
   generate(data, relations) {
     return relations.linkTemplate
-      .slot('path', ['media.albumAdditionalFile', data.albumDirectory, data.file])
-      .slot('content', data.file);
+      .slots({
+        path: ['media.albumAdditionalFile', data.albumDirectory, data.file],
+        content: data.file,
+      });
   },
 };
diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js
index acac99be..b87f3180 100644
--- a/src/content/dependencies/linkTemplate.js
+++ b/src/content/dependencies/linkTemplate.js
@@ -14,15 +14,25 @@ export default {
     html,
     to,
   }) {
-    return html.template(slot =>
-      slot('color', ([color]) =>
-      slot('hash', ([hash]) =>
-      slot('href', ([href]) =>
-      slot('path', ([...path]) => {
+    return html.template({
+      annotation: 'linkTemplate',
+
+      slots: {
+        href: {type: 'string'},
+        path: {validate: v => v.validateArrayItems(v.isString)},
+        hash: {type: 'string'},
+
+        attributes: {validate: v => v.isAttributes},
+        color: {validate: v => v.isColor},
+        content: {type: 'html'},
+      },
+
+      content(slots) {
+        let href = slots.href;
         let style;
 
-        if (!href && !empty(path)) {
-          href = to(...path);
+        if (!href && !empty(slots.path)) {
+          href = to(...slots.path);
         }
 
         if (appendIndexHTML) {
@@ -34,23 +44,23 @@ export default {
           }
         }
 
-        if (hash) {
-          href += (hash.startsWith('#') ? '' : '#') + hash;
+        if (slots.hash) {
+          href += (slots.hash.startsWith('#') ? '' : '#') + slots.hash;
         }
 
-        if (color) {
-          const {primary, dim} = getColors(color);
+        if (slots.color) {
+          const {primary, dim} = getColors(slots.color);
           style = `--primary-color: ${primary}; --dim-color: ${dim}`;
         }
 
-        return slot('attributes', ([attributes]) =>
-          html.tag('a',
-            {
-              ...attributes ?? {},
-              href,
-              style,
-            },
-            slot('content')));
-      })))));
+        return html.tag('a',
+          {
+            ...slots.attributes ?? {},
+            href,
+            style,
+          },
+          slots.content);
+      },
+    });
   },
 }
diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js
index ebff6761..70c86fc4 100644
--- a/src/content/dependencies/linkThing.js
+++ b/src/content/dependencies/linkThing.js
@@ -1,5 +1,3 @@
-import {empty} from '../../util/sugar.js';
-
 export default {
   contentDependencies: [
     'linkTemplate',
@@ -30,22 +28,40 @@ export default {
   generate(data, relations, {html}) {
     const path = [data.pathKey, data.directory];
 
-    return html.template(slot =>
-      slot('content', ([...content]) =>
-      slot('preferShortName', ([preferShortName]) => {
-        if (empty(content)) {
+    return html.template({
+      annotation: 'linkThing',
+
+      slots: {
+        content: relations.linkTemplate.getSlotDescription('content'),
+        preferShortName: {type: 'boolean', default: false},
+
+        color: relations.linkTemplate.getSlotDescription('color'),
+        attributes: relations.linkTemplate.getSlotDescription('attributes'),
+        hash: relations.linkTemplate.getSlotDescription('hash'),
+      },
+
+      content(slots) {
+        let content = slots.content;
+
+        if (html.isBlank(content)) {
           content =
-            (preferShortName
+            (slots.preferShortName
               ? data.nameShort ?? data.name
               : data.name);
         }
 
+        const color = slots.color ?? data.color ?? null;
+
         return relations.linkTemplate
-          .slot('path', path)
-          .slot('color', slot('color', data.color))
-          .slot('attributes', slot('attributes', {}))
-          .slot('hash', slot('hash'))
-          .slot('content', content);
-      })));
+          .slots({
+            path,
+            content,
+            color,
+
+            attributes: slots.attributes,
+            hash: slots.hash,
+          });
+      },
+    });
   },
 }