@@ -1,3 +1,4 @@
+import { useMemo } from "react";
import ReactMarkdown from "react-markdown";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeExternalLinks from "rehype-external-links";
@@ -11,10 +12,6 @@ import { cn } from "@/lib/utils";
// Sanitization schema: start from the safe default and allow a small set of
// presentational/structural HTML tags commonly found in READMEs.
-// Script, style, iframe, object, embed and event-handler attributes are
-// blocked by the default schema and remain blocked.
-// rehype-autolink-headings injects <a> with aria-hidden and class, so we
-// allow those attributes on anchors.
const sanitizeSchema = {
...defaultSchema,
tagNames: [...(defaultSchema.tagNames ?? []), "details", "summary", "picture", "source"],
@@ -28,11 +25,46 @@ const sanitizeSchema = {
interface MarkdownProps {
content: string;
className?: string;
+ /** When set, relative links/images are resolved against the code browser. */
+ repoContext?: {
+ repo: string;
+ ref: string;
+ /** Directory containing the markdown file (e.g. "doc" for doc/README.md). */
+ basePath: string;
+ };
+}
+
+function isRelativeUrl(url: string): boolean {
+ // Absolute URLs, protocol-relative, anchors, and data URIs are not relative
+ return !/^(?:[a-z][a-z0-9+.-]*:|\/\/|#|data:)/i.test(url);
+}
+
+function resolveRelativePath(basePath: string, relativePath: string): string {
+ const parts = basePath ? basePath.split("/") : [];
+ for (const segment of relativePath.split("/")) {
+ if (segment === "..") {
+ parts.pop();
+ } else if (segment !== "." && segment !== "") {
+ parts.push(segment);
+ }
+ }
+ return parts.join("/");
}
// Renders a Markdown string with GitHub-flavoured extensions (tables, task
-// lists, strikethrough). Used in Timeline comments and NewBugPage preview.
-export function Markdown({ content, className }: MarkdownProps) {
+// lists, strikethrough). Used in Timeline comments and code browser READMEs.
+export function Markdown({ content, className, repoContext }: MarkdownProps) {
+ // Build a urlTransform that rewrites relative URLs to the code browser
+ const urlTransform = useMemo(() => {
+ if (!repoContext) return undefined;
+ const { repo, ref, basePath } = repoContext;
+ return (url: string) => {
+ if (!isRelativeUrl(url)) return url;
+ const resolved = resolveRelativePath(basePath, url);
+ return `/${repo}/blob/${ref}/${resolved}`;
+ };
+ }, [repoContext]);
+
return (
<div
className={cn(
@@ -52,6 +84,7 @@ export function Markdown({ content, className }: MarkdownProps) {
[rehypeAutolinkHeadings, { behavior: "append" }],
[rehypeExternalLinks, { target: "_blank", rel: ["noopener", "noreferrer"] }],
]}
+ urlTransform={urlTransform}
>
{content}
</ReactMarkdown>
@@ -116,7 +116,10 @@ function TreeView() {
<div className="rounded-md border">
<div className="text-muted-foreground border-b px-4 py-2 text-xs font-medium">README</div>
<div className="px-6 py-4">
- <Markdown content={readme} />
+ <Markdown
+ content={readme}
+ repoContext={{ repo, ref: currentRef, basePath: currentPath }}
+ />
</div>
</div>
)}