From d878ab29f20c0727acafb4b1150d4e31d69c55c0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 11 Sep 2023 10:09:48 -0300 Subject: data, html, infra: supporting changes for sanitizing content --- src/data/things/language.js | 88 +++++++++++++++++++++++++++++++-------------- src/data/things/thing.js | 4 +-- src/util/html.js | 57 ++++++++++++++++++++--------- 3 files changed, 104 insertions(+), 45 deletions(-) diff --git a/src/data/things/language.js b/src/data/things/language.js index 7755c505..cc49b735 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -1,5 +1,10 @@ import Thing from './thing.js'; +import {Tag} from '#html'; +import {isLanguageCode} from '#validators'; + +import CacheableObject from './cacheable-object.js'; + export class Language extends Thing { static [Thing.getPropertyDescriptors] = ({ validators: { @@ -68,7 +73,7 @@ export class Language extends Thing { // Update only - escapeHTML: Thing.common.externalFunction(), + escapeHTML: Thing.common.externalFunction({expose: true}), // Expose only @@ -140,19 +145,9 @@ export class Language extends Thing { } formatString(key, args = {}) { - if (this.strings && !this.strings_htmlEscaped) { - throw new Error(`HTML-escaped strings unavailable - please ensure escapeHTML function is provided`); - } - - return this.formatStringHelper(this.strings_htmlEscaped, key, args); - } + const strings = this.strings_htmlEscaped; - formatStringNoHTMLEscape(key, args = {}) { - return this.formatStringHelper(this.strings, key, args); - } - - formatStringHelper(strings, key, args = {}) { - if (!strings) { + if (!this.strings) { throw new Error(`Strings unavailable`); } @@ -160,22 +155,25 @@ export class Language extends Thing { throw new Error(`Invalid key ${key} accessed`); } - const template = strings[key]; + const template = this.strings[key]; // Convert the keys on the args dict from camelCase to CONSTANT_CASE. // (This isn't an OUTRAGEOUSLY versatile algorithm for doing that, 8ut // like, who cares, dude?) Also, this is an array, 8ecause it's handy - // for the iterating we're a8out to do. - const processedArgs = Object.entries(args).map(([k, v]) => [ - k.replace(/[A-Z]/g, '_$&').toUpperCase(), - v, - ]); + // for the iterating we're a8out to do. Also strip HTML from arguments + // that are literal strings - real HTML content should always be proper + // HTML objects (see html.js). + const processedArgs = + Object.entries(args).map(([k, v]) => [ + k.replace(/[A-Z]/g, '_$&').toUpperCase(), + this.#sanitizeStringArg(v), + ]); // Replacement time! Woot. Reduce comes in handy here! - const output = processedArgs.reduce( - (x, [k, v]) => x.replaceAll(`{${k}}`, v), - template - ); + const output = + processedArgs.reduce( + (x, [k, v]) => x.replaceAll(`{${k}}`, v), + template); // Post-processing: if any expected arguments *weren't* replaced, that // is almost definitely an error. @@ -183,7 +181,37 @@ export class Language extends Thing { throw new Error(`Args in ${key} were missing - output: ${output}`); } - return output; + // Last caveat: Wrap the output in an HTML tag so that it doesn't get + // treated as unsanitized HTML if *it* gets passed as an argument to + // *another* formatString call. + return this.#wrapSanitized(output); + } + + // Escapes HTML special characters so they're displayed as-are instead of + // treated by the browser as a tag. This does *not* have an effect on actual + // html.Tag objects, which are treated as sanitized by default (so that they + // can be nested inside strings at all). + #sanitizeStringArg(arg) { + const escapeHTML = this.escapeHTML; + + if (!escapeHTML) { + throw new Error(`escapeHTML unavailable`); + } + + if (typeof arg !== 'string') { + return arg.toString(); + } + + return escapeHTML(arg); + } + + // Wraps the output of a formatting function in a no-name-nor-attributes + // HTML tag, which will indicate to other calls to formatString that this + // content is a string *that may contain HTML* and doesn't need to + // sanitized any further. It'll still .toString() to just the string + // contents, if needed. + #wrapSanitized(output) { + return new Tag(null, null, output); } formatDate(date) { @@ -252,19 +280,25 @@ export class Language extends Thing { // Conjunction list: A, B, and C formatConjunctionList(array) { this.assertIntlAvailable('intl_listConjunction'); - return this.intl_listConjunction.format(array.map(arr => arr.toString())); + return this.#wrapSanitized( + this.intl_listConjunction.format( + array.map(item => this.#sanitizeStringArg(item)))); } // Disjunction lists: A, B, or C formatDisjunctionList(array) { this.assertIntlAvailable('intl_listDisjunction'); - return this.intl_listDisjunction.format(array.map(arr => arr.toString())); + return this.#wrapSanitized( + this.intl_listDisjunction.format( + array.map(item => this.#sanitizeStringArg(item)))); } // Unit lists: A, B, C formatUnitList(array) { this.assertIntlAvailable('intl_listUnit'); - return this.intl_listUnit.format(array.map(arr => arr.toString())); + return this.#wrapSanitized( + this.intl_listUnit.format( + array.map(item => this.#sanitizeStringArg(item)))); } // File sizes: 42.5 kB, 127.2 MB, 4.13 GB, 998.82 TB diff --git a/src/data/things/thing.js b/src/data/things/thing.js index c2876f56..5705ee7e 100644 --- a/src/data/things/thing.js +++ b/src/data/things/thing.js @@ -105,8 +105,8 @@ export default class Thing extends CacheableObject { // External function. These should only be used as dependencies for other // properties, so they're left unexposed. - externalFunction: () => ({ - flags: {update: true}, + externalFunction: ({expose = false} = {}) => ({ + flags: {update: true, expose}, update: {validate: (t) => typeof t === 'function'}, }), diff --git a/src/util/html.js b/src/util/html.js index a311bbba..f0c7bfdf 100644 --- a/src/util/html.js +++ b/src/util/html.js @@ -806,24 +806,43 @@ export class Template { } // Null is always an acceptable slot value. - if (value !== null) { - if ('validate' in description) { - description.validate({ - ...commonValidators, - ...validators, - })(value); - } + if (value === null) { + return true; + } + + if ('validate' in description) { + description.validate({ + ...commonValidators, + ...validators, + })(value); + } - if ('type' in description) { - const {type} = description; - if (type === 'html') { - if (!isHTML(value)) { + if ('type' in description) { + switch (description.type) { + case 'html': { + if (!isHTML(value)) throw new TypeError(`Slot expects html (tag, template or blank), got ${typeof value}`); - } - } else { - if (typeof value !== type) { - throw new TypeError(`Slot expects ${type}, got ${typeof value}`); - } + + return true; + } + + case 'string': { + // Tags and templates are valid in string arguments - they'll be + // stringified when exposed to the description's .content() function. + if (isTag(value) || isTemplate(value)) + return true; + + if (typeof value !== 'string') + throw new TypeError(`Slot expects string, got ${typeof value}`); + + return true; + } + + default: { + if (typeof value !== description.type) + throw new TypeError(`Slot expects ${description.type}, got ${typeof value}`); + + return true; } } } @@ -847,6 +866,12 @@ export class Template { return providedValue; } + if (description.type === 'string') { + if (isTag(providedValue) || isTemplate(providedValue)) { + return providedValue.toString(); + } + } + if (providedValue !== null) { return providedValue; } -- cgit 1.3.0-6-gf8a5 From 3eb82ab2e3f9d921095af05cf0bc284f335aaa35 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 11 Sep 2023 10:11:44 -0300 Subject: content: misc. changes to handle HTML sanitization --- src/content/dependencies/generateCoverGrid.js | 12 ++++++--- .../generateFooterLocalizationLinks.js | 2 +- src/content/dependencies/generatePageLayout.js | 6 ++--- src/content/dependencies/linkTemplate.js | 2 +- src/content/dependencies/linkThing.js | 2 +- src/data/things/language.js | 29 ++++++++++++++++++++++ 6 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/content/dependencies/generateCoverGrid.js b/src/content/dependencies/generateCoverGrid.js index 9822e1ae..5636e4f3 100644 --- a/src/content/dependencies/generateCoverGrid.js +++ b/src/content/dependencies/generateCoverGrid.js @@ -2,7 +2,7 @@ import {stitchArrays} from '#sugar'; export default { contentDependencies: ['generateGridActionLinks'], - extraDependencies: ['html'], + extraDependencies: ['html', 'language'], relations(relation) { return { @@ -20,7 +20,7 @@ export default { actionLinks: {validate: v => v.sparseArrayOf(v.isHTML)}, }, - generate(relations, slots, {html}) { + generate(relations, slots, {html, language}) { return ( html.tag('div', {class: 'grid-listing'}, [ stitchArrays({ @@ -42,8 +42,12 @@ export default { ? slots.lazy : false), }), - html.tag('span', {[html.onlyIfContent]: true}, name), - html.tag('span', {[html.onlyIfContent]: true}, info), + + html.tag('span', {[html.onlyIfContent]: true}, + language.sanitize(name)), + + html.tag('span', {[html.onlyIfContent]: true}, + language.sanitize(info)), ], })), diff --git a/src/content/dependencies/generateFooterLocalizationLinks.js b/src/content/dependencies/generateFooterLocalizationLinks.js index b4970b17..5df83566 100644 --- a/src/content/dependencies/generateFooterLocalizationLinks.js +++ b/src/content/dependencies/generateFooterLocalizationLinks.js @@ -38,7 +38,7 @@ export default { return html.tag('div', {class: 'footer-localization-links'}, language.$('misc.uiLanguage', { - languages: links.join('\n'), + languages: language.formatListWithoutSeparator(links), })); }, }; diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js index 95a5dbec..5377f80d 100644 --- a/src/content/dependencies/generatePageLayout.js +++ b/src/content/dependencies/generatePageLayout.js @@ -105,7 +105,7 @@ export default { color: {validate: v => v.isColor}, styleRules: { - validate: v => v.sparseArrayOf(v.isString), + validate: v => v.sparseArrayOf(v.isHTML), default: [], }, @@ -183,7 +183,7 @@ export default { } else { aggregate.call(v.validateProperties({ path: v.strictArrayOf(v.isString), - title: v.isString, + title: v.isHTML, }), { path: object.path, title: object.title, @@ -521,7 +521,7 @@ export default { ]), slots.bannerPosition === 'bottom' && slots.banner, footerHTML, - ].filter(Boolean).join('\n'); + ]; const pageHTML = html.tags([ ``, diff --git a/src/content/dependencies/linkTemplate.js b/src/content/dependencies/linkTemplate.js index ba7c7cda..7206e960 100644 --- a/src/content/dependencies/linkTemplate.js +++ b/src/content/dependencies/linkTemplate.js @@ -16,7 +16,7 @@ export default { path: {validate: v => v.validateArrayItems(v.isString)}, hash: {type: 'string'}, - tooltip: {validate: v => v.isString}, + tooltip: {type: 'string'}, attributes: {validate: v => v.isAttributes}, color: {validate: v => v.isColor}, content: {type: 'html'}, diff --git a/src/content/dependencies/linkThing.js b/src/content/dependencies/linkThing.js index e3e2608f..643bf4b1 100644 --- a/src/content/dependencies/linkThing.js +++ b/src/content/dependencies/linkThing.js @@ -26,7 +26,7 @@ export default { preferShortName: {type: 'boolean', default: false}, tooltip: { - validate: v => v.oneOf(v.isBoolean, v.isString), + validate: v => v.oneOf(v.isBoolean, v.isHTML), default: false, }, diff --git a/src/data/things/language.js b/src/data/things/language.js index cc49b735..afa9f1ee 100644 --- a/src/data/things/language.js +++ b/src/data/things/language.js @@ -214,6 +214,28 @@ export class Language extends Thing { return new Tag(null, null, output); } + // Similar to the above internal methods, but this one is public. + // It should be used when embedding content that may not have previously + // been sanitized directly into an HTML tag or template's contents. + // The templating engine usually handles this on its own, as does passing + // a value (sanitized or not) directly as an argument to formatString, + // but if you used a custom validation function ({validate: v => v.isHTML} + // instead of {type: 'string'} / {type: 'html'}) and are embedding the + // contents of a slot directly, it should be manually sanitized with this + // function first. + sanitize(arg) { + const escapeHTML = this.escapeHTML; + + if (!escapeHTML) { + throw new Error(`escapeHTML unavailable`); + } + + return ( + (typeof arg === 'string' + ? new Tag(null, null, escapeHTML(arg)) + : arg)); + } + formatDate(date) { this.assertIntlAvailable('intl_date'); return this.intl_date.format(date); @@ -301,6 +323,13 @@ export class Language extends Thing { array.map(item => this.#sanitizeStringArg(item)))); } + // Lists without separator: A B C + formatListWithoutSeparator(array) { + return this.#wrapSanitized( + array.map(item => this.#sanitizeStringArg(item)) + .join(' ')); + } + // File sizes: 42.5 kB, 127.2 MB, 4.13 GB, 998.82 TB formatFileSize(bytes) { if (!bytes) return ''; -- cgit 1.3.0-6-gf8a5 From 1ed6ddbfc682ff36ec85dfe42cdac159d6de0394 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 11 Sep 2023 10:13:23 -0300 Subject: test: snapshot test updates --- .../test/snapshot/generatePreviousNextLinks.js.test.cjs | 12 ++++++------ .../test/snapshot/generateTrackReleaseInfo.js.test.cjs | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tap-snapshots/test/snapshot/generatePreviousNextLinks.js.test.cjs b/tap-snapshots/test/snapshot/generatePreviousNextLinks.js.test.cjs index fa641830..8171725b 100644 --- a/tap-snapshots/test/snapshot/generatePreviousNextLinks.js.test.cjs +++ b/tap-snapshots/test/snapshot/generatePreviousNextLinks.js.test.cjs @@ -6,13 +6,13 @@ */ 'use strict' exports[`test/snapshot/generatePreviousNextLinks.js TAP generatePreviousNextLinks (snapshot) > basic behavior 1`] = ` -previous: { tooltip: true, color: false, attributes: { id: 'previous-button' }, content: 'Previous' } -next: { tooltip: true, color: false, attributes: { id: 'next-button' }, content: 'Next' } +previous: { tooltip: true, color: false, attributes: { id: 'previous-button' }, content: Tag (no name, 1 items) } +next: { tooltip: true, color: false, attributes: { id: 'next-button' }, content: Tag (no name, 1 items) } ` exports[`test/snapshot/generatePreviousNextLinks.js TAP generatePreviousNextLinks (snapshot) > disable id 1`] = ` -previous: { tooltip: true, color: false, attributes: { id: false }, content: 'Previous' } -next: { tooltip: true, color: false, attributes: { id: false }, content: 'Next' } +previous: { tooltip: true, color: false, attributes: { id: false }, content: Tag (no name, 1 items) } +next: { tooltip: true, color: false, attributes: { id: false }, content: Tag (no name, 1 items) } ` exports[`test/snapshot/generatePreviousNextLinks.js TAP generatePreviousNextLinks (snapshot) > neither link present 1`] = ` @@ -20,9 +20,9 @@ exports[`test/snapshot/generatePreviousNextLinks.js TAP generatePreviousNextLink ` exports[`test/snapshot/generatePreviousNextLinks.js TAP generatePreviousNextLinks (snapshot) > next missing 1`] = ` -previous: { tooltip: true, color: false, attributes: { id: 'previous-button' }, content: 'Previous' } +previous: { tooltip: true, color: false, attributes: { id: 'previous-button' }, content: Tag (no name, 1 items) } ` exports[`test/snapshot/generatePreviousNextLinks.js TAP generatePreviousNextLinks (snapshot) > previous missing 1`] = ` -next: { tooltip: true, color: false, attributes: { id: 'next-button' }, content: 'Next' } +next: { tooltip: true, color: false, attributes: { id: 'next-button' }, content: Tag (no name, 1 items) } ` diff --git a/tap-snapshots/test/snapshot/generateTrackReleaseInfo.js.test.cjs b/tap-snapshots/test/snapshot/generateTrackReleaseInfo.js.test.cjs index e94ed822..bfd7446a 100644 --- a/tap-snapshots/test/snapshot/generateTrackReleaseInfo.js.test.cjs +++ b/tap-snapshots/test/snapshot/generateTrackReleaseInfo.js.test.cjs @@ -18,19 +18,19 @@ exports[`test/snapshot/generateTrackReleaseInfo.js TAP generateTrackReleaseInfo exports[`test/snapshot/generateTrackReleaseInfo.js TAP generateTrackReleaseInfo (snapshot) > cover artist contribs, non-unique 1`] = `

By Toby Fox.

-

This wiki doesn't have any listening links for Suspicious Track.

+

This wiki doesn't have any listening links for Suspicious Track.

` exports[`test/snapshot/generateTrackReleaseInfo.js TAP generateTrackReleaseInfo (snapshot) > cover artist contribs, unique 1`] = `

By Toby Fox.
- Cover art by Alpaca (🔥). + Cover art by Alpaca (🔥).

-

This wiki doesn't have any listening links for Suspicious Track.

+

This wiki doesn't have any listening links for Suspicious Track.

` exports[`test/snapshot/generateTrackReleaseInfo.js TAP generateTrackReleaseInfo (snapshot) > reduced details 1`] = `

By Toby Fox.

-

This wiki doesn't have any listening links for Suspicious Track.

+

This wiki doesn't have any listening links for Suspicious Track.

` -- cgit 1.3.0-6-gf8a5 From 4ac038fce1f99ca12a9bca578fb9d0ffd8482ecd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 11 Sep 2023 14:33:47 -0300 Subject: content: generateAlbumSidebarTrackSection: don't pre-encode endash --- src/content/dependencies/generateAlbumSidebarTrackSection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content/dependencies/generateAlbumSidebarTrackSection.js b/src/content/dependencies/generateAlbumSidebarTrackSection.js index 2aca6da1..d71b0bdb 100644 --- a/src/content/dependencies/generateAlbumSidebarTrackSection.js +++ b/src/content/dependencies/generateAlbumSidebarTrackSection.js @@ -82,7 +82,7 @@ export default { (data.hasTrackNumbers ? language.$('albumSidebar.trackList.group.withRange', { group: sectionName, - range: `${data.firstTrackNumber}–${data.lastTrackNumber}` + range: `${data.firstTrackNumber}–${data.lastTrackNumber}` }) : language.$('albumSidebar.trackList.group', { group: sectionName, -- cgit 1.3.0-6-gf8a5 From 78115f0be17ee405d3711204aaa53e0597a29826 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 11 Sep 2023 14:34:12 -0300 Subject: thumbs: read win32-style path from cache --- src/gen-thumbs.js | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index 741cdff3..a5b550ad 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -168,28 +168,45 @@ getThumbnailsAvailableForDimensions.all = .map(([name, {size}]) => [name, size]) .sort((a, b) => b[1] - a[1]); -export function checkIfImagePathHasCachedThumbnails(imagePath, cache) { +function getCacheEntryForMediaPath(mediaPath, cache) { + // Gets the cache entry for the provided image path, which should always be + // a forward-slashes path (i.e. suitable for display online). Since the cache + // file may have forward or back-slashes, this checks both. + + const entryFromMediaPath = cache[mediaPath]; + if (entryFromMediaPath) return entryFromMediaPath; + + const winPath = mediaPath.split(path.posix.sep).join(path.win32.sep); + const entryFromWinPath = cache[winPath]; + if (entryFromWinPath) return entryFromWinPath; + + return null; +} + +export function checkIfImagePathHasCachedThumbnails(mediaPath, cache) { // Generic utility for checking if the thumbnail cache includes any info for // the provided image path, so that the other functions don't hard-code the // cache format. - return !!cache[imagePath]; + return !!getCacheEntryForMediaPath(mediaPath, cache); } -export function getDimensionsOfImagePath(imagePath, cache) { +export function getDimensionsOfImagePath(mediaPath, 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. - if (!cache[imagePath]) { - throw new Error(`Expected imagePath to be included in cache, got ${imagePath}`); + const cacheEntry = getCacheEntryForMediaPath(mediaPath, cache); + + if (!cacheEntry) { + throw new Error(`Expected mediaPath to be included in cache, got ${mediaPath}`); } - const [width, height] = cache[imagePath].slice(1); + const [width, height] = cacheEntry.slice(1); return [width, height]; } -export function getThumbnailEqualOrSmaller(preferred, imagePath, cache) { +export function getThumbnailEqualOrSmaller(preferred, mediaPath, 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 @@ -197,12 +214,12 @@ export function getThumbnailEqualOrSmaller(preferred, imagePath, cache) { // one which is being thumbnail-ified, this just returns the name of the // selected thumbnail size. - if (!cache[imagePath]) { - throw new Error(`Expected imagePath to be included in cache, got ${imagePath}`); + if (!getCacheEntryForMediaPath(mediaPath, cache)) { + throw new Error(`Expected mediaPath to be included in cache, got ${mediaPath}`); } const {size: preferredSize} = thumbnailSpec[preferred]; - const [width, height] = getDimensionsOfImagePath(imagePath, cache); + const [width, height] = getDimensionsOfImagePath(mediaPath, cache); const available = getThumbnailsAvailableForDimensions([width, height]); const [selected] = available.find(([name, size]) => size <= preferredSize); return selected; -- cgit 1.3.0-6-gf8a5 From b19e165dc8ba13cd0e2d1862e645d34d86142566 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 11 Sep 2023 15:09:19 -0300 Subject: thumbs, infra: expose list of missing image paths --- src/gen-thumbs.js | 2 ++ src/upd8.js | 4 +++- src/write/bind-utilities.js | 2 ++ src/write/build-modes/live-dev-server.js | 2 ++ src/write/build-modes/static-build.js | 2 ++ 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/gen-thumbs.js b/src/gen-thumbs.js index a5b550ad..b7c192c3 100644 --- a/src/gen-thumbs.js +++ b/src/gen-thumbs.js @@ -689,6 +689,8 @@ export async function verifyImagePaths(mediaPath, {urls, wikiData}) { console.warn(color.yellow(` - `) + file); } } + + return {missing, misplaced}; } // Recursively traverses the provided (extant) media path, filtering so only diff --git a/src/upd8.js b/src/upd8.js index 2ec231c9..b3646c9d 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -677,7 +677,8 @@ async function main() { const urls = generateURLs(urlSpec); - await verifyImagePaths(mediaPath, {urls, wikiData}); + const {missing: missingImagePaths} = + await verifyImagePaths(mediaPath, {urls, wikiData}); const fileSizePreloader = new FileSizePreloader(); @@ -795,6 +796,7 @@ async function main() { defaultLanguage: finalDefaultLanguage, languages, + missingImagePaths, thumbsCache, urls, urlSpec, diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js index 942cce89..3d4ecc7a 100644 --- a/src/write/bind-utilities.js +++ b/src/write/bind-utilities.js @@ -25,6 +25,7 @@ export function bindUtilities({ getSizeOfImagePath, language, languages, + missingImagePaths, pagePath, thumbsCache, to, @@ -43,6 +44,7 @@ export function bindUtilities({ html, language, languages, + missingImagePaths, pagePath, thumb, to, diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index 9889b3f0..730686db 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -58,6 +58,7 @@ export async function go({ defaultLanguage, languages, + missingImagePaths, srcRootPath, thumbsCache, urls, @@ -347,6 +348,7 @@ export async function go({ getSizeOfImagePath, language, languages, + missingImagePaths, pagePath: servePath, thumbsCache, to, diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index 82a947c7..79c8defd 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -90,6 +90,7 @@ export async function go({ defaultLanguage, languages, + missingImagePaths, srcRootPath, thumbsCache, urls, @@ -302,6 +303,7 @@ export async function go({ getSizeOfImagePath, language, languages, + missingImagePaths, pagePath, thumbsCache, to, -- cgit 1.3.0-6-gf8a5 From a88d346f9e7c2c9b01795c10761a5ec698e88f94 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 11 Sep 2023 15:11:24 -0300 Subject: content: image: custom-handle images assessed to be missing --- src/content/dependencies/image.js | 42 +++++++++++++++++++++++++++------------ src/strings-default.json | 1 + 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js index b5591e6d..64fe8533 100644 --- a/src/content/dependencies/image.js +++ b/src/content/dependencies/image.js @@ -1,4 +1,4 @@ -import {logWarn} from '#cli'; +import {logInfo, logWarn} from '#cli'; import {empty} from '#sugar'; export default { @@ -10,6 +10,7 @@ export default { 'getThumbnailsAvailableForDimensions', 'html', 'language', + 'missingImagePaths', 'to', ], @@ -63,6 +64,7 @@ export default { getThumbnailsAvailableForDimensions, html, language, + missingImagePaths, to, }) { let originalSrc; @@ -75,8 +77,27 @@ export default { originalSrc = ''; } - const willLink = typeof slots.link === 'string' || slots.link; - const customLink = typeof slots.link === 'string'; + let mediaSrc = null; + if (originalSrc.startsWith(to('media.root'))) { + mediaSrc = + originalSrc + .slice(to('media.root').length) + .replace(/^\//, ''); + } + + const isMissingImageFile = + missingImagePaths.includes(mediaSrc); + + if (isMissingImageFile) { + logInfo`No image file for ${mediaSrc} - build again for list of missing images.`; + } + + const willLink = + !isMissingImageFile && + (typeof slots.link === 'string' || slots.link); + + const customLink = + typeof slots.link === 'string'; const willReveal = slots.reveal && @@ -87,13 +108,16 @@ export default { const idOnImg = willLink ? null : slots.id; const idOnLink = willLink ? slots.id : null; + const classOnImg = willLink ? null : slots.class; const classOnLink = willLink ? slots.class : null; - if (!originalSrc) { + if (!originalSrc || isMissingImageFile) { return prepare( html.tag('div', {class: 'image-text-area'}, - slots.missingSourceContent)); + (html.isBlank(slots.missingSourceContent) + ? language.$(`misc.missingImage`) + : slots.missingSourceContent))); } let reveal = null; @@ -108,14 +132,6 @@ export default { ]; } - let mediaSrc = null; - if (originalSrc.startsWith(to('media.root'))) { - mediaSrc = - originalSrc - .slice(to('media.root').length) - .replace(/^\//, ''); - } - const hasThumbnails = mediaSrc && checkIfImagePathHasCachedThumbnails(mediaSrc); diff --git a/src/strings-default.json b/src/strings-default.json index 8d7756ad..b5e39e97 100644 --- a/src/strings-default.json +++ b/src/strings-default.json @@ -197,6 +197,7 @@ "misc.external.flash.homestuck.page": "{LINK} (page {PAGE})", "misc.external.flash.homestuck.secret": "{LINK} (secret page)", "misc.external.flash.youtube": "{LINK} (on any device)", + "misc.missingImage": "(This image file is missing)", "misc.missingLinkContent": "(Missing link content)", "misc.nav.previous": "Previous", "misc.nav.next": "Next", -- cgit 1.3.0-6-gf8a5 From 8a23eb5888242d6243eb6954d7d68622c24bdbcd Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 11 Sep 2023 15:12:13 -0300 Subject: client: hide missing cover image from sticky heading --- src/static/client2.js | 10 ++++++++-- src/upd8.js | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/static/client2.js b/src/static/client2.js index 8ae9876e..78970410 100644 --- a/src/static/client2.js +++ b/src/static/client2.js @@ -534,11 +534,17 @@ const stickyHeadingInfo = Array.from(document.querySelectorAll('.content-sticky- const {parentElement: contentContainer} = stickyContainer; const stickySubheadingRow = stickyContainer.querySelector('.content-sticky-subheading-row'); const stickySubheading = stickySubheadingRow.querySelector('h2'); - const stickyCoverContainer = stickyContainer.querySelector('.content-sticky-heading-cover-container'); - const stickyCover = stickyCoverContainer?.querySelector('.content-sticky-heading-cover'); + let stickyCoverContainer = stickyContainer.querySelector('.content-sticky-heading-cover-container'); + let stickyCover = stickyCoverContainer?.querySelector('.content-sticky-heading-cover'); const contentHeadings = Array.from(contentContainer.querySelectorAll('.content-heading')); const contentCover = contentContainer.querySelector('#cover-art-container'); + if (stickyCover.querySelector('.image-text-area')) { + stickyCoverContainer.remove(); + stickyCoverContainer = null; + stickyCover = null; + } + return { contentContainer, contentCover, diff --git a/src/upd8.js b/src/upd8.js index b3646c9d..df172e48 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -80,7 +80,7 @@ import * as buildModes from './write/build-modes/index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const CACHEBUST = 20; +const CACHEBUST = 21; let COMMIT; try { -- cgit 1.3.0-6-gf8a5 From 44f1442bf28bac7b07ac25c1ea15c6b3a9d1223a Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 11 Sep 2023 15:13:54 -0300 Subject: css: give square images a self-confidence boost --- src/static/site4.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/static/site4.css b/src/static/site4.css index f79c0c2d..ab8976bc 100644 --- a/src/static/site4.css +++ b/src/static/site4.css @@ -558,7 +558,7 @@ a.box img { height: auto; } -a.box .square .image-container { +.square .image-container { width: 100%; height: 100%; } -- cgit 1.3.0-6-gf8a5