« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/content/dependencies/image.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/content/dependencies/image.js')
-rw-r--r--src/content/dependencies/image.js224
1 files changed, 132 insertions, 92 deletions
diff --git a/src/content/dependencies/image.js b/src/content/dependencies/image.js
index bc268ec1..d979b0bc 100644
--- a/src/content/dependencies/image.js
+++ b/src/content/dependencies/image.js
@@ -2,82 +2,85 @@ import {logWarn} from '#cli';
 import {empty} from '#sugar';
 
 export default {
-  extraDependencies: [
-    'checkIfImagePathHasCachedThumbnails',
-    'getDimensionsOfImagePath',
-    'getSizeOfMediaFile',
-    'getThumbnailEqualOrSmaller',
-    'getThumbnailsAvailableForDimensions',
-    'html',
-    'language',
-    'missingImagePaths',
-    'to',
-  ],
-
-  contentDependencies: ['generateColorStyleAttribute'],
-
-  relations: (relation) => ({
+  relations: (relation, _artwork) => ({
     colorStyle:
       relation('generateColorStyleAttribute'),
   }),
 
-  data(artTags) {
-    const data = {};
-
-    if (artTags) {
-      data.contentWarnings =
-        artTags
-          .filter(artTag => artTag.isContentWarning)
-          .map(artTag => artTag.name);
-    } else {
-      data.contentWarnings = null;
-    }
-
-    return data;
-  },
+  data: (artwork) => ({
+    path:
+      (artwork
+        ? artwork.path
+        : null),
+
+    warnings:
+      (artwork
+        ? artwork.artTags
+            .filter(artTag => artTag.isContentWarning)
+            .map(artTag => artTag.name)
+        : null),
+
+    dimensions:
+      (artwork
+        ? artwork.dimensions
+        : null),
+  }),
 
   slots: {
-    src: {type: 'string'},
-
-    path: {
-      validate: v => v.validateArrayItems(v.isString),
-    },
-
     thumb: {type: 'string'},
+    responsiveThumb: {type: 'boolean', default: false},
+    responsiveSizes: {type: 'string'},
+
+    reveal: {type: 'boolean', default: true},
+    lazy: {type: 'boolean', default: false},
+    square: {type: 'boolean', default: false},
 
     link: {
       validate: v => v.anyOf(v.isBoolean, v.isString),
       default: false,
     },
 
-    color: {
-      validate: v => v.isColor,
-    },
+    color: {validate: v => v.isColor},
 
-    warnings: {
-      validate: v => v.looseArrayOf(v.isString),
+    // Added to the .image-container.
+    attributes: {
+      type: 'attributes',
+      mutable: false,
     },
 
-    reveal: {type: 'boolean', default: true},
-    lazy: {type: 'boolean', default: false},
-
-    square: {type: 'boolean', default: false},
-
-    dimensions: {
-      validate: v => v.isDimensions,
+    // Added to the <img>.
+    imgAttributes: {
+      type: 'attributes',
+      mutable: false,
     },
 
+    // Added to the <img> itself.
     alt: {type: 'string'},
 
-    attributes: {
-      type: 'attributes',
-      mutable: false,
+    // Specify 'src' or 'path', or the path will be used from the artwork.
+    // If none of the above is present, the message in missingSourceContent
+    // will be displayed instead.
+
+    src: {type: 'string'},
+
+    path: {
+      validate: v => v.validateArrayItems(v.isString),
     },
 
     missingSourceContent: {
       type: 'html',
       mutable: false,
     },
+
+    // These will also be used from the artwork if not specified as slots.
+
+    warnings: {
+      validate: v => v.looseArrayOf(v.isString),
+    },
+
+    dimensions: {
+      validate: v => v.isDimensions,
+    },
   },
 
   generate(data, relations, slots, {
@@ -91,15 +94,14 @@ export default {
     missingImagePaths,
     to,
   }) {
-    let originalSrc;
-
-    if (slots.src) {
-      originalSrc = slots.src;
-    } else if (!empty(slots.path)) {
-      originalSrc = to(...slots.path);
-    } else {
-      originalSrc = '';
-    }
+    const originalSrc =
+      (slots.src
+        ? slots.src
+     : slots.path
+        ? to(...slots.path)
+     : data.path
+        ? to(...data.path)
+        : '');
 
     // TODO: This feels janky. It's necessary to deal with static content that
     // includes strings like <img src="media/misc/foo.png">, but processing the
@@ -121,29 +123,29 @@ export default {
       !isMissingImageFile &&
       (typeof slots.link === 'string' || slots.link);
 
-    const contentWarnings =
-      slots.warnings ??
-      data.contentWarnings;
+    const warnings = slots.warnings ?? data.warnings;
+    const dimensions = slots.dimensions ?? data.dimensions;
 
     const willReveal =
       slots.reveal &&
       originalSrc &&
       !isMissingImageFile &&
-      !empty(contentWarnings);
-
-    const willSquare =
-      slots.square;
+      !empty(warnings);
 
     const imgAttributes = html.attributes([
       {class: 'image'},
 
+      slots.imgAttributes,
+
       slots.alt && {alt: slots.alt},
 
-      slots.dimensions?.[0] &&
-        {width: slots.dimensions[0]},
+      dimensions &&
+      dimensions[0] &&
+        {width: dimensions[0]},
 
-      slots.dimensions?.[1] &&
-        {height: slots.dimensions[1]},
+      dimensions &&
+      dimensions[1] &&
+        {height: dimensions[1]},
     ]);
 
     const isPlaceholder =
@@ -169,7 +171,7 @@ export default {
 
         html.tag('span', {class: 'reveal-warnings'},
           language.$('misc.contentWarnings.warnings', {
-            warnings: language.formatUnitList(contentWarnings),
+            warnings: language.formatUnitList(warnings),
           })),
 
         html.tag('br'),
@@ -199,31 +201,29 @@ export default {
     // so it won't be set if thumbnails aren't available.
     let revealSrc = null;
 
+    let originalDimensions;
+    let availableThumbs;
+    let selectedThumbtack;
+
+    const getThumbSrc = (thumbtack) =>
+      to('thumb.path', mediaSrc.replace(/\.(png|jpg)$/, `.${thumbtack}.jpg`));
+
     // If thumbnails are available *and* being used, calculate thumbSrc,
     // and provide some attributes relevant to the large image overlay.
     if (hasThumbnails && slots.thumb) {
-      const selectedSize =
+      selectedThumbtack =
         getThumbnailEqualOrSmaller(slots.thumb, mediaSrc);
 
-      const mediaSrcJpeg =
-        mediaSrc.replace(/\.(png|jpg)$/, `.${selectedSize}.jpg`);
-
       displaySrc =
-        to('thumb.path', mediaSrcJpeg);
+        getThumbSrc(selectedThumbtack);
 
       if (willReveal) {
-        const miniSize =
-          getThumbnailEqualOrSmaller('mini', mediaSrc);
-
-        const mediaSrcJpeg =
-          mediaSrc.replace(/\.(png|jpg)$/, `.${miniSize}.jpg`);
-
         revealSrc =
-          to('thumb.path', mediaSrcJpeg);
+          getThumbSrc(getThumbnailEqualOrSmaller('mini', mediaSrc));
       }
 
-      const originalDimensions = getDimensionsOfImagePath(mediaSrc);
-      const availableThumbs = getThumbnailsAvailableForDimensions(originalDimensions);
+      originalDimensions = getDimensionsOfImagePath(mediaSrc);
+      availableThumbs = getThumbnailsAvailableForDimensions(originalDimensions);
 
       const fileSize =
         (willLink && mediaSrc
@@ -239,11 +239,54 @@ export default {
         !empty(availableThumbs) &&
           {'data-thumbs':
               availableThumbs
-                .map(([name, size]) => `${name}:${size}`)
+                .map(([tack, size]) => `${tack}:${size}`)
                 .join(' ')},
       ]);
     }
 
+    let displayStaticImg =
+      html.tag('img',
+        imgAttributes,
+        {src: displaySrc});
+
+    if (hasThumbnails && slots.responsiveThumb) responsive: {
+      if (slots.lazy) {
+        logWarn`${'responsiveThumb'} and ${'lazy'} are used together, but not compatible`;
+        break responsive;
+      }
+
+      if (!slots.thumb) {
+        logWarn`${'responsiveThumb'} must be used alongside a default ${'thumb'}`;
+        break responsive;
+      }
+
+      const srcset = [
+        // Never load the original source, which might be a very large
+        // uncompressed file. Bah!
+        /* [originalSrc, `${Math.min(...originalDimensions)}w`], */
+
+        ...availableThumbs.map(([tack, size]) =>
+          [getThumbSrc(tack), `${Math.floor(0.95 * size)}w`]),
+
+        // fallback
+        [displaySrc],
+      ].map(line => line.join(' ')).join(',\n');
+
+      displayStaticImg =
+        html.tag('img',
+          imgAttributes,
+
+          {sizes:
+            (slots.responsiveSizes.match(/(?=(?:,|^))\s*\S/)
+                // slot provided fallback size
+              ? slots.responsiveSizes
+                // default fallback size
+              : slots.responsiveSizes + ',\n' +
+                new Map(availableThumbs).get(selectedThumbtack) + 'px')},
+
+          {srcset});
+    }
+
     if (!displaySrc) {
       return (
         prepare(
@@ -252,10 +295,7 @@ export default {
     }
 
     const images = {
-      displayStatic:
-        html.tag('img',
-          imgAttributes,
-          {src: displaySrc}),
+      displayStatic: displayStaticImg,
 
       displayLazy:
         slots.lazy &&
@@ -323,14 +363,14 @@ export default {
 
       wrapped =
         html.tag('div', {class: 'image-outer-area'},
-          willSquare &&
+          slots.square &&
             {class: 'square-content'},
 
           wrapped);
 
       wrapped =
         html.tag('div', {class: 'image-container'},
-          willSquare &&
+          slots.square &&
             {class: 'square'},
 
           typeof slots.link === 'string' &&