Skip to content

Commit 2edd391

Browse files
authored
feat(markdown): add presets (#121)
1 parent 6303981 commit 2edd391

File tree

9 files changed

+205
-1
lines changed

9 files changed

+205
-1
lines changed

markdown/deno.jsonc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@
2424
"./plugins/ruby": "./plugins/ruby.ts",
2525
"./plugins/sanitize": "./plugins/sanitize.ts",
2626
"./plugins/uncomments": "./plugins/uncomments.ts",
27-
"./plugins/wikilinks": "./plugins/wikilinks.ts"
27+
"./plugins/wikilinks": "./plugins/wikilinks.ts",
28+
"./presets/default": "./presets/default.ts",
29+
"./presets/svg": "./presets/svg.ts",
30+
"./presets/text": "./presets/text.ts",
31+
"./presets/web": "./presets/web.ts"
2832
},
2933
"test:permissions": {
3034
"read": true,

markdown/presets/default.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Imports
2+
import { Renderer } from "../renderer.ts"
3+
4+
/** Renderer instance. */
5+
const renderer = new Renderer()
6+
7+
/**
8+
* Renders a markdown expression.
9+
*/
10+
export function markdown(text: string): Promise<string> {
11+
return renderer.render(text)
12+
}

markdown/presets/default_test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) - 2025+ the lowlighter/esquie authors. AGPL-3.0-or-later
2+
import { expect, inspect, test } from "@libs/testing"
3+
import { markdown } from "./default.ts"
4+
5+
for (
6+
const { text, render } of [
7+
{ text: "**foo**", mode: "default", render: "<p><strong>foo</strong></p>" },
8+
]
9+
) {
10+
test(`\`markdown(${inspect(text)})\` returns ${inspect(render)}`, async () => {
11+
const rendered = await markdown(text)
12+
expect(rendered).toBe(render)
13+
})
14+
}

markdown/presets/svg.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Imports
2+
import { Renderer } from "../renderer.ts"
3+
import gfm from "../plugins/gfm.ts"
4+
import { create as sanitize } from "../plugins/sanitize.ts"
5+
import highlighting from "../plugins/highlighting.ts"
6+
7+
/** Renderer instance. */
8+
const renderer = new Renderer({
9+
plugins: [
10+
gfm,
11+
sanitize({
12+
tagNames: [
13+
// Headers
14+
"h1",
15+
"h2",
16+
"h3",
17+
"h4",
18+
"h5",
19+
"h6",
20+
// Text
21+
"p",
22+
"strong",
23+
"em",
24+
"del",
25+
"sup",
26+
"sub",
27+
// Blockquotes
28+
"blockquote",
29+
// Code
30+
"pre",
31+
"code",
32+
"kbd",
33+
// Links
34+
"a",
35+
// Images
36+
"img",
37+
// Tables
38+
"table",
39+
"thead",
40+
"tbody",
41+
"tfoot",
42+
"tr",
43+
"th",
44+
"td",
45+
// Lists
46+
"ul",
47+
"ol",
48+
"li",
49+
"input",
50+
// Horizontal rules
51+
"hr",
52+
// Line breaks
53+
"br",
54+
],
55+
strip: ["script"],
56+
required: {
57+
input: { disabled: true, type: "checkbox" },
58+
},
59+
ancestors: {
60+
tbody: ["table"],
61+
td: ["table"],
62+
th: ["table"],
63+
thead: ["table"],
64+
tfoot: ["table"],
65+
tr: ["table"],
66+
},
67+
attributes: {
68+
a: ["href"],
69+
code: [["className", /^language-./]],
70+
img: ["src", "alt"],
71+
input: [["disabled", true], ["type", "checkbox"], "checked"],
72+
li: [["className", "task-list-item"]],
73+
ol: [["className", "contains-task-list"]],
74+
ul: [["className", "contains-task-list"]],
75+
"*": ["align", "alt", "height", "width", "title", "width"],
76+
},
77+
protocols: {
78+
href: ["http", "https"],
79+
src: ["http", "https", "data"],
80+
},
81+
}),
82+
highlighting,
83+
],
84+
})
85+
86+
/**
87+
* Renders a markdown expression suitable for SVG (sanitized with a subset of tags and attributes).
88+
*/
89+
export function markdown(text: string): Promise<string> {
90+
return renderer.render(text)
91+
}

markdown/presets/svg_test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) - 2025+ the lowlighter/esquie authors. AGPL-3.0-or-later
2+
import { expect, inspect, test } from "@libs/testing"
3+
import { markdown } from "./svg.ts"
4+
5+
for (
6+
const { text, render } of [
7+
{ text: "**foo**", mode: "svg", render: "<p><strong>foo</strong></p>" },
8+
{ text: "```ts\nconst foo = true\n```", mode: "svg", render: /class="hljs language-ts"/ },
9+
{ text: "foo <script>1 + 1</script> bar", mode: "svg", render: "<p>foo bar</p>" },
10+
]
11+
) {
12+
test(`\`markdown(${inspect(text)})\` ${render instanceof RegExp ? "matches" : "returns"} ${inspect(render)}`, async () => {
13+
const rendered = await markdown(text)
14+
if (render instanceof RegExp) {
15+
expect(rendered).toMatch(render)
16+
} else {
17+
expect(rendered).toBe(render)
18+
}
19+
})
20+
}

markdown/presets/text.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) - 2025+ the lowlighter/esquie authors. AGPL-3.0-or-later
2+
import { Renderer } from "../renderer.ts"
3+
import { create as sanitize } from "../plugins/sanitize.ts"
4+
5+
/** Renderer instances. */
6+
const renderer = new Renderer({ plugins: [sanitize({ tagNames: [] })] })
7+
8+
/**
9+
* Renders a markdown expression as plain text (all HTML is stripped).
10+
*/
11+
export function markdown(text: string): Promise<string> {
12+
return renderer.render(text)
13+
}

markdown/presets/text_test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) - 2025+ the lowlighter/esquie authors. AGPL-3.0-or-later
2+
import { expect, inspect, test } from "@libs/testing"
3+
import { markdown } from "./text.ts"
4+
5+
for (
6+
const { text, render } of [
7+
{ text: "**foo**", mode: "text", render: "foo" },
8+
{ text: "```ts\nconst foo = true\n```", mode: "text", render: "const foo = true\n" },
9+
{ text: "foo <script>1 + 1</script> bar", mode: "text", render: "foo bar" },
10+
]
11+
) {
12+
test(`\`markdown(${inspect(text)})\` returns ${inspect(render)}`, async () => {
13+
const rendered = await markdown(text)
14+
expect(rendered).toBe(render)
15+
})
16+
}

markdown/presets/web.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Imports
2+
import { Renderer } from "../renderer.ts"
3+
import gfm from "../plugins/gfm.ts"
4+
import highlighting from "../plugins/highlighting.ts"
5+
6+
/** Renderer instance. */
7+
const renderer = new Renderer({ plugins: [gfm, highlighting] })
8+
9+
/**
10+
* Renders a markdown expression suitable for web pages (including untrusted HTML).
11+
*/
12+
export function markdown(text: string): Promise<string> {
13+
return renderer.render(text)
14+
}

markdown/presets/web_test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) - 2025+ the lowlighter/esquie authors. AGPL-3.0-or-later
2+
import { expect, inspect, test } from "@libs/testing"
3+
import { markdown } from "./web.ts"
4+
5+
for (
6+
const { text, render } of [
7+
{ text: "**foo**", mode: "web", render: "<p><strong>foo</strong></p>" },
8+
{ text: "```ts\nconst foo = true\n```", mode: "web", render: /class="hljs language-ts"/ },
9+
{ text: "foo <script>1 + 1</script> bar", mode: "web", render: "<p>foo <script>1 + 1</script> bar</p>" },
10+
]
11+
) {
12+
test(`\`markdown(${inspect(text)}})\` ${render instanceof RegExp ? "matches" : "returns"} ${inspect(render)}`, async () => {
13+
const rendered = await markdown(text)
14+
if (render instanceof RegExp) {
15+
expect(rendered).toMatch(render)
16+
} else {
17+
expect(rendered).toBe(render)
18+
}
19+
})
20+
}

0 commit comments

Comments
 (0)