« get me outta code hell

thumbs, content: integrate cached thumb sizes into content - 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-30 09:51:26 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-09-01 14:06:07 -0300
commit029210cc329a015a939472a688209d3f3423242b (patch)
treed0b3d682b699b0ba9e21642f4884c937d7f9f97c
parent641d821d03ad96bca28b8b09d2408457443a9f7f (diff)
thumbs, content: integrate cached thumb sizes into content
-rw-r--r--src/content/dependencies/image.js85
-rw-r--r--src/gen-thumbs.js31
-rwxr-xr-xsrc/upd8.js36
-rw-r--r--src/write/bind-utilities.js20
-rw-r--r--src/write/build-modes/live-dev-server.js10
-rw-r--r--src/write/build-modes/static-build.js6
-rw-r--r--tap-snapshots/test/snapshot/image.js.test.cjs22
-rw-r--r--test/lib/content-function.js7
-rw-r--r--test/snapshot/image.js4
9 files changed, 171 insertions, 50 deletions
diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js
index 71b905f7..905b3c2e 100644
--- a/src/content/dependencies/image.js
+++ b/src/content/dependencies/image.js
@@ -2,10 +2,12 @@ import {empty} from '#sugar';
 
 export default {
   extraDependencies: [
-    'getSizeOfImageFile',
+    'getDimensionsOfImagePath',
+    'getSizeOfImagePath',
+    'getThumbnailEqualOrSmaller',
+    'getThumbnailsAvailableForDimensions',
     'html',
     'language',
-    'thumb',
     'to',
   ],
 
@@ -52,10 +54,12 @@ export default {
   },
 
   generate(data, slots, {
-    getSizeOfImageFile,
+    getDimensionsOfImagePath,
+    getSizeOfImagePath,
+    getThumbnailEqualOrSmaller,
+    getThumbnailsAvailableForDimensions,
     html,
     language,
-    thumb,
     to,
   }) {
     let originalSrc;
@@ -68,12 +72,6 @@ export default {
       originalSrc = '';
     }
 
-    const thumbSrc =
-      originalSrc &&
-        (slots.thumb
-          ? thumb[slots.thumb](originalSrc)
-          : originalSrc);
-
     const willLink = typeof slots.link === 'string' || slots.link;
     const customLink = typeof slots.link === 'string';
 
@@ -95,18 +93,6 @@ export default {
           slots.missingSourceContent));
     }
 
-    let fileSize = null;
-    if (willLink) {
-      const mediaRoot = to('media.root');
-      if (originalSrc.startsWith(mediaRoot)) {
-        fileSize =
-          getSizeOfImageFile(
-            originalSrc
-              .slice(mediaRoot.length)
-              .replace(/^\//, ''));
-      }
-    }
-
     let reveal = null;
     if (willReveal) {
       reveal = [
@@ -119,16 +105,67 @@ export default {
       ];
     }
 
+    let mediaSrc = null;
+    if (originalSrc.startsWith(to('media.root'))) {
+      mediaSrc =
+        originalSrc
+          .slice(to('media.root').length)
+          .replace(/^\//, '');
+    }
+
+    let thumbSrc = null;
+    if (mediaSrc) {
+      // Note: This provides mediaSrc to getThumbnailEqualOrSmaller, since
+      // it's the identifier which thumbnail utilities use to query from the
+      // thumbnail cache. But we use the result to operate on originalSrc,
+      // which is the HTML output-appropriate path including `../../` or
+      // another alternate base path.
+      const selectedSize = getThumbnailEqualOrSmaller(slots.thumb, mediaSrc);
+      thumbSrc = originalSrc.replace(/\.(jpg|png)$/, `.${selectedSize}.jpg`);
+    }
+
+    let originalWidth = null;
+    let originalHeight = null;
+    let availableThumbs = null;
+    if (mediaSrc) {
+      [originalWidth, originalHeight] =
+        getDimensionsOfImagePath(mediaSrc);
+      availableThumbs =
+        getThumbnailsAvailableForDimensions([originalWidth, originalHeight]);
+    }
+
+    let fileSize = null;
+    if (willLink && mediaSrc) {
+      fileSize = getSizeOfImagePath(mediaSrc);
+    }
+
     const imgAttributes = {
       id: idOnImg,
       class: classOnImg,
       alt: slots.alt,
       width: slots.width,
       height: slots.height,
-      'data-original-size': fileSize,
-      'data-no-image-preview': customLink,
     };
 
+    if (customLink) {
+      imgAttributes['data-no-image-preview'] = true;
+    }
+
+    if (fileSize) {
+      imgAttributes['data-original-size'] = fileSize;
+    }
+
+    if (originalWidth && originalHeight) {
+      imgAttributes['data-original-length'] = Math.max(originalWidth, originalHeight);
+    }
+
+    if (!empty(availableThumbs)) {
+      imgAttributes['data-thumbs'] =
+        availableThumbs
+          .map(([name, size]) => `${name}:${size}`)
+          .join(' ');
+    }
+
     const nonlazyHTML =
       originalSrc &&
         prepare(
diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js
index bf6de286..f6a8eaaf 100644
--- a/src/gen-thumbs.js
+++ b/src/gen-thumbs.js
@@ -74,7 +74,7 @@
 
 'use strict';
 
-const CACHE_FILE = 'thumbnail-cache.json';
+export const CACHE_FILE = 'thumbnail-cache.json';
 const WARNING_DELAY_TIME = 10000;
 
 const thumbnailSpec = {
@@ -168,6 +168,30 @@ getThumbnailsAvailableForDimensions.all =
     .map(([name, {size}]) => [name, size])
     .sort((a, b) => b[1] - a[1]);
 
+export function getDimensionsOfImagePath(imagePath, cache) {
+  // This function is really generic. It takes the gen-thumbs image cache and
+  // returns the dimensions in that cache, so that other functions don't need
+  // to hard-code the cache format.
+
+  const [width, height] = cache[imagePath].slice(1);
+  return [width, height];
+}
+
+export function getThumbnailEqualOrSmaller(preferred, imagePath, cache) {
+  // This function is totally exclusive to page generation. It's a shorthand
+  // for accessing dimensions from the thumbnail cache, calculating all the
+  // thumbnails available, and selecting the one which is equal to or smaller
+  // than the provided size. Since the path provided might not be the actual
+  // one which is being thumbnail-ified, this just returns the name of the
+  // selected thumbnail size.
+
+  const {size: preferredSize} = thumbnailSpec[preferred];
+  const [width, height] = getDimensionsOfImagePath(imagePath, cache);
+  const available = getThumbnailsAvailableForDimensions([width, height]);
+  const [selected] = available.find(([name, size]) => size <= preferredSize);
+  return selected;
+}
+
 function readFileMD5(filePath) {
   return new Promise((resolve, reject) => {
     const md5 = createHash('md5');
@@ -302,7 +326,7 @@ export async function clearThumbs(mediaPath, {
         console.error(file);
       }
       fileIssue();
-      return;
+      return {success: false};
     }
 
     logInfo`Clearing out ${thumbFiles.length} thumbs.`;
@@ -327,6 +351,7 @@ export async function clearThumbs(mediaPath, {
         console.error(file);
       }
       logError`Check for permission errors?`;
+      return {success: false};
     } else {
       logInfo`Successfully deleted all ${thumbFiles.length} thumbnail files!`;
     }
@@ -355,6 +380,8 @@ export async function clearThumbs(mediaPath, {
       logWarn`Failed to remove cache file. Check its permissions?`;
     }
   }
+
+  return {success: true};
 }
 
 export default async function genThumbs(mediaPath, {
diff --git a/src/upd8.js b/src/upd8.js
index 2051517b..2f08204a 100755
--- a/src/upd8.js
+++ b/src/upd8.js
@@ -32,6 +32,7 @@
 // node.js and you'll 8e fine. ...Within the project root. O8viously.
 
 import {execSync} from 'node:child_process';
+import {readFile} from 'node:fs/promises';
 import * as path from 'node:path';
 import {fileURLToPath} from 'node:url';
 
@@ -57,6 +58,7 @@ import {
 } from '#cli';
 
 import genThumbs, {
+  CACHE_FILE as thumbsCacheFile,
   clearThumbs,
   defaultMagickThreads,
   isThumb,
@@ -428,7 +430,33 @@ async function main() {
     return;
   }
 
+  let thumbsCache;
+
   if (skipThumbs) {
+    const thumbsCachePath = path.join(mediaPath, thumbsCacheFile);
+    try {
+      thumbsCache = JSON.parse(await readFile(thumbsCachePath));
+      logInfo`Thumbnail cache file successfully read.`;
+    } catch (error) {
+      if (error.code === 'ENOENT') {
+        logError`The thumbnail cache doesn't exist, and it's necessary to build`
+        logError`the website. Please run once without ${'--skip-thumbs'} - after`
+        logError`that you'll be good to go and don't need to process thumbnails`
+        logError`again!`;
+        return;
+      } else {
+        logError`Malformed or unreadable thumbnail cache file: ${error}`;
+        logError`Path: ${thumbsCachePath}`;
+        logError`The thumbbnail cache is necessary to build the site, so you'll`;
+        logError`have to investigate this to get the build working. Try running`;
+        logError`again without ${'--skip-thumbs'}. If you can't get it working,`;
+        logError`you're welcome to message in the HSMusic Discord and we'll try`;
+        logError`to help you out with troubleshooting!`;
+        logError`${'https://hsmusic.wiki/discord/'}`;
+        return;
+      }
+    }
+
     logInfo`Skipping thumbnail generation.`;
   } else {
     logInfo`Begin thumbnail generation... -----+`;
@@ -440,6 +468,7 @@ async function main() {
     logInfo`Done thumbnail generation! --------+`;
     if (!result.success) return;
     if (thumbsOnly) return;
+    thumbsCache = result.cache;
   }
 
   if (noBuild) {
@@ -705,7 +734,7 @@ async function main() {
   };
 
   const getSizeOfAdditionalFile = getSizeOfMediaFileHelper(additionalFilePaths);
-  const getSizeOfImageFile = getSizeOfMediaFileHelper(imageFilePaths);
+  const getSizeOfImagePath = getSizeOfMediaFileHelper(imageFilePaths);
 
   logInfo`Preloading filesizes for ${additionalFilePaths.length} additional files...`;
 
@@ -760,14 +789,15 @@ async function main() {
 
     defaultLanguage: finalDefaultLanguage,
     languages,
-    wikiData,
+    thumbsCache,
     urls,
     urlSpec,
+    wikiData,
 
     cachebust: '?' + CACHEBUST,
     developersComment,
     getSizeOfAdditionalFile,
-    getSizeOfImageFile,
+    getSizeOfImagePath,
     niceShowAggregate,
   });
 }
diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js
index 8e2adea7..c32035f1 100644
--- a/src/write/bind-utilities.js
+++ b/src/write/bind-utilities.js
@@ -10,15 +10,22 @@ import * as html from '#html';
 import {bindOpts} from '#sugar';
 import {thumb} from '#urls';
 
+import {
+  getDimensionsOfImagePath,
+  getThumbnailEqualOrSmaller,
+  getThumbnailsAvailableForDimensions,
+} from '#thumbs';
+
 export function bindUtilities({
   absoluteTo,
   cachebust,
   defaultLanguage,
   getSizeOfAdditionalFile,
-  getSizeOfImageFile,
+  getSizeOfImagePath,
   language,
   languages,
   pagePath,
+  thumbsCache,
   to,
   urls,
   wikiData,
@@ -30,7 +37,8 @@ export function bindUtilities({
     cachebust,
     defaultLanguage,
     getSizeOfAdditionalFile,
-    getSizeOfImageFile,
+    getSizeOfImagePath,
+    getThumbnailsAvailableForDimensions,
     html,
     language,
     languages,
@@ -46,5 +54,13 @@ export function bindUtilities({
 
   bound.find = bindFind(wikiData, {mode: 'warn'});
 
+  bound.getDimensionsOfImagePath =
+    (imagePath) =>
+      getDimensionsOfImagePath(imagePath, thumbsCache);
+
+  bound.getThumbnailEqualOrSmaller =
+    (preferred, imagePath) =>
+      getThumbnailEqualOrSmaller(preferred, imagePath, thumbsCache);
+
   return bound;
 }
diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js
index 28cf7a48..9889b3f0 100644
--- a/src/write/build-modes/live-dev-server.js
+++ b/src/write/build-modes/live-dev-server.js
@@ -59,13 +59,14 @@ export async function go({
   defaultLanguage,
   languages,
   srcRootPath,
+  thumbsCache,
   urls,
   wikiData,
 
   cachebust,
   developersComment,
   getSizeOfAdditionalFile,
-  getSizeOfImageFile,
+  getSizeOfImagePath,
   niceShowAggregate,
 }) {
   const showError = (error) => {
@@ -343,10 +344,11 @@ export async function go({
         cachebust,
         defaultLanguage,
         getSizeOfAdditionalFile,
-        getSizeOfImageFile,
+        getSizeOfImagePath,
         language,
         languages,
         pagePath: servePath,
+        thumbsCache,
         to,
         urls,
         wikiData,
@@ -367,10 +369,10 @@ export async function go({
       response.writeHead(200, contentTypeHTML);
       response.end(pageHTML);
     } catch (error) {
-      response.writeHead(500, contentTypePlain);
-      response.end(`Error generating page, view server log for details\n`);
       console.error(`${requestHead} [500] ${pathname}`);
       showError(error);
+      response.writeHead(500, contentTypePlain);
+      response.end(`Error generating page, view server log for details\n`);
     }
   });
 
diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js
index 192b7966..82a947c7 100644
--- a/src/write/build-modes/static-build.js
+++ b/src/write/build-modes/static-build.js
@@ -91,6 +91,7 @@ export async function go({
   defaultLanguage,
   languages,
   srcRootPath,
+  thumbsCache,
   urls,
   urlSpec,
   wikiData,
@@ -98,7 +99,7 @@ export async function go({
   cachebust,
   developersComment,
   getSizeOfAdditionalFile,
-  getSizeOfImageFile,
+  getSizeOfImagePath,
   niceShowAggregate,
 }) {
   const outputPath = cliOptions['out-path'] || process.env.HSMUSIC_OUT;
@@ -298,10 +299,11 @@ export async function go({
           cachebust,
           defaultLanguage,
           getSizeOfAdditionalFile,
-          getSizeOfImageFile,
+          getSizeOfImagePath,
           language,
           languages,
           pagePath,
+          thumbsCache,
           to,
           urls,
           wikiData,
diff --git a/tap-snapshots/test/snapshot/image.js.test.cjs b/tap-snapshots/test/snapshot/image.js.test.cjs
index c2f6aea6..e451e1d5 100644
--- a/tap-snapshots/test/snapshot/image.js.test.cjs
+++ b/tap-snapshots/test/snapshot/image.js.test.cjs
@@ -7,7 +7,7 @@
 'use strict'
 exports[`test/snapshot/image.js TAP image (snapshot) > content warnings via tags 1`] = `
 <div class="reveal">
-    <div class="image-container"><div class="image-inner-area"><img src="media/album-art/beyond-canon/cover.png"></div></div>
+    <div class="image-container"><div class="image-inner-area"><img data-original-length="600" data-thumbs="large:800 medium:400 small:250" src="media/album-art/beyond-canon/cover.medium.jpg"></div></div>
     <span class="reveal-text-container">
         <span class="reveal-text">
             cw: too cool for school
@@ -19,24 +19,24 @@ exports[`test/snapshot/image.js TAP image (snapshot) > content warnings via tags
 `
 
 exports[`test/snapshot/image.js TAP image (snapshot) > id with link 1`] = `
-<a id="banana" class="box image-link" href="foobar"><div class="image-container"><div class="image-inner-area"><img src="foobar"></div></div></a>
+<a id="banana" class="box image-link" href="foobar"><div class="image-container"><div class="image-inner-area"><img></div></div></a>
 `
 
 exports[`test/snapshot/image.js TAP image (snapshot) > id with square 1`] = `
-<div class="square"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img id="banana" src="foobar"></div></div></div></div>
+<div class="square"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img id="banana"></div></div></div></div>
 `
 
 exports[`test/snapshot/image.js TAP image (snapshot) > id without link 1`] = `
-<div class="image-container"><div class="image-inner-area"><img id="banana" src="foobar"></div></div>
+<div class="image-container"><div class="image-inner-area"><img id="banana"></div></div>
 `
 
 exports[`test/snapshot/image.js TAP image (snapshot) > lazy with square 1`] = `
-<noscript><div class="square"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img src="foobar"></div></div></div></div></noscript>
-<div class="square js-hide"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img class="lazy" data-original="foobar"></div></div></div></div>
+<noscript><div class="square"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img></div></div></div></div></noscript>
+<div class="square js-hide"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img class="lazy"></div></div></div></div>
 `
 
 exports[`test/snapshot/image.js TAP image (snapshot) > link with file size 1`] = `
-<a class="box image-link" href="media/album-art/pingas/cover.png"><div class="image-container"><div class="image-inner-area"><img data-original-size="1000000" src="media/album-art/pingas/cover.png"></div></div></a>
+<a class="box image-link" href="media/album-art/pingas/cover.png"><div class="image-container"><div class="image-inner-area"><img data-original-size="1000000" data-original-length="600" data-thumbs="large:800 medium:400 small:250" src="media/album-art/pingas/cover.medium.jpg"></div></div></a>
 `
 
 exports[`test/snapshot/image.js TAP image (snapshot) > source missing 1`] = `
@@ -44,17 +44,17 @@ exports[`test/snapshot/image.js TAP image (snapshot) > source missing 1`] = `
 `
 
 exports[`test/snapshot/image.js TAP image (snapshot) > source via path 1`] = `
-<div class="image-container"><div class="image-inner-area"><img src="media/album-art/beyond-canon/cover.png"></div></div>
+<div class="image-container"><div class="image-inner-area"><img data-original-length="600" data-thumbs="large:800 medium:400 small:250" src="media/album-art/beyond-canon/cover.medium.jpg"></div></div>
 `
 
 exports[`test/snapshot/image.js TAP image (snapshot) > source via src 1`] = `
-<div class="image-container"><div class="image-inner-area"><img src="https://example.com/bananas.gif"></div></div>
+<div class="image-container"><div class="image-inner-area"><img></div></div>
 `
 
 exports[`test/snapshot/image.js TAP image (snapshot) > square 1`] = `
-<div class="square"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img src="foobar"></div></div></div></div>
+<div class="square"><div class="square-content"><div class="image-container"><div class="image-inner-area"><img></div></div></div></div>
 `
 
 exports[`test/snapshot/image.js TAP image (snapshot) > width & height 1`] = `
-<div class="image-container"><div class="image-inner-area"><img width="600" height="400" src="foobar"></div></div>
+<div class="image-container"><div class="image-inner-area"><img width="600" height="400"></div></div>
 `
diff --git a/test/lib/content-function.js b/test/lib/content-function.js
index bb12be82..cd86e9bc 100644
--- a/test/lib/content-function.js
+++ b/test/lib/content-function.js
@@ -49,8 +49,15 @@ export function testContentFunctions(t, message, fn) {
             thumb,
             to,
             urls,
+
             appendIndexHTML: false,
+
             getColors: c => getColors(c, {chroma}),
+            getDimensionsOfImagePath: () => [600, 600],
+            getThumbnailEqualOrSmaller: () => 'medium',
+            getThumbnailsAvailableForDimensions: () =>
+              [['large', 800], ['medium', 400], ['small', 250]],
+
             ...extraDependencies,
           },
         });
diff --git a/test/snapshot/image.js b/test/snapshot/image.js
index 6bec1cca..5e12cc25 100644
--- a/test/snapshot/image.js
+++ b/test/snapshot/image.js
@@ -8,7 +8,7 @@ testContentFunctions(t, 'image (snapshot)', async (t, evaluate) => {
     evaluate.snapshot(message, {
       name: 'image',
       extraDependencies: {
-        getSizeOfImageFile: () => 0,
+        getSizeOfImagePath: () => 0,
       },
       ...opts,
     });
@@ -79,7 +79,7 @@ testContentFunctions(t, 'image (snapshot)', async (t, evaluate) => {
 
   quickSnapshot('link with file size', {
     extraDependencies: {
-      getSizeOfImageFile: () => 10 ** 6,
+      getSizeOfImagePath: () => 10 ** 6,
     },
     slots: {
       path: ['media.albumCover', 'pingas', 'png'],