fumadocs-core
Version:
The React.js library for building a documentation website
192 lines (190 loc) • 5.41 kB
JavaScript
import { visit } from "unist-util-visit";
import * as path$1 from "node:path";
import { fileURLToPath } from "node:url";
//#region src/mdx-plugins/remark-image.ts
const VALID_BLUR_EXT = [
".jpeg",
".png",
".webp",
".avif",
".jpg"
];
const EXTERNAL_URL_REGEX = /^https?:\/\//;
/**
* Turn images into Next.js Image compatible usage.
*/
function remarkImage({ placeholder = "blur", external = true, useImport = true, onError = "error", publicDir = path$1.join(process.cwd(), "public") } = {}) {
return async (tree, file) => {
const importsToInject = [];
const promises = [];
async function onImage(src, node) {
const attributes = [{
type: "mdxJsxAttribute",
name: "alt",
value: node.alt ?? "image"
}];
if (node.title) attributes.push({
type: "mdxJsxAttribute",
name: "title",
value: node.title
});
if (src.type === "file" && useImport) {
const variableName = `__img${importsToInject.length}`;
const hasBlur = placeholder === "blur" && VALID_BLUR_EXT.some((ext) => src.file.endsWith(ext));
if (!file.dirname) throw new Error("When `useImport` is enabled, you must specify `dirname` in the VFile passed to compiler.");
importsToInject.push({
variableName,
importPath: getImportPath(src.file, file.dirname)
});
attributes.push({
type: "mdxJsxAttribute",
name: "src",
value: {
type: "mdxJsxAttributeValueExpression",
value: variableName,
data: { estree: {
body: [{
type: "ExpressionStatement",
expression: {
type: "Identifier",
name: variableName
}
}],
type: "Program",
sourceType: "script"
} }
}
});
const out = {
children: [],
type: "mdxJsxFlowElement",
name: "img",
attributes
};
if (hasBlur) out.attributes.push({
type: "mdxJsxAttribute",
name: "placeholder",
value: "blur"
});
return out;
}
const size = await getImageSize(src, external).catch((e) => {
throw new Error(`[Remark Image] Failed obtain image size for ${node.url} (public directory configured as ${publicDir})`, { cause: e });
});
if (!size) return;
attributes.push({
type: "mdxJsxAttribute",
name: "src",
value: src.type === "url" ? src.url.toString() : node.url
}, {
type: "mdxJsxAttribute",
name: "width",
value: size.width.toString()
}, {
type: "mdxJsxAttribute",
name: "height",
value: size.height.toString()
});
return {
type: "mdxJsxFlowElement",
name: "img",
attributes,
children: []
};
}
visit(tree, "image", (node) => {
const src = parseSrc(decodeURI(node.url), publicDir, file.dirname);
if (!src) return;
const task = onImage(src, node).catch((e) => {
if (onError === "ignore" || node.url.endsWith(".svg")) return;
if (onError === "hide") return {
type: "mdxJsxFlowElement",
name: null,
attributes: [],
children: []
};
if (onError === "error") throw e;
onError(e);
}).then((res) => {
if (res) Object.assign(node, res);
});
promises.push(task);
});
await Promise.all(promises);
if (importsToInject.length === 0) return;
const imports = importsToInject.map(({ variableName, importPath }) => ({
type: "mdxjsEsm",
data: { estree: { body: [{
type: "ImportDeclaration",
source: {
type: "Literal",
value: importPath
},
specifiers: [{
type: "ImportDefaultSpecifier",
local: {
type: "Identifier",
name: variableName
}
}]
}] } }
}));
tree.children.unshift(...imports);
};
}
function getImportPath(file, dir) {
const relative = path$1.relative(dir, file).replaceAll(path$1.sep, "/");
return relative.startsWith("../") ? relative : `./${relative}`;
}
/**
* @param src - src href
* @param publicDir - dir/url to resolve absolute paths
* @param dir - dir to resolve relative paths
*/
function parseSrc(src, publicDir, dir) {
if (src.startsWith("file:///")) return {
type: "file",
file: fileURLToPath(src)
};
if (EXTERNAL_URL_REGEX.test(src)) return {
type: "url",
url: new URL(src)
};
if (src.startsWith("/")) {
if (EXTERNAL_URL_REGEX.test(publicDir)) {
const url = new URL(publicDir);
url.pathname = `/${[...url.pathname.split("/"), ...src.split("/")].filter((v) => v.length > 0).join("/")}`;
return {
type: "url",
url
};
}
return {
type: "file",
file: path$1.join(publicDir, src)
};
}
if (!dir) {
console.warn(`[Remark Image] found relative path ${src} but missing 'dirname' in VFile, this image will be skipped for now.`);
return;
}
return {
type: "file",
file: path$1.join(dir, src)
};
}
async function getImageSize(src, onExternal) {
if (src.type === "file") {
const { imageSizeFromFile } = await import("image-size/fromFile");
return imageSizeFromFile(src.file);
}
if (onExternal === false) return;
const { timeout } = typeof onExternal === "object" ? onExternal : {};
const res = await fetch(src.url, { signal: typeof timeout === "number" ? AbortSignal.timeout(timeout) : void 0 });
if (!res.ok) throw new Error(`[Remark Image] Failed to fetch ${src.url} (${res.status}): ${await res.text()}`);
const { imageSize } = await import("image-size");
return imageSize(new Uint8Array(await res.arrayBuffer()));
}
//#endregion
export { remarkImage };
//# sourceMappingURL=remark-image.js.map