diff --git a/frontend/package.json b/frontend/package.json index 37a0674..040d3ac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,8 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-markdown": "^10.1.0", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", "tailwindcss": "^4.2.2" }, "devDependencies": { diff --git a/frontend/src/components/ProjectsReadme.jsx b/frontend/src/components/ProjectsReadme.jsx index 09ab4bd..d1a34c4 100644 --- a/frontend/src/components/ProjectsReadme.jsx +++ b/frontend/src/components/ProjectsReadme.jsx @@ -1,23 +1,80 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useMemo } from "react"; import ReactMarkdown from "react-markdown"; +import rehypeRaw from "rehype-raw"; + +const toRawUrl = (repoUrl) => { + if (!repoUrl) return repoUrl; + + if ( + repoUrl.includes("github.com") && + !repoUrl.includes("raw.githubusercontent.com") + ) { + return repoUrl + .replace("github.com", "raw.githubusercontent.com") + .replace("/blob/", "/"); + } + + if (repoUrl.includes("/src/branch/")) { + return repoUrl.replace("/src/branch/", "/raw/branch/"); + } + + return repoUrl; +}; + +const getRepoRawBase = (repoUrl) => { + const rawUrl = toRawUrl(repoUrl); + if (!rawUrl) return null; + + try { + const url = new URL(rawUrl); + const parts = url.pathname.split("/").filter(Boolean); + + if (url.hostname.includes("raw.githubusercontent.com")) { + return `${url.origin}/${parts.slice(0, 3).join("/")}`; + } + + const rawIndex = parts.indexOf("raw"); + if (rawIndex !== -1) { + return `${url.origin}/${parts.slice(0, rawIndex + 2).join("/")}`; + } + + return null; + } catch { + return null; + } +}; + +const resolveImageSrc = (src, base) => { + if (!src) return src; + + if (src.startsWith("http")) return src; + + if (src.includes("/blob/")) { + return src + .replace("github.com", "raw.githubusercontent.com") + .replace("/blob/", "/"); + } + + if (!base) return src; + + const clean = src.replace(/^.\//, ""); + + if (src.startsWith("/")) { + return base + src; + } + + return `${base}/${clean}`; +}; const fetchREADME = async (repoUrl) => { try { - let rawUrl = repoUrl; + const rawUrl = toRawUrl(repoUrl); + const response = await fetch(rawUrl); - if (repoUrl.includes("/src/branch/")) { - rawUrl = repoUrl.replace("/src/branch/", "/raw/branch/"); - } else if ( - repoUrl.includes("github.com") && - !repoUrl.includes("raw.githubusercontent.com") - ) { - rawUrl = repoUrl - .replace("github.com", "raw.githubusercontent.com") - .replace("/blob/", "/"); + if (!response.ok) { + throw new Error("Failed to fetch README"); } - const response = await fetch(rawUrl); - if (!response.ok) throw new Error("Failed to fetch README"); return await response.text(); } catch (error) { console.error(error); @@ -25,14 +82,15 @@ const fetchREADME = async (repoUrl) => { } }; -export default function ProjectsReadme({ - repoUrl, -}) { +export default function ProjectsReadme({ repoUrl }) { const [readmeContent, setReadmeContent] = useState(""); const [isLoading, setIsLoading] = useState(true); + const base = useMemo(() => getRepoRawBase(repoUrl), [repoUrl]); + useEffect(() => { setIsLoading(true); + fetchREADME(repoUrl).then((content) => { setReadmeContent(content); setIsLoading(false); @@ -50,13 +108,15 @@ export default function ProjectsReadme({ ) : (
); } + return ( ); }, + pre({ children, ...props }) { return (
                                 );
                             },
+
                             h1: ({ children }) => (
                                 

{children}

), + h2: ({ children }) => (

{children}

), + h3: ({ children }) => ( -

+

{children}

), + p: ({ children }) => (

{children}

), + ul: ({ children }) => ( -
    +
      {children}
    ), + ol: ({ children }) => ( -
      +
        {children}
      ), + li: ({ children }) => ( -
    1. {children}
    2. +
    3. {children}
    4. ), + a: ({ children, href }) => ( {children} ), + blockquote: ({ children }) => ( -
      +
      {children}
      ), - img: ({ src, alt }) => ( - {alt} - ), + + img: ({ src, alt }) => { + const resolved = resolveImageSrc(src, base); + + return ( + {alt} + ); + }, }} > {readmeContent}