diff --git a/webui2/src/components/content/Markdown.tsx b/webui2/src/components/content/Markdown.tsx index a5244323339ed20f73b45b1275d60b55c0804981..390412391681e92364b4b95ebce17ea47ca6fe61 100644 --- a/webui2/src/components/content/Markdown.tsx +++ b/webui2/src/components/content/Markdown.tsx @@ -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 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 (
{content} diff --git a/webui2/src/routes/$repo/_code/tree/$ref/$.tsx b/webui2/src/routes/$repo/_code/tree/$ref/$.tsx index f98d80af74669d591d2e1486b52d975f3d1b1ad4..98c7818bb17015b38a2d83c42d2e4d83e4faff12 100644 --- a/webui2/src/routes/$repo/_code/tree/$ref/$.tsx +++ b/webui2/src/routes/$repo/_code/tree/$ref/$.tsx @@ -116,7 +116,10 @@ function TreeView() {
README
- +
)}