ImageCommand.ts

Summary

Maintainability
D
1 day
Test Coverage
import { DomReplaceCommand, DomReplacer } from 'ssg-api';
import { AnchorReplacer } from './anchor/AnchorReplacer';
import { HtmlRR0SsgContext } from './RR0SsgContext';
import * as path from 'path';
import * as fs from 'fs';
import sizeOf from 'image-size';

/**
 * Register images (`<img>` tags) required in an HTML file.
 */
export class ImageCommand extends DomReplaceCommand<HTMLImageElement> {

  protected readonly singleton: AnchorReplacer;

  constructor(protected outBaseDir: string, protected maxWidth: number, protected maxHeight: number,
              protected baseUrl = '') {
    super('img');
  }

  protected async createReplacer(context: HtmlRR0SsgContext): Promise<DomReplacer<HTMLImageElement>> {
    return {
      replace: async (imgEl: HTMLImageElement): Promise<HTMLImageElement> => {
        const src = imgEl.src;
        const imgParentEl = imgEl.parentElement;
        if (imgParentEl.tagName === 'FIGURE') {
          const captionEl = imgParentEl.querySelector('figcaption');
          if (captionEl) {
            const caption = captionEl.textContent;
            imgEl.alt = caption.replace(/\n+/g, '').trim();
          } else {
            const caption = imgEl.alt;
            if (caption) {
              const newCaptionEl = imgParentEl.ownerDocument.createElement('figcaption');
              newCaptionEl.textContent = caption;
              imgParentEl.appendChild(newCaptionEl);
            }
          }
        }
        context.debug(context.file.name, "requires image", src)
        try {
          let isExternal = src.startsWith('http');
          let isAbsolute = src.startsWith('/');
          if (isAbsolute) {
            imgEl.src = this.baseUrl + src;
          }
          let imgPath = isExternal ? src : isAbsolute ? path.join('.', src) : path.join(
            path.dirname(context.file.name),
            src);
          imgEl.loading = 'lazy';
          if (!imgEl.width && !imgEl.height) {
            const dimensions = sizeOf(imgPath);
            let width = dimensions.width;
            let height = dimensions.height;
            if (width > this.maxWidth) {
              const ratio = this.maxWidth / width;
              width = this.maxWidth;
              height *= ratio;
            }
            if (height > this.maxHeight) {
              const ratio = this.maxHeight / height;
              height = this.maxHeight;
              width *= ratio;
            }
            imgEl.width = width;
            imgEl.height = height;
            imgEl.setAttribute('onclick',
              `this.classList.contains('zoomed') ? document.exitFullscreen() && this.classList.toggle('zoomed', false): this.classList.toggle('zoomed', true) && this.requestFullscreen()`);
          }
        } catch (e) {
          context.debug('Could not determine size of image ', src, e);
        }
        context.images.add(src);
        return imgEl;
      }
    };
  }

  protected async postExecute(context: HtmlRR0SsgContext) {
    const imagesUrls = context.images;
    if (imagesUrls.size > 0) {
      for (const imageUrl of imagesUrls) {
        this.handleImage(context, imageUrl);
      }
      imagesUrls.clear();
    }
  }

  private handleImage(context: HtmlRR0SsgContext, imageUrl: string) {
    const inputFile = context.file.name
    if (imageUrl) {
      const isLocal = !imageUrl.startsWith('http');
      if (isLocal) {
        const contextDir = path.dirname(inputFile);
        const isAbsolute = path.isAbsolute(imageUrl);
        const inFile = isAbsolute ? path.resolve('.' + imageUrl) : path.resolve(path.join(contextDir, imageUrl));
        const outBaseDir = isAbsolute ? this.outBaseDir : path.join(this.outBaseDir, contextDir);
        const outRel = path.join(outBaseDir, imageUrl);
        context.debug('Copying', imageUrl, 'to', outRel);
        const outFile = path.resolve(outRel);
        try {
          const outDir = path.dirname(outFile);
          if (!fs.existsSync(outDir)) {
            fs.mkdirSync(outDir, {recursive: true});
          }
          fs.copyFileSync(inFile, outFile);
        } catch (e) {
          if (e.code === 'ENOENT') {
            context.warn(`File ${inFile} does not exist`);
          } else {
            throw e;
          }
        }
      }
    } else {
      context.warn(`Empty image src in ${inputFile}`);
    }
  }
}