diff --git a/http-client/lib/shared.ts b/http-client/lib/shared.ts index 9c9e36fa8..64b193f69 100644 --- a/http-client/lib/shared.ts +++ b/http-client/lib/shared.ts @@ -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, diff --git a/src/components/Markdown.svelte b/src/components/Markdown.svelte index 1c9d7790e..e9a7f7086 100644 --- a/src/components/Markdown.svelte +++ b/src/components/Markdown.svelte @@ -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, diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index 9f2b9c07f..8457bb1b7 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -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. -> -const anchorMarkedExtension = { - name: "sanitizedAnchor", - level: "block", - start: (src: string) => src.match(//)?.index, - tokenizer(src: string) { - const match = src.match(/^/); - if (match) { - return { - type: "sanitizedAnchor", - raw: match[0], - text: match[1].trim(), - }; - } - }, - renderer: (token: Tokens.Generic): string => ``, -}; - 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 `${token.name}`; +// Converts self closing anchor tags into empty anchor tags, to avoid erratic wrapping behaviour +// e.g. -> +const anchorExtension: MarkedExtension = { + extensions: [ + { + name: "sanitizedAnchor", + level: "block", + start: (src: string) => src.match(//)?.index, + tokenizer(src: string) { + const match = src.match(/^/); + if (match) { + return { + type: "sanitizedAnchor", + raw: match[0], + text: match[1].trim(), + }; + } + }, + renderer: (token: Tokens.Generic): string => + ``, }, - }), - ((): 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 `${token.name}`; + }, +}); + +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] : []), + ], + ); +} diff --git a/src/views/nodes/View.svelte b/src/views/nodes/View.svelte index 4d8516c1a..b69c53b1f 100644 --- a/src/views/nodes/View.svelte +++ b/src/views/nodes/View.svelte @@ -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, + ); + }