@livemark/markdown 0.0.0-stage → 0.45.0
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,235 @@
|
|
|
1
|
+
import { textlane } from "textlane"
|
|
2
|
+
import * as html from "textlane/html"
|
|
3
|
+
import * as md from "textlane/md"
|
|
4
|
+
import * as mdx from "textlane/mdx"
|
|
5
|
+
import type { MarkdownPlugins } from "../../models/plugins.ts"
|
|
6
|
+
import { twoslashStage } from "../../plugins/twoslash.ts"
|
|
7
|
+
|
|
8
|
+
/** The frozen textlane processor Markdown compiles with: Markdown or MDX, with the
|
|
9
|
+
* twoslash stage only for documents that have a `twoslash` fence, and the `plugins`
|
|
10
|
+
* in their two slots. */
|
|
11
|
+
export function createProcessor(props: {
|
|
12
|
+
format: "md" | "mdx"
|
|
13
|
+
twoslash: boolean
|
|
14
|
+
outputFormat: "program" | "function-body"
|
|
15
|
+
allowDangerousHtml: boolean
|
|
16
|
+
codeThemeLight: string
|
|
17
|
+
codeThemeDark: string
|
|
18
|
+
plugins?: MarkdownPlugins
|
|
19
|
+
root?: string
|
|
20
|
+
base?: string
|
|
21
|
+
}) {
|
|
22
|
+
const themes = { light: props.codeThemeLight, dark: props.codeThemeDark }
|
|
23
|
+
const processor = textlane()
|
|
24
|
+
.use(md.parse())
|
|
25
|
+
.use(md.gfm())
|
|
26
|
+
.use(md.gemoji())
|
|
27
|
+
.use(md.definitionList())
|
|
28
|
+
.use(md.math())
|
|
29
|
+
.use(md.directive())
|
|
30
|
+
if (props.format === "mdx") processor.use(md.mdx())
|
|
31
|
+
// Before md.component, which renders an unknown directive as its source text
|
|
32
|
+
processor
|
|
33
|
+
.use(props.plugins?.md ?? [])
|
|
34
|
+
.use(md.headingId({ attributes: true, markers: HEADING_MARKERS }))
|
|
35
|
+
.use(md.packageCommand({ component: "PackageTabs" }))
|
|
36
|
+
.use(md.component(COMPONENTS))
|
|
37
|
+
if (props.root !== undefined) {
|
|
38
|
+
processor.use(
|
|
39
|
+
md.pathRewrite({ root: props.root, base: props.base, nodes: ["image"] }),
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
processor.use(
|
|
43
|
+
md.toHtml({
|
|
44
|
+
allowDangerousHtml: props.allowDangerousHtml,
|
|
45
|
+
taskListLabels: TASK_LIST_LABELS,
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
// html.katex first, or display math is a `<pre><code>` html.highlight takes for code
|
|
49
|
+
processor.use(html.katex()).use(
|
|
50
|
+
html.highlight({
|
|
51
|
+
themes,
|
|
52
|
+
// a fence with no or an unknown language still gets padding, theme colors and
|
|
53
|
+
// transformers
|
|
54
|
+
defaultLanguage: "text",
|
|
55
|
+
fallbackLanguage: "text",
|
|
56
|
+
// catppuccin-latte's green (3.2:1) and comment grey (3.8:1) sit under WCAG AA on
|
|
57
|
+
// the code background
|
|
58
|
+
colorReplacements: {
|
|
59
|
+
"catppuccin-latte": { "#40a02b": "#2a7e16", "#7c7f93": "#6c6f85" },
|
|
60
|
+
},
|
|
61
|
+
addLanguageClass: true,
|
|
62
|
+
metaProperty: "data-meta",
|
|
63
|
+
inline: "tailing-curly-colon",
|
|
64
|
+
transformers: [
|
|
65
|
+
"notationDiff",
|
|
66
|
+
"notationFocus",
|
|
67
|
+
"notationHighlight",
|
|
68
|
+
"notationErrorLevel",
|
|
69
|
+
"metaHighlight",
|
|
70
|
+
"metaWordHighlight",
|
|
71
|
+
],
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
// html.raw rebuilds every element and drops `data`, where code keeps its meta for
|
|
75
|
+
// html.highlight
|
|
76
|
+
// Without allowDangerousHtml md.toHtml has dropped raw HTML, which html.raw would parse
|
|
77
|
+
if (props.format === "md" && props.allowDangerousHtml) {
|
|
78
|
+
processor.use(html.raw({ passThrough: MDX_NODE_TYPES }))
|
|
79
|
+
}
|
|
80
|
+
processor.use(html.unwrapImages())
|
|
81
|
+
if (props.twoslash) processor.use(twoslashStage, { themes })
|
|
82
|
+
return processor
|
|
83
|
+
.use(props.plugins?.html ?? [])
|
|
84
|
+
.use(html.slug())
|
|
85
|
+
.use(html.extractToc({ skip: ["data-step", "data-toc-hidden"] }))
|
|
86
|
+
.use(mdx.compile({ outputFormat: props.outputFormat }))
|
|
87
|
+
.freeze()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const HEADING_MARKERS = {
|
|
91
|
+
"[step]": { "data-step": true },
|
|
92
|
+
"[!toc]": { "data-toc-hidden": true },
|
|
93
|
+
"[toc]": { hidden: true },
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const TASK_LIST_LABELS = { checked: "Completed task", unchecked: "Task" }
|
|
97
|
+
|
|
98
|
+
// md.toHtml hands MDX nodes to hast, which html.raw must leave as they are
|
|
99
|
+
const MDX_NODE_TYPES = [
|
|
100
|
+
"mdxFlowExpression",
|
|
101
|
+
"mdxJsxFlowElement",
|
|
102
|
+
"mdxJsxTextElement",
|
|
103
|
+
"mdxTextExpression",
|
|
104
|
+
"mdxjsEsm",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
const CALLOUTS = ["note", "tip", "info", "warning", "danger"]
|
|
108
|
+
|
|
109
|
+
// Each livemark directive, alert and node as the component it renders
|
|
110
|
+
const COMPONENTS = {
|
|
111
|
+
directives: [
|
|
112
|
+
{
|
|
113
|
+
name: CALLOUTS,
|
|
114
|
+
kind: "container" as const,
|
|
115
|
+
component: "Callout",
|
|
116
|
+
nameAs: "type",
|
|
117
|
+
labelAs: "title",
|
|
118
|
+
attributes: [],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "card",
|
|
122
|
+
kind: "container" as const,
|
|
123
|
+
component: "Card",
|
|
124
|
+
attributes: [{ name: "title", default: "" }, "href", "icon"],
|
|
125
|
+
group: { component: "Cards" },
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "column",
|
|
129
|
+
kind: "container" as const,
|
|
130
|
+
component: "Column",
|
|
131
|
+
attributes: [],
|
|
132
|
+
group: { component: "Columns" },
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "tab",
|
|
136
|
+
kind: "container" as const,
|
|
137
|
+
component: "Tab",
|
|
138
|
+
attributes: [{ name: "title", default: "" }, "sync"],
|
|
139
|
+
group: { component: "Tabs", min: 1, inherit: ["sync"] },
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "details",
|
|
143
|
+
kind: "container" as const,
|
|
144
|
+
component: "Details",
|
|
145
|
+
labelAs: "summary",
|
|
146
|
+
labelDefault: "Details",
|
|
147
|
+
attributes: [],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "filetree",
|
|
151
|
+
kind: "container" as const,
|
|
152
|
+
component: "Filetree",
|
|
153
|
+
attributes: [],
|
|
154
|
+
treeAs: "tree",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "badge",
|
|
158
|
+
kind: "text" as const,
|
|
159
|
+
component: "Badge",
|
|
160
|
+
labelAs: "label",
|
|
161
|
+
attributes: ["variant"],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "button",
|
|
165
|
+
kind: "leaf" as const,
|
|
166
|
+
component: "Button",
|
|
167
|
+
labelAs: "label",
|
|
168
|
+
attributes: ["href", "label", "variant", "size"],
|
|
169
|
+
required: ["href", "label"],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: "icon",
|
|
173
|
+
kind: "text" as const,
|
|
174
|
+
component: "Icon",
|
|
175
|
+
attributes: ["name", "className"],
|
|
176
|
+
required: ["name"],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "abbr",
|
|
180
|
+
kind: "text" as const,
|
|
181
|
+
component: "Abbr",
|
|
182
|
+
labelAs: "text",
|
|
183
|
+
attributes: [{ name: "title", default: "" }],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "toc",
|
|
187
|
+
kind: "leaf" as const,
|
|
188
|
+
component: "Toc",
|
|
189
|
+
attributes: ["maxLevel"],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: "video",
|
|
193
|
+
kind: "leaf" as const,
|
|
194
|
+
where: { type: "youtube" },
|
|
195
|
+
component: "Video",
|
|
196
|
+
attributes: ["url"],
|
|
197
|
+
required: ["url"],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "audio",
|
|
201
|
+
kind: "leaf" as const,
|
|
202
|
+
where: { type: "soundcloud" },
|
|
203
|
+
component: "Audio",
|
|
204
|
+
attributes: ["url"],
|
|
205
|
+
required: ["url"],
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
alerts: {
|
|
209
|
+
component: "Callout",
|
|
210
|
+
typeAs: "type",
|
|
211
|
+
types: {
|
|
212
|
+
NOTE: "note",
|
|
213
|
+
TIP: "tip",
|
|
214
|
+
WARNING: "warning",
|
|
215
|
+
CAUTION: "danger",
|
|
216
|
+
IMPORTANT: "info",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
nodes: {
|
|
220
|
+
defList: "DefinitionList",
|
|
221
|
+
defListTerm: "DefinitionTerm",
|
|
222
|
+
defListDescription: "DefinitionDetail",
|
|
223
|
+
},
|
|
224
|
+
codes: [
|
|
225
|
+
{ lang: "mermaid", component: "Mermaid" },
|
|
226
|
+
{ lang: "ansi", component: "AnsiCode" },
|
|
227
|
+
{ meta: "tab", group: { component: "CodeTabs", collect: "tabs", inherit: ["sync"] } },
|
|
228
|
+
],
|
|
229
|
+
unknownDirectives: "source" as const,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Every directive name an article may use; any other renders as its source text. */
|
|
233
|
+
export const DIRECTIVE_NAMES = new Set(
|
|
234
|
+
COMPONENTS.directives.flatMap(directive => directive.name),
|
|
235
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../elements/tooltip.tsx"
|
|
2
|
+
|
|
3
|
+
/** Renders an abbreviation with a shadcn tooltip showing its full form */
|
|
4
|
+
export function Abbr(props: { text: string; title: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<Tooltip>
|
|
7
|
+
<TooltipTrigger className="border-b border-dotted border-muted-foreground cursor-help">
|
|
8
|
+
{props.text}
|
|
9
|
+
</TooltipTrigger>
|
|
10
|
+
<TooltipContent>{props.title}</TooltipContent>
|
|
11
|
+
</Tooltip>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ansiToHtml } from "../helpers/ansi.ts"
|
|
2
|
+
|
|
3
|
+
/** Renders terminal output, coloring its ANSI escape sequences */
|
|
4
|
+
export function AnsiCode(props: { children: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<figure className="shiki not-prose my-4 relative rounded-card border shadow-sm overflow-hidden text-sm bg-sidebar dark:bg-sidebar">
|
|
7
|
+
<div className="text-[0.8125rem] py-3.5 overflow-auto">
|
|
8
|
+
<pre className="min-w-full w-max">
|
|
9
|
+
<code
|
|
10
|
+
className="flex flex-col"
|
|
11
|
+
dangerouslySetInnerHTML={{ __html: ansiToHtml(props.children) }}
|
|
12
|
+
/>
|
|
13
|
+
</pre>
|
|
14
|
+
</div>
|
|
15
|
+
</figure>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useMessages } from "../hooks/messages.ts"
|
|
2
|
+
/** Renders a SoundCloud iframe embed */
|
|
3
|
+
export function Audio(props: { url: string }) {
|
|
4
|
+
const t = useMessages()
|
|
5
|
+
const src = `https://w.soundcloud.com/player/?url=${encodeURIComponent(props.url)}&color=%23ff5500&auto_play=false&hide_related=true&show_comments=false&show_user=true&show_reposts=false&show_teaser=false`
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className="not-prose my-4 overflow-hidden rounded-card border">
|
|
9
|
+
<iframe
|
|
10
|
+
className="w-full"
|
|
11
|
+
height={166}
|
|
12
|
+
src={src}
|
|
13
|
+
title={t("media.soundcloud")}
|
|
14
|
+
allow="autoplay"
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Badge as BadgeElement } from "../elements/badge.tsx"
|
|
2
|
+
|
|
3
|
+
/** Inline badge rendered from :badge text directives */
|
|
4
|
+
export function Badge(props: {
|
|
5
|
+
label: string
|
|
6
|
+
variant?: "default" | "secondary" | "positive" | "warning" | "destructive" | "outline"
|
|
7
|
+
}) {
|
|
8
|
+
return <BadgeElement variant={props.variant}>{props.label}</BadgeElement>
|
|
9
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { buttonVariants } from "../elements/button.tsx"
|
|
2
|
+
import { cn } from "cn"
|
|
3
|
+
|
|
4
|
+
/** Styled link button rendered from ::button leaf directives */
|
|
5
|
+
export function Button(props: {
|
|
6
|
+
href: string
|
|
7
|
+
label: string
|
|
8
|
+
variant?:
|
|
9
|
+
| "default"
|
|
10
|
+
| "outline"
|
|
11
|
+
| "secondary"
|
|
12
|
+
| "ghost"
|
|
13
|
+
| "positive"
|
|
14
|
+
| "warning"
|
|
15
|
+
| "destructive"
|
|
16
|
+
| "link"
|
|
17
|
+
size?: "default" | "sm" | "lg"
|
|
18
|
+
}) {
|
|
19
|
+
return (
|
|
20
|
+
<a
|
|
21
|
+
href={props.href}
|
|
22
|
+
className={cn(
|
|
23
|
+
buttonVariants({
|
|
24
|
+
variant: props.variant,
|
|
25
|
+
size: props.size,
|
|
26
|
+
}),
|
|
27
|
+
"no-underline",
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
{props.label}
|
|
31
|
+
</a>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { AlertTriangle, Flame, Info, Lightbulb } from "lucide-react"
|
|
2
|
+
import { cn } from "cn"
|
|
3
|
+
import type { MessageKey } from "../models/message.ts"
|
|
4
|
+
import { useMessages } from "../hooks/messages.ts"
|
|
5
|
+
|
|
6
|
+
type CalloutType = "note" | "tip" | "info" | "warning" | "danger"
|
|
7
|
+
|
|
8
|
+
interface CalloutConfig {
|
|
9
|
+
icon: React.ElementType
|
|
10
|
+
label: MessageKey
|
|
11
|
+
classes: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const configs: Record<CalloutType, CalloutConfig> = {
|
|
15
|
+
note: {
|
|
16
|
+
icon: Info,
|
|
17
|
+
label: "callout.note",
|
|
18
|
+
classes:
|
|
19
|
+
"border-blue-500/30 bg-blue-50/50 text-blue-900 dark:bg-blue-950/30 dark:text-blue-200",
|
|
20
|
+
},
|
|
21
|
+
info: {
|
|
22
|
+
icon: Info,
|
|
23
|
+
label: "callout.info",
|
|
24
|
+
classes:
|
|
25
|
+
"border-blue-500/30 bg-blue-50/50 text-blue-900 dark:bg-blue-950/30 dark:text-blue-200",
|
|
26
|
+
},
|
|
27
|
+
tip: {
|
|
28
|
+
icon: Lightbulb,
|
|
29
|
+
label: "callout.tip",
|
|
30
|
+
classes: "border-positive/30 bg-positive-tint/50 text-positive-tint-foreground",
|
|
31
|
+
},
|
|
32
|
+
warning: {
|
|
33
|
+
icon: AlertTriangle,
|
|
34
|
+
label: "callout.warning",
|
|
35
|
+
classes: "border-warning/30 bg-warning-tint/50 text-warning-tint-foreground",
|
|
36
|
+
},
|
|
37
|
+
danger: {
|
|
38
|
+
icon: Flame,
|
|
39
|
+
label: "callout.danger",
|
|
40
|
+
classes:
|
|
41
|
+
"border-destructive/30 bg-destructive-tint/50 text-destructive-tint-foreground",
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Styled callout block for notes, tips, warnings, and other admonitions */
|
|
46
|
+
export function Callout(props: {
|
|
47
|
+
type?: CalloutType
|
|
48
|
+
title?: string
|
|
49
|
+
children: React.ReactNode
|
|
50
|
+
}) {
|
|
51
|
+
const t = useMessages()
|
|
52
|
+
const type = props.type ?? "note"
|
|
53
|
+
const config = configs[type]
|
|
54
|
+
const Icon = config.icon
|
|
55
|
+
const title = props.title ?? t(config.label)
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<aside className={cn("not-prose my-4 rounded-card border px-4 py-3", config.classes)}>
|
|
59
|
+
<div className="flex items-center gap-2 font-semibold text-sm mb-2">
|
|
60
|
+
<Icon className="size-4 shrink-0" />
|
|
61
|
+
<span>{title}</span>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="text-sm [&>p]:my-1 [&>p:first-child]:mt-0 [&>p:last-child]:mb-0">
|
|
64
|
+
{props.children}
|
|
65
|
+
</div>
|
|
66
|
+
</aside>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Link } from "@tanstack/react-router"
|
|
2
|
+
import { DynamicIcon } from "../helpers/dynamic-icon.tsx"
|
|
3
|
+
import {
|
|
4
|
+
Card as CardElement,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardHeader,
|
|
7
|
+
CardTitle,
|
|
8
|
+
} from "../elements/card.tsx"
|
|
9
|
+
|
|
10
|
+
/** Card component with optional icon, title, description, and link */
|
|
11
|
+
export function Card(props: {
|
|
12
|
+
title: string
|
|
13
|
+
href?: string
|
|
14
|
+
icon?: string
|
|
15
|
+
children?: React.ReactNode
|
|
16
|
+
}) {
|
|
17
|
+
const card = (
|
|
18
|
+
<CardElement size="sm" className="transition-colors hover:bg-accent">
|
|
19
|
+
<CardHeader>
|
|
20
|
+
<CardTitle className="flex items-center gap-2">
|
|
21
|
+
{props.icon && (
|
|
22
|
+
<DynamicIcon name={props.icon} className="size-5 text-primary" />
|
|
23
|
+
)}
|
|
24
|
+
{props.title}
|
|
25
|
+
</CardTitle>
|
|
26
|
+
</CardHeader>
|
|
27
|
+
{props.children && (
|
|
28
|
+
<CardContent>
|
|
29
|
+
<div className="text-muted-foreground text-sm">{props.children}</div>
|
|
30
|
+
</CardContent>
|
|
31
|
+
)}
|
|
32
|
+
</CardElement>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if (props.href?.startsWith("http")) {
|
|
36
|
+
return (
|
|
37
|
+
<a href={props.href} target="_blank" rel="noopener noreferrer">
|
|
38
|
+
{card}
|
|
39
|
+
</a>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (props.href) {
|
|
44
|
+
return <Link to={props.href}>{card}</Link>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return card
|
|
48
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Check, ChevronDown, Clipboard, WrapText } from "lucide-react"
|
|
2
|
+
import { isValidElement, useRef, useState } from "react"
|
|
3
|
+
import type { ReactNode } from "react"
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../elements/tooltip.tsx"
|
|
5
|
+
import { readCodeIcon } from "../helpers/code-icon.ts"
|
|
6
|
+
import { useMessages } from "../hooks/messages.ts"
|
|
7
|
+
|
|
8
|
+
export function CodeBlock(props: React.ComponentProps<"pre">) {
|
|
9
|
+
const [copied, setCopied] = useState(false)
|
|
10
|
+
const t = useMessages()
|
|
11
|
+
const [wrap, setWrap] = useState(false)
|
|
12
|
+
const [expanded, setExpanded] = useState(false)
|
|
13
|
+
const preRef = useRef<HTMLPreElement>(null)
|
|
14
|
+
const extra = props as Record<string, unknown>
|
|
15
|
+
const meta = extra["data-meta"] as string | undefined
|
|
16
|
+
const title = meta?.match(/title="([^"]+)"/)?.[1]
|
|
17
|
+
const maxLines = meta?.match(/maxLines=(\d+)/)?.[1]
|
|
18
|
+
const icon = readCodeIcon(readLanguage(props.children))
|
|
19
|
+
const lineNumbers = meta?.match(/\blineNumbers(?:=(\d+))?(?!\S)/)
|
|
20
|
+
const style = props.style as Record<string, string> | undefined
|
|
21
|
+
const collapsedHeight = maxLines ? `${Number(maxLines) * 1.5 + 1.75}rem` : undefined
|
|
22
|
+
|
|
23
|
+
const handleCopy = () => {
|
|
24
|
+
const text = preRef.current?.textContent ?? ""
|
|
25
|
+
navigator.clipboard.writeText(text)
|
|
26
|
+
setCopied(true)
|
|
27
|
+
setTimeout(() => setCopied(false), 2000)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const handleWrapToggle = () => {
|
|
31
|
+
setWrap(prev => !prev)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<figure
|
|
36
|
+
className="shiki not-prose my-4 relative rounded-card border shadow-sm overflow-hidden text-sm bg-sidebar dark:bg-sidebar"
|
|
37
|
+
style={{ ...style, backgroundColor: undefined }}
|
|
38
|
+
>
|
|
39
|
+
{title ? (
|
|
40
|
+
<div className="flex items-center gap-2 h-9.5 border-b px-4 text-muted-foreground">
|
|
41
|
+
{icon && (
|
|
42
|
+
<span
|
|
43
|
+
className="size-3.5 [&>svg]:size-full"
|
|
44
|
+
dangerouslySetInnerHTML={{ __html: icon }}
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
<span className="flex-1 truncate text-xs font-medium">{title}</span>
|
|
48
|
+
<Tooltip>
|
|
49
|
+
<TooltipTrigger
|
|
50
|
+
onClick={handleWrapToggle}
|
|
51
|
+
aria-label={wrap ? t("code.wrapOff") : t("code.wrapOn")}
|
|
52
|
+
className={`-me-2 p-1.5 rounded-control hover:text-foreground transition-colors ${wrap ? "text-foreground" : ""}`}
|
|
53
|
+
>
|
|
54
|
+
<WrapText className="size-3.5" />
|
|
55
|
+
</TooltipTrigger>
|
|
56
|
+
<TooltipContent>{wrap ? t("code.wrapOff") : t("code.wrapOn")}</TooltipContent>
|
|
57
|
+
</Tooltip>
|
|
58
|
+
<Tooltip>
|
|
59
|
+
<TooltipTrigger
|
|
60
|
+
onClick={handleCopy}
|
|
61
|
+
aria-label={copied ? t("code.copied") : t("code.copy")}
|
|
62
|
+
className="-me-2 p-1.5 rounded-control hover:text-foreground transition-colors"
|
|
63
|
+
>
|
|
64
|
+
{copied ? (
|
|
65
|
+
<Check className="size-3.5" />
|
|
66
|
+
) : (
|
|
67
|
+
<Clipboard className="size-3.5" />
|
|
68
|
+
)}
|
|
69
|
+
</TooltipTrigger>
|
|
70
|
+
<TooltipContent>{copied ? t("code.copied") : t("code.copy")}</TooltipContent>
|
|
71
|
+
</Tooltip>
|
|
72
|
+
</div>
|
|
73
|
+
) : (
|
|
74
|
+
<div className="empty:hidden absolute top-2 right-2 z-10 backdrop-blur-lg rounded-control text-muted-foreground">
|
|
75
|
+
<Tooltip>
|
|
76
|
+
<TooltipTrigger
|
|
77
|
+
onClick={handleWrapToggle}
|
|
78
|
+
aria-label={wrap ? t("code.wrapOff") : t("code.wrapOn")}
|
|
79
|
+
className={`p-1.5 rounded-control hover:text-foreground transition-colors ${wrap ? "text-foreground" : ""}`}
|
|
80
|
+
>
|
|
81
|
+
<WrapText className="size-3.5" />
|
|
82
|
+
</TooltipTrigger>
|
|
83
|
+
<TooltipContent>{wrap ? t("code.wrapOff") : t("code.wrapOn")}</TooltipContent>
|
|
84
|
+
</Tooltip>
|
|
85
|
+
<Tooltip>
|
|
86
|
+
<TooltipTrigger
|
|
87
|
+
onClick={handleCopy}
|
|
88
|
+
aria-label={copied ? t("code.copied") : t("code.copy")}
|
|
89
|
+
className="p-1.5 rounded-control hover:text-foreground transition-colors"
|
|
90
|
+
>
|
|
91
|
+
{copied ? (
|
|
92
|
+
<Check className="size-3.5" />
|
|
93
|
+
) : (
|
|
94
|
+
<Clipboard className="size-3.5" />
|
|
95
|
+
)}
|
|
96
|
+
</TooltipTrigger>
|
|
97
|
+
<TooltipContent>{copied ? t("code.copied") : t("code.copy")}</TooltipContent>
|
|
98
|
+
</Tooltip>
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
<div
|
|
102
|
+
className="text-[0.8125rem] py-3.5 overflow-auto transition-[max-height] duration-300"
|
|
103
|
+
style={{
|
|
104
|
+
maxHeight: collapsedHeight && !expanded ? collapsedHeight : "600px",
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<pre
|
|
108
|
+
{...props}
|
|
109
|
+
ref={preRef}
|
|
110
|
+
style={
|
|
111
|
+
lineNumbers
|
|
112
|
+
? { counterReset: `line ${Number(lineNumbers[1] ?? 1) - 1}` }
|
|
113
|
+
: undefined
|
|
114
|
+
}
|
|
115
|
+
className={[
|
|
116
|
+
props.className,
|
|
117
|
+
lineNumbers && "line-numbers",
|
|
118
|
+
wrap
|
|
119
|
+
? "min-w-full whitespace-pre-wrap break-words [&>code]:flex [&>code]:flex-col"
|
|
120
|
+
: "min-w-full w-max [&>code]:flex [&>code]:flex-col",
|
|
121
|
+
]
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
.join(" ")}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
{collapsedHeight && !expanded && (
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
onClick={() => setExpanded(true)}
|
|
130
|
+
className="flex items-center justify-center gap-1 w-full py-1.5 text-xs text-muted-foreground hover:text-foreground border-t cursor-pointer transition-colors"
|
|
131
|
+
>
|
|
132
|
+
<ChevronDown className="size-3.5" />
|
|
133
|
+
{t("code.expand")}
|
|
134
|
+
</button>
|
|
135
|
+
)}
|
|
136
|
+
</figure>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// html.highlight names the language in a class of the `<code>` inside `<pre>`
|
|
141
|
+
function readLanguage(children: ReactNode) {
|
|
142
|
+
if (!isValidElement<{ className?: string }>(children)) return undefined
|
|
143
|
+
return children.props.className?.match(/\blanguage-(\S+)/)?.[1]
|
|
144
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Children, useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
/** Tabbed code block component for grouping multiple code blocks with optional sync */
|
|
4
|
+
export function CodeTabs(props: {
|
|
5
|
+
tabs: string
|
|
6
|
+
sync?: string
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
}) {
|
|
9
|
+
const names = JSON.parse(props.tabs) as string[]
|
|
10
|
+
const children = Children.toArray(props.children)
|
|
11
|
+
|
|
12
|
+
const getInitialIndex = () => {
|
|
13
|
+
if (!props.sync) return 0
|
|
14
|
+
const saved =
|
|
15
|
+
typeof window !== "undefined"
|
|
16
|
+
? localStorage.getItem("livemark-tab-" + props.sync)
|
|
17
|
+
: null
|
|
18
|
+
if (!saved) return 0
|
|
19
|
+
const idx = names.indexOf(saved)
|
|
20
|
+
return idx >= 0 ? idx : 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const [active, setActive] = useState(getInitialIndex)
|
|
24
|
+
|
|
25
|
+
const handleSelect = (i: number) => {
|
|
26
|
+
setActive(i)
|
|
27
|
+
if (props.sync) {
|
|
28
|
+
localStorage.setItem("livemark-tab-" + props.sync, names[i]!)
|
|
29
|
+
window.dispatchEvent(
|
|
30
|
+
new CustomEvent("livemark-tab-sync", {
|
|
31
|
+
detail: { sync: props.sync, title: names[i] },
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!props.sync) return
|
|
39
|
+
const handler = (e: Event) => {
|
|
40
|
+
const { sync, title } = (e as CustomEvent<{ sync: string; title: string }>).detail
|
|
41
|
+
if (sync !== props.sync) return
|
|
42
|
+
const idx = names.indexOf(title)
|
|
43
|
+
if (idx >= 0) setActive(idx)
|
|
44
|
+
}
|
|
45
|
+
window.addEventListener("livemark-tab-sync", handler)
|
|
46
|
+
return () => window.removeEventListener("livemark-tab-sync", handler)
|
|
47
|
+
}, [props.sync, names])
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="not-prose my-4 rounded-card border shadow-sm overflow-hidden text-sm bg-sidebar dark:bg-sidebar">
|
|
51
|
+
<div className="flex border-b">
|
|
52
|
+
{names.map((name, i) => (
|
|
53
|
+
<button
|
|
54
|
+
key={name}
|
|
55
|
+
type="button"
|
|
56
|
+
onClick={() => handleSelect(i)}
|
|
57
|
+
className={`px-4 py-2 text-xs font-medium transition-colors cursor-pointer ${
|
|
58
|
+
i === active
|
|
59
|
+
? "text-foreground border-b-2 border-primary -mb-px"
|
|
60
|
+
: "text-muted-foreground hover:text-foreground"
|
|
61
|
+
}`}
|
|
62
|
+
>
|
|
63
|
+
{name}
|
|
64
|
+
</button>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
{children.map((child, i) => (
|
|
68
|
+
<div
|
|
69
|
+
key={names[i]}
|
|
70
|
+
className={
|
|
71
|
+
i === active
|
|
72
|
+
? "[&>figure]:my-0 [&>figure]:rounded-none [&>figure]:border-0 [&>figure]:shadow-none"
|
|
73
|
+
: "hidden"
|
|
74
|
+
}
|
|
75
|
+
>
|
|
76
|
+
{child}
|
|
77
|
+
</div>
|
|
78
|
+
))}
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|