main
  1import fs from "node:fs/promises";
  2import path from "node:path";
  3import { fileURLToPath } from "node:url";
  4import { z } from "astro/zod";
  5import { defineIntegration } from "astro-integration-kit";
  6import * as cheerio from "cheerio";
  7import { getBase64ImageInfo } from "../utils/image.js";
  8import { joinUrl, url } from "../utils/url.js";
  9import type { AstroConfig } from "astro";
 10
 11// TODO: After Typst 0.15 published, use jsx and path to auto optimize images in typst
 12// See https://github.com/typst/typst/pull/7555
 13// It's not possible now because image path is string in Typst 0.14,
 14// so we can't resolve images' actual path to import them by Astro's `<Image />` component.
 15export const extractImage = defineIntegration({
 16  name: "tybios-extract-image",
 17  optionsSchema: z.object({
 18    dir: z.string().optional().default("assets/img/typ"),
 19  }),
 20  setup({ options }) {
 21    let _astroConfig: AstroConfig;
 22    return {
 23      hooks: {
 24        "astro:config:done": async ({ config }) => {
 25          _astroConfig = config;
 26        },
 27        "astro:build:done": async ({ dir, logger }) => {
 28          const rootDir = fileURLToPath(dir);
 29          const outDir = path.join(rootDir, options.dir);
 30          await fs.mkdir(outDir, { recursive: true });
 31
 32          const seenImages = new Set<string>();
 33
 34          async function processDirectory(currentDir: string) {
 35            const entries = await fs.readdir(currentDir, {
 36              withFileTypes: true,
 37            });
 38            for (const entry of entries) {
 39              const fullPath = path.join(currentDir, entry.name);
 40              if (entry.isDirectory()) {
 41                await processDirectory(fullPath);
 42              } else if (entry.isFile() && entry.name.endsWith(".html")) {
 43                const html = await fs.readFile(fullPath, "utf-8");
 44                const $ = cheerio.load(html);
 45                let htmlChanged = false;
 46
 47                const articleImages = $("article img");
 48
 49                for (let i = 0; i < articleImages.length; i++) {
 50                  const el = articleImages[i];
 51                  const src = $(el).attr("src") || "";
 52
 53                  const imageInfo = getBase64ImageInfo(src);
 54                  if (!imageInfo) {
 55                    continue;
 56                  }
 57
 58                  const { filename, base64Data } = imageInfo;
 59
 60                  if (!seenImages.has(filename)) {
 61                    seenImages.add(filename);
 62                    const imageFilePath = path.join(outDir, filename);
 63
 64                    if (filename.endsWith(".svg")) {
 65                      const svgStr = Buffer.from(base64Data, "base64").toString(
 66                        "utf-8",
 67                      );
 68                      const minifiedSvg = svgStr
 69                        .replace(/>\s+</g, "><")
 70                        .replace(/\s{2,}/g, " ")
 71                        .trim();
 72                      await fs.writeFile(imageFilePath, minifiedSvg, "utf-8");
 73                    } else {
 74                      const imageBuffer = Buffer.from(base64Data, "base64");
 75                      await fs.writeFile(imageFilePath, imageBuffer);
 76                    }
 77                  }
 78
 79                  $(el).attr(
 80                    "src",
 81                    joinUrl(_astroConfig.base, `/${options.dir}/${filename}`),
 82                  );
 83                  htmlChanged = true;
 84                }
 85
 86                if (htmlChanged) {
 87                  await fs.writeFile(fullPath, $.html());
 88                }
 89              }
 90            }
 91          }
 92
 93          await processDirectory(rootDir);
 94          logger.info(
 95            `Extracted ${seenImages.size} base64 images to ${options.dir}`,
 96          );
 97        },
 98      },
 99      api: {
100        get dir() {
101          return options.dir;
102        },
103      },
104    };
105  },
106});