163 lines
5.0 KiB
JavaScript
163 lines
5.0 KiB
JavaScript
/**
|
|
* Copyright (c) 2020 Google Inc
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
* this software and associated documentation files (the "Software"), to deal in
|
|
* the Software without restriction, including without limitation the rights to
|
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
* subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
// Transformer to set `srcset` for all images, and to create respective
|
|
// image versions in different formats and resolutions.
|
|
// Modification of original script from https://github.com/google/eleventy-high-performance-blog
|
|
|
|
const { JSDOM } = require("jsdom");
|
|
const sharp = require("sharp");
|
|
const { copyFileSync, existsSync, mkdirSync, readFileSync } = require("fs");
|
|
const { MD5 } = require("crypto-js");
|
|
const { extname, join } = require("path");
|
|
|
|
const widths = [1024, 820, 640, 320];
|
|
|
|
const extension = {
|
|
jpeg: "jpg",
|
|
webp: "webp"
|
|
};
|
|
|
|
// Map filenames to types and width, and then resize
|
|
async function srcset(filename, hash, format, metadataWidth) {
|
|
// Create a map of all file names
|
|
const names = await Promise.all(
|
|
widths.map((width) =>
|
|
resize(filename, width, hash, format, metadataWidth)
|
|
)
|
|
);
|
|
return names.map((n, i) => `${n} ${widths[i]}w`).join(", ");
|
|
}
|
|
|
|
async function resize(filename, width, hash, format, metadataWidth) {
|
|
const out = sizedName(filename, width, hash, format);
|
|
if (existsSync("_site/" + out)) {
|
|
return out;
|
|
}
|
|
|
|
const file = join(process.cwd(), filename);
|
|
|
|
const resizeWidth = metadataWidth < width ? metadataWidth : width;
|
|
|
|
await sharp(file)
|
|
.resize({ width: resizeWidth })
|
|
[format]({
|
|
quality: 80,
|
|
reductionEffort: 6
|
|
})
|
|
.toFile("_site/" + out);
|
|
|
|
return out;
|
|
}
|
|
|
|
function sizedName(filename, width, hash, format) {
|
|
const ext = extension[format];
|
|
if (!ext) {
|
|
throw new Error(`Unknown format ${format}`);
|
|
}
|
|
return filename.replace(
|
|
/\.\w+$/,
|
|
() => "-" + hash + "-" + width + "w." + ext
|
|
);
|
|
}
|
|
|
|
function hashedName(filename, hash) {
|
|
return filename.replace(extname(filename), "-" + hash + extname(filename));
|
|
}
|
|
|
|
async function setSrcset(img, src, hash, format, metadataWidth) {
|
|
img.setAttribute("srcset", await srcset(src, hash, format, metadataWidth));
|
|
}
|
|
|
|
const processImage = async (el) => {
|
|
const filename = el.getAttribute("src");
|
|
|
|
if (/^(https?:\/\/|\/\/)/i.test(filename)) {
|
|
return;
|
|
}
|
|
|
|
if (extname(filename.toLowerCase()) === ".svg") {
|
|
return;
|
|
}
|
|
|
|
const file = join(process.cwd(), filename);
|
|
|
|
// Generate file hash
|
|
const hash = MD5(readFileSync(file).toString());
|
|
|
|
// Get image metadata
|
|
const metadata = await sharp(file).metadata();
|
|
|
|
el.setAttribute("decoding", "async");
|
|
el.setAttribute("loading", "lazy");
|
|
el.setAttribute("height", metadata.height);
|
|
el.setAttribute("width", metadata.width);
|
|
|
|
const doc = el.ownerDocument;
|
|
const picture = doc.createElement("picture");
|
|
const webp = doc.createElement("source");
|
|
const jpeg = doc.createElement("source");
|
|
|
|
await setSrcset(webp, filename, hash, "webp", metadata.width);
|
|
webp.setAttribute("type", "image/webp");
|
|
await setSrcset(jpeg, filename, hash, "jpeg", metadata.width);
|
|
jpeg.setAttribute("type", "image/jpeg");
|
|
|
|
picture.appendChild(webp);
|
|
picture.appendChild(jpeg);
|
|
el.parentElement.replaceChild(picture, el);
|
|
picture.appendChild(el);
|
|
|
|
copyFileSync(
|
|
join(process.cwd(), filename),
|
|
join("_site", hashedName(filename, hash))
|
|
);
|
|
};
|
|
|
|
const convert = async (rawContent, outputPath) => {
|
|
let content = rawContent;
|
|
|
|
const targetDirectory = "./_site/assets/images";
|
|
|
|
if (!existsSync(targetDirectory)) {
|
|
mkdirSync(targetDirectory, { recursive: true });
|
|
}
|
|
|
|
if (outputPath && outputPath.endsWith(".html")) {
|
|
const dom = new JSDOM(content);
|
|
const images = [...dom.window.document.querySelectorAll("img")];
|
|
|
|
if (images.length > 0) {
|
|
await Promise.all(images.map((i) => processImage(i, outputPath)));
|
|
content = dom.serialize();
|
|
}
|
|
}
|
|
|
|
return content;
|
|
};
|
|
|
|
module.exports = {
|
|
initArguments: {},
|
|
configFunction: async (eleventyConfig = {}) => {
|
|
eleventyConfig.addTransform("imageConversion", convert);
|
|
}
|
|
};
|