@nolebase/markdown-it-unlazy-img
Version:
A markdown-it plugin wraps and transforms image tags to support lazy loading, blurhash, thumbhash, and more.
123 lines (119 loc) • 4.79 kB
JavaScript
const node_fs = require('node:fs');
const posix = require('node:path/posix');
const colorette = require('colorette');
const tinyglobby = require('tinyglobby');
const vite = require('vite');
const defaultMapGlobPatterns = [
"**/.vitepress/cache/@nolebase/vitepress-plugin-thumbnail-hash/thumbhashes/map.json",
".vitepress/cache/@nolebase/vitepress-plugin-thumbnail-hash/thumbhashes/map.json",
"**/thumbhashes/map.json",
"thumbhashes/map.json"
];
const logModulePrefix = `${colorette.cyan(`@nolebase/markdown-it-unlazy-img`)}${colorette.gray(":")}`;
const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i;
function ensureThumbhashMap(options, cachedMap) {
if (cachedMap)
return cachedMap;
if ("map" in options)
return options.map;
if ("mapFilePath" in options)
return JSON.parse(node_fs.readFileSync(options.mapFilePath, "utf-8"));
if (!("mapGlobPatterns" in options))
throw new Error("either thumbhash.map, thumbhash.mapFilePath, or thumbhash.mapGlobPatterns is required");
let mapGlobPatterns = [];
if (Array.isArray(options.mapGlobPatterns))
mapGlobPatterns = options.mapGlobPatterns;
else
mapGlobPatterns = [options.mapGlobPatterns];
mapGlobPatterns = mapGlobPatterns.filter((pattern) => typeof pattern === "string" && pattern.trim() !== "");
if (mapGlobPatterns.length === 0)
mapGlobPatterns = defaultMapGlobPatterns;
let foundThumbhashMapPath = "";
for (const pattern of mapGlobPatterns) {
const matchedFiles = tinyglobby.globSync(
pattern,
{
ignore: "node_modules/**",
onlyFiles: true
}
);
if (matchedFiles.length === 0)
continue;
if (!matchedFiles[0])
continue;
foundThumbhashMapPath = matchedFiles[0];
}
if (!foundThumbhashMapPath) {
throw new Error(`No thumbhash map file found in the glob patterns: ${mapGlobPatterns.join(", ")}`);
}
return JSON.parse(node_fs.readFileSync(foundThumbhashMapPath, "utf-8"));
}
const UnlazyImages = () => {
return (md, options) => {
const {
thumbhash = {
mapGlobPatterns: defaultMapGlobPatterns
},
imgElementTag = "UnLazyImage"
} = options ?? {};
if (!thumbhash)
throw new Error("thumbhash is required");
let thumbhashMap;
if ("map" in thumbhash)
thumbhashMap = thumbhash.map;
const imageRule = md.renderer.rules.image;
md.renderer.rules.image = (tokens, idx, mdOptions, env, self) => {
thumbhashMap = ensureThumbhashMap(thumbhash, thumbhashMap);
if (!env.path && !env.relativePath)
throw new Error("env.path and env.relativePath are required");
const token = tokens[idx];
const imgSrc = token.attrGet("src");
if (!imgSrc)
return imageRule(tokens, idx, mdOptions, env, self);
if (EXTERNAL_URL_RE.test(imgSrc))
return imageRule(tokens, idx, mdOptions, env, self);
if (![".png", ".jpg", ".jpeg"].some((ext) => imgSrc.endsWith(ext))) {
if (options?.logFormatNotSupportedWarning) {
console.warn(`${logModulePrefix} ${colorette.yellow("[WARN]")} unsupported image format for ${imgSrc}`);
}
return imageRule(tokens, idx, mdOptions, env, self);
}
let resolvedImgSrc = decodeURIComponent(imgSrc);
const props = {
src: imgSrc,
alt: token.attrGet("alt") || "",
thumbhash: void 0
};
token.attrs?.forEach(([name, value]) => {
if (name === "src" || name === "alt")
return;
props[name] = value;
});
if (!/^\.?\//.test(resolvedImgSrc)) {
props.src = `./${decodeURIComponent(resolvedImgSrc)}`;
}
if (resolvedImgSrc.startsWith("/")) {
resolvedImgSrc = resolvedImgSrc.slice(1);
} else {
const relativePathDir = vite.normalizePath(posix.dirname(env.relativePath));
resolvedImgSrc = posix.join(relativePathDir, resolvedImgSrc);
if (resolvedImgSrc.startsWith("/"))
resolvedImgSrc = resolvedImgSrc.slice(1);
}
const matchedThumbhashData = thumbhashMap?.[resolvedImgSrc];
if (!matchedThumbhashData) {
console.warn(`${logModulePrefix} ${colorette.yellow(`[WARN]`)} thumbhash data not found for ${resolvedImgSrc}`);
return imageRule(tokens, idx, mdOptions, env, self);
}
props.thumbhash = matchedThumbhashData.dataBase64;
props.placeholderSrc = matchedThumbhashData.dataUrl;
props.width = matchedThumbhashData.originalWidth.toString();
props.height = matchedThumbhashData.originalHeight.toString();
props.autoSizes = "true";
return `<${imgElementTag} ${Object.entries(props).map(([name, value]) => `${name}="${value}"`).join(" ")} />`;
};
};
};
exports.EXTERNAL_URL_RE = EXTERNAL_URL_RE;
exports.UnlazyImages = UnlazyImages;
;