mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-18 19:30:27 +08:00
Render GitHub blob image URLs
Image URLs pointing at a GitHub "blob" page, such as https://github.com/<owner>/<repo>/blob/<ref>/<path>, serve an HTML page rather than the raw image bytes, so they render as broken images in markdown. Add a canonicalizeGithubImageUrl helper that appends ?raw=true to such URLs, which makes GitHub redirect to the raw content. URLs already pointing at the raw content, via ?raw=true or raw.githubusercontent.com, are left unchanged, as are non-GitHub and non-URL sources. Wire the helper into the markdown image loop so external image URLs are canonicalized while relative paths keep resolving against the repo's raw endpoint.
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
twemoji,
|
||||
scrollIntoView,
|
||||
canonicalize,
|
||||
canonicalizeGithubImageUrl,
|
||||
isCommit,
|
||||
} from "@app/lib/utils";
|
||||
import { Renderer, markdown, sanitizeConfig } from "@app/lib/markdown";
|
||||
@@ -226,10 +227,14 @@
|
||||
const imagePath = i.getAttribute("src");
|
||||
const imageClass = i.getAttribute("class");
|
||||
|
||||
// Make sure the source isn't a URL before trying to fetch it from the repo
|
||||
const emoji = imageClass && imageClass === "txt-emoji";
|
||||
if (imagePath && !isUrl(imagePath) && !emoji) {
|
||||
i.setAttribute("src", `${rawPath}/${canonicalize(imagePath, path)}`);
|
||||
if (imagePath && !emoji) {
|
||||
if (isUrl(imagePath)) {
|
||||
// Rewrite GitHub "blob" URLs so the raw image content is rendered.
|
||||
i.setAttribute("src", canonicalizeGithubImageUrl(imagePath));
|
||||
} else {
|
||||
i.setAttribute("src", `${rawPath}/${canonicalize(imagePath, path)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -184,6 +184,29 @@ export function isUrl(input: string): boolean {
|
||||
return /^https?:\/\//.test(input);
|
||||
}
|
||||
|
||||
// Rewrite GitHub "blob" image URLs so they resolve to the raw image content.
|
||||
// A URL like https://github.com/<owner>/<repo>/blob/<ref>/<path> serves an
|
||||
// HTML page rather than the image bytes, so appending `?raw=true` makes GitHub
|
||||
// redirect to the raw content. URLs already pointing at the raw content (either
|
||||
// via `?raw=true` or raw.githubusercontent.com) are returned unchanged.
|
||||
export function canonicalizeGithubImageUrl(input: string): string {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(input);
|
||||
} catch {
|
||||
return input;
|
||||
}
|
||||
if (
|
||||
(url.hostname === "github.com" || url.hostname === "www.github.com") &&
|
||||
/^\/[^/]+\/[^/]+\/blob\//.test(url.pathname) &&
|
||||
url.searchParams.get("raw") !== "true"
|
||||
) {
|
||||
url.searchParams.set("raw", "true");
|
||||
return url.toString();
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
export function isCommit(input: string): boolean {
|
||||
return /^[a-f0-9]{40}$/.test(input);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,57 @@ describe("String Assertions", () => {
|
||||
])("isUrl $url => $expected", ({ url, expected }) => {
|
||||
expect(utils.isUrl(url)).toBe(expected);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{
|
||||
url: "https://github.com/radicle-dev/radicle-explorer/blob/master/img.png",
|
||||
expected:
|
||||
"https://github.com/radicle-dev/radicle-explorer/blob/master/img.png?raw=true",
|
||||
},
|
||||
{
|
||||
// Already has ?raw=true, leave it untouched.
|
||||
url: "https://github.com/radicle-dev/radicle-explorer/blob/master/img.png?raw=true",
|
||||
expected:
|
||||
"https://github.com/radicle-dev/radicle-explorer/blob/master/img.png?raw=true",
|
||||
},
|
||||
{
|
||||
// Raw content host is already correct, leave it untouched.
|
||||
url: "https://raw.githubusercontent.com/radicle-dev/radicle-explorer/master/img.png",
|
||||
expected:
|
||||
"https://raw.githubusercontent.com/radicle-dev/radicle-explorer/master/img.png",
|
||||
},
|
||||
{
|
||||
// An existing query string is preserved.
|
||||
url: "https://github.com/radicle-dev/radicle-explorer/blob/master/img.png?token=abc",
|
||||
expected:
|
||||
"https://github.com/radicle-dev/radicle-explorer/blob/master/img.png?token=abc&raw=true",
|
||||
},
|
||||
{
|
||||
// The www subdomain is handled too.
|
||||
url: "https://www.github.com/radicle-dev/radicle-explorer/blob/master/img.png",
|
||||
expected:
|
||||
"https://www.github.com/radicle-dev/radicle-explorer/blob/master/img.png?raw=true",
|
||||
},
|
||||
{
|
||||
// A "blob" segment that isn't in the <owner>/<repo>/blob/ position is
|
||||
// left untouched.
|
||||
url: "https://github.com/radicle-dev/radicle-explorer/raw/master/blob/img.png",
|
||||
expected:
|
||||
"https://github.com/radicle-dev/radicle-explorer/raw/master/blob/img.png",
|
||||
},
|
||||
{
|
||||
// Non-GitHub URLs are returned unchanged.
|
||||
url: "https://example.com/img.png",
|
||||
expected: "https://example.com/img.png",
|
||||
},
|
||||
{
|
||||
// Not a valid URL, returned unchanged.
|
||||
url: "not a url",
|
||||
expected: "not a url",
|
||||
},
|
||||
])("canonicalizeGithubImageUrl $url => $expected", ({ url, expected }) => {
|
||||
expect(utils.canonicalizeGithubImageUrl(url)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Parse Functions", () => {
|
||||
|
||||
Reference in New Issue
Block a user