From 2efd0ad58e2dfe70bf9bab1c511a7d4570de9b7e Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Mon, 25 Mar 2024 15:14:28 +0100 Subject: [PATCH] Handle absolute markdown links with leading slash --- src/components/InlineMarkdown.svelte | 5 ++++- src/components/Markdown.svelte | 9 ++++---- src/lib/markdown.ts | 30 ++++++++++++++++---------- src/lib/utils.ts | 20 ++++++++++------- src/views/projects/Source.svelte | 4 +--- src/views/projects/Source/Blob.svelte | 30 +------------------------- tests/e2e/project.spec.ts | 20 +++++++++++++++++ tests/fixtures/repos/markdown.tar.bz2 | Bin 4938 -> 5120 bytes tests/unit/utils.test.ts | 6 ++++++ 9 files changed, 68 insertions(+), 56 deletions(-) diff --git a/src/components/InlineMarkdown.svelte b/src/components/InlineMarkdown.svelte index 878bd005b..5ea4b5b0b 100644 --- a/src/components/InlineMarkdown.svelte +++ b/src/components/InlineMarkdown.svelte @@ -3,6 +3,7 @@ import markdown from "@app/lib/markdown"; import { twemoji } from "@app/lib/utils"; + import { activeUnloadedRouteStore } from "@app/lib/router"; import { Renderer } from "@app/lib/markdown"; export let content: string; @@ -13,7 +14,9 @@ const render = (content: string): string => dompurify.sanitize( markdown.parseInline(content, { - renderer: new Renderer(undefined, stripEmphasizedStyling), + renderer: new Renderer($activeUnloadedRouteStore, { + stripEmphasizedStyling, + }), }) as string, ); diff --git a/src/components/Markdown.svelte b/src/components/Markdown.svelte index 3e1920339..8383f8d5e 100644 --- a/src/components/Markdown.svelte +++ b/src/components/Markdown.svelte @@ -11,6 +11,7 @@ import ErrorModal from "@app/modals/ErrorModal.svelte"; import markdown from "@app/lib/markdown"; import { Renderer } from "@app/lib/markdown"; + import { activeUnloadedRouteStore } from "@app/lib/router"; import { highlight } from "@app/lib/syntax"; import { isUrl, @@ -22,8 +23,6 @@ import { mimes } from "@app/lib/file"; export let content: string; - // If present, resolve all relative links with respect to this URL - export let linkBaseUrl: string | undefined = undefined; export let path: string = "/"; export let rawPath: string; // If present, means we are in a preview context, @@ -63,7 +62,7 @@ } /** - * Do internal navigation on for clicks on anchor elements if possible + * Do internal navigation for clicks on anchor elements if possible */ function navigateInternalOnAnchor(event: MouseEvent) { if (router.useDefaultNavigation(event)) { @@ -94,7 +93,9 @@ function render(content: string): string { return dompurify.sanitize( markdown.parse(content, { - renderer: new Renderer(linkBaseUrl, false), + renderer: new Renderer($activeUnloadedRouteStore, { + stripEmphasizedStyling: false, + }), breaks, }) as string, ); diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index a7c13b103..371ee581b 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -1,4 +1,5 @@ import type { Tokens } from "marked"; +import type { Route } from "@app/lib/router"; import dompurify from "dompurify"; import katexMarkedExtension from "marked-katex-extension"; @@ -6,6 +7,8 @@ import markedLinkifyIt from "marked-linkify-it"; import { Marked, Renderer as BaseRenderer } from "marked"; import emojis from "@app/lib/emojis"; +import { routeToPath } from "@app/lib/router"; +import { canonicalize, isUrl } from "@app/lib/utils"; dompurify.setConfig({ // eslint-disable-next-line @typescript-eslint/naming-convention @@ -101,16 +104,19 @@ const anchorMarkedExtension = { }; export class Renderer extends BaseRenderer { - #baseUrl: string | undefined; + #route: Route; #stripEmphasizedStyling: boolean | undefined; /** * If `baseUrl` is provided, all hrefs attributes in anchor tags, except those * starting with `#`, are resolved with respect to `baseUrl` */ - constructor(baseUrl: string | undefined, stripEmphasizedStyling: boolean) { + constructor( + activeUnloadedRoute: Route, + { stripEmphasizedStyling }: { stripEmphasizedStyling: boolean }, + ) { super(); - this.#baseUrl = baseUrl; + this.#route = activeUnloadedRoute; this.#stripEmphasizedStyling = stripEmphasizedStyling; } // Overwrites the rendering of heading tokens. @@ -139,15 +145,17 @@ export class Renderer extends BaseRenderer { if (href.startsWith("#")) { // By lowercasing we avoid casing mismatches, between headings and links. return `${text}`; - } else { - try { - href = new URL(href, this.#baseUrl).href; - } catch { - // Use original href value - } - - return `${text}`; } + + if ("path" in this.#route && this.#route.path && !isUrl(href)) { + href = routeToPath({ + ...this.#route, + path: canonicalize(href, this.#route.path), + route: undefined, + }); + } + + return `${text}`; } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e77fdda98..8ed5d8bd1 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -115,19 +115,23 @@ export function formatPublicExplorer( .replace("$path", fullPath.replace(`/nodes/${host}/${rid}`, "")); } -// Takes a path, eg. "../images/image.png", and a base from where to start resolving, e.g. "static/images/index.html". -// Returns the resolved path. +// Takes a path, eg. "../images/image.png", and a base from where to start +// resolving, e.g. "static/images/index.html". Returns the resolved path. export function canonicalize( path: string, base: string, origin = document.location.origin, ): string { - path = path.replace(/^\//, ""); // Remove leading slash - const finalPath = base - .split("/") - .slice(0, -1) // Remove file name. - .concat([path]) // Add image file path. - .join("/"); + let finalPath: string | undefined; + if (path.startsWith("/")) { + finalPath = new URL(path, origin).pathname; + } else { + finalPath = base + .split("/") + .slice(0, -1) // Remove file name. + .concat([path]) // Add image file path. + .join("/"); + } // URL is used to resolve relative paths, eg. `../../assets/image.png`. const url = new URL(finalPath, origin); diff --git a/src/views/projects/Source.svelte b/src/views/projects/Source.svelte index 810d4909d..750a2fd21 100644 --- a/src/views/projects/Source.svelte +++ b/src/views/projects/Source.svelte @@ -179,11 +179,9 @@
{#if blobResult.ok} diff --git a/src/views/projects/Source/Blob.svelte b/src/views/projects/Source/Blob.svelte index 71aefb473..8eb6868c0 100644 --- a/src/views/projects/Source/Blob.svelte +++ b/src/views/projects/Source/Blob.svelte @@ -7,7 +7,6 @@ import * as Syntax from "@app/lib/syntax"; import { isImagePath, isMarkdownPath, isSvgPath } from "@app/lib/utils"; import { lineNumbersGutter } from "@app/lib/syntax"; - import { routeToPath } from "@app/lib/router"; import Button from "@app/components/Button.svelte"; import CommitButton from "@app/views/projects/components/CommitButton.svelte"; @@ -20,8 +19,6 @@ export let baseUrl: BaseUrl; export let projectId: string; - export let peer: string | undefined; - export let revision: string | undefined; export let path: string; export let blob: Blob; export let highlighted: Syntax.Root | undefined; @@ -53,31 +50,6 @@ $: enablePreview = isMarkdown || isSvg; $: preview = enablePreview && selectedLineId === undefined; - let linkBaseUrl: string | undefined; - - $: { - if (!path || path === "/") { - // For the default root path, the `tree/` portion is omitted - // from the URL. This means that links cannot be resolved with respect - // to the current location. To work around this we provide path that - // results a fully expanded URL with which we can resolve all links in the - // Markdown. - linkBaseUrl = new URL( - routeToPath({ - resource: "project.source", - project: projectId, - node: baseUrl, - peer, - revision, - path: "README.md", - }), - window.origin, - ).href; - } else { - linkBaseUrl = undefined; - } - } - afterUpdate(() => { for (const item of document.getElementsByClassName("highlight")) { item.classList.remove("highlight"); @@ -217,7 +189,7 @@ {:else if preview && blob.content} {#if isMarkdown}
- +
{:else if isSvg}
diff --git a/tests/e2e/project.spec.ts b/tests/e2e/project.spec.ts index 4f88eb7b2..dfc1bc74f 100644 --- a/tests/e2e/project.spec.ts +++ b/tests/e2e/project.spec.ts @@ -468,6 +468,26 @@ test("external markdown link", async ({ context, page }) => { await expect(newPage).toHaveURL("https://example.com"); }); +test("absolute markdown link", async ({ page }) => { + await page.goto(`${markdownUrl}/tree/main/link-files.md`); + await page.getByRole("link", { name: "Absolute Link" }).click(); + await expect(page).toHaveURL( + `${markdownUrl}/tree/main/relative-files/linked-file.md`, + ); + await page.getByRole("link", { name: "nested file", exact: true }).click(); + await expect(page).toHaveURL( + `${markdownUrl}/tree/main/relative-files/nested-file.md`, + ); + await page.goBack(); + await page.getByRole("link", { name: "nested file with" }).click(); + await expect(page).toHaveURL( + `${markdownUrl}/tree/main/relative-files/nested-file.md`, + ); + await page.goBack(); + await page.getByRole("link", { name: "Back to link-files with" }).click(); + await expect(page).toHaveURL(`${markdownUrl}/tree/main/link-files.md`); +}); + test("internal file markdown link", async ({ page }) => { await page.goto(`${markdownUrl}/tree/main/link-files.md`); await page.getByRole("link", { name: "Markdown Cheatsheet" }).click(); diff --git a/tests/fixtures/repos/markdown.tar.bz2 b/tests/fixtures/repos/markdown.tar.bz2 index 99eb2ec01ce98d18fe57147e3689c7a811814c33..a699da8f9d031a5f3e1d05400db1f449bf4117d1 100644 GIT binary patch literal 5120 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 literal 4938 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 I+5k`h060vcumAu6 diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts index aed88f67e..5365c7aaf 100644 --- a/tests/unit/utils.test.ts +++ b/tests/unit/utils.test.ts @@ -166,6 +166,12 @@ describe("Path Manipulation", () => { origin: "https://app.radicle.xyz", expected: "assets/images/tux.png", }, + { + imagePath: "/tux.md", + base: "/components/assets/README.md", + origin: "http://localhost:3000", + expected: "tux.md", + }, { imagePath: "assets/images/tux.png", base: "/",