Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Add InlineTitle component
Open did:key:z6MkkfM3...sVz5 opened 1 year ago

This new component doesn’t rely on marked.js but escapes any special characters and then replaces any backtick with a <code> HTML tag to render any inline codeblocks in all titles.

Also updates tsconfig target to es2021, this gives us support for String.replaceAll

check check-visual check-unit-test check-http-client-unit-test check-radicle-httpd check-e2e check-build check-http

👉 Preview 👉 Workflow runs 👉 Branch on GitHub

16 files changed +76 -91 02057b5b e4b15e72
deleted src/components/InlineMarkdown.svelte
@@ -1,45 +0,0 @@
-
<script lang="ts">
-
  import dompurify from "dompurify";
-

-
  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;
-
  export let stripEmphasizedStyling: boolean = false;
-
  export let fontSize: "tiny" | "small" | "regular" | "medium" | "large" =
-
    "small";
-

-
  const render = (content: string): string =>
-
    dompurify.sanitize(
-
      markdown.parseInline(content, {
-
        renderer: new Renderer($activeUnloadedRouteStore, {
-
          stripEmphasizedStyling,
-
        }),
-
      }) as string,
-
    );
-
</script>
-

-
<style>
-
  .markdown :global(code) {
-
    font-family: var(--font-family-monospace);
-
    background-color: var(--color-fill-ghost);
-
    border-radius: var(--border-radius-tiny);
-
    padding: 0.125rem 0.25rem;
-
  }
-
  .markdown :global(strong) {
-
    font-weight: var(--font-weight-semibold);
-
  }
-
</style>
-

-
<span
-
  class="markdown"
-
  use:twemoji
-
  class:txt-large={fontSize === "large"}
-
  class:txt-medium={fontSize === "medium"}
-
  class:txt-regular={fontSize === "regular"}
-
  class:txt-small={fontSize === "small"}
-
  class:txt-tiny={fontSize === "tiny"}>
-
  {@html render(content)}
-
</span>
modified src/components/Markdown.svelte
@@ -93,9 +93,7 @@
  function render(content: string): string {
    return dompurify.sanitize(
      markdownWithExtensions.parse(content, {
-
        renderer: new Renderer($activeUnloadedRouteStore, {
-
          stripEmphasizedStyling: false,
-
        }),
+
        renderer: new Renderer($activeUnloadedRouteStore),
        breaks,
      }) as string,
    );
modified src/lib/markdown.ts
@@ -40,19 +40,14 @@ const anchorMarkedExtension = {

export class Renderer extends BaseRenderer {
  #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(
-
    activeUnloadedRoute: Route,
-
    { stripEmphasizedStyling }: { stripEmphasizedStyling: boolean },
-
  ) {
+
  constructor(activeUnloadedRoute: Route) {
    super();
    this.#route = activeUnloadedRoute;
-
    this.#stripEmphasizedStyling = stripEmphasizedStyling;
  }
  // Overwrites the rendering of heading tokens.
  // Since there are possible non ASCII characters in headings,
@@ -69,16 +64,6 @@ export class Renderer extends BaseRenderer {
    return `<h${depth} id="${escapedText}">${text}</h${depth}>`;
  }

-
  strong({ tokens }: Tokens.Strong) {
-
    const text = this.parser.parseInline(tokens);
-
    return this.#stripEmphasizedStyling ? text : `<strong>${text}</strong>`;
-
  }
-

-
  em({ tokens }: Tokens.Em) {
-
    const text = this.parser.parseInline(tokens);
-
    return this.#stripEmphasizedStyling ? text : `<em>${text}</em>`;
-
  }
-

  link({ href, title, tokens }: Tokens.Link): string {
    const text = this.parser.parseInline(tokens);
    if (href.startsWith("#")) {
modified src/lib/utils.ts
@@ -26,6 +26,10 @@ export function formatUserAgent(agent: string): string {
  return agent.slice(1, -1);
}

+
export function formatInlineTitle(input: string): string {
+
  return input.replaceAll(/`([^`]+)`/g, "<code>$1</code>");
+
}
+

export function parseNodeId(
  nid: string,
): { prefix: string; pubkey: string } | undefined {
modified src/views/projects/Cob/CobCommitTeaser.svelte
@@ -8,7 +8,7 @@
  import IconButton from "@app/components/IconButton.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
  import Id from "@app/components/Id.svelte";
-
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
+
  import InlineTitle from "@app/views/projects/components/InlineTitle.svelte";
  import Link from "@app/components/Link.svelte";

  export let baseUrl: BaseUrl;
@@ -68,7 +68,7 @@
          commit: commit.id,
        }}>
        <div class="summary" use:twemoji>
-
          <InlineMarkdown fontSize="small" content={commit.summary} />
+
          <InlineTitle fontSize="small" content={commit.summary} />
        </div>
      </Link>
      {#if commit.description}
modified src/views/projects/Commit.svelte
@@ -5,7 +5,7 @@
  import Changeset from "@app/views/projects/Changeset.svelte";
  import CommitAuthorship from "@app/views/projects/Commit/CommitAuthorship.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
-
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
+
  import InlineTitle from "@app/views/projects/components/InlineTitle.svelte";
  import Layout from "./Layout.svelte";
  import Link from "@app/components/Link.svelte";
  import Share from "./Share.svelte";
@@ -50,10 +50,7 @@
    <div class="header">
      <div style="display:flex; flex-direction: column; gap: 0.5rem;">
        <span class="title">
-
          <InlineMarkdown
-
            stripEmphasizedStyling
-
            fontSize="large"
-
            content={header.summary} />
+
          <InlineTitle fontSize="large" content={header.summary} />
          <div class="button-container">
            <Link
              route={{
modified src/views/projects/Commit/CommitTeaser.svelte
@@ -7,7 +7,7 @@
  import ExpandButton from "@app/components/ExpandButton.svelte";
  import IconButton from "@app/components/IconButton.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
-
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
+
  import InlineTitle from "@app/views/projects/components/InlineTitle.svelte";
  import Link from "@app/components/Link.svelte";
  import Id from "@app/components/Id.svelte";

@@ -86,7 +86,7 @@
        }}>
        <div style="position: relative;">
          <div class="summary" use:twemoji>
-
            <InlineMarkdown fontSize="regular" content={commit.summary} />
+
            <InlineTitle fontSize="regular" content={commit.summary} />
          </div>
        </div>
      </Link>
modified src/views/projects/Issue.svelte
@@ -37,7 +37,7 @@
  import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
  import Id from "@app/components/Id.svelte";
-
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
+
  import InlineTitle from "@app/views/projects/components/InlineTitle.svelte";
  import LabelInput from "./Cob/LabelInput.svelte";
  import Layout from "./Layout.svelte";
  import Markdown from "@app/components/Markdown.svelte";
@@ -499,10 +499,7 @@
              <span class="txt-missing">No title</span>
            {:else}
              <div class="title">
-
                <InlineMarkdown
-
                  stripEmphasizedStyling
-
                  fontSize="large"
-
                  content={newTitle} />
+
                <InlineTitle fontSize="large" content={newTitle} />
              </div>
            {/if}
          </div>
modified src/views/projects/Issue/IssueTeaser.svelte
@@ -4,7 +4,7 @@
  import { absoluteTimestamp, formatTimestamp } from "@app/lib/utils";

  import IconSmall from "@app/components/IconSmall.svelte";
-
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
+
  import InlineTitle from "@app/views/projects/components/InlineTitle.svelte";
  import Link from "@app/components/Link.svelte";
  import NodeId from "@app/components/NodeId.svelte";

@@ -93,7 +93,7 @@
          {#if !issue.title}
            <span class="txt-missing">No title</span>
          {:else}
-
            <InlineMarkdown fontSize="regular" content={issue.title} />
+
            <InlineTitle fontSize="regular" content={issue.title} />
          {/if}
        </Link>
      </span>
modified src/views/projects/Patch.svelte
@@ -75,7 +75,7 @@
  import ExtendedTextarea from "@app/components/ExtendedTextarea.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
  import Id from "@app/components/Id.svelte";
-
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
+
  import InlineTitle from "@app/views/projects/components/InlineTitle.svelte";
  import LabelInput from "@app/views/projects/Cob/LabelInput.svelte";
  import Layout from "@app/views/projects/Layout.svelte";
  import Link from "@app/components/Link.svelte";
@@ -724,10 +724,7 @@
              <span class="txt-missing">No title</span>
            {:else}
              <div class="title">
-
                <InlineMarkdown
-
                  stripEmphasizedStyling
-
                  fontSize="large"
-
                  content={patch.title} />
+
                <InlineTitle fontSize="large" content={patch.title} />
              </div>
            {/if}
          </div>
modified src/views/projects/Patch/PatchTeaser.svelte
@@ -5,7 +5,7 @@
  import { absoluteTimestamp, formatTimestamp } from "@app/lib/utils";

  import IconSmall from "@app/components/IconSmall.svelte";
-
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
+
  import InlineTitle from "@app/views/projects/components/InlineTitle.svelte";
  import Link from "@app/components/Link.svelte";
  import NodeId from "@app/components/NodeId.svelte";

@@ -105,7 +105,7 @@
          node: baseUrl,
          patch: patch.id,
        }}>
-
        <InlineMarkdown fontSize="regular" content={patch.title} />
+
        <InlineTitle fontSize="regular" content={patch.title} />
      </Link>
      {#if patch.labels.length > 0}
        <span
modified src/views/projects/Source/ProjectNameHeader.svelte
@@ -1,13 +1,14 @@
<script lang="ts">
  import type { BaseUrl, Project } from "@http-client";

+
  import dompurify from "dompurify";
+
  import { markdownWithExtensions } from "@app/lib/markdown";
  import { twemoji } from "@app/lib/utils";

  import Badge from "@app/components/Badge.svelte";
  import CloneButton from "@app/views/projects/Header/CloneButton.svelte";
  import IconSmall from "@app/components/IconSmall.svelte";
  import Id from "@app/components/Id.svelte";
-
  import InlineMarkdown from "@app/components/InlineMarkdown.svelte";
  import Link from "@app/components/Link.svelte";
  import SeedButton from "@app/views/projects/Header/SeedButton.svelte";
  import Share from "@app/views/projects/Share.svelte";
@@ -15,6 +16,12 @@
  export let project: Project;
  export let baseUrl: BaseUrl;
  export let seeding: boolean;
+

+
  function render(content: string): string {
+
    return dompurify.sanitize(
+
      markdownWithExtensions.parseInline(content) as string,
+
    );
+
  }
</script>

<style>
@@ -94,5 +101,5 @@
  </div>
</div>
<div class="description" use:twemoji>
-
  <InlineMarkdown fontSize="regular" content={project.description} />
+
  {render(project.description)}
</div>
added src/views/projects/components/InlineTitle.svelte
@@ -0,0 +1,28 @@
+
<script lang="ts">
+
  import dompurify from "dompurify";
+
  import escape from "lodash/escape";
+
  import { formatInlineTitle } from "@app/lib/utils";
+

+
  export let content: string;
+
  export let fontSize: "tiny" | "small" | "regular" | "medium" | "large" =
+
    "small";
+
</script>
+

+
<style>
+
  .content :global(code) {
+
    font-family: var(--font-family-monospace);
+
    background-color: var(--color-fill-ghost);
+
    border-radius: var(--border-radius-tiny);
+
    padding: 0.125rem 0.25rem;
+
  }
+
</style>
+

+
<span
+
  class="content"
+
  class:txt-large={fontSize === "large"}
+
  class:txt-medium={fontSize === "medium"}
+
  class:txt-regular={fontSize === "regular"}
+
  class:txt-small={fontSize === "small"}
+
  class:txt-tiny={fontSize === "tiny"}>
+
  {@html dompurify.sanitize(formatInlineTitle(escape(content)))}
+
</span>
modified tests/e2e/project/issue.spec.ts
@@ -2,7 +2,7 @@ import { test, cobUrl, expect } from "@tests/support/fixtures.js";

test("navigate single issue", async ({ page }) => {
  await page.goto(`${cobUrl}/issues`);
-
  await page.getByText("This title has markdown").click();
+
  await page.getByText("This title has **markdown**").click();

  await expect(page).toHaveURL(/\/issues\/[0-9a-f]{40}/);
});
modified tests/unit/utils.test.ts
@@ -34,6 +34,23 @@ describe("Format functions", () => {

  test.each([
    {
+
      input: "<TR> Hello `new` world",
+
      expected: "<TR> Hello <code>new</code> world",
+
    },
+
    { input: "Hello `new` world", expected: "Hello <code>new</code> world" },
+
    {
+
      input: "Hello `new` world `radicle`",
+
      expected: "Hello <code>new</code> world <code>radicle</code>",
+
    },
+
    { input: "Hello `` world", expected: "Hello `` world" },
+
    { input: "Hello `", expected: "Hello `" },
+
    { input: "Hello", expected: "Hello" },
+
  ])("formatInlineTitle $input => $expected", ({ input, expected }) => {
+
    expect(utils.formatInlineTitle(input)).toEqual(expected);
+
  });
+

+
  test.each([
+
    {
      id: "did:key:z6MkmzRwg47UWQxczLLLFfkEwpBGitjzJ1vKPE8U9ymd6fz6",
      expected: "did:key:z6Mkmz…md6fz6",
    },
modified tsconfig.json
@@ -4,7 +4,7 @@
  "exclude": ["node_modules/*", "radicle-httpd/*"],
  "compilerOptions": {
    "noEmit": true,
-
    "target": "es2020",
+
    "target": "es2021",
    "module": "es2022",
    "types": ["vite/client"],
    "sourceMap": true,