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 ``;
+// 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 ``;
+ },
+});
+
+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,
+ );
+ }