Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Revert "Handle absolute markdown links with leading slash"
Rūdolfs Ošiņš committed 2 years ago
commit 127efc49a56eca7fedec41242c1d35cb9de4a201
parent 2efd0ad58e2dfe70bf9bab1c511a7d4570de9b7e
9 files changed +56 -68
modified src/components/InlineMarkdown.svelte
@@ -3,7 +3,6 @@

  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;
@@ -14,9 +13,7 @@
  const render = (content: string): string =>
    dompurify.sanitize(
      markdown.parseInline(content, {
-
        renderer: new Renderer($activeUnloadedRouteStore, {
-
          stripEmphasizedStyling,
-
        }),
+
        renderer: new Renderer(undefined, stripEmphasizedStyling),
      }) as string,
    );
</script>
modified src/components/Markdown.svelte
@@ -11,7 +11,6 @@
  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,
@@ -23,6 +22,8 @@
  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,
@@ -62,7 +63,7 @@
  }

  /**
-
   * Do internal navigation for clicks on anchor elements if possible
+
   * Do internal navigation on for clicks on anchor elements if possible
   */
  function navigateInternalOnAnchor(event: MouseEvent) {
    if (router.useDefaultNavigation(event)) {
@@ -93,9 +94,7 @@
  function render(content: string): string {
    return dompurify.sanitize(
      markdown.parse(content, {
-
        renderer: new Renderer($activeUnloadedRouteStore, {
-
          stripEmphasizedStyling: false,
-
        }),
+
        renderer: new Renderer(linkBaseUrl, false),
        breaks,
      }) as string,
    );
modified src/lib/markdown.ts
@@ -1,5 +1,4 @@
import type { Tokens } from "marked";
-
import type { Route } from "@app/lib/router";

import dompurify from "dompurify";
import katexMarkedExtension from "marked-katex-extension";
@@ -7,8 +6,6 @@ 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
@@ -104,19 +101,16 @@ const anchorMarkedExtension = {
};

export class Renderer extends BaseRenderer {
-
  #route: Route;
+
  #baseUrl: string | undefined;
  #stripEmphasizedStyling: boolean | undefined;

  /**
   * If `baseUrl` is provided, all hrefs attributes in anchor tags, except those
   * starting with `#`, are resolved with respect to `baseUrl`
   */
-
  constructor(
-
    activeUnloadedRoute: Route,
-
    { stripEmphasizedStyling }: { stripEmphasizedStyling: boolean },
-
  ) {
+
  constructor(baseUrl: string | undefined, stripEmphasizedStyling: boolean) {
    super();
-
    this.#route = activeUnloadedRoute;
+
    this.#baseUrl = baseUrl;
    this.#stripEmphasizedStyling = stripEmphasizedStyling;
  }
  // Overwrites the rendering of heading tokens.
@@ -145,17 +139,15 @@ export class Renderer extends BaseRenderer {
    if (href.startsWith("#")) {
      // By lowercasing we avoid casing mismatches, between headings and links.
      return `<a ${title ? `title="${title}"` : ""} href="${href.toLowerCase()}">${text}</a>`;
+
    } else {
+
      try {
+
        href = new URL(href, this.#baseUrl).href;
+
      } catch {
+
        // Use original href value
+
      }
+

+
      return `<a ${title ? `title="${title}"` : ""} href="${href}">${text}</a>`;
    }
-

-
    if ("path" in this.#route && this.#route.path && !isUrl(href)) {
-
      href = routeToPath({
-
        ...this.#route,
-
        path: canonicalize(href, this.#route.path),
-
        route: undefined,
-
      });
-
    }
-

-
    return `<a ${title ? `title="${title}"` : ""} href="${href}">${text}</a>`;
  }
}

modified src/lib/utils.ts
@@ -115,23 +115,19 @@ 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 {
-
  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("/");
-
  }
+
  path = path.replace(/^\//, ""); // Remove leading slash
+
  const 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);
modified src/views/projects/Source.svelte
@@ -179,9 +179,11 @@
      <div class="column-right">
        {#if blobResult.ok}
          <BlobComponent
-
            {path}
            {baseUrl}
            projectId={project.id}
+
            {peer}
+
            {revision}
+
            {path}
            blob={blobResult.blob}
            highlighted={blobResult.highlighted}
            rawPath={rawPath(tree.lastCommit.id)} />
modified src/views/projects/Source/Blob.svelte
@@ -7,6 +7,7 @@
  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";
@@ -19,6 +20,8 @@

  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;
@@ -50,6 +53,31 @@
  $: enablePreview = isMarkdown || isSvg;
  $: preview = enablePreview && selectedLineId === undefined;

+
  let linkBaseUrl: string | undefined;
+

+
  $: {
+
    if (!path || path === "/") {
+
      // For the default root path, the `tree/<revision>` 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");
@@ -189,7 +217,7 @@
  {:else if preview && blob.content}
    {#if isMarkdown}
      <div style:padding="2rem">
-
        <Markdown content={blob.content} {rawPath} {path} />
+
        <Markdown {rawPath} {path} {linkBaseUrl} content={blob.content} />
      </div>
    {:else if isSvg}
      <div style:margin="1rem 0" style:text-align="center">
modified tests/e2e/project.spec.ts
@@ -468,26 +468,6 @@ 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();
modified tests/fixtures/repos/markdown.tar.bz2
modified tests/unit/utils.test.ts
@@ -167,12 +167,6 @@ describe("Path Manipulation", () => {
      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: "/",
      origin: "https://app.radicle.xyz",