mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-18 19:30:27 +08:00
Add markdown support to node descriptions
Instead of escaping all html by svelte, we should pass the node description through the markdown parser and sanitize the parsed content.
This commit is contained in:
committed by
Sebastian Martinez
parent
b21b2bae5e
commit
2075c320f3
@@ -87,8 +87,8 @@ export const configSchema = object({
|
||||
pinned: object({
|
||||
repositories: array(string()),
|
||||
}),
|
||||
imageUrl: string().optional(),
|
||||
name: string().optional(),
|
||||
bannerUrl: string().optional(),
|
||||
avatarUrl: string().optional(),
|
||||
description: string().optional(),
|
||||
}),
|
||||
node: nodeConfigSchema,
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
import { afterUpdate } from "svelte";
|
||||
import { toDom } from "hast-util-to-dom";
|
||||
|
||||
import * as modal from "@app/lib/modal";
|
||||
import * as router from "@app/lib/router";
|
||||
import * as modal from "@app/lib/modal";
|
||||
import ErrorModal from "@app/modals/ErrorModal.svelte";
|
||||
import { Renderer, markdownWithExtensions } from "@app/lib/markdown";
|
||||
import { activeUnloadedRouteStore } from "@app/lib/router";
|
||||
import { highlight } from "@app/lib/syntax";
|
||||
import { mimes } from "@app/lib/file";
|
||||
import {
|
||||
isUrl,
|
||||
twemoji,
|
||||
@@ -19,7 +19,7 @@
|
||||
canonicalize,
|
||||
isCommit,
|
||||
} from "@app/lib/utils";
|
||||
import { mimes } from "@app/lib/file";
|
||||
import { Renderer, markdown } from "@app/lib/markdown";
|
||||
|
||||
export let content: string;
|
||||
export let path: string = "/";
|
||||
@@ -92,7 +92,12 @@
|
||||
|
||||
function render(content: string): string {
|
||||
return dompurify.sanitize(
|
||||
markdownWithExtensions.parse(content, {
|
||||
markdown({
|
||||
katex: true,
|
||||
emojis: true,
|
||||
footnotes: true,
|
||||
linkify: true,
|
||||
}).parse(content, {
|
||||
renderer: new Renderer($activeUnloadedRouteStore),
|
||||
breaks,
|
||||
}) as string,
|
||||
|
||||
+62
-37
@@ -2,15 +2,15 @@ import type { MarkedExtension, Tokens } from "marked";
|
||||
import type { Route } from "@app/lib/router";
|
||||
|
||||
import dompurify from "dompurify";
|
||||
import footnoteMarkedExtension from "marked-footnote";
|
||||
import katexMarkedExtension from "marked-katex-extension";
|
||||
import markedFootnote from "marked-footnote";
|
||||
import markedLinkifyIt from "marked-linkify-it";
|
||||
import linkifyMarkedExtension from "marked-linkify-it";
|
||||
import { Marked, Renderer as BaseRenderer } from "marked";
|
||||
import { markedEmoji } from "marked-emoji";
|
||||
|
||||
import emojis from "@app/lib/emojis";
|
||||
import { routeToPath } from "@app/lib/router";
|
||||
import { canonicalize, isUrl } from "@app/lib/utils";
|
||||
import { routeToPath } from "@app/lib/router";
|
||||
|
||||
dompurify.setConfig({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@@ -19,25 +19,6 @@ dompurify.setConfig({
|
||||
FORBID_TAGS: ["textarea", "style"],
|
||||
});
|
||||
|
||||
// Converts self closing anchor tags into empty anchor tags, to avoid erratic wrapping behaviour
|
||||
// e.g. <a name="test"/> -> <a name="test"></a>
|
||||
const anchorMarkedExtension = {
|
||||
name: "sanitizedAnchor",
|
||||
level: "block",
|
||||
start: (src: string) => src.match(/<a name="([\w]+)"\/>/)?.index,
|
||||
tokenizer(src: string) {
|
||||
const match = src.match(/^<a name="([\w]+)"\/>/);
|
||||
if (match) {
|
||||
return {
|
||||
type: "sanitizedAnchor",
|
||||
raw: match[0],
|
||||
text: match[1].trim(),
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer: (token: Tokens.Generic): string => `<a name="${token.text}"></a>`,
|
||||
};
|
||||
|
||||
export class Renderer extends BaseRenderer {
|
||||
#route: Route;
|
||||
|
||||
@@ -83,20 +64,64 @@ export class Renderer extends BaseRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
export default new Marked();
|
||||
interface MarkedOptions {
|
||||
/** Converts double colon separated strings like `:emoji:` into img tags. */
|
||||
emojis?: boolean;
|
||||
/** Enable footnotes support. */
|
||||
footnotes?: boolean;
|
||||
/** Detect links and convert them into anchor tags. */
|
||||
linkify?: boolean;
|
||||
/** Enable katex support. */
|
||||
katex?: boolean;
|
||||
}
|
||||
|
||||
export const markdownWithExtensions = new Marked(
|
||||
katexMarkedExtension({ throwOnError: false }),
|
||||
markedLinkifyIt({}, { fuzzyLink: false }),
|
||||
markedFootnote({ refMarkers: true }),
|
||||
markedEmoji({
|
||||
emojis,
|
||||
renderer: (token: { name: string; emoji: string }) => {
|
||||
const src = token.emoji.codePointAt(0)?.toString(16);
|
||||
return `<img alt="${token.name}" src="/twemoji/${src}.svg" class="txt-emoji">`;
|
||||
// Converts self closing anchor tags into empty anchor tags, to avoid erratic wrapping behaviour
|
||||
// e.g. <a name="test"/> -> <a name="test"></a>
|
||||
const anchorExtension: MarkedExtension = {
|
||||
extensions: [
|
||||
{
|
||||
name: "sanitizedAnchor",
|
||||
level: "block",
|
||||
start: (src: string) => src.match(/<a name="([\w]+)"\/>/)?.index,
|
||||
tokenizer(src: string) {
|
||||
const match = src.match(/^<a name="([\w]+)"\/>/);
|
||||
if (match) {
|
||||
return {
|
||||
type: "sanitizedAnchor",
|
||||
raw: match[0],
|
||||
text: match[1].trim(),
|
||||
};
|
||||
}
|
||||
},
|
||||
renderer: (token: Tokens.Generic): string =>
|
||||
`<a name="${token.text}"></a>`,
|
||||
},
|
||||
}),
|
||||
((): MarkedExtension => ({
|
||||
extensions: [anchorMarkedExtension],
|
||||
}))(),
|
||||
);
|
||||
],
|
||||
};
|
||||
|
||||
// Converts double colon separated strings like `:emoji:` into img tags.
|
||||
const emojiExtension = markedEmoji({
|
||||
emojis,
|
||||
renderer: (token: { name: string; emoji: string }) => {
|
||||
const src = token.emoji.codePointAt(0)?.toString(16);
|
||||
return `<img alt="${token.name}" src="/twemoji/${src}.svg" class="txt-emoji">`;
|
||||
},
|
||||
});
|
||||
|
||||
const footnoteExtension = footnoteMarkedExtension({ refMarkers: true });
|
||||
const linkifyExtension = linkifyMarkedExtension({}, { fuzzyLink: false });
|
||||
const katexExtension = katexMarkedExtension({ throwOnError: false });
|
||||
|
||||
export function markdown(options: MarkedOptions): Marked {
|
||||
return new Marked(
|
||||
// Default extensions to always include.
|
||||
...[anchorExtension],
|
||||
// Optional extensions to include according to use case.
|
||||
...[
|
||||
...(options.emojis ? [emojiExtension] : []),
|
||||
...(options.footnotes ? [footnoteExtension] : []),
|
||||
...(options.katex ? [katexExtension] : []),
|
||||
...(options.linkify ? [linkifyExtension] : []),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import type { BaseUrl, Node, NodeStats } from "@http-client";
|
||||
|
||||
import * as router from "@app/lib/router";
|
||||
import dompurify from "dompurify";
|
||||
import { markdown } from "@app/lib/markdown";
|
||||
import { baseUrlToString } from "@app/lib/utils";
|
||||
import { fetchRepoInfos } from "@app/components/RepoCard";
|
||||
import { handleError } from "@app/views/nodes/error";
|
||||
@@ -42,6 +44,12 @@
|
||||
$: background = node.bannerUrl
|
||||
? `url("${node.bannerUrl}")`
|
||||
: `url("/images/default-seed-header.png")`;
|
||||
|
||||
function render(content: string): string {
|
||||
return dompurify.sanitize(
|
||||
markdown({ linkify: true, emojis: true }).parse(content) as string,
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -274,7 +282,7 @@
|
||||
</div>
|
||||
{#if node.description}
|
||||
<div class="description txt-small">
|
||||
{node.description}
|
||||
{@html render(node.description)}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
@@ -398,7 +406,7 @@
|
||||
style:gap="0.25rem">
|
||||
{#if node.description}
|
||||
<div class="description txt-small">
|
||||
{node.description}
|
||||
{@html render(node.description)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { BaseUrl, Repo } from "@http-client";
|
||||
|
||||
import dompurify from "dompurify";
|
||||
import { markdownWithExtensions } from "@app/lib/markdown";
|
||||
import { markdown } from "@app/lib/markdown";
|
||||
import { twemoji } from "@app/lib/utils";
|
||||
|
||||
import Badge from "@app/components/Badge.svelte";
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
function render(content: string): string {
|
||||
return dompurify.sanitize(
|
||||
markdownWithExtensions.parseInline(content) as string,
|
||||
markdown({ linkify: true, emojis: true }).parseInline(content) as string,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,9 @@ export default async function globalSetup(): Promise<() => void> {
|
||||
pinned: {
|
||||
repositories: ["rad:z4BwwjPCFNVP27FwVbDFgwVwkjcir"],
|
||||
},
|
||||
description: `:seedling: Radicle is an open source, peer-to-peer code collaboration stack built on Git.
|
||||
|
||||
:construction: [radicle.xyz](https://radicle.xyz)`,
|
||||
},
|
||||
node: {
|
||||
...defaultConfig.node,
|
||||
|
||||
Reference in New Issue
Block a user