Skip to content

Commit

Permalink
improvements and fixes to picture plugin #492
Browse files Browse the repository at this point in the history
  • Loading branch information
oscarotero committed Sep 25, 2023
1 parent 50a4576 commit 5e35d76
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 31 deletions.
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ Any BREAKING CHANGE between minor versions will be documented here in upper case
- New function `search.files()` to return the files to be exported [#468].
- New `vto` filter [#480].
- New argument for `site.component()` to define the directory scope.
- Plugin `picture`: Support sizes attribute [#482].
- Plugin `picture`:
- Support sizes attribute [#482].
- New options `name` and `order`.
- Don't create the `picture` element if there's only one `source`.
- Sort the output formats from the most modern to the most compatible #492.
- Support to transform only the formats, but not sizes #492.
- Plugin `sitemap`: Support for multilanguage sites.

### Changed
Expand All @@ -32,7 +37,10 @@ Any BREAKING CHANGE between minor versions will be documented here in upper case
- Use the wasm version of `lightningcss` due many bugs with the [N-API version in Deno](https://github.com/denoland/deno/issues/20072).
- Urls with spaces [#481].
- `on_demand` plugin: Fixed _preload.ts generation.
- `picture` plugin: support object and array imagick data [#490].
- `picture` plugin:
- Support object and array imagick data [#490].
- Fixed the value of the image's `src` attribute #492.
- Fixed attribute values starting/ending with space. For example `imagick=" png w600 "`.

## [1.18.5] - 2023-09-01
### Added
Expand Down
4 changes: 2 additions & 2 deletions plugins/metas.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getLumeVersion, merge } from "../core/utils.ts";
import { getDataValue } from "./utils.ts";

import type { Page, Site } from "../core.ts";
import type { Page, Plugin, Site } from "../core.ts";
import type { HTMLDocument } from "../deps/dom.ts";

export interface Options {
Expand Down Expand Up @@ -58,7 +58,7 @@ const defaults: Options = {
const defaultGenerator = `Lume ${getLumeVersion()}`;

/** A plugin to insert meta tags for SEO and social media */
export default function (userOptions?: Partial<Options>) {
export default function (userOptions?: Partial<Options>): Plugin {
const options = merge(defaults, userOptions);

return (site: Site) => {
Expand Down
154 changes: 139 additions & 15 deletions plugins/picture.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { posix } from "../deps/path.ts";
import { getPathAndExtension } from "../core/utils.ts";
import { getPathAndExtension, merge } from "../core/utils.ts";
import { typeByExtension } from "../deps/media_types.ts";

import type { MagickFormat } from "../deps/imagick.ts";
import type { Document, Element } from "../deps/dom.ts";
import type { Plugin, Site } from "../core.ts";

interface SourceFormat {
width: number;
width?: number;
scales: Record<string, number>;
format: string;
}
Expand All @@ -16,7 +16,23 @@ interface Source extends SourceFormat {
paths: string[];
}

export default function (): Plugin {
export interface Options {
/** The key name for the transformations definitions */
name: string;

/** The priority order of the formats */
order: string[];
}

// Default options
export const defaults: Options = {
name: "imagick",
order: ["jxl", "avif", "webp", "png", "jpg"],
};

export default function (userOptions?: Partial<Options>): Plugin {
const options = merge(defaults, userOptions);

return (site: Site) => {
const transforms = new Map<string, Source>();

Expand Down Expand Up @@ -66,15 +82,23 @@ export default function (): Plugin {
if (!paths.includes(path)) {
continue;
}
const imagick = page.data.imagick = page.data.imagick
? Array.isArray(page.data.imagick)
? page.data.imagick
: [page.data.imagick]

const { name } = options;
const imagick = page.data[name] = page.data[name]
? Array.isArray(page.data[name]) ? page.data[name] : [page.data[name]]
: [];

for (const [suffix, scale] of Object.entries(scales)) {
if (width) {
imagick.push({
resize: width * scale,
suffix,
format: format as MagickFormat,
});
continue;
}

imagick.push({
resize: width * scale,
suffix,
format: format as MagickFormat,
});
Expand All @@ -92,7 +116,14 @@ export default function (): Plugin {
const sizes = img.getAttribute("sizes");
const sourceFormats = saveTransform(basePath, src, imagick);

sortSources(sourceFormats);
const last = sourceFormats[sourceFormats.length - 1];

for (const sourceFormat of sourceFormats) {
if (sourceFormat === last) {
editImg(img, src, last, sizes);
break;
}
const source = createSource(
img.ownerDocument!,
src,
Expand All @@ -107,11 +138,27 @@ export default function (): Plugin {
const src = img.getAttribute("src") as string;
const sizes = img.getAttribute("sizes");
const sourceFormats = saveTransform(basePath, src, imagick);

sortSources(sourceFormats);

// Just only one format, no need to create a picture element
if (sourceFormats.length === 1) {
editImg(img, src, sourceFormats[0], sizes);
return;
}

const picture = img.ownerDocument!.createElement("picture");

img.replaceWith(picture);

const last = sourceFormats[sourceFormats.length - 1];

for (const sourceFormat of sourceFormats) {
if (sourceFormat === last) {
editImg(img, src, last, sizes);
break;
}

const source = createSource(
img.ownerDocument!,
src,
Expand All @@ -124,6 +171,25 @@ export default function (): Plugin {
picture.append(img);
}

function sortSources(sources: SourceFormat[]) {
const { order } = options;

sources.sort((a, b) => {
const aIndex = order.indexOf(a.format);
const bIndex = order.indexOf(b.format);

if (aIndex === -1) {
return 1;
}

if (bIndex === -1) {
return -1;
}

return aIndex - bIndex;
});
}

function saveTransform(
basePath: string,
src: string,
Expand All @@ -133,7 +199,7 @@ export default function (): Plugin {
const sizes: string[] = [];
const formats: string[] = [];

imagick.split(/\s+/).forEach((piece) => {
imagick.trim().split(/\s+/).forEach((piece) => {
if (piece.match(/^\d/)) {
sizes.push(piece);
} else {
Expand All @@ -143,6 +209,35 @@ export default function (): Plugin {

const sourceFormats: SourceFormat[] = [];

// No sizes, only transform to the formats
if (!sizes.length) {
for (const format of formats) {
const key = `:${format}`;
const sourceFormat = {
format,
scales: { "": 1 },
};
sourceFormats.push(sourceFormat);

const transform = transforms.get(key);

if (transform) {
if (!transform.paths.includes(path)) {
transform.paths.push(path);
}

Object.assign(transform.scales, sourceFormat.scales);
} else {
transforms.set(key, {
...sourceFormat,
paths: [path],
});
}
}

return sourceFormats;
}

for (const size of sizes) {
const [width, scales] = parseSize(size);

Expand Down Expand Up @@ -201,28 +296,38 @@ function parseSize(size: string): [number, number[]] {
];
}

function createSource(
document: Document,
function createSrcset(
src: string,
srcFormat: SourceFormat,
sizes?: string | null | undefined,
) {
const source = document.createElement("source");
): string[] {
const { scales, format, width } = srcFormat;
const path = encodeURI(getPathAndExtension(src)[0]);
const srcset: string[] = [];

for (const [suffix, scale] of Object.entries(scales)) {
const scaleSuffix = sizes
const scaleSuffix = sizes && width
? ` ${scale * width}w`
: scale === 1
? ""
: ` ${scale}x`;
srcset.push(`${path}${suffix}.${format}${scaleSuffix}`);
}

return srcset;
}

function createSource(
document: Document,
src: string,
srcFormat: SourceFormat,
sizes?: string | null | undefined,
) {
const source = document.createElement("source");
const srcset = createSrcset(src, srcFormat, sizes);

source.setAttribute("srcset", srcset.join(", "));
source.setAttribute("type", typeByExtension(format));
source.setAttribute("type", typeByExtension(srcFormat.format));

if (sizes) {
source.setAttribute("sizes", sizes);
Expand All @@ -231,6 +336,25 @@ function createSource(
return source;
}

function editImg(
img: Element,
src: string,
srcFormat: SourceFormat,
sizes?: string | null | undefined,
) {
const srcset = createSrcset(src, srcFormat, sizes);
const newSrc = srcset.shift()!;

if (srcset.length) {
img.setAttribute("srcset", srcset.join(", "));
}
img.setAttribute("src", newSrc);

if (sizes) {
img.setAttribute("sizes", sizes);
}
}

// Missing Element.closest in Deno DOM (https://github.com/b-fuze/deno-dom/issues/99)
function closest(element: Element, selector: string) {
while (element && !element.matches(selector)) {
Expand Down
Loading

0 comments on commit 5e35d76

Please sign in to comment.