From 67b9501bbf096929c5ebb04697724aa85437b2b2 Mon Sep 17 00:00:00 2001 From: Oscar Otero Date: Thu, 7 Dec 2023 15:36:40 +0100 Subject: [PATCH] replaced imagick with transform_images, renamed Lume.PageData to Lume.Data, etc --- CHANGELOG.md | 6 +- core/cache.ts | 2 +- core/file.ts | 3 + core/utils/lume_config.ts | 4 +- deps/sharp.ts | 12 ++ deps/svg2png.ts | 9 - init.ts | 4 +- plugins/attributes.ts | 4 +- plugins/date.ts | 4 +- plugins/decap_cms.ts | 4 +- plugins/favicon.ts | 73 ++++---- plugins/jsx.ts | 4 +- plugins/liquid.ts | 4 +- plugins/markdown.ts | 4 +- plugins/metas.ts | 4 +- plugins/nunjucks.ts | 4 +- plugins/paginate.ts | 4 +- plugins/picture.ts | 51 +++--- plugins/postcss.ts | 4 +- plugins/pug.ts | 4 +- plugins/search.ts | 4 +- plugins/slugify_urls.ts | 4 +- plugins/source_maps.ts | 4 +- plugins/terser.ts | 4 +- plugins/{imagick.ts => transform_images.ts} | 173 +++++++++--------- plugins/url.ts | 4 +- tests/__snapshots__/favicon.test.ts.snap | 8 +- tests/__snapshots__/picture.test.ts.snap | 88 ++++----- ....ts.snap => transform_images.test.ts.snap} | 116 +----------- tests/assets/picture/index.vto | 14 +- .../{imagick => transform_images}/_data.yml | 9 +- .../{imagick => transform_images}/lume.png | Bin tests/picture.test.ts | 4 +- tests/plugins.test.ts | 4 +- ...agick.test.ts => transform_images.test.ts} | 8 +- types.ts | 14 +- 36 files changed, 293 insertions(+), 373 deletions(-) create mode 100644 deps/sharp.ts delete mode 100644 deps/svg2png.ts rename plugins/{imagick.ts => transform_images.ts} (60%) rename tests/__snapshots__/{imagick.test.ts.snap => transform_images.test.ts.snap} (58%) rename tests/assets/{imagick => transform_images}/_data.yml (61%) rename tests/assets/{imagick => transform_images}/lume.png (100%) rename tests/{imagick.test.ts => transform_images.test.ts} (52%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d05a19cf..b4f6b141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ Go to the `v1` branch to see the changelog of Lume 1. ## 2.0.0 - Unreleased ### Added -- New plugin UnoCSS, to replace WindiCSS. +- New plugin `unocss`, to replace WindiCSS. +- New plugin `transform_images`, to replace Imagick. - New option `server.root` to `Site`. - New `basename` variable to change the final name of files/directories - New function `site.getOrCreatePage()`. @@ -54,6 +55,7 @@ Go to the `v1` branch to see the changelog of Lume 1. - Replace `fn-date` with `Temporal` polyfill to convert dates. - Refactor of `Server` class to work with `Deno.serve()` API [#501]. - Renamed `core/filesystem.ts` to `core/file.ts`. +- Picture plugin: Renamed the attribute `imagick` to `transform-images`. - TOML plugin: - is installed by default - Changed `extensions` option type to `string[]`. @@ -109,6 +111,8 @@ Go to the `v1` branch to see the changelog of Lume 1. - New option `item.updated`; ### Removed +- Removed plugin `windi_css`. Use `unocss` instead. +- Removed plugin `imagick`. Use `transform_images` instead. - Removed output extension detection in the filename: [#430] - Removed `processAll` and `preprocessAll`. - Removed `Page.dest` property [#290]. diff --git a/core/cache.ts b/core/cache.ts index 332680da..f641abb0 100644 --- a/core/cache.ts +++ b/core/cache.ts @@ -8,7 +8,7 @@ export interface Options { } /** - * Class to cache the content transformations (like imagick manipulations) + * Class to cache the content transformations (like transform_images manipulations) */ export default class Cache { #folder: string; diff --git a/core/file.ts b/core/file.ts index 33febc83..384ce20b 100644 --- a/core/file.ts +++ b/core/file.ts @@ -196,6 +196,9 @@ export interface RawData { /** The data of a page/folder once loaded and processed */ export interface Data extends RawData { + /** The title of the page */ + title?: string; + /** The language of the page */ lang?: string; diff --git a/core/utils/lume_config.ts b/core/utils/lume_config.ts index 7e0533b1..6e5a8fd1 100644 --- a/core/utils/lume_config.ts +++ b/core/utils/lume_config.ts @@ -4,12 +4,12 @@ export const pluginNames = [ "base_path", "code_highlight", "date", + "decap_cms", "esbuild", "eta", "favicon", "feed", "filter_pages", - "imagick", "inline", "jsx", "jsx_preact", @@ -23,7 +23,6 @@ export const pluginNames = [ "multilanguage", "nav", "nunjucks", - "decap_cms", "on_demand", "pagefind", "picture", @@ -43,6 +42,7 @@ export const pluginNames = [ "svgo", "tailwindcss", "terser", + "transform_images", "toml", "unocss", ]; diff --git a/deps/sharp.ts b/deps/sharp.ts new file mode 100644 index 00000000..6c277601 --- /dev/null +++ b/deps/sharp.ts @@ -0,0 +1,12 @@ +export { default } from "npm:sharp@0.33.0"; + +import sharp from "npm:sharp@0.33.0"; +import icoEndec from "npm:ico-endec@0.1.6"; + +export async function sharpsToIco(...images: sharp.Sharp[]) { + const buffers = await Promise.all( + images.map((image) => image.toFormat("png").toBuffer()), + ); + + return icoEndec.encode(buffers.map((buffer) => buffer.buffer)); +} diff --git a/deps/svg2png.ts b/deps/svg2png.ts deleted file mode 100644 index 2e8081b0..00000000 --- a/deps/svg2png.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { initialize, svg2png } from "npm:svg2png-wasm@1.4.1"; -import { read } from "../core/utils/read.ts"; - -// Initialize the WASM module -const url = "https://unpkg.com/svg2png-wasm@1.4.1/svg2png_wasm_bg.wasm"; -const wasm = await read(url, true); -await initialize(wasm); - -export { svg2png }; diff --git a/init.ts b/init.ts index 2109b503..5f621c58 100644 --- a/init.ts +++ b/init.ts @@ -206,8 +206,8 @@ function initPlugins(plugins: string[], denoConfig: DenoConfigResult) { // Ensure that tailwindcss is loaded before postcss fixPluginOrder(plugins, "tailwindcss", "postcss"); - // Ensure that picture is loaded before imagick - fixPluginOrder(plugins, "picture", "imagick"); + // Ensure that picture is loaded before transform_images + fixPluginOrder(plugins, "picture", "transform_images"); } function fixPluginOrder(plugins: string[], plugin1: string, plugin2: string) { diff --git a/plugins/attributes.ts b/plugins/attributes.ts index ab4ff96e..4dc9c8a6 100644 --- a/plugins/attributes.ts +++ b/plugins/attributes.ts @@ -128,10 +128,10 @@ function isValid(name: string, validNames: string[]) { return name && (!validNames.length || validNames.includes(name)); } -/** Extends PageHelpers interface */ +/** Extends Helpers interface */ declare global { namespace Lume { - export interface PageHelpers { + export interface Helpers { /** @see https://lume.land/plugins/attributes/ */ attr: (values: unknown, ...validNames: string[]) => string; diff --git a/plugins/date.ts b/plugins/date.ts index 770aafd5..db9bfd26 100644 --- a/plugins/date.ts +++ b/plugins/date.ts @@ -61,10 +61,10 @@ export default function (userOptions?: Options) { }; } -/** Extends PageHelpers interface */ +/** Extends Helpers interface */ declare global { namespace Lume { - export interface PageHelpers { + export interface Helpers { /** @see https://lume.land/plugins/date/ */ date: ( date: string | Date, diff --git a/plugins/decap_cms.ts b/plugins/decap_cms.ts index c61c8143..35e8355e 100644 --- a/plugins/decap_cms.ts +++ b/plugins/decap_cms.ts @@ -126,10 +126,10 @@ export default function (userOptions?: Options) { }; } -/** Extends PageData interface */ +/** Extends Data interface */ declare global { namespace Lume { - export interface PageData { + export interface Data { /** * Decap CMS configuration * @see https://lume.land/plugins/decap_cms/ diff --git a/plugins/favicon.ts b/plugins/favicon.ts index d3693f02..4578c737 100644 --- a/plugins/favicon.ts +++ b/plugins/favicon.ts @@ -1,13 +1,11 @@ import { merge } from "../core/utils/object.ts"; import binLoader from "../core/loaders/binary.ts"; import textLoader from "../core/loaders/text.ts"; -import { ImageMagick, MagickFormat, MagickGeometry } from "../deps/imagick.ts"; import { Page } from "../core/file.ts"; import Cache from "../core/cache.ts"; -import { svg2png } from "../deps/svg2png.ts"; +import sharp, { sharpsToIco } from "../deps/sharp.ts"; import type Site from "../core/site.ts"; -import type { IMagickImage } from "../deps/imagick.ts"; export interface Options { /** @@ -35,13 +33,13 @@ export const defaults: Options = { url: "/favicon.ico", size: 32, rel: "icon", - format: MagickFormat.Ico, + format: "ico", }, { url: "/apple-touch-icon.png", size: 180, rel: "apple-touch-icon", - format: MagickFormat.Png, + format: "png", }, ], }; @@ -69,37 +67,30 @@ export default function (userOptions?: Options) { } async function getContent(): Promise { - const path = options.input; + const content = await site.getContent(options.input, binLoader); - // Convert the SVG to PNG - if (path.endsWith(".svg")) { - const content = await site.getContent(path, textLoader) as - | string - | undefined; - - if (!content) { - throw new Error(`Favicon: ${path} not found`); - } - - return await svg2png(content, { width: 180, height: 180 }); + if (!content) { + throw new Error(`File not found: ${options.input}`); } - return await site.getContent(path, binLoader) as Uint8Array; + return typeof content === "string" + ? new TextEncoder().encode(content) + : content; } site.addEventListener("afterRender", async (event) => { const content = await getContent(); - if (!(content instanceof Uint8Array)) { - throw new Error(`Favicon: ${options.input} not found`); - } - for (const favicon of options.favicons) { - const format = favicon.format.toUpperCase() as MagickFormat; event.pages?.push( Page.create({ url: favicon.url, - content: await buildIco(content, format, favicon.size, cache), + content: await buildIco( + content, + favicon.format as keyof sharp.FormatEnum, + favicon.size, + cache, + ), }), ); } @@ -157,7 +148,7 @@ function addIcon(document: Document, attributes: Record) { async function buildIco( content: Uint8Array, - format: MagickFormat, + format: keyof sharp.FormatEnum | "ico", size: number, cache?: Cache, ): Promise { @@ -169,17 +160,25 @@ async function buildIco( } } - return new Promise((resolve) => { - ImageMagick.read(content, (image: IMagickImage) => { - const geometry = new MagickGeometry(size, size); - image.resize(geometry); + let image: Uint8Array; + + if (format === "ico") { + const resizeOptions = { background: { r: 0, g: 0, b: 0, alpha: 0 } }; + const img = sharp(content); + image = await sharpsToIco( + img.clone().resize(16, 16, resizeOptions), + img.clone().resize(32, 32, resizeOptions), + ); + } else { + image = await sharp(content) + .resize(size, size) + .toFormat(format) + .toBuffer(); + } - image.write(format, (output: Uint8Array) => { - if (cache) { - cache.set(content, { format, size }, output); - } - resolve(new Uint8Array(output)); - }); - }); - }); + if (cache) { + cache.set(content, { format, size }, image); + } + + return image; } diff --git a/plugins/jsx.ts b/plugins/jsx.ts index 760cb093..ff0e85b5 100644 --- a/plugins/jsx.ts +++ b/plugins/jsx.ts @@ -119,10 +119,10 @@ export default function (userOptions?: Options) { }; } -/** Extends PageData interface */ +/** Extends Data interface */ declare global { namespace Lume { - export interface PageData { + export interface Data { /** * The JSX children elements * @see https://lume.land/plugins/jsx/ diff --git a/plugins/liquid.ts b/plugins/liquid.ts index 87527c8c..c242a010 100644 --- a/plugins/liquid.ts +++ b/plugins/liquid.ts @@ -241,10 +241,10 @@ function createCustomTagWithBody(fn: Helper): TagClass { }; } -/** Extends PageHelpers interface */ +/** Extends Helpers interface */ declare global { namespace Lume { - export interface PageHelpers { + export interface Helpers { /** @see https://lume.land/plugins/liquid/ */ liquid: ( string: string, diff --git a/plugins/markdown.ts b/plugins/markdown.ts index 3ae9a881..f24369dc 100644 --- a/plugins/markdown.ts +++ b/plugins/markdown.ts @@ -136,10 +136,10 @@ export default function (userOptions?: Options) { }; } -/** Extends PageHelpers interface */ +/** Extends Helpers interface */ declare global { namespace Lume { - export interface PageHelpers { + export interface Helpers { /** @see https://lume.land/plugins/markdown/ */ md: (string: string, inline?: boolean) => string; } diff --git a/plugins/metas.ts b/plugins/metas.ts index 61f6d28a..9029d3ab 100644 --- a/plugins/metas.ts +++ b/plugins/metas.ts @@ -166,10 +166,10 @@ function addMeta( document.head.appendChild(document.createTextNode("\n")); } -/** Extends PageData interface */ +/** Extends Data interface */ declare global { namespace Lume { - export interface PageData { + export interface Data { /** * Meta elements * @see https://lume.land/plugins/metas/ diff --git a/plugins/nunjucks.ts b/plugins/nunjucks.ts index 31f40c5b..bf43144a 100644 --- a/plugins/nunjucks.ts +++ b/plugins/nunjucks.ts @@ -350,10 +350,10 @@ function createCustomTag(name: string, fn: Helper, options: HelperOptions) { return tagExtension; } -/** Extends PageHelpers interface */ +/** Extends Helpers interface */ declare global { namespace Lume { - export interface PageHelpers { + export interface Helpers { /** @see https://lume.land/plugins/nunjucks/ */ njk: (string: string, data?: Record) => Promise; } diff --git a/plugins/paginate.ts b/plugins/paginate.ts index a368dc7d..d644574a 100644 --- a/plugins/paginate.ts +++ b/plugins/paginate.ts @@ -126,10 +126,10 @@ export function createPaginator(defaults: PaginateOptions): Paginator { }; } -/** Extends PageData interface */ +/** Extends Data interface */ declare global { namespace Lume { - export interface PageData { + export interface Data { /** * The paginator helper * @see https://lume.land/plugins/paginate/ diff --git a/plugins/picture.ts b/plugins/picture.ts index e7d7d74a..957cde54 100644 --- a/plugins/picture.ts +++ b/plugins/picture.ts @@ -3,7 +3,6 @@ import { getPathAndExtension } from "../core/utils/path.ts"; import { merge } from "../core/utils/object.ts"; import { contentType } from "../deps/media_types.ts"; -import type { MagickFormat } from "../deps/imagick.ts"; import type Site from "../core/site.ts"; interface SourceFormat { @@ -26,7 +25,7 @@ export interface Options { // Default options export const defaults: Options = { - name: "imagick", + name: "transformImages", order: ["jxl", "avif", "webp", "png", "jpg"], }; @@ -48,9 +47,12 @@ export default function (userOptions?: Options) { const images = document.querySelectorAll("img"); for (const img of Array.from(images)) { - const imagick = img.closest("[imagick]")?.getAttribute("imagick"); + const transformImages = img.closest("[image-transform]") + ?.getAttribute( + "image-transform", + ); - if (!imagick) { + if (!transformImages) { continue; } @@ -61,20 +63,21 @@ export default function (userOptions?: Options) { const picture = img.closest("picture"); if (picture) { - handlePicture(imagick, img, picture, basePath); + handlePicture(transformImages, img, picture, basePath); continue; } - handleImg(imagick, img, basePath); + handleImg(transformImages, img, basePath); } } - }); - site.process([".html"], (pages) => { + // Remove the image-transform attribute from the HTML for (const page of pages) { - page.document?.querySelectorAll("[imagick]").forEach((element) => { - element.removeAttribute("imagick"); - }); + page.document?.querySelectorAll("[image-transform]").forEach( + (element) => { + element.removeAttribute("image-transform"); + }, + ); } }); @@ -88,7 +91,7 @@ export default function (userOptions?: Options) { } const { name } = options; - const imagick = page.data[name] = page.data[name] + const transformImages = page.data[name] = page.data[name] ? Array.isArray(page.data[name]) ? page.data[name] : [page.data[name]] @@ -96,17 +99,17 @@ export default function (userOptions?: Options) { for (const [suffix, scale] of Object.entries(scales)) { if (width) { - imagick.push({ + transformImages.push({ resize: width * scale, suffix, - format: format as MagickFormat, + format, }); continue; } - imagick.push({ + transformImages.push({ suffix, - format: format as MagickFormat, + format, }); } } @@ -114,14 +117,14 @@ export default function (userOptions?: Options) { }); function handlePicture( - imagick: string, + transformImages: string, img: Element, picture: Element, basePath: string, ) { const src = img.getAttribute("src") as string; const sizes = img.getAttribute("sizes"); - const sourceFormats = saveTransform(basePath, src, imagick); + const sourceFormats = saveTransform(basePath, src, transformImages); sortSources(sourceFormats); const last = sourceFormats[sourceFormats.length - 1]; @@ -141,10 +144,14 @@ export default function (userOptions?: Options) { } } - function handleImg(imagick: string, img: Element, basePath: string) { + function handleImg( + transformImages: string, + img: Element, + basePath: string, + ) { const src = img.getAttribute("src") as string; const sizes = img.getAttribute("sizes"); - const sourceFormats = saveTransform(basePath, src, imagick); + const sourceFormats = saveTransform(basePath, src, transformImages); sortSources(sourceFormats); @@ -200,13 +207,13 @@ export default function (userOptions?: Options) { function saveTransform( basePath: string, src: string, - imagick: string, + transformImages: string, ): SourceFormat[] { const path = src.startsWith("/") ? src : posix.join(basePath, src); const sizes: string[] = []; const formats: string[] = []; - imagick.trim().split(/\s+/).forEach((piece) => { + transformImages.trim().split(/\s+/).forEach((piece) => { if (piece.match(/^\d/)) { sizes.push(piece); } else { diff --git a/plugins/postcss.ts b/plugins/postcss.ts index 8d8bbf95..12f6227f 100644 --- a/plugins/postcss.ts +++ b/plugins/postcss.ts @@ -131,10 +131,10 @@ function configureImport(site: Site, includes: string) { }); } -/** Extends PageHelpers interface */ +/** Extends Helpers interface */ declare global { namespace Lume { - export interface PageHelpers { + export interface Helpers { /** @see https://lume.land/plugins/postcss/ */ postcss: (code: string) => Promise; } diff --git a/plugins/pug.ts b/plugins/pug.ts index df73adaf..6f1f4b40 100644 --- a/plugins/pug.ts +++ b/plugins/pug.ts @@ -152,10 +152,10 @@ export default function (userOptions?: Options) { }; } -/** Extends PageHelpers interface */ +/** Extends Helpers interface */ declare global { namespace Lume { - export interface PageHelpers { + export interface Helpers { /** @see https://lume.land/plugins/pug/ */ pug: (string: string, data?: Record) => string; } diff --git a/plugins/search.ts b/plugins/search.ts index 23cf62ad..e83dcd76 100644 --- a/plugins/search.ts +++ b/plugins/search.ts @@ -21,10 +21,10 @@ export default function (userOptions?: Options) { }; } -/** Extends PageData interface */ +/** Extends Data interface */ declare global { namespace Lume { - export interface PageData { + export interface Data { /** * The searcher helper * @see https://lume.land/plugins/search/ diff --git a/plugins/slugify_urls.ts b/plugins/slugify_urls.ts index ba8ee20f..f05f941a 100644 --- a/plugins/slugify_urls.ts +++ b/plugins/slugify_urls.ts @@ -47,10 +47,10 @@ function extensionMatches(path: string, extensions: Extensions): boolean { return extensions === "*" || extensions.some((ext) => path.endsWith(ext)); } -/** Extends PageHelpers interface */ +/** Extends Helpers interface */ declare global { namespace Lume { - export interface PageHelpers { + export interface Helpers { /** @see https://lume.land/plugins/slugify_urls/ */ slugify: (string: string) => string; } diff --git a/plugins/source_maps.ts b/plugins/source_maps.ts index aaa58555..4cb0e284 100644 --- a/plugins/source_maps.ts +++ b/plugins/source_maps.ts @@ -192,10 +192,10 @@ function addSourceMap(url: string, sourceMap: string): string { return `\n/*# sourceMappingURL=${sourceMap} */`; } -/** Extends PageData interface */ +/** Extends Data interface */ declare global { namespace Lume { - export interface PageData { + export interface Data { /** * The source map data (if it's an asset) * @see https://lume.land/plugins/source_maps/ diff --git a/plugins/terser.ts b/plugins/terser.ts index 5195584b..ac4c9d63 100644 --- a/plugins/terser.ts +++ b/plugins/terser.ts @@ -76,10 +76,10 @@ export default function (userOptions?: Options) { }; } -/** Extends PageHelpers interface */ +/** Extends Helpers interface */ declare global { namespace Lume { - export interface PageHelpers { + export interface Helpers { /** @see https://lume.land/plugins/terser/#the-terser-filter */ terser: (code: string) => Promise; } diff --git a/plugins/imagick.ts b/plugins/transform_images.ts similarity index 60% rename from plugins/imagick.ts rename to plugins/transform_images.ts index f2603ea6..51ebf70e 100644 --- a/plugins/imagick.ts +++ b/plugins/transform_images.ts @@ -1,31 +1,30 @@ import { getPathAndExtension } from "../core/utils/path.ts"; +import { log } from "../core/utils/log.ts"; import { merge } from "../core/utils/object.ts"; import { concurrent } from "../core/utils/concurrent.ts"; -import { log } from "../core/utils/log.ts"; import binaryLoader from "../core/loaders/binary.ts"; -import { ImageMagick } from "../deps/imagick.ts"; +import sharp from "../deps/sharp.ts"; import Cache from "../core/cache.ts"; import type Site from "../core/site.ts"; import type { Page } from "../core/file.ts"; -import type { IMagickImage, MagickFormat } from "../deps/imagick.ts"; export interface Options { /** The list extensions this plugin applies to */ - extensions?: string[]; + extensions: string[]; /** The key name for the transformations definitions */ - name?: string; + name: string; /** The cache folder */ - cache?: string | boolean; + cache: string | boolean; /** Custom transform functions */ - functions?: Record; + functions: Record; } export type TransformationFunction = ( - image: IMagickImage, + image: sharp.Sharp, // deno-lint-ignore no-explicit-any ...args: any[] ) => void; @@ -33,51 +32,59 @@ export type TransformationFunction = ( // Default options export const defaults: Options = { extensions: [".jpg", ".jpeg", ".png"], - name: "imagick", + name: "transformImages", cache: true, functions: { - resize(image: IMagickImage, width: number, height = width): void { - image.resize(width, height); - }, - crop(image: IMagickImage, width: number, height = width): void { - image.crop(width, height); + resize( + image: sharp.Sharp, + width: number, + height?: number, + options?: sharp.ResizeOptions, + ): void { + image.resize(width, height, options); }, - blur(image: IMagickImage, radius: number, sigma: number): void { - image.blur(radius, sigma); + blur(image: sharp.Sharp, sigma?: number | boolean): void { + image.blur(sigma); }, - sharpen(image: IMagickImage, radius: number, sigma: number): void { - image.sharpen(radius, sigma); - }, - rotate(image: IMagickImage, degrees: number): void { + rotate(image: sharp.Sharp, degrees: number): void { image.rotate(degrees); }, - autoOrient(image: IMagickImage): void { - image.autoOrient(); - }, }, }; +export type Format = + | "jpeg" + | "jp2" + | "jxl" + | "png" + | "webp" + | "gif" + | "avif" + | "heif" + | "tiff"; +export interface FormatOptions { + format: Format; + [key: string]: unknown; +} + export interface Transformation { suffix?: string; - format?: MagickFormat | MagickFormat[]; + format?: Format | Format[] | FormatOptions | FormatOptions[]; matches?: RegExp | string; // deno-lint-ignore no-explicit-any [key: string]: any; } interface SingleTransformation extends Transformation { - format?: MagickFormat; + format?: Format | FormatOptions; } /** A plugin to transform images in Lume */ -export default function (userOptions?: Options) { +export default function (userOptions?: Partial) { const options = merge(defaults, userOptions); return (site: Site) => { site.loadAssets(options.extensions, binaryLoader); - site.process( - options.extensions, - (pages, allPages) => concurrent(pages, (page) => imagick(page, allPages)), - ); + site.process(options.extensions, process); // Configure the cache folder const cacheFolder = options.cache === true ? "_cache" : options.cache; @@ -90,26 +97,32 @@ export default function (userOptions?: Options) { site.options.watcher.ignore.push(cacheFolder); } - async function imagick(page: Page, pages: Page[]) { - const imagick = page.data[options.name] as + async function process(pages: Page[], allPages: Page[]) { + await concurrent( + pages, + (page) => processPage(page, allPages), + ); + } + async function processPage(page: Page, allPages: Page[]) { + const transData = page.data[options.name] as | Transformation | Transformation[] | undefined; - if (!imagick) { + if (!transData) { return; } const content = page.content as Uint8Array; const transformations = removeDuplicatedTransformations( - getTransformations(imagick), + getTransformations(transData), ); let transformed = false; let index = 0; for (const transformation of transformations) { if (transformation.matches) { const regex = new RegExp(transformation.matches); - if (!regex.test(page.data.url)) { + if (!regex.test(page.data.url as string)) { continue; } } @@ -127,81 +140,75 @@ export default function (userOptions?: Options) { if (result) { output.content = result; } else { - transform(content, output, transformation, options); + await transform(content, output, transformation, options); transformed = true; await cache.set(content, transformation, output.content!); } } else { - transform(content, output, transformation, options); + await transform(content, output, transformation, options); transformed = true; } if (output !== page) { - pages.push(output); + allPages.push(output); } } if (transformed) { - log.info(`[imagick plugin] Processed ${page.sourcePath}`); + log.info(`[transform_images plugin] Processed ${page.sourcePath}`); } // Remove the original page - pages.splice(pages.indexOf(page), 1); + allPages.splice(allPages.indexOf(page), 1); } }; } -function transform( +async function transform( content: Uint8Array, page: Page, transformation: Transformation, - options: Required, -): void { - let format: MagickFormat | undefined = undefined; - - ImageMagick.read(content, (image: IMagickImage) => { - for (const [name, args] of Object.entries(transformation)) { - switch (name) { - case "suffix": - case "matches": - break; - - case "format": - format = args; - break; - - default: - if (!options.functions[name]) { - throw new Error(`Unknown transformation: ${name}`); - } + options: Options, +): Promise { + const image = sharp(content); + + for (const [name, args] of Object.entries(transformation)) { + switch (name) { + case "suffix": + case "matches": + break; + + case "format": + if (typeof args === "string") { + image.toFormat(args as Format); + } else { + const { format, ...options } = args as Record; + image.toFormat(format as Format, options); + } + break; - if (Array.isArray(args)) { - options.functions[name](image, ...args); - } else { - options.functions[name](image, args); - } - } - } + default: + if (!options.functions[name]) { + throw new Error(`Unknown transformation: ${name}`); + } - if (format) { - image.write( - format, - (content: Uint8Array) => page.content = new Uint8Array(content), - ); - } else { - image.write((content: Uint8Array) => - page.content = new Uint8Array(content) - ); + if (Array.isArray(args)) { + options.functions[name](image, ...args); + } else { + options.functions[name](image, args); + } } - }); + } + + page.content = new Uint8Array(await image.toBuffer()); } -function rename(page: Page, transformation: Transformation): void { +function rename(page: Page, transformation: SingleTransformation): void { const { format, suffix } = transformation; let [path, ext] = getPathAndExtension(page.data.url); if (format) { - ext = `.${format}`; + ext = typeof format === "string" ? `.${format}` : `.${format.format}`; } if (suffix) { @@ -250,15 +257,15 @@ function removeDuplicatedTransformations( return [...result.values()]; } -/** Extends PageData interface */ +/** Extends Data interface */ declare global { namespace Lume { - export interface PageData { + export interface Data { /** * Image transformations - * @see https://lume.land/plugins/imagick/ + * @see https://lume.land/plugins/transform_images/ */ - imagick?: Transformation | Transformation[]; + transformImages?: Transformation | Transformation[]; } } } diff --git a/plugins/url.ts b/plugins/url.ts index 4167e4ab..2991e36f 100644 --- a/plugins/url.ts +++ b/plugins/url.ts @@ -41,10 +41,10 @@ export default function (userOptions?: Options) { }; } -/** Extends PageHelpers interface */ +/** Extends Helpers interface */ declare global { namespace Lume { - export interface PageHelpers { + export interface Helpers { /** @see https://lume.land/plugins/url/#url-filter */ url: (path: string, absolute?: boolean) => string; diff --git a/tests/__snapshots__/favicon.test.ts.snap b/tests/__snapshots__/favicon.test.ts.snap index 76f1c931..94d61e03 100644 --- a/tests/__snapshots__/favicon.test.ts.snap +++ b/tests/__snapshots__/favicon.test.ts.snap @@ -121,10 +121,10 @@ snapshot[`favicon plugin 2`] = `[]`; snapshot[`favicon plugin 3`] = ` [ { - content: "Uint8Array(2172)", + content: "Uint8Array(2179)", data: { basename: "apple-touch-icon", - content: "Uint8Array(2172)", + content: "Uint8Array(2179)", page: [ "src", "data", @@ -139,10 +139,10 @@ snapshot[`favicon plugin 3`] = ` }, }, { - content: "Uint8Array(4286)", + content: "Uint8Array(905)", data: { basename: "favicon", - content: "Uint8Array(4286)", + content: "Uint8Array(905)", page: [ "src", "data", diff --git a/tests/__snapshots__/picture.test.ts.snap b/tests/__snapshots__/picture.test.ts.snap index e8d5274c..3f13996d 100644 --- a/tests/__snapshots__/picture.test.ts.snap +++ b/tests/__snapshots__/picture.test.ts.snap @@ -182,33 +182,33 @@ snapshot[`picture plugin 3`] = ` -
+
- + - + - + -
+
- + - + \`, @@ -221,33 +221,33 @@ snapshot[`picture plugin 3`] = ` -
+
- + - + - + -
+
- + - + \`, @@ -272,12 +272,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(191297)", + content: "Uint8Array(281995)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -288,6 +287,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash-600w.png", }, src: { @@ -298,12 +298,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(6338)", + content: "Uint8Array(3368)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -314,6 +313,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash-300w.webp", }, src: { @@ -324,12 +324,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(10764)", + content: "Uint8Array(8056)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -340,6 +339,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash-300w@2.webp", }, src: { @@ -350,12 +350,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(9526)", + content: "Uint8Array(6615)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -366,6 +365,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash-300w.jpg", }, src: { @@ -376,12 +376,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(19816)", + content: "Uint8Array(17393)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -392,6 +391,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash-300w@2.jpg", }, src: { @@ -402,12 +402,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(15548)", + content: "Uint8Array(12701)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -418,6 +417,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash.avif", }, src: { @@ -428,12 +428,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(551915)", + content: "Uint8Array(822727)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -444,6 +443,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash-600w@2.png", }, src: { @@ -454,12 +454,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(6723)", + content: "Uint8Array(3717)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -470,6 +469,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash-600w.avif", }, src: { @@ -480,12 +480,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(10306)", + content: "Uint8Array(7369)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -496,6 +495,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash-600w@2.avif", }, src: { @@ -506,12 +506,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(10764)", + content: "Uint8Array(8056)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -522,6 +521,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash-600w.webp", }, src: { @@ -532,12 +532,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(21016)", + content: "Uint8Array(19756)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -548,6 +547,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash-600w@2.webp", }, src: { @@ -558,12 +558,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(19816)", + content: "Uint8Array(17393)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -574,6 +573,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash-600w.jpg", }, src: { @@ -584,12 +584,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(47837)", + content: "Uint8Array(46947)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -600,6 +599,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash-600w@2.jpg", }, src: { @@ -610,12 +610,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(5067)", + content: "Uint8Array(1952)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -626,6 +625,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash-300w.avif", }, src: { @@ -636,12 +636,11 @@ snapshot[`picture plugin 3`] = ` }, }, { - content: "Uint8Array(6723)", + content: "Uint8Array(3717)", data: { basename: "kevin schmid unsplash", content: "Uint8Array(139102)", date: [], - imagick: "undefined", mergedKeys: [ "tags", ], @@ -652,6 +651,7 @@ snapshot[`picture plugin 3`] = ` paginate: "paginate", search: [], tags: "Array(0)", + transformImages: "undefined", url: "/kevin schmid unsplash-300w@2.avif", }, src: { diff --git a/tests/__snapshots__/imagick.test.ts.snap b/tests/__snapshots__/transform_images.test.ts.snap similarity index 58% rename from tests/__snapshots__/imagick.test.ts.snap rename to tests/__snapshots__/transform_images.test.ts.snap index 73425460..76c0a289 100644 --- a/tests/__snapshots__/imagick.test.ts.snap +++ b/tests/__snapshots__/transform_images.test.ts.snap @@ -1,6 +1,6 @@ export const snapshot = {}; -snapshot[`imagick plugin 1`] = ` +snapshot[`Image transform plugin 1`] = ` { formats: [ { @@ -128,121 +128,17 @@ snapshot[`imagick plugin 1`] = ` } `; -snapshot[`imagick plugin 2`] = `[]`; +snapshot[`Image transform plugin 2`] = `[]`; -snapshot[`imagick plugin 3`] = ` +snapshot[`Image transform plugin 3`] = ` [ { - content: "Uint8Array(935)", + content: "Uint8Array(29014)", data: { basename: "lume", content: "Uint8Array(29014)", date: [], - imagick: "undefined", - mergedKeys: [ - "tags", - ], - page: [ - "src", - "data", - ], - paginate: "paginate", - search: [], - tags: "Array(0)", - url: "/lume-small.png", - }, - src: { - asset: true, - ext: ".png", - path: "/lume[0]", - remote: undefined, - }, - }, - { - content: "Uint8Array(935)", - data: { - basename: "lume", - content: "Uint8Array(29014)", - date: [], - imagick: "undefined", - mergedKeys: [ - "tags", - ], - page: [ - "src", - "data", - ], - paginate: "paginate", - search: [], - tags: "Array(0)", - url: "/lume-big.png", - }, - src: { - asset: true, - ext: ".png", - path: "/lume[1]", - remote: undefined, - }, - }, - { - content: "Uint8Array(1231)", - data: { - basename: "lume", - content: "Uint8Array(29014)", - date: [], - imagick: "undefined", - mergedKeys: [ - "tags", - ], - page: [ - "src", - "data", - ], - paginate: "paginate", - search: [], - tags: "Array(0)", - url: "/lume.jpg", - }, - src: { - asset: true, - ext: ".png", - path: "/lume[2]", - remote: undefined, - }, - }, - { - content: "Uint8Array(8648)", - data: { - basename: "lume", - content: "Uint8Array(29014)", - date: [], - imagick: "undefined", - mergedKeys: [ - "tags", - ], - page: [ - "src", - "data", - ], - paginate: "paginate", - search: [], - tags: "Array(0)", - url: "/lume.webp", - }, - src: { - asset: true, - ext: ".png", - path: "/lume[3]", - remote: undefined, - }, - }, - { - content: "Uint8Array(14410)", - data: { - basename: "lume", - content: "Uint8Array(29014)", - date: [], - imagick: "undefined", + imageTransform: "Array(5)", mergedKeys: [ "tags", ], @@ -258,7 +154,7 @@ snapshot[`imagick plugin 3`] = ` src: { asset: true, ext: ".png", - path: "/lume[4]", + path: "/lume", remote: undefined, }, }, diff --git a/tests/assets/picture/index.vto b/tests/assets/picture/index.vto index 8a65c11c..a79978e7 100644 --- a/tests/assets/picture/index.vto +++ b/tests/assets/picture/index.vto @@ -7,32 +7,32 @@ -
+
- + - + - + -
+
- + - + diff --git a/tests/assets/imagick/_data.yml b/tests/assets/transform_images/_data.yml similarity index 61% rename from tests/assets/imagick/_data.yml rename to tests/assets/transform_images/_data.yml index 17743a4b..f71c820f 100644 --- a/tests/assets/imagick/_data.yml +++ b/tests/assets/transform_images/_data.yml @@ -1,4 +1,4 @@ -imagick: +imageTransform: - suffix: -small resize: 20 blur: 10 @@ -6,8 +6,11 @@ imagick: resize: 20 blur: 10 matches: \.png$ - - format: [jpg, webp] + - format: + - format: jpg + progressive: true + - webp - format: png - format: jpg suffix: -never - matches: \.jpg$ \ No newline at end of file + matches: \.jpg$ diff --git a/tests/assets/imagick/lume.png b/tests/assets/transform_images/lume.png similarity index 100% rename from tests/assets/imagick/lume.png rename to tests/assets/transform_images/lume.png diff --git a/tests/picture.test.ts b/tests/picture.test.ts index cab9b368..e117fde6 100644 --- a/tests/picture.test.ts +++ b/tests/picture.test.ts @@ -1,5 +1,5 @@ import { assertSiteSnapshot, build, getSite } from "./utils.ts"; -import imagick from "../plugins/imagick.ts"; +import transformImages from "../plugins/transform_images.ts"; import picture from "../plugins/picture.ts"; Deno.test("picture plugin", async (t) => { @@ -8,7 +8,7 @@ Deno.test("picture plugin", async (t) => { }); site.use(picture()); - site.use(imagick({ + site.use(transformImages({ cache: false, })); diff --git a/tests/plugins.test.ts b/tests/plugins.test.ts index ec8cc0a3..f34fb758 100644 --- a/tests/plugins.test.ts +++ b/tests/plugins.test.ts @@ -11,12 +11,12 @@ Deno.test("Plugins list in init", () => { "base_path", "code_highlight", "date", + "decap_cms", "esbuild", "eta", "favicon", "feed", "filter_pages", - "imagick", "inline", "jsx", "jsx_preact", @@ -30,7 +30,6 @@ Deno.test("Plugins list in init", () => { "multilanguage", "nav", "nunjucks", - "decap_cms", "on_demand", "pagefind", "picture", @@ -50,6 +49,7 @@ Deno.test("Plugins list in init", () => { "svgo", "tailwindcss", "terser", + "transform_images", "toml", "unocss", ]); diff --git a/tests/imagick.test.ts b/tests/transform_images.test.ts similarity index 52% rename from tests/imagick.test.ts rename to tests/transform_images.test.ts index 9c4c21be..7dd1b91a 100644 --- a/tests/imagick.test.ts +++ b/tests/transform_images.test.ts @@ -1,12 +1,12 @@ import { assertSiteSnapshot, build, getSite } from "./utils.ts"; -import imagick from "../plugins/imagick.ts"; +import imageTransform from "../plugins/transform_images.ts"; -Deno.test("imagick plugin", async (t) => { +Deno.test("Image transform plugin", async (t) => { const site = getSite({ - src: "imagick", + src: "transform_images", }); - site.use(imagick({ + site.use(imageTransform({ cache: false, })); diff --git a/types.ts b/types.ts index df99bbff..11c32c7b 100644 --- a/types.ts +++ b/types.ts @@ -1,5 +1,5 @@ import type { Engine, Helper } from "./core/renderer.ts"; -import type { Data, Page } from "./core/file.ts"; +import type { Data as PageData, Page } from "./core/file.ts"; import type { default as Site, Plugin } from "./core/site.ts"; import type { Archetype } from "./cli/create.ts"; import type { Middleware, RequestHandler } from "./core/server.ts"; @@ -16,16 +16,14 @@ declare global { Site, }; - /** The data of a page */ - // deno-lint-ignore no-explicit-any - export interface PageData = any> - extends Data, Type { - /** The title of the page */ - title?: string; + /** The page data */ + export interface Data extends PageData { + // deno-lint-ignore no-explicit-any + [index: string]: any; } /** The page helpers */ - export interface PageHelpers { + export interface Helpers { [key: string]: Helper; } }