@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.
Files changed (184) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +5 -2
  3. package/build/actions/markdown/compile.d.ts +27 -0
  4. package/build/actions/markdown/compile.js +48 -0
  5. package/build/actions/processor/create.d.ts +17 -0
  6. package/build/actions/processor/create.js +212 -0
  7. package/build/components/Abbr.d.ts +5 -0
  8. package/build/components/Abbr.js +7 -0
  9. package/build/components/AnsiCode.d.ts +4 -0
  10. package/build/components/AnsiCode.js +7 -0
  11. package/build/components/Audio.d.ts +4 -0
  12. package/build/components/Audio.js +9 -0
  13. package/build/components/Badge.d.ts +5 -0
  14. package/build/components/Badge.js +7 -0
  15. package/build/components/Button.d.ts +7 -0
  16. package/build/components/Button.js +11 -0
  17. package/build/components/Callout.d.ts +8 -0
  18. package/build/components/Callout.js +41 -0
  19. package/build/components/Card.d.ts +7 -0
  20. package/build/components/Card.js +16 -0
  21. package/build/components/Cards.d.ts +4 -0
  22. package/build/components/Cards.js +6 -0
  23. package/build/components/CodeBlock.d.ts +1 -0
  24. package/build/components/CodeBlock.js +50 -0
  25. package/build/components/CodeTabs.d.ts +6 -0
  26. package/build/components/CodeTabs.js +48 -0
  27. package/build/components/Column.d.ts +4 -0
  28. package/build/components/Column.js +6 -0
  29. package/build/components/Columns.d.ts +5 -0
  30. package/build/components/Columns.js +8 -0
  31. package/build/components/DefinitionList.d.ts +7 -0
  32. package/build/components/DefinitionList.js +14 -0
  33. package/build/components/Details.d.ts +5 -0
  34. package/build/components/Details.js +10 -0
  35. package/build/components/Filetree.d.ts +4 -0
  36. package/build/components/Filetree.js +22 -0
  37. package/build/components/FootnoteRef.d.ts +4 -0
  38. package/build/components/FootnoteRef.js +33 -0
  39. package/build/components/Heading.d.ts +8 -0
  40. package/build/components/Heading.js +18 -0
  41. package/build/components/Icon.d.ts +5 -0
  42. package/build/components/Icon.js +10 -0
  43. package/build/components/Markdown.d.ts +13 -0
  44. package/build/components/Markdown.js +75 -0
  45. package/build/components/Mermaid.d.ts +4 -0
  46. package/build/components/Mermaid.js +10 -0
  47. package/build/components/MermaidRenderer.d.ts +4 -0
  48. package/build/components/MermaidRenderer.js +44 -0
  49. package/build/components/PackageTabs.d.ts +9 -0
  50. package/build/components/PackageTabs.js +26 -0
  51. package/build/components/SortableTable.d.ts +5 -0
  52. package/build/components/SortableTable.js +63 -0
  53. package/build/components/Tab.d.ts +5 -0
  54. package/build/components/Tab.js +6 -0
  55. package/build/components/Tabs.d.ts +5 -0
  56. package/build/components/Tabs.js +46 -0
  57. package/build/components/Toc.d.ts +4 -0
  58. package/build/components/Toc.js +14 -0
  59. package/build/components/Video.d.ts +4 -0
  60. package/build/components/Video.js +13 -0
  61. package/build/components/ZoomImage.d.ts +4 -0
  62. package/build/components/ZoomImage.js +26 -0
  63. package/build/elements/badge.d.ts +7 -0
  64. package/build/elements/badge.js +36 -0
  65. package/build/elements/button.d.ts +8 -0
  66. package/build/elements/button.js +37 -0
  67. package/build/elements/card.d.ts +11 -0
  68. package/build/elements/card.js +25 -0
  69. package/build/elements/collapsible.d.ts +5 -0
  70. package/build/elements/collapsible.js +13 -0
  71. package/build/elements/hover-card.d.ts +5 -0
  72. package/build/elements/hover-card.js +14 -0
  73. package/build/elements/separator.d.ts +3 -0
  74. package/build/elements/separator.js +8 -0
  75. package/build/elements/table.d.ts +9 -0
  76. package/build/elements/table.js +28 -0
  77. package/build/elements/tooltip.d.ts +6 -0
  78. package/build/elements/tooltip.js +17 -0
  79. package/build/helpers/ansi.d.ts +3 -0
  80. package/build/helpers/ansi.js +208 -0
  81. package/build/helpers/article-link.d.ts +7 -0
  82. package/build/helpers/article-link.js +8 -0
  83. package/build/helpers/code-icon.d.ts +2 -0
  84. package/build/helpers/code-icon.js +57 -0
  85. package/build/helpers/content-warnings.d.ts +8 -0
  86. package/build/helpers/content-warnings.js +47 -0
  87. package/build/helpers/dynamic-icon-resolver.d.ts +5 -0
  88. package/build/helpers/dynamic-icon-resolver.js +11 -0
  89. package/build/helpers/dynamic-icon.d.ts +5 -0
  90. package/build/helpers/dynamic-icon.js +14 -0
  91. package/build/helpers/frontmatter.d.ts +5 -0
  92. package/build/helpers/frontmatter.js +16 -0
  93. package/build/helpers/icon-names.d.ts +11 -0
  94. package/build/helpers/icon-names.js +35 -0
  95. package/build/helpers/message-format.d.ts +2 -0
  96. package/build/helpers/message-format.js +7 -0
  97. package/build/helpers/node-text.d.ts +9 -0
  98. package/build/helpers/node-text.js +9 -0
  99. package/build/helpers/site-messages.d.ts +86 -0
  100. package/build/helpers/site-messages.js +7 -0
  101. package/build/helpers/toc-context.d.ts +5 -0
  102. package/build/helpers/toc-context.js +8 -0
  103. package/build/helpers/toc-outline.d.ts +3 -0
  104. package/build/helpers/toc-outline.js +16 -0
  105. package/build/helpers/youtube-id.d.ts +2 -0
  106. package/build/helpers/youtube-id.js +25 -0
  107. package/build/hooks/icons.d.ts +13 -0
  108. package/build/hooks/icons.js +12 -0
  109. package/build/hooks/messages.d.ts +95 -0
  110. package/build/hooks/messages.js +17 -0
  111. package/build/models/message.d.ts +177 -0
  112. package/build/models/message.js +88 -0
  113. package/build/models/plugins.d.ts +10 -0
  114. package/build/models/plugins.js +6 -0
  115. package/build/models/theme.d.ts +76 -0
  116. package/build/models/theme.js +71 -0
  117. package/build/models/toc.d.ts +5 -0
  118. package/build/models/toc.js +2 -0
  119. package/build/plugins/twoslash.d.ts +13 -0
  120. package/build/plugins/twoslash.js +146 -0
  121. package/package.json +63 -3
  122. package/source/actions/markdown/compile.ts +82 -0
  123. package/source/actions/processor/create.ts +235 -0
  124. package/source/components/Abbr.tsx +13 -0
  125. package/source/components/AnsiCode.tsx +17 -0
  126. package/source/components/Audio.tsx +18 -0
  127. package/source/components/Badge.tsx +9 -0
  128. package/source/components/Button.tsx +33 -0
  129. package/source/components/Callout.tsx +68 -0
  130. package/source/components/Card.tsx +48 -0
  131. package/source/components/Cards.tsx +8 -0
  132. package/source/components/CodeBlock.tsx +144 -0
  133. package/source/components/CodeTabs.tsx +81 -0
  134. package/source/components/Column.tsx +4 -0
  135. package/source/components/Columns.tsx +10 -0
  136. package/source/components/DefinitionList.tsx +18 -0
  137. package/source/components/Details.tsx +30 -0
  138. package/source/components/Filetree.tsx +63 -0
  139. package/source/components/FootnoteRef.tsx +52 -0
  140. package/source/components/Heading.tsx +20 -0
  141. package/source/components/Icon.tsx +10 -0
  142. package/source/components/Markdown.tsx +89 -0
  143. package/source/components/Mermaid.tsx +20 -0
  144. package/source/components/MermaidRenderer.tsx +48 -0
  145. package/source/components/PackageTabs.tsx +72 -0
  146. package/source/components/SortableTable.tsx +81 -0
  147. package/source/components/Tab.tsx +4 -0
  148. package/source/components/Tabs.tsx +75 -0
  149. package/source/components/Toc.tsx +30 -0
  150. package/source/components/Video.tsx +28 -0
  151. package/source/components/ZoomImage.tsx +25 -0
  152. package/source/elements/badge.tsx +53 -0
  153. package/source/elements/button.tsx +61 -0
  154. package/source/elements/card.tsx +102 -0
  155. package/source/elements/collapsible.tsx +15 -0
  156. package/source/elements/hover-card.tsx +47 -0
  157. package/source/elements/separator.tsx +22 -0
  158. package/source/elements/table.tsx +103 -0
  159. package/source/elements/tooltip.tsx +60 -0
  160. package/source/helpers/ansi.ts +210 -0
  161. package/source/helpers/article-link.ts +11 -0
  162. package/source/helpers/code-icon.ts +59 -0
  163. package/source/helpers/content-warnings.ts +56 -0
  164. package/source/helpers/dynamic-icon-resolver.tsx +9 -0
  165. package/source/helpers/dynamic-icon.tsx +19 -0
  166. package/source/helpers/frontmatter.ts +16 -0
  167. package/source/helpers/icon-names.ts +37 -0
  168. package/source/helpers/message-format.ts +5 -0
  169. package/source/helpers/node-text.ts +14 -0
  170. package/source/helpers/site-messages.ts +8 -0
  171. package/source/helpers/toc-context.ts +10 -0
  172. package/source/helpers/toc-outline.ts +23 -0
  173. package/source/helpers/youtube-id.ts +23 -0
  174. package/source/hooks/icons.ts +18 -0
  175. package/source/hooks/messages.ts +28 -0
  176. package/source/models/message.ts +96 -0
  177. package/source/models/plugins.ts +11 -0
  178. package/source/models/theme.ts +78 -0
  179. package/source/models/toc.ts +5 -0
  180. package/source/plugins/twoslash.ts +163 -0
  181. package/source/standalone.css +7 -0
  182. package/source/styles/general.css +214 -0
  183. package/source/styles/markdown.css +231 -0
  184. 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, "&amp;")
54
+ .replace(/</g, "&lt;")
55
+ .replace(/>/g, "&gt;")
56
+ .replace(/"/g, "&quot;")
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,8 @@
1
+ import { Messages } from "../models/message.ts"
2
+
3
+ const DEFAULT_MESSAGES = Messages.parse({})
4
+
5
+ /** The UI strings outside a Livemark site: English. */
6
+ export function useSiteMessages() {
7
+ return DEFAULT_MESSAGES
8
+ }
@@ -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
+ }