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 `
${html}`;
};
}
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
};