@livemark/markdown 0.0.0-stage → 0.44.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +9 -0
- package/README.md +5 -2
- package/build/actions/markdown/compile.d.ts +27 -0
- package/build/actions/markdown/compile.js +48 -0
- package/build/actions/processor/create.d.ts +17 -0
- package/build/actions/processor/create.js +212 -0
- package/build/components/Abbr.d.ts +5 -0
- package/build/components/Abbr.js +7 -0
- package/build/components/AnsiCode.d.ts +4 -0
- package/build/components/AnsiCode.js +7 -0
- package/build/components/Audio.d.ts +4 -0
- package/build/components/Audio.js +9 -0
- package/build/components/Badge.d.ts +5 -0
- package/build/components/Badge.js +7 -0
- package/build/components/Button.d.ts +7 -0
- package/build/components/Button.js +11 -0
- package/build/components/Callout.d.ts +8 -0
- package/build/components/Callout.js +41 -0
- package/build/components/Card.d.ts +7 -0
- package/build/components/Card.js +16 -0
- package/build/components/Cards.d.ts +4 -0
- package/build/components/Cards.js +6 -0
- package/build/components/CodeBlock.d.ts +1 -0
- package/build/components/CodeBlock.js +50 -0
- package/build/components/CodeTabs.d.ts +6 -0
- package/build/components/CodeTabs.js +48 -0
- package/build/components/Column.d.ts +4 -0
- package/build/components/Column.js +6 -0
- package/build/components/Columns.d.ts +5 -0
- package/build/components/Columns.js +8 -0
- package/build/components/DefinitionList.d.ts +7 -0
- package/build/components/DefinitionList.js +14 -0
- package/build/components/Details.d.ts +5 -0
- package/build/components/Details.js +10 -0
- package/build/components/Filetree.d.ts +4 -0
- package/build/components/Filetree.js +22 -0
- package/build/components/FootnoteRef.d.ts +4 -0
- package/build/components/FootnoteRef.js +33 -0
- package/build/components/Heading.d.ts +8 -0
- package/build/components/Heading.js +18 -0
- package/build/components/Icon.d.ts +5 -0
- package/build/components/Icon.js +10 -0
- package/build/components/Markdown.d.ts +13 -0
- package/build/components/Markdown.js +75 -0
- package/build/components/Mermaid.d.ts +4 -0
- package/build/components/Mermaid.js +10 -0
- package/build/components/MermaidRenderer.d.ts +4 -0
- package/build/components/MermaidRenderer.js +44 -0
- package/build/components/PackageTabs.d.ts +9 -0
- package/build/components/PackageTabs.js +26 -0
- package/build/components/SortableTable.d.ts +5 -0
- package/build/components/SortableTable.js +63 -0
- package/build/components/Tab.d.ts +5 -0
- package/build/components/Tab.js +6 -0
- package/build/components/Tabs.d.ts +5 -0
- package/build/components/Tabs.js +46 -0
- package/build/components/Toc.d.ts +4 -0
- package/build/components/Toc.js +14 -0
- package/build/components/Video.d.ts +4 -0
- package/build/components/Video.js +13 -0
- package/build/components/ZoomImage.d.ts +4 -0
- package/build/components/ZoomImage.js +26 -0
- package/build/elements/badge.d.ts +7 -0
- package/build/elements/badge.js +36 -0
- package/build/elements/button.d.ts +8 -0
- package/build/elements/button.js +37 -0
- package/build/elements/card.d.ts +11 -0
- package/build/elements/card.js +25 -0
- package/build/elements/collapsible.d.ts +5 -0
- package/build/elements/collapsible.js +13 -0
- package/build/elements/hover-card.d.ts +5 -0
- package/build/elements/hover-card.js +14 -0
- package/build/elements/separator.d.ts +3 -0
- package/build/elements/separator.js +8 -0
- package/build/elements/table.d.ts +9 -0
- package/build/elements/table.js +28 -0
- package/build/elements/tooltip.d.ts +6 -0
- package/build/elements/tooltip.js +17 -0
- package/build/helpers/ansi.d.ts +3 -0
- package/build/helpers/ansi.js +208 -0
- package/build/helpers/article-link.d.ts +7 -0
- package/build/helpers/article-link.js +8 -0
- package/build/helpers/code-icon.d.ts +2 -0
- package/build/helpers/code-icon.js +57 -0
- package/build/helpers/content-warnings.d.ts +8 -0
- package/build/helpers/content-warnings.js +47 -0
- package/build/helpers/dynamic-icon-resolver.d.ts +5 -0
- package/build/helpers/dynamic-icon-resolver.js +11 -0
- package/build/helpers/dynamic-icon.d.ts +5 -0
- package/build/helpers/dynamic-icon.js +14 -0
- package/build/helpers/frontmatter.d.ts +5 -0
- package/build/helpers/frontmatter.js +16 -0
- package/build/helpers/icon-names.d.ts +11 -0
- package/build/helpers/icon-names.js +35 -0
- package/build/helpers/message-format.d.ts +2 -0
- package/build/helpers/message-format.js +7 -0
- package/build/helpers/node-text.d.ts +9 -0
- package/build/helpers/node-text.js +9 -0
- package/build/helpers/site-messages.d.ts +86 -0
- package/build/helpers/site-messages.js +7 -0
- package/build/helpers/toc-context.d.ts +5 -0
- package/build/helpers/toc-context.js +8 -0
- package/build/helpers/toc-outline.d.ts +3 -0
- package/build/helpers/toc-outline.js +16 -0
- package/build/helpers/youtube-id.d.ts +2 -0
- package/build/helpers/youtube-id.js +25 -0
- package/build/hooks/icons.d.ts +13 -0
- package/build/hooks/icons.js +12 -0
- package/build/hooks/messages.d.ts +95 -0
- package/build/hooks/messages.js +17 -0
- package/build/models/message.d.ts +177 -0
- package/build/models/message.js +88 -0
- package/build/models/plugins.d.ts +10 -0
- package/build/models/plugins.js +6 -0
- package/build/models/theme.d.ts +76 -0
- package/build/models/theme.js +71 -0
- package/build/models/toc.d.ts +5 -0
- package/build/models/toc.js +2 -0
- package/build/plugins/twoslash.d.ts +13 -0
- package/build/plugins/twoslash.js +146 -0
- package/package.json +63 -3
- package/source/actions/markdown/compile.ts +82 -0
- package/source/actions/processor/create.ts +235 -0
- package/source/components/Abbr.tsx +13 -0
- package/source/components/AnsiCode.tsx +17 -0
- package/source/components/Audio.tsx +18 -0
- package/source/components/Badge.tsx +9 -0
- package/source/components/Button.tsx +33 -0
- package/source/components/Callout.tsx +68 -0
- package/source/components/Card.tsx +48 -0
- package/source/components/Cards.tsx +8 -0
- package/source/components/CodeBlock.tsx +144 -0
- package/source/components/CodeTabs.tsx +81 -0
- package/source/components/Column.tsx +4 -0
- package/source/components/Columns.tsx +10 -0
- package/source/components/DefinitionList.tsx +18 -0
- package/source/components/Details.tsx +30 -0
- package/source/components/Filetree.tsx +63 -0
- package/source/components/FootnoteRef.tsx +52 -0
- package/source/components/Heading.tsx +20 -0
- package/source/components/Icon.tsx +10 -0
- package/source/components/Markdown.tsx +89 -0
- package/source/components/Mermaid.tsx +20 -0
- package/source/components/MermaidRenderer.tsx +48 -0
- package/source/components/PackageTabs.tsx +72 -0
- package/source/components/SortableTable.tsx +81 -0
- package/source/components/Tab.tsx +4 -0
- package/source/components/Tabs.tsx +75 -0
- package/source/components/Toc.tsx +30 -0
- package/source/components/Video.tsx +28 -0
- package/source/components/ZoomImage.tsx +25 -0
- package/source/elements/badge.tsx +53 -0
- package/source/elements/button.tsx +61 -0
- package/source/elements/card.tsx +102 -0
- package/source/elements/collapsible.tsx +15 -0
- package/source/elements/hover-card.tsx +47 -0
- package/source/elements/separator.tsx +22 -0
- package/source/elements/table.tsx +103 -0
- package/source/elements/tooltip.tsx +60 -0
- package/source/helpers/ansi.ts +210 -0
- package/source/helpers/article-link.ts +11 -0
- package/source/helpers/code-icon.ts +59 -0
- package/source/helpers/content-warnings.ts +56 -0
- package/source/helpers/dynamic-icon-resolver.tsx +9 -0
- package/source/helpers/dynamic-icon.tsx +19 -0
- package/source/helpers/frontmatter.ts +16 -0
- package/source/helpers/icon-names.ts +37 -0
- package/source/helpers/message-format.ts +5 -0
- package/source/helpers/node-text.ts +14 -0
- package/source/helpers/site-messages.ts +8 -0
- package/source/helpers/toc-context.ts +10 -0
- package/source/helpers/toc-outline.ts +23 -0
- package/source/helpers/youtube-id.ts +23 -0
- package/source/hooks/icons.ts +18 -0
- package/source/hooks/messages.ts +28 -0
- package/source/models/message.ts +96 -0
- package/source/models/plugins.ts +11 -0
- package/source/models/theme.ts +78 -0
- package/source/models/toc.ts +5 -0
- package/source/plugins/twoslash.ts +163 -0
- package/source/standalone.css +7 -0
- package/source/styles/general.css +214 -0
- package/source/styles/markdown.css +231 -0
- package/source/styles/shape.css +14 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
interface AnsiState {
|
|
2
|
+
bold: boolean
|
|
3
|
+
dim: boolean
|
|
4
|
+
italic: boolean
|
|
5
|
+
underline: boolean
|
|
6
|
+
strikethrough: boolean
|
|
7
|
+
fg: string | undefined
|
|
8
|
+
bg: string | undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const STANDARD_COLORS: ReadonlyArray<string> = [
|
|
12
|
+
"#4f5b66", // 0 black
|
|
13
|
+
"#bf616a", // 1 red
|
|
14
|
+
"#a3be8c", // 2 green
|
|
15
|
+
"#ebcb8b", // 3 yellow
|
|
16
|
+
"#8fa1b3", // 4 blue
|
|
17
|
+
"#b48ead", // 5 magenta
|
|
18
|
+
"#96b5b4", // 6 cyan
|
|
19
|
+
"#c0c5ce", // 7 white
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const BRIGHT_COLORS: ReadonlyArray<string> = [
|
|
23
|
+
"#65737e", // 8 bright black
|
|
24
|
+
"#bf616a", // 9 bright red
|
|
25
|
+
"#a3be8c", // 10 bright green
|
|
26
|
+
"#ebcb8b", // 11 bright yellow
|
|
27
|
+
"#8fa1b3", // 12 bright blue
|
|
28
|
+
"#b48ead", // 13 bright magenta
|
|
29
|
+
"#96b5b4", // 14 bright cyan
|
|
30
|
+
"#eff1f5", // 15 bright white
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
function color256(n: number): string | undefined {
|
|
34
|
+
if (n < 8) return STANDARD_COLORS[n]
|
|
35
|
+
if (n < 16) return BRIGHT_COLORS[n - 8]
|
|
36
|
+
if (n < 232) {
|
|
37
|
+
const idx = n - 16
|
|
38
|
+
const r = Math.floor(idx / 36)
|
|
39
|
+
const g = Math.floor((idx % 36) / 6)
|
|
40
|
+
const b = idx % 6
|
|
41
|
+
const toHex = (v: number) => (v === 0 ? 0 : 55 + v * 40).toString(16).padStart(2, "0")
|
|
42
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
|
43
|
+
}
|
|
44
|
+
if (n < 256) {
|
|
45
|
+
const v = (8 + (n - 232) * 10).toString(16).padStart(2, "0")
|
|
46
|
+
return `#${v}${v}${v}`
|
|
47
|
+
}
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function escapeHtml(text: string): string {
|
|
52
|
+
return text
|
|
53
|
+
.replace(/&/g, "&")
|
|
54
|
+
.replace(/</g, "<")
|
|
55
|
+
.replace(/>/g, ">")
|
|
56
|
+
.replace(/"/g, """)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildStyle(state: AnsiState): string {
|
|
60
|
+
const parts: string[] = []
|
|
61
|
+
if (state.fg) parts.push(`color:${state.fg}`)
|
|
62
|
+
if (state.bg) parts.push(`background-color:${state.bg}`)
|
|
63
|
+
if (state.bold) parts.push("font-weight:bold")
|
|
64
|
+
if (state.dim) parts.push("opacity:0.7")
|
|
65
|
+
if (state.italic) parts.push("font-style:italic")
|
|
66
|
+
if (state.underline) parts.push("text-decoration:underline")
|
|
67
|
+
if (state.strikethrough) parts.push("text-decoration:line-through")
|
|
68
|
+
return parts.join(";")
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function makeDefaultState(): AnsiState {
|
|
72
|
+
return {
|
|
73
|
+
bold: false,
|
|
74
|
+
dim: false,
|
|
75
|
+
italic: false,
|
|
76
|
+
underline: false,
|
|
77
|
+
strikethrough: false,
|
|
78
|
+
fg: undefined,
|
|
79
|
+
bg: undefined,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function applyParams(state: AnsiState, params: ReadonlyArray<number>) {
|
|
84
|
+
let i = 0
|
|
85
|
+
while (i < params.length) {
|
|
86
|
+
const code = params[i]!
|
|
87
|
+
switch (code) {
|
|
88
|
+
case 0: {
|
|
89
|
+
const reset = makeDefaultState()
|
|
90
|
+
state.bold = reset.bold
|
|
91
|
+
state.dim = reset.dim
|
|
92
|
+
state.italic = reset.italic
|
|
93
|
+
state.underline = reset.underline
|
|
94
|
+
state.strikethrough = reset.strikethrough
|
|
95
|
+
state.fg = reset.fg
|
|
96
|
+
state.bg = reset.bg
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
case 1:
|
|
100
|
+
state.bold = true
|
|
101
|
+
break
|
|
102
|
+
case 2:
|
|
103
|
+
state.dim = true
|
|
104
|
+
break
|
|
105
|
+
case 3:
|
|
106
|
+
state.italic = true
|
|
107
|
+
break
|
|
108
|
+
case 4:
|
|
109
|
+
state.underline = true
|
|
110
|
+
break
|
|
111
|
+
case 9:
|
|
112
|
+
state.strikethrough = true
|
|
113
|
+
break
|
|
114
|
+
case 22:
|
|
115
|
+
state.bold = false
|
|
116
|
+
state.dim = false
|
|
117
|
+
break
|
|
118
|
+
case 23:
|
|
119
|
+
state.italic = false
|
|
120
|
+
break
|
|
121
|
+
case 24:
|
|
122
|
+
state.underline = false
|
|
123
|
+
break
|
|
124
|
+
case 29:
|
|
125
|
+
state.strikethrough = false
|
|
126
|
+
break
|
|
127
|
+
case 39:
|
|
128
|
+
state.fg = undefined
|
|
129
|
+
break
|
|
130
|
+
case 49:
|
|
131
|
+
state.bg = undefined
|
|
132
|
+
break
|
|
133
|
+
default:
|
|
134
|
+
if (code >= 30 && code <= 37) {
|
|
135
|
+
state.fg = STANDARD_COLORS[code - 30]
|
|
136
|
+
} else if (code >= 40 && code <= 47) {
|
|
137
|
+
state.bg = STANDARD_COLORS[code - 40]
|
|
138
|
+
} else if (code >= 90 && code <= 97) {
|
|
139
|
+
state.fg = BRIGHT_COLORS[code - 90]
|
|
140
|
+
} else if (code >= 100 && code <= 107) {
|
|
141
|
+
state.bg = BRIGHT_COLORS[code - 100]
|
|
142
|
+
} else if (code === 38 || code === 48) {
|
|
143
|
+
const kind = params[i + 1]
|
|
144
|
+
if (kind === 5) {
|
|
145
|
+
const colorIdx = params[i + 2]
|
|
146
|
+
if (colorIdx !== undefined) {
|
|
147
|
+
const c = color256(colorIdx)
|
|
148
|
+
if (code === 38) state.fg = c
|
|
149
|
+
else state.bg = c
|
|
150
|
+
}
|
|
151
|
+
i += 2
|
|
152
|
+
} else if (kind === 2) {
|
|
153
|
+
const r = params[i + 2] ?? 0
|
|
154
|
+
const g = params[i + 3] ?? 0
|
|
155
|
+
const b = params[i + 4] ?? 0
|
|
156
|
+
const toHex = (v: number) => v.toString(16).padStart(2, "0")
|
|
157
|
+
const c = `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
|
158
|
+
if (code === 38) state.fg = c
|
|
159
|
+
else state.bg = c
|
|
160
|
+
i += 4
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
break
|
|
164
|
+
}
|
|
165
|
+
i++
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Convert a string containing ANSI escape sequences into HTML with inline styles */
|
|
170
|
+
export function ansiToHtml(input: string): string {
|
|
171
|
+
// oxlint-disable-next-line no-control-regex -- ANSI escape sequences start with U+001B
|
|
172
|
+
const sgr = /\u001b\[([0-9;]*)m/g
|
|
173
|
+
const state = makeDefaultState()
|
|
174
|
+
const parts: string[] = []
|
|
175
|
+
let lastIndex = 0
|
|
176
|
+
|
|
177
|
+
let match = sgr.exec(input)
|
|
178
|
+
while (match !== null) {
|
|
179
|
+
const textBefore = input.slice(lastIndex, match.index)
|
|
180
|
+
if (textBefore) {
|
|
181
|
+
const style = buildStyle(state)
|
|
182
|
+
if (style) {
|
|
183
|
+
parts.push(`<span style="${style}">${escapeHtml(textBefore)}</span>`)
|
|
184
|
+
} else {
|
|
185
|
+
parts.push(escapeHtml(textBefore))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const rawParams = match[1]!
|
|
190
|
+
const params =
|
|
191
|
+
rawParams === "" ? [0] : rawParams.split(";").map(s => Number.parseInt(s, 10))
|
|
192
|
+
applyParams(state, params)
|
|
193
|
+
lastIndex = match.index + match[0].length
|
|
194
|
+
match = sgr.exec(input)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const trailing = input.slice(lastIndex)
|
|
198
|
+
if (trailing) {
|
|
199
|
+
const style = buildStyle(state)
|
|
200
|
+
if (style) {
|
|
201
|
+
parts.push(`<span style="${style}">${escapeHtml(trailing)}</span>`)
|
|
202
|
+
} else {
|
|
203
|
+
parts.push(escapeHtml(trailing))
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return parts.join("")
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Walk a mdast tree and convert ansi code blocks into AnsiCode JSX elements */
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createContext } from "react"
|
|
2
|
+
|
|
3
|
+
/** The article whose body is rendering, for links relative to its file */
|
|
4
|
+
export const ArticleSourceContext = createContext<
|
|
5
|
+
{ file: string; language: string } | undefined
|
|
6
|
+
>(undefined)
|
|
7
|
+
|
|
8
|
+
/** An href as rendered outside a Livemark site, where no articles resolve: unchanged. */
|
|
9
|
+
export function useArticleHref(href: string | undefined) {
|
|
10
|
+
return href
|
|
11
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
type Icon = { viewBox: string; fill: string; d: string }
|
|
2
|
+
|
|
3
|
+
const shortcuts: Record<string, string> = {
|
|
4
|
+
js: "javascript",
|
|
5
|
+
jsx: "react",
|
|
6
|
+
ts: "typescript",
|
|
7
|
+
tsx: "react",
|
|
8
|
+
py: "python",
|
|
9
|
+
bash: "shellscript",
|
|
10
|
+
sh: "shellscript",
|
|
11
|
+
shell: "shellscript",
|
|
12
|
+
zsh: "shellscript",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const icons: Record<string, Icon> = {
|
|
16
|
+
typescript: {
|
|
17
|
+
viewBox: "0 0 24 24",
|
|
18
|
+
fill: "currentColor",
|
|
19
|
+
d: "M1.125 0C.502 0 0 .502 0 1.125v21.75C0 23.498.502 24 1.125 24h21.75c.623 0 1.125-.502 1.125-1.125V1.125C24 .502 23.498 0 22.875 0zm17.363 9.75c.612 0 1.154.037 1.627.111a6.38 6.38 0 0 1 1.306.34v2.458a3.95 3.95 0 0 0-.643-.361 5.093 5.093 0 0 0-.717-.26 5.453 5.453 0 0 0-1.426-.2c-.3 0-.573.028-.819.086a2.1 2.1 0 0 0-.623.242c-.17.104-.3.229-.393.374a.888.888 0 0 0-.14.49c0 .196.053.373.156.529.104.156.252.304.443.444s.423.276.696.41c.273.135.582.274.926.416.47.197.892.407 1.266.628.374.222.695.473.963.753.268.279.472.598.614.957.142.359.214.776.214 1.253 0 .657-.125 1.21-.373 1.656a3.033 3.033 0 0 1-1.012 1.085 4.38 4.38 0 0 1-1.487.596c-.566.12-1.163.18-1.79.18a9.916 9.916 0 0 1-1.84-.164 5.544 5.544 0 0 1-1.512-.493v-2.63a5.033 5.033 0 0 0 3.237 1.2c.333 0 .624-.03.872-.09.249-.06.456-.144.623-.25.166-.108.29-.234.373-.38a1.023 1.023 0 0 0-.074-1.089 2.12 2.12 0 0 0-.537-.5 5.597 5.597 0 0 0-.807-.444 27.72 27.72 0 0 0-1.007-.436c-.918-.383-1.602-.852-2.053-1.405-.45-.553-.676-1.222-.676-2.005 0-.614.123-1.141.369-1.582.246-.441.58-.804 1.004-1.089a4.494 4.494 0 0 1 1.47-.629 7.536 7.536 0 0 1 1.77-.201zm-15.113.188h9.563v2.166H9.506v9.646H6.789v-9.646H3.375z",
|
|
20
|
+
},
|
|
21
|
+
javascript: {
|
|
22
|
+
viewBox: "0 0 24 24",
|
|
23
|
+
fill: "currentColor",
|
|
24
|
+
d: "M0 0h24v24H0V0zm22.034 18.276c-.175-1.095-.888-2.015-3.003-2.873-.736-.345-1.554-.585-1.797-1.14-.091-.33-.105-.51-.046-.705.15-.646.915-.84 1.515-.66.39.12.75.42.976.9 1.034-.676 1.034-.676 1.755-1.125-.27-.42-.405-.6-.586-.78-.63-.705-1.469-1.065-2.834-1.034l-.705.089c-.676.165-1.32.525-1.71 1.005-1.14 1.291-.811 3.541.569 4.471 1.365 1.02 3.361 1.244 3.616 2.205.24 1.17-.87 1.545-1.966 1.41-.811-.18-1.26-.586-1.755-1.336l-1.83 1.051c.21.48.45.689.81 1.109 1.74 1.756 6.09 1.666 6.871-1.004.029-.09.24-.705.074-1.65l.046.067zm-8.983-7.245h-2.248c0 1.938-.009 3.864-.009 5.805 0 1.232.063 2.363-.138 2.711-.33.689-1.18.601-1.566.48-.396-.196-.597-.466-.83-.855-.063-.105-.11-.196-.127-.196l-1.825 1.125c.305.63.75 1.172 1.324 1.517.855.51 2.004.675 3.207.405.783-.226 1.458-.691 1.811-1.411.51-.93.402-2.07.397-3.346.012-2.054 0-4.109 0-6.179l.004-.056z",
|
|
25
|
+
},
|
|
26
|
+
react: {
|
|
27
|
+
viewBox: "0 0 24 24",
|
|
28
|
+
fill: "currentColor",
|
|
29
|
+
d: "M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z",
|
|
30
|
+
},
|
|
31
|
+
python: {
|
|
32
|
+
viewBox: "0 0 24 24",
|
|
33
|
+
fill: "currentColor",
|
|
34
|
+
d: "M14.25.18l.9.2.73.26.59.3.45.32.34.34.25.34.16.33.1.3.04.26.02.2-.01.13V8.5l-.05.63-.13.55-.21.46-.26.38-.3.31-.33.25-.35.19-.35.14-.33.1-.3.07-.26.04-.21.02H8.77l-.69.05-.59.14-.5.22-.41.27-.33.32-.27.35-.2.36-.15.37-.1.35-.07.32-.04.27-.02.21v3.06H3.17l-.21-.03-.28-.07-.32-.12-.35-.18-.36-.26-.36-.36-.35-.46-.32-.59-.28-.73-.21-.88-.14-1.05-.05-1.23.06-1.22.16-1.04.24-.87.32-.71.36-.57.4-.44.42-.33.42-.24.4-.16.36-.1.32-.05.24-.01h.16l.06.01H8.16v-.83H6.18l-.01-2.75-.02-.37.05-.34.11-.31.17-.28.25-.26.31-.23.38-.2.44-.18.51-.15.58-.12.64-.1.71-.06.77-.04.84-.02 1.27.05zm-6.3 1.98l-.23.33-.08.41.08.41.23.34.33.22.41.09.41-.09.33-.22.23-.34.08-.41-.08-.41-.23-.33-.33-.22-.41-.09-.41.09zm13.09 3.95l.28.06.32.12.35.18.36.27.36.35.35.47.32.59.28.73.21.88.14 1.04.05 1.23-.06 1.23-.16 1.04-.24.86-.32.71-.36.57-.4.45-.42.33-.42.24-.4.16-.36.09-.32.05-.24.02-.16-.01h-8.22v.82h5.84l.01 2.76.02.36-.05.34-.11.31-.17.29-.25.25-.31.24-.38.2-.44.17-.51.15-.58.13-.64.09-.71.07-.77.04-.84.01-1.27-.04-1.07-.14-.9-.2-.73-.25-.59-.3-.45-.33-.34-.34-.25-.34-.16-.33-.1-.3-.04-.25-.02-.2.01-.13v-5.34l.05-.64.13-.54.21-.46.26-.38.3-.32.33-.24.35-.2.35-.14.33-.1.3-.06.26-.04.21-.02.13-.01h5.84l.69-.05.59-.14.5-.21.41-.28.33-.32.27-.35.2-.36.15-.36.1-.35.07-.32.04-.28.02-.21V6.07h2.09l.14.01zm-6.47 14.25l-.23.33-.08.41.08.41.23.33.33.23.41.08.41-.08.33-.23.23-.33.08-.41-.08-.41-.23-.33-.33-.23-.41-.08-.41.08z",
|
|
35
|
+
},
|
|
36
|
+
rust: {
|
|
37
|
+
viewBox: "0 0 24 24",
|
|
38
|
+
fill: "currentColor",
|
|
39
|
+
d: "M23.8346 11.7033l-1.0073-.6236a13.7268 13.7268 0 00-.0283-.2936l.8656-.8069a.3483.3483 0 00-.1154-.578l-1.1066-.414a8.4958 8.4958 0 00-.087-.2856l.6904-.9587a.3462.3462 0 00-.2257-.5446l-1.1663-.1894a9.3574 9.3574 0 00-.1407-.2622l.4895-1.0761a.3438.3438 0 00-.3300-.4908l-1.1845.0416a6.7444 6.7444 0 00-.1873-.2268l.2723-1.153a.3472.3472 0 00-.4233-.4164l-1.1592.2683a14.1794 14.1794 0 00-.2231-.1817l.0467-1.1872a.3462.3462 0 00-.5765-.322l-1.0907.4878c-.0799-.0499-.1613-.0983-.2449-.1456l-.1787-1.1758a.3456.3456 0 00-.5766-.2329l-.9813.6919a13.596 13.596 0 00-.2571-.1049l-.3982-1.1175a.348.348 0 00-.5815-.1272l-.8352.8738a10.2102 10.2102 0 00-.2606-.0584l-.6082-1.0131a.3459.3459 0 00-.5659-.0197l-.6602 1.0257a6.3249 6.3249 0 00-.2574.0068L8.3862.8721a.3437.3437 0 00-.5368.0921l-.4676 1.1376a10.3014 10.3014 0 00-.2444.0856L6.2788 1.394a.3447.3447 0 00-.4916.2004l-.2615 1.2048a11.5632 11.5632 0 00-.2242.149l-.9215-.5549a.3445.3445 0 00-.4345.2945l-.0488 1.2261a11.0239 11.0239 0 00-.1989.2028l-1.0498-.2744a.3441.3441 0 00-.3684.3752l.1643 1.2026c-.0553.075-.1084.1515-.1598.2293l-1.1374.0132a.3427.3427 0 00-.2938.4422l.3698 1.1347a10.4633 10.4633 0 00-.1162.2474l-1.1817.2923a.3433.3433 0 00-.2134.4944l.5627 1.0262c-.0265.085-.0502.1713-.0713.258l-1.1812.5586a.343.343 0 00-.1299.5316l.7396.8793a13.0102 13.0102 0 00-.0236.264l-1.1355.8086a.3445.3445 0 00-.0429.5513l.8937.6986a9.0776 9.0776 0 00.0235.2643L.3108 14.515a.3455.3455 0 00.0432.5508l1.0185.4941a14.4526 14.4526 0 00.0694.2602l-.6564.9682a.3443.3443 0 00.1273.5483l1.1066.2713c.0437.0833.0891.1653.136.2462l-.4348 1.0848a.3445.3445 0 00.2069.5304l1.1564.0436c.0583.0749.1182.1485.1797.2205l-.2004 1.1603a.348.348 0 00.2824.4902l1.1669-.1838c.0694.064.1404.1265.2127.1874l.0387 1.1912a.3476.3476 0 00.3498.4306l1.1388-.4038a7.4207 7.4207 0 00.2381.1482l.2723 1.1772a.3459.3459 0 00.4074.3543l1.0738-.6104c.084.0393.169.077.2553.113l.4964 1.1178a.3476.3476 0 00.4529.2643l.9744-.7985c.0873.0285.1757.0553.2649.0803l.707 1.015a.3475.3475 0 00.4827.165l.8435-.9633a8.7717 8.7717 0 00.267.0418l.8978.8715a.347.347 0 00.4959.0582l.6863-1.1004c.0877-.0039.1754-.0094.2631-.0166l1.0645.6907a.3478.3478 0 00.4921-.0466l.5043-1.2024c.0829-.0168.1657-.0349.2476-.0546l1.2024.478a.3444.3444 0 00.4721-.1462l.305-1.2685a9.4986 9.4986 0 00.2264-.0963l1.3088.241a.3462.3462 0 00.4373-.2383l.0931-1.2948a12.775 12.775 0 00.1996-.1384l1.3811-.0133a.3442.3442 0 00.3879-.3209l-.1204-1.2792a12.0697 12.0697 0 00.1677-.1767l1.4174-.2612a.3462.3462 0 00.3266-.3935l-.3286-1.222a13.5765 13.5765 0 00.1307-.2093l1.4154-.4985a.3454.3454 0 00.2558-.4536l-.5275-1.1259a8.7256 8.7256 0 00.0899-.2341l1.3741-.7219a.3439.3439 0 00.1787-.4993l-.7113-.9937a9.1373 9.1373 0 00.0449-.2543l1.2944-.9275a.3446.3446 0 00.0963-.5323zM12.0522 4.5705a.703.703 0 01.5765-.098c.3473.0982.6615.2679.9263.4973a.3873.3873 0 01-.5765.098c-.3473-.0982-.6615-.2679-.9263-.4973zm-3.2 7.5508c.2.0 .4-.0 .6-.0l.2 .6h-1l.2-.6zm4.5 2.7l-.4-.3.3-.4c-.3-.2-.7-.3-1.1-.3l-.1.5-.1-.5c-.4.0-.8.1-1.1.3l.3.4-.4.3c-.3-.3-.5-.6-.6-1l.5-.1-.5-.2c0-.4.1-.8.3-1.2l.4.3-.3-.4c.3-.3.6-.5 1-.6l.1.5.1-.5c.4.0.8.2 1.1.4l-.3.4.4-.3c.2.3.3.7.4 1.1l-.5.1.5.2c-.0.4-.2.8-.4 1.1zm3.3-2.1l-.6.0-.2-.6h1l-.2.6z",
|
|
40
|
+
},
|
|
41
|
+
shellscript: {
|
|
42
|
+
viewBox: "0 0 24 24",
|
|
43
|
+
fill: "currentColor",
|
|
44
|
+
d: "M4.814 0h14.371a2.813 2.813 0 0 1 2.814 2.814v18.371A2.813 2.813 0 0 1 19.186 24H4.814A2.813 2.813 0 0 1 2 21.186V2.814A2.813 2.813 0 0 1 4.814 0zM5.8 21.2h12.39v-1.6H5.8zm6.636-4.762L7.394 12.17l1.07-1.258 5.043 4.268zm-1.111-5.889 1.07-1.258 5.043 4.268-5.043 4.268-1.07-1.258 3.972-3.01z",
|
|
45
|
+
},
|
|
46
|
+
default: {
|
|
47
|
+
viewBox: "0 0 24 24",
|
|
48
|
+
fill: "currentColor",
|
|
49
|
+
d: "M 6,1 C 4.354992,1 3,2.354992 3,4 v 16 c 0,1.645008 1.354992,3 3,3 h 12 c 1.645008,0 3,-1.354992 3,-3 V 8 7 A 1.0001,1.0001 0 0 0 20.707031,6.2929687 l -5,-5 A 1.0001,1.0001 0 0 0 15,1 h -1 z m 0,2 h 7 v 3 c 0,1.645008 1.354992,3 3,3 h 3 v 11 c 0,0.564129 -0.435871,1 -1,1 H 6 C 5.4358712,21 5,20.564129 5,20 V 4 C 5,3.4358712 5.4358712,3 6,3 Z M 15,3.4140625 18.585937,7 H 16 C 15.435871,7 15,6.5641288 15,6 Z",
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The SVG icon of a code block's language, a file icon for one without its own */
|
|
54
|
+
export function readCodeIcon(lang: string | undefined) {
|
|
55
|
+
const name = lang && lang in shortcuts ? shortcuts[lang] : lang
|
|
56
|
+
const icon = (name && icons[name]) || icons.default
|
|
57
|
+
if (!icon) return undefined
|
|
58
|
+
return `<svg viewBox="${icon.viewBox}"><path d="${icon.d}" fill="${icon.fill}" /></svg>`
|
|
59
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { dirname, resolve } from "node:path"
|
|
3
|
+
|
|
4
|
+
/** What in an article's Markdown renders silently wrong, by file line: a directive
|
|
5
|
+
* written with a space or under an unknown name, which shows as plain text, and a
|
|
6
|
+
* relative image whose file is missing. Fenced code is skipped. */
|
|
7
|
+
export function findContentWarnings(props: {
|
|
8
|
+
text: string
|
|
9
|
+
filePath: string
|
|
10
|
+
directives: Set<string>
|
|
11
|
+
}) {
|
|
12
|
+
const warnings: string[] = []
|
|
13
|
+
let fence: string | undefined
|
|
14
|
+
props.text.split(/\r?\n/).forEach((line, index) => {
|
|
15
|
+
const marker = FENCE.exec(line)?.[1]
|
|
16
|
+
if (marker && (!fence || marker.startsWith(fence))) {
|
|
17
|
+
fence = fence ? undefined : marker
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
if (fence) return
|
|
21
|
+
const warn = (message: string) => warnings.push(`line ${index + 1}: ${message}`)
|
|
22
|
+
|
|
23
|
+
const spaced = SPACED_DIRECTIVE.exec(line)
|
|
24
|
+
if (spaced) {
|
|
25
|
+
const [, colons, name] = spaced
|
|
26
|
+
warn(
|
|
27
|
+
`"${colons} ${name}" has a space after the colons, so it renders as text; write "${colons}${name}"`,
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
const directive = DIRECTIVE.exec(line)
|
|
31
|
+
if (directive?.[2] && !props.directives.has(directive[2])) {
|
|
32
|
+
warn(`unknown directive "${directive[1]}${directive[2]}" renders as text`)
|
|
33
|
+
}
|
|
34
|
+
for (const match of line.matchAll(IMAGE)) {
|
|
35
|
+
const src = match[1] ?? match[2]
|
|
36
|
+
if (src && isRelative(src) && !existsSync(resolveImage(src, props.filePath))) {
|
|
37
|
+
warn(`image "${src}" not found`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
return warnings
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const FENCE = /^\s{0,3}(`{3,}|~{3,})/
|
|
45
|
+
const SPACED_DIRECTIVE = /^\s*(:{2,})\s+([A-Za-z][\w-]*)/
|
|
46
|
+
const DIRECTIVE = /^\s*(:{2,})([A-Za-z][\w-]*)/
|
|
47
|
+
const IMAGE = /!\[[^\]]*\]\(\s*<?([^)\s>]+)|<img\b[^>]*\bsrc=["']([^"']+)["']/g
|
|
48
|
+
|
|
49
|
+
function isRelative(src: string) {
|
|
50
|
+
return !/^(?:[a-z][a-z\d+.-]*:|\/|#)/i.test(src)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveImage(src: string, filePath: string) {
|
|
54
|
+
const path = src.split(/[?#]/)[0] ?? src
|
|
55
|
+
return resolve(dirname(filePath), decodeURI(path))
|
|
56
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useIcons } from "../hooks/icons.ts"
|
|
2
|
+
import { toIconKey } from "./icon-names.ts"
|
|
3
|
+
|
|
4
|
+
/** Resolves a Lucide icon by kebab-case name from the icons an `IconsProvider` gives */
|
|
5
|
+
export default function DynamicIconResolver(props: { name: string; className?: string }) {
|
|
6
|
+
const Icon = useIcons()[toIconKey(props.name)]
|
|
7
|
+
if (!Icon) return null
|
|
8
|
+
return <Icon className={props.className} />
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Suspense, lazy } from "react"
|
|
2
|
+
|
|
3
|
+
const importResolver = () => import("./dynamic-icon-resolver.tsx")
|
|
4
|
+
|
|
5
|
+
// Static on the server: a lazy one is still pending on the first page prerendered, which
|
|
6
|
+
// then streams its icons as hidden Suspense segments whose throttled reveal holds the
|
|
7
|
+
// first paint back by up to 2.3s. The client keeps the lazy chunk.
|
|
8
|
+
const Resolver = import.meta.env.SSR
|
|
9
|
+
? (await importResolver()).default
|
|
10
|
+
: lazy(importResolver)
|
|
11
|
+
|
|
12
|
+
/** Lazily loads and renders a Lucide icon by kebab-case name */
|
|
13
|
+
export function DynamicIcon(props: { name: string; className?: string }) {
|
|
14
|
+
return (
|
|
15
|
+
<Suspense>
|
|
16
|
+
<Resolver {...props} />
|
|
17
|
+
</Suspense>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { parse } from "yaml"
|
|
2
|
+
|
|
3
|
+
const FRONTMATTER = /^---\r?\n(?:([\s\S]*?)\r?\n)?---[ \t]*(?:\r?\n|$)/
|
|
4
|
+
|
|
5
|
+
/** Split a Markdown file into its YAML frontmatter and its trimmed body. */
|
|
6
|
+
export function parseFrontmatter(source: string) {
|
|
7
|
+
const text = source.replace(/^/, "")
|
|
8
|
+
const match = FRONTMATTER.exec(text)
|
|
9
|
+
if (!match) return { data: {}, content: text.trim() }
|
|
10
|
+
// The leading newline stands for the opening `---`, so YAML errors name file lines
|
|
11
|
+
const data: unknown = parse(`\n${match[1] ?? ""}`)
|
|
12
|
+
return {
|
|
13
|
+
data: typeof data === "object" && data !== null ? data : {},
|
|
14
|
+
content: text.slice(match[0].length).trim(),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** The key of a Lucide icon in its `icons` map: `file-pen-line` → `FilePenLine`. */
|
|
2
|
+
export function toIconKey(name: string) {
|
|
3
|
+
return name
|
|
4
|
+
.split("-")
|
|
5
|
+
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
|
|
6
|
+
.join("")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The icons a site can render: every name given outright, plus every quoted string in
|
|
10
|
+
* `texts` (compiled articles, escaped components) that names a known icon. */
|
|
11
|
+
export function collectIconKeys(props: {
|
|
12
|
+
names: string[]
|
|
13
|
+
texts: string[]
|
|
14
|
+
known: ReadonlySet<string>
|
|
15
|
+
}) {
|
|
16
|
+
const keys = new Set<string>()
|
|
17
|
+
const add = (name: string) => {
|
|
18
|
+
const key = toIconKey(name)
|
|
19
|
+
if (props.known.has(key)) keys.add(key)
|
|
20
|
+
}
|
|
21
|
+
props.names.forEach(add)
|
|
22
|
+
for (const text of props.texts) {
|
|
23
|
+
for (const match of text.matchAll(/["'`]([A-Za-z][\w-]*)["'`]/g)) {
|
|
24
|
+
if (match[1]) add(match[1])
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...keys].sort()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The module `DynamicIcon` resolves names from: only the icons the site uses. */
|
|
31
|
+
export function renderIconsModule(keys: string[]) {
|
|
32
|
+
if (keys.length === 0) return "export const icons = {}"
|
|
33
|
+
return [
|
|
34
|
+
`import { ${keys.join(", ")} } from "lucide-react"`,
|
|
35
|
+
`export const icons = { ${keys.join(", ")} }`,
|
|
36
|
+
].join("\n")
|
|
37
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** A UI string with its `{name}` placeholders filled from `values`. */
|
|
2
|
+
export function formatMessage(message: string, values?: Record<string, string>) {
|
|
3
|
+
if (!values) return message
|
|
4
|
+
return message.replace(/\{(\w+)\}/g, (match, name: string) => values[name] ?? match)
|
|
5
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** A syntax-tree node as `readNodeText` reads it, mdast or hast alike. */
|
|
2
|
+
export interface TextNode {
|
|
3
|
+
type: string
|
|
4
|
+
value?: unknown
|
|
5
|
+
alt?: unknown
|
|
6
|
+
children?: TextNode[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A node's text: its own value, an image's alt, else its children's text joined. */
|
|
10
|
+
export function readNodeText(node: TextNode): string {
|
|
11
|
+
if (typeof node.value === "string" && node.type !== "comment") return node.value
|
|
12
|
+
if (node.type === "image") return typeof node.alt === "string" ? node.alt : ""
|
|
13
|
+
return node.children?.map(readNodeText).join("") ?? ""
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext, useContext } from "react"
|
|
2
|
+
import type { TocItem } from "../models/toc.ts"
|
|
3
|
+
|
|
4
|
+
/** React context that provides TOC items to inline components */
|
|
5
|
+
export const TocContext = createContext<TocItem[]>([])
|
|
6
|
+
|
|
7
|
+
/** Consume the TOC items from the nearest TocContext provider */
|
|
8
|
+
export function useTocItems() {
|
|
9
|
+
return useContext(TocContext)
|
|
10
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TocItem } from "../models/toc.ts"
|
|
2
|
+
|
|
3
|
+
interface TocEntry {
|
|
4
|
+
depth: number
|
|
5
|
+
value: string
|
|
6
|
+
id?: string
|
|
7
|
+
children?: TocEntry[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The h2 to h4 headings of html.extractToc's nested outline, in document order. */
|
|
11
|
+
export function flattenToc(toc: unknown) {
|
|
12
|
+
const items: TocItem[] = []
|
|
13
|
+
const stack: TocEntry[] = Array.isArray(toc) ? [...toc].reverse() : []
|
|
14
|
+
while (stack.length > 0) {
|
|
15
|
+
const entry = stack.pop()
|
|
16
|
+
if (!entry) continue
|
|
17
|
+
if (entry.depth >= 2 && entry.depth <= 4 && entry.id) {
|
|
18
|
+
items.push({ url: `#${entry.id}`, title: entry.value, depth: entry.depth })
|
|
19
|
+
}
|
|
20
|
+
stack.push(...[...(entry.children ?? [])].reverse())
|
|
21
|
+
}
|
|
22
|
+
return items
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const VIDEO_ID = /^[\w-]{11}$/
|
|
2
|
+
const PATH_PREFIXES = ["embed", "shorts", "live", "v"]
|
|
3
|
+
|
|
4
|
+
/** Derives the video id from a YouTube watch, youtu.be, embed, shorts or live URL */
|
|
5
|
+
export function parseYoutubeId(url: string) {
|
|
6
|
+
let parsed: URL
|
|
7
|
+
try {
|
|
8
|
+
parsed = new URL(url)
|
|
9
|
+
} catch {
|
|
10
|
+
return undefined
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const host = parsed.hostname.replace(/^(www|m|music)\./, "")
|
|
14
|
+
const segments = parsed.pathname.split("/").filter(Boolean)
|
|
15
|
+
let id: string | null | undefined
|
|
16
|
+
if (host === "youtu.be") id = segments[0]
|
|
17
|
+
else if (host === "youtube.com" || host === "youtube-nocookie.com") {
|
|
18
|
+
if (segments[0] === "watch") id = parsed.searchParams.get("v")
|
|
19
|
+
else if (PATH_PREFIXES.includes(segments[0] ?? "")) id = segments[1]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return id && VIDEO_ID.test(id) ? id : undefined
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext, createElement, useContext } from "react"
|
|
2
|
+
import type { ComponentType, ReactNode } from "react"
|
|
3
|
+
|
|
4
|
+
/** Lucide icons by their key in Lucide's `icons` map, such as `ArrowRight`. */
|
|
5
|
+
export type IconMap = Record<string, ComponentType<{ className?: string }> | undefined>
|
|
6
|
+
|
|
7
|
+
const IconsContext = createContext<IconMap>({})
|
|
8
|
+
|
|
9
|
+
/** Supplies the icons `:icon` and `::card{icon}` render outside a Livemark site, such
|
|
10
|
+
* as lucide-react's `icons` or a subset of it. */
|
|
11
|
+
export function IconsProvider(props: { icons: IconMap; children?: ReactNode }) {
|
|
12
|
+
return createElement(IconsContext, { value: props.icons }, props.children)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The icons an `IconsProvider` supplies; none without one. */
|
|
16
|
+
export function useIcons() {
|
|
17
|
+
return useContext(IconsContext)
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createContext, createElement, useCallback, useContext } from "react"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
import { formatMessage } from "../helpers/message-format.ts"
|
|
4
|
+
import { useSiteMessages } from "../helpers/site-messages.ts"
|
|
5
|
+
import type { MessageKey, Messages } from "../models/message.ts"
|
|
6
|
+
|
|
7
|
+
const MessagesContext = createContext<Partial<Messages> | undefined>(undefined)
|
|
8
|
+
|
|
9
|
+
/** Supplies UI strings to Livemark components rendered outside a Livemark site; a key
|
|
10
|
+
* left out keeps its English default. */
|
|
11
|
+
export function MessagesProvider(props: {
|
|
12
|
+
messages: Partial<Messages>
|
|
13
|
+
children?: ReactNode
|
|
14
|
+
}) {
|
|
15
|
+
return createElement(MessagesContext, { value: props.messages }, props.children)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A translator for Livemark's UI strings: `t("toc.title")`, with `{name}` placeholders
|
|
19
|
+
* filled from `values`. A `MessagesProvider` wins over the site's language. */
|
|
20
|
+
export function useMessages() {
|
|
21
|
+
const provided = useContext(MessagesContext)
|
|
22
|
+
const site = useSiteMessages()
|
|
23
|
+
return useCallback(
|
|
24
|
+
(key: MessageKey, values?: Record<string, string>) =>
|
|
25
|
+
formatMessage(provided?.[key] ?? site[key], values),
|
|
26
|
+
[provided, site],
|
|
27
|
+
)
|
|
28
|
+
}
|