248 lines
8.5 KiB
JavaScript
248 lines
8.5 KiB
JavaScript
import { isRemoteAllowed } from "@astrojs/internal-helpers/remote";
|
|
import {
|
|
defaultExcludeLanguages,
|
|
markdownConfigDefaults,
|
|
syntaxHighlightDefaults
|
|
} from "@astrojs/internal-helpers/markdown";
|
|
import Slugger from "github-slugger";
|
|
import { createShikiHighlighter } from "@astrojs/internal-helpers/shiki";
|
|
let satteri;
|
|
async function loadSatteri() {
|
|
satteri ??= await import("satteri");
|
|
return satteri;
|
|
}
|
|
function createCollectImagesPlugin(image = {}) {
|
|
const domains = image?.domains ?? [];
|
|
const remotePatterns = image?.remotePatterns ?? [];
|
|
return {
|
|
name: "collect-images",
|
|
image(node, ctx) {
|
|
const url = node.url ? decodeURI(node.url) : void 0;
|
|
if (!url) return;
|
|
const astro = ctx.data.astro;
|
|
if (URL.canParse(url)) {
|
|
if (isRemoteAllowed(url, { domains, remotePatterns })) {
|
|
astro?.remoteImagePaths.add(url);
|
|
}
|
|
} else if (!url.startsWith("/")) {
|
|
astro?.localImagePaths.add(url);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
function resolveFrontmatterExpression(expr, frontmatter) {
|
|
if (!expr.startsWith("frontmatter.") && !expr.startsWith("frontmatter[")) return void 0;
|
|
const pathStr = expr.slice("frontmatter".length);
|
|
const parts = [];
|
|
const pathRegex = /\.(\w+)|\[(\d+)\]|\["([^"]+)"\]|\['([^']+)'\]/g;
|
|
let match;
|
|
while ((match = pathRegex.exec(pathStr)) !== null) {
|
|
parts.push(match[1] ?? match[2] ?? match[3] ?? match[4]);
|
|
}
|
|
if (parts.length === 0) return void 0;
|
|
let value = frontmatter;
|
|
for (const key of parts) {
|
|
if (value == null || typeof value !== "object") return void 0;
|
|
value = value[key];
|
|
}
|
|
return typeof value === "string" ? value : void 0;
|
|
}
|
|
function collectHastText(node, frontmatter) {
|
|
let text = "";
|
|
if (node.type === "mdxFlowExpression" || node.type === "mdxTextExpression") {
|
|
if (node.value != null && frontmatter) {
|
|
const resolved = resolveFrontmatterExpression(node.value.trim(), frontmatter);
|
|
text += resolved ?? node.value;
|
|
}
|
|
} else if ("value" in node && node.value != null) {
|
|
text += node.value;
|
|
}
|
|
if ("children" in node && node.children) {
|
|
for (const child of node.children) {
|
|
text += collectHastText(child, frontmatter);
|
|
}
|
|
}
|
|
return text;
|
|
}
|
|
function makeFragmentNode(html) {
|
|
return {
|
|
type: "mdxJsxFlowElement",
|
|
name: "Fragment",
|
|
attributes: [{ type: "mdxJsxAttribute", name: "set:html", value: html }],
|
|
children: []
|
|
};
|
|
}
|
|
function createHeadingIdsPlugin() {
|
|
const slugger = new Slugger();
|
|
const headings = [];
|
|
return {
|
|
name: "heading-ids",
|
|
element: {
|
|
filter: ["h1", "h2", "h3", "h4", "h5", "h6"],
|
|
visit(node, ctx) {
|
|
const astro = ctx.data.astro;
|
|
const rawText = ctx.textContent(node);
|
|
const text = rawText.includes("frontmatter") ? collectHastText(node, astro?.frontmatter ?? {}) : rawText;
|
|
const existingId = node.properties?.id;
|
|
const slug = typeof existingId === "string" ? existingId : slugger.slug(text);
|
|
const depth = Number.parseInt(node.tagName[1], 10);
|
|
headings.push({ depth, slug, text });
|
|
if (astro) astro.headings = headings;
|
|
if (typeof existingId !== "string") {
|
|
ctx.setProperty(node, "id", slug);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
function makeRawNode(html) {
|
|
return { type: "raw", value: html };
|
|
}
|
|
const HAST_PRESERVED_PROPERTIES = /* @__PURE__ */ new Set(["className", "htmlFor"]);
|
|
function createImageMarkerPlugin() {
|
|
const indexBySrc = /* @__PURE__ */ new Map();
|
|
return {
|
|
name: "image-marker",
|
|
element: {
|
|
filter: ["img"],
|
|
visit(node, ctx) {
|
|
const props = node.properties ?? {};
|
|
const rawSrc = typeof props.src === "string" ? props.src : void 0;
|
|
if (!rawSrc) return;
|
|
const src = decodeURI(rawSrc);
|
|
const astro = ctx.data.astro;
|
|
const isLocal = astro?.localImagePaths.has(src) ?? false;
|
|
const isRemote = !isLocal && (astro?.remoteImagePaths.has(src) ?? false);
|
|
if (!isLocal && !isRemote) return;
|
|
const { src: _src, ...rest } = props;
|
|
const index = indexBySrc.get(rawSrc) ?? 0;
|
|
indexBySrc.set(rawSrc, index + 1);
|
|
const imageProperties = { ...rest, src, index };
|
|
if (isRemote && !("width" in props) && !("height" in props)) {
|
|
imageProperties.inferSize = true;
|
|
}
|
|
ctx.setProperty(node, "__ASTRO_IMAGE_", JSON.stringify(imageProperties));
|
|
for (const key of Object.keys(rest)) {
|
|
if (!HAST_PRESERVED_PROPERTIES.has(key)) {
|
|
ctx.setProperty(node, key, null);
|
|
}
|
|
}
|
|
ctx.setProperty(node, "src", null);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
function createHighlightPlugin(highlight, excludeLangs, options) {
|
|
const wrapResult = options?.mdx ? makeFragmentNode : makeRawNode;
|
|
return {
|
|
name: "highlight",
|
|
element: {
|
|
filter: ["pre"],
|
|
async visit(node, ctx) {
|
|
const codeChild = node.children?.find(
|
|
(c) => c.type === "element" && c.tagName === "code"
|
|
);
|
|
if (!codeChild || codeChild.type !== "element") return;
|
|
const lang = codeChild.data?.lang ?? "plaintext";
|
|
const meta = codeChild.data?.meta ?? void 0;
|
|
if (excludeLangs && excludeLangs.includes(lang) || defaultExcludeLanguages.includes(lang)) {
|
|
return;
|
|
}
|
|
const code = ctx.textContent(codeChild).replace(/\n$/, "");
|
|
const html = await highlight(code, lang, meta);
|
|
return wrapResult(html);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
async function createHighlightFn(syntaxHighlight, shikiConfig) {
|
|
const syntaxHighlightType = typeof syntaxHighlight === "string" ? syntaxHighlight : syntaxHighlight ? syntaxHighlight.type : void 0;
|
|
if (syntaxHighlightType === "shiki") {
|
|
const hl = await createShikiHighlighter({
|
|
langs: shikiConfig?.langs,
|
|
theme: shikiConfig?.theme,
|
|
themes: shikiConfig?.themes,
|
|
langAlias: shikiConfig?.langAlias
|
|
});
|
|
return (code, lang, meta) => hl.codeToHtml(code, lang, {
|
|
meta,
|
|
wrap: shikiConfig?.wrap,
|
|
defaultColor: shikiConfig?.defaultColor,
|
|
transformers: shikiConfig?.transformers
|
|
});
|
|
}
|
|
if (syntaxHighlightType === "prism") {
|
|
const { runHighlighterWithAstro } = await import("@astrojs/prism/dist/highlighter");
|
|
return async (code, lang) => {
|
|
const { html, classLanguage } = await runHighlighterWithAstro(lang, code);
|
|
return `<pre class="${classLanguage}" data-language="${lang}"><code class="${classLanguage}">${html}</code></pre>`;
|
|
};
|
|
}
|
|
return void 0;
|
|
}
|
|
async function createSatteriMarkdownProcessor(opts) {
|
|
const s = await loadSatteri();
|
|
const {
|
|
syntaxHighlight = syntaxHighlightDefaults,
|
|
shikiConfig = markdownConfigDefaults.shikiConfig,
|
|
gfm = markdownConfigDefaults.gfm,
|
|
smartypants = markdownConfigDefaults.smartypants,
|
|
mdastPlugins: userMdastPlugins = [],
|
|
hastPlugins: userHastPlugins = [],
|
|
features: userFeatures
|
|
} = opts ?? {};
|
|
const highlightFn = await createHighlightFn(syntaxHighlight, shikiConfig);
|
|
const syntaxHighlightExcludeLangs = typeof syntaxHighlight === "object" ? syntaxHighlight.excludeLangs : void 0;
|
|
return {
|
|
async render(content, renderOpts) {
|
|
const astro = {
|
|
frontmatter: renderOpts?.frontmatter ?? {},
|
|
headings: [],
|
|
localImagePaths: /* @__PURE__ */ new Set(),
|
|
remoteImagePaths: /* @__PURE__ */ new Set()
|
|
};
|
|
const allMdastPlugins = [
|
|
...userMdastPlugins,
|
|
createCollectImagesPlugin(opts?.image)
|
|
];
|
|
const hastPlugins = [];
|
|
if (highlightFn) {
|
|
hastPlugins.push(createHighlightPlugin(highlightFn, syntaxHighlightExcludeLangs));
|
|
}
|
|
hastPlugins.push(...userHastPlugins);
|
|
hastPlugins.push(createImageMarkerPlugin());
|
|
hastPlugins.push(createHeadingIdsPlugin());
|
|
const { html, data } = await s.markdownToHtml(content, {
|
|
mdastPlugins: allMdastPlugins,
|
|
hastPlugins,
|
|
features: {
|
|
gfm: gfm !== false,
|
|
smartPunctuation: smartypants !== false,
|
|
...userFeatures
|
|
},
|
|
fileURL: renderOpts?.fileURL,
|
|
data: { astro }
|
|
});
|
|
const result = data.astro;
|
|
return {
|
|
code: html,
|
|
metadata: {
|
|
headings: result?.headings ?? [],
|
|
localImagePaths: result ? Array.from(result.localImagePaths) : [],
|
|
remoteImagePaths: result ? Array.from(result.remoteImagePaths) : [],
|
|
frontmatter: result?.frontmatter ?? {}
|
|
}
|
|
};
|
|
}
|
|
};
|
|
}
|
|
export {
|
|
collectHastText,
|
|
createCollectImagesPlugin,
|
|
createHeadingIdsPlugin,
|
|
createHighlightFn,
|
|
createHighlightPlugin,
|
|
createImageMarkerPlugin,
|
|
createSatteriMarkdownProcessor,
|
|
makeFragmentNode
|
|
};
|