From 029210cc329a015a939472a688209d3f3423242b Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 30 May 2023 09:51:26 -0300 Subject: thumbs, content: integrate cached thumb sizes into content --- src/content/dependencies/image.js | 85 +++++++++++++++++++-------- src/gen-thumbs.js | 31 +++++++++- src/upd8.js | 36 +++++++++++- src/write/bind-utilities.js | 20 ++++++- src/write/build-modes/live-dev-server.js | 10 ++-- src/write/build-modes/static-build.js | 6 +- tap-snapshots/test/snapshot/image.js.test.cjs | 22 +++---- test/lib/content-function.js | 7 +++ test/snapshot/image.js | 4 +- 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`] = `
-
+
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`] = ` -
+
` exports[`test/snapshot/image.js TAP image (snapshot) > id with square 1`] = ` -
+
` exports[`test/snapshot/image.js TAP image (snapshot) > id without link 1`] = ` -
+
` exports[`test/snapshot/image.js TAP image (snapshot) > lazy with square 1`] = ` - -
+ +
` exports[`test/snapshot/image.js TAP image (snapshot) > link with file size 1`] = ` -
+
` 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`] = ` -
+
` exports[`test/snapshot/image.js TAP image (snapshot) > source via src 1`] = ` -
+
` exports[`test/snapshot/image.js TAP image (snapshot) > square 1`] = ` -
+
` exports[`test/snapshot/image.js TAP image (snapshot) > width & height 1`] = ` -
+
` 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'], -- cgit 1.3.0-6-gf8a5