import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import rehypeSlug from "rehype-slug"; import rehypeHighlight from "rehype-highlight"; import "highlight.js/styles/github-dark.css"; interface Props { body: string; /** The current doc's slug (e.g. `editor/`) — used to rewrite relative * links inside the markdown so they stay inside the SPA. */ baseSlug: string; } /** * Resolve a relative path against a doc slug, returning the dot-free * `a/b/c` form. Used as the foundation for both link and image rewriting. */ function resolveAgainstSlug(href: string, baseSlug: string): string { const baseParts = baseSlug.split("/").filter(Boolean); if (!baseSlug.endsWith("/")) baseParts.pop(); // drop the file segment const parts = baseParts.slice(); for (const seg of href.split("/")) { if (seg === "" || seg === ".") continue; if (seg === "..") { parts.pop(); continue; } parts.push(seg); } return parts.join("/"); } /** * Rewrite a relative href found inside a markdown file so it resolves * against the doc's slug *inside* the SPA. Anchors and absolute URLs are * passed through untouched. */ function rewriteHref(href: string, baseSlug: string): string { if (!href) return href; if (/^[a-z]+:/i.test(href)) return href; // http(s):, mailto:, … if (href.startsWith("#")) return href; if (href.startsWith("/")) return href; // absolute SPA path let resolved = resolveAgainstSlug(href, baseSlug); // Markdown links commonly point at README.md or sibling .md files. // Map them onto our slug shape (folder/ for README, folder/page for // anything else). resolved = resolved.replace(/\/?README\.md$/i, "/").replace(/\.md$/i, ""); return `#/docs/${resolved}`; } /** * Rewrite a relative image src so it points at the bundled copy in * `public/screenshots/` (or wherever in the docs tree it sits). The docs * markdown references images like `../screenshots/editor/00-panel-gate.png` * — we resolve those against the doc slug, strip the leading `screenshots/` * prefix, and re-anchor onto `./screenshots/...` which the static host * serves verbatim from `public/`. * * Returns `null` if the path cannot be mapped onto the screenshots folder * (so the caller can fall back to the original src). */ function rewriteImg(src: string, baseSlug: string): string | null { if (!src) return null; if (/^[a-z]+:/i.test(src)) return src; // http(s):, data:, … if (src.startsWith("/")) return src; // absolute path — host already handles it const resolved = resolveAgainstSlug(src, baseSlug); const m = resolved.match(/(?:^|\/)screenshots\/(.+)$/i); if (!m) return null; return `./screenshots/${m[1]}`; } export function MarkdownView({ body, baseSlug }: Props) { return (
{children} ); }, img({ src, alt, ...rest }) { // Resolve relative image paths against the doc's slug. // Anything inside `screenshots/` is served verbatim by the // static host out of `public/screenshots/`, so re-anchor // those onto `./screenshots/...`. Other relative paths // (none currently exist) fall through to the original src. const finalSrc = typeof src === "string" ? (rewriteImg(src, baseSlug) ?? src) : src; return ( {alt} ); }, }} > {body}
); }