@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,10 @@
|
|
|
1
|
+
/** Grid layout for arranging content side by side */
|
|
2
|
+
export function Columns(props: { cols?: string; children: React.ReactNode }) {
|
|
3
|
+
const cols = props.cols ? parseInt(props.cols, 10) : undefined
|
|
4
|
+
const style = cols ? { gridTemplateColumns: `repeat(${cols}, 1fr)` } : undefined
|
|
5
|
+
return (
|
|
6
|
+
<div className="grid gap-6 my-4 sm:grid-cols-2" style={style}>
|
|
7
|
+
{props.children}
|
|
8
|
+
</div>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ComponentProps } from "react"
|
|
2
|
+
|
|
3
|
+
/** Styled wrapper for definition lists produced by remark-definition-list */
|
|
4
|
+
export function DefinitionList(props: ComponentProps<"dl">) {
|
|
5
|
+
return <dl {...props} className="not-prose space-y-4" />
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Styled definition term rendered as bold text */
|
|
9
|
+
export function DefinitionTerm(props: ComponentProps<"dt">) {
|
|
10
|
+
return <dt {...props} className="text-foreground font-semibold" />
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Styled definition detail with a left border accent */
|
|
14
|
+
export function DefinitionDetail(props: ComponentProps<"dd">) {
|
|
15
|
+
return (
|
|
16
|
+
<dd {...props} className="ml-0 pl-4 border-l-2 border-border text-muted-foreground" />
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
import { ChevronRight } from "lucide-react"
|
|
3
|
+
import {
|
|
4
|
+
Collapsible,
|
|
5
|
+
CollapsibleTrigger,
|
|
6
|
+
CollapsibleContent,
|
|
7
|
+
} from "../elements/collapsible.tsx"
|
|
8
|
+
|
|
9
|
+
/** Collapsible content section with a summary label */
|
|
10
|
+
export function Details(props: { summary: string; children: React.ReactNode }) {
|
|
11
|
+
const [open, setOpen] = useState(false)
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Collapsible
|
|
15
|
+
open={open}
|
|
16
|
+
onOpenChange={setOpen}
|
|
17
|
+
className="not-prose my-4 rounded-card border border-border bg-card px-4 py-3"
|
|
18
|
+
>
|
|
19
|
+
<CollapsibleTrigger className="flex w-full cursor-pointer items-center gap-2 text-sm font-semibold select-none">
|
|
20
|
+
<ChevronRight
|
|
21
|
+
className={`size-4 shrink-0 transition-transform ${open ? "rotate-90" : ""}`}
|
|
22
|
+
/>
|
|
23
|
+
{props.summary}
|
|
24
|
+
</CollapsibleTrigger>
|
|
25
|
+
<CollapsibleContent className="mt-3 text-sm [&>p]:my-1 [&>p:first-child]:mt-0 [&>p:last-child]:mb-0">
|
|
26
|
+
{props.children}
|
|
27
|
+
</CollapsibleContent>
|
|
28
|
+
</Collapsible>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { File, Folder } from "lucide-react"
|
|
2
|
+
|
|
3
|
+
interface TreeItem {
|
|
4
|
+
name: string
|
|
5
|
+
type: "directory" | "file" | "placeholder"
|
|
6
|
+
comment?: string
|
|
7
|
+
highlighted?: boolean
|
|
8
|
+
children?: TreeItem[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Renders a file/directory tree from a JSON-serialized tree structure */
|
|
12
|
+
export function Filetree(props: { tree: string }) {
|
|
13
|
+
const items: TreeItem[] = JSON.parse(props.tree)
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="not-prose my-4 rounded-card border border-border bg-card p-4 font-mono text-sm">
|
|
17
|
+
<TreeItems items={items} depth={0} />
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function TreeItems(props: { items: TreeItem[]; depth: number }) {
|
|
23
|
+
return (
|
|
24
|
+
<ul className={props.depth > 0 ? "ml-4 border-l border-border pl-2" : ""}>
|
|
25
|
+
{props.items.map((item, index) => (
|
|
26
|
+
<TreeNode key={`${item.name}-${index}`} item={item} depth={props.depth} />
|
|
27
|
+
))}
|
|
28
|
+
</ul>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function TreeNode(props: { item: TreeItem; depth: number }) {
|
|
33
|
+
const { item, depth } = props
|
|
34
|
+
const isDir = item.type === "directory"
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<li className="py-0.5">
|
|
38
|
+
<span className="inline-flex items-center gap-2">
|
|
39
|
+
{isDir && <Folder className="size-4 shrink-0 text-primary" />}
|
|
40
|
+
{item.type === "file" && (
|
|
41
|
+
<File className="size-4 shrink-0 text-muted-foreground" />
|
|
42
|
+
)}
|
|
43
|
+
<span
|
|
44
|
+
className={[
|
|
45
|
+
isDir && "font-semibold",
|
|
46
|
+
item.type === "placeholder" && "text-muted-foreground",
|
|
47
|
+
item.highlighted && "rounded bg-primary/10 px-1 text-primary",
|
|
48
|
+
]
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join(" ")}
|
|
51
|
+
>
|
|
52
|
+
{item.name}
|
|
53
|
+
</span>
|
|
54
|
+
{item.comment && (
|
|
55
|
+
<span className="font-sans text-xs text-muted-foreground">{item.comment}</span>
|
|
56
|
+
)}
|
|
57
|
+
</span>
|
|
58
|
+
{item.children && item.children.length > 0 && (
|
|
59
|
+
<TreeItems items={item.children} depth={depth + 1} />
|
|
60
|
+
)}
|
|
61
|
+
</li>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ComponentProps, useEffect, useState } from "react"
|
|
2
|
+
import { HoverCard, HoverCardContent, HoverCardTrigger } from "../elements/hover-card.tsx"
|
|
3
|
+
import { useArticleHref } from "../helpers/article-link.ts"
|
|
4
|
+
import { useMessages } from "../hooks/messages.ts"
|
|
5
|
+
|
|
6
|
+
const FOOTNOTE_PREFIX = "#user-content-fn-"
|
|
7
|
+
|
|
8
|
+
/** Wraps footnote reference links in a HoverCard that previews footnote content inline,
|
|
9
|
+
* and points relative links to Markdown files at their articles */
|
|
10
|
+
export function FootnoteRef(props: ComponentProps<"a">) {
|
|
11
|
+
const href = useArticleHref(props.href) ?? ""
|
|
12
|
+
if (!href.startsWith(FOOTNOTE_PREFIX)) {
|
|
13
|
+
return <a {...props} href={props.href && href} />
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return <FootnoteHoverCard {...props} href={href} />
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function FootnoteHoverCard(props: ComponentProps<"a"> & { href: string }) {
|
|
20
|
+
const [footnoteHtml, setFootnoteHtml] = useState("")
|
|
21
|
+
const t = useMessages()
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const targetId = props.href.slice(1)
|
|
25
|
+
const el = document.getElementById(targetId)
|
|
26
|
+
if (!el) return
|
|
27
|
+
|
|
28
|
+
const wrapper = document.createElement("div")
|
|
29
|
+
wrapper.innerHTML = el.innerHTML
|
|
30
|
+
for (const backref of wrapper.querySelectorAll("[data-footnote-backref]")) {
|
|
31
|
+
backref.remove()
|
|
32
|
+
}
|
|
33
|
+
setFootnoteHtml(wrapper.innerHTML)
|
|
34
|
+
}, [props.href])
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<HoverCard>
|
|
38
|
+
<HoverCardTrigger render={<a {...props} />} />
|
|
39
|
+
<HoverCardContent
|
|
40
|
+
className="prose dark:prose-invert text-sm w-80 [&>p]:my-1 [&>p:first-child]:mt-0 [&>p:last-child]:mb-0"
|
|
41
|
+
side="top"
|
|
42
|
+
sideOffset={8}
|
|
43
|
+
>
|
|
44
|
+
{footnoteHtml ? (
|
|
45
|
+
<div dangerouslySetInnerHTML={{ __html: footnoteHtml }} />
|
|
46
|
+
) : (
|
|
47
|
+
<p className="text-muted-foreground">{t("footnote.missing")}</p>
|
|
48
|
+
)}
|
|
49
|
+
</HoverCardContent>
|
|
50
|
+
</HoverCard>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Heading component that wraps content in an anchor link to itself */
|
|
2
|
+
function createHeading(Tag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6") {
|
|
3
|
+
return function Heading(props: React.HTMLAttributes<HTMLHeadingElement>) {
|
|
4
|
+
if (!props.id) return <Tag {...props} />
|
|
5
|
+
return (
|
|
6
|
+
<Tag {...props}>
|
|
7
|
+
<a href={`#${props.id}`}>{props.children}</a>
|
|
8
|
+
</Tag>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const headingComponents = {
|
|
14
|
+
h1: createHeading("h1"),
|
|
15
|
+
h2: createHeading("h2"),
|
|
16
|
+
h3: createHeading("h3"),
|
|
17
|
+
h4: createHeading("h4"),
|
|
18
|
+
h5: createHeading("h5"),
|
|
19
|
+
h6: createHeading("h6"),
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DynamicIcon } from "../helpers/dynamic-icon.tsx"
|
|
2
|
+
|
|
3
|
+
/** Renders a Lucide icon inline by name */
|
|
4
|
+
export function Icon(props: { name: string; className?: string }) {
|
|
5
|
+
const className = props.className
|
|
6
|
+
? `inline-block size-4 align-text-bottom ${props.className}`
|
|
7
|
+
: "inline-block size-4 align-text-bottom"
|
|
8
|
+
|
|
9
|
+
return <DynamicIcon name={props.name} className={className} />
|
|
10
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ComponentType } from "react"
|
|
2
|
+
import { TocContext } from "../helpers/toc-context.ts"
|
|
3
|
+
import type { TocItem } from "../models/toc.ts"
|
|
4
|
+
import { Abbr } from "./Abbr.tsx"
|
|
5
|
+
import { AnsiCode } from "./AnsiCode.tsx"
|
|
6
|
+
import { Audio } from "./Audio.tsx"
|
|
7
|
+
import { Badge } from "./Badge.tsx"
|
|
8
|
+
import { Button } from "./Button.tsx"
|
|
9
|
+
import { Callout } from "./Callout.tsx"
|
|
10
|
+
import { Card } from "./Card.tsx"
|
|
11
|
+
import { Cards } from "./Cards.tsx"
|
|
12
|
+
import { CodeBlock } from "./CodeBlock.tsx"
|
|
13
|
+
import { CodeTabs } from "./CodeTabs.tsx"
|
|
14
|
+
import { Column } from "./Column.tsx"
|
|
15
|
+
import { Columns } from "./Columns.tsx"
|
|
16
|
+
import { DefinitionDetail, DefinitionList, DefinitionTerm } from "./DefinitionList.tsx"
|
|
17
|
+
import { Details } from "./Details.tsx"
|
|
18
|
+
import { Filetree } from "./Filetree.tsx"
|
|
19
|
+
import { FootnoteRef } from "./FootnoteRef.tsx"
|
|
20
|
+
import { headingComponents } from "./Heading.tsx"
|
|
21
|
+
import { Icon } from "./Icon.tsx"
|
|
22
|
+
import { Mermaid } from "./Mermaid.tsx"
|
|
23
|
+
import { PackageTabs } from "./PackageTabs.tsx"
|
|
24
|
+
import {
|
|
25
|
+
SortableTable,
|
|
26
|
+
TableBody,
|
|
27
|
+
TableCell,
|
|
28
|
+
TableHead,
|
|
29
|
+
TableHeader,
|
|
30
|
+
TableRow,
|
|
31
|
+
} from "./SortableTable.tsx"
|
|
32
|
+
import { Tab } from "./Tab.tsx"
|
|
33
|
+
import { Tabs } from "./Tabs.tsx"
|
|
34
|
+
import { Toc } from "./Toc.tsx"
|
|
35
|
+
import { Video } from "./Video.tsx"
|
|
36
|
+
import { ZoomImage } from "./ZoomImage.tsx"
|
|
37
|
+
import { Separator } from "../elements/separator.tsx"
|
|
38
|
+
|
|
39
|
+
type MarkdownComponents = Record<string, ComponentType<never>>
|
|
40
|
+
|
|
41
|
+
/** Renders compiled Markdown with Livemark's components for its HTML elements,
|
|
42
|
+
* directives and code blocks; `components` adds or replaces any of them. */
|
|
43
|
+
export function Markdown(props: {
|
|
44
|
+
component: ComponentType<{ components?: object }>
|
|
45
|
+
toc?: TocItem[]
|
|
46
|
+
components?: MarkdownComponents
|
|
47
|
+
}) {
|
|
48
|
+
const Content = props.component
|
|
49
|
+
const content = <Content components={{ ...COMPONENTS, ...props.components }} />
|
|
50
|
+
if (!props.toc) return content
|
|
51
|
+
return <TocContext.Provider value={props.toc}>{content}</TocContext.Provider>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const COMPONENTS = {
|
|
55
|
+
hr: Separator,
|
|
56
|
+
img: ZoomImage,
|
|
57
|
+
pre: CodeBlock,
|
|
58
|
+
table: SortableTable,
|
|
59
|
+
thead: TableHeader,
|
|
60
|
+
tbody: TableBody,
|
|
61
|
+
tr: TableRow,
|
|
62
|
+
th: TableHead,
|
|
63
|
+
td: TableCell,
|
|
64
|
+
a: FootnoteRef,
|
|
65
|
+
Abbr,
|
|
66
|
+
AnsiCode,
|
|
67
|
+
Audio,
|
|
68
|
+
Badge,
|
|
69
|
+
Button,
|
|
70
|
+
Callout,
|
|
71
|
+
Card,
|
|
72
|
+
Cards,
|
|
73
|
+
Column,
|
|
74
|
+
Columns,
|
|
75
|
+
CodeTabs,
|
|
76
|
+
DefinitionDetail,
|
|
77
|
+
DefinitionList,
|
|
78
|
+
DefinitionTerm,
|
|
79
|
+
Details,
|
|
80
|
+
Filetree,
|
|
81
|
+
Icon,
|
|
82
|
+
Mermaid,
|
|
83
|
+
PackageTabs,
|
|
84
|
+
Tab,
|
|
85
|
+
Tabs,
|
|
86
|
+
Toc,
|
|
87
|
+
Video,
|
|
88
|
+
...headingComponents,
|
|
89
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Suspense, lazy } from "react"
|
|
2
|
+
import { useMessages } from "../hooks/messages.ts"
|
|
3
|
+
|
|
4
|
+
const MermaidRenderer = lazy(() => import("./MermaidRenderer.tsx"))
|
|
5
|
+
|
|
6
|
+
/** Lazily loads and renders a Mermaid diagram */
|
|
7
|
+
export function Mermaid(props: { children: string }) {
|
|
8
|
+
const t = useMessages()
|
|
9
|
+
return (
|
|
10
|
+
<Suspense
|
|
11
|
+
fallback={
|
|
12
|
+
<div className="not-prose mermaid flex justify-center">
|
|
13
|
+
{t("media.loadingDiagram")}
|
|
14
|
+
</div>
|
|
15
|
+
}
|
|
16
|
+
>
|
|
17
|
+
<MermaidRenderer>{props.children}</MermaidRenderer>
|
|
18
|
+
</Suspense>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react"
|
|
2
|
+
import mermaid from "mermaid"
|
|
3
|
+
|
|
4
|
+
mermaid.initialize({ startOnLoad: false })
|
|
5
|
+
|
|
6
|
+
// Module-wide so runs never overlap, even across effect re-runs: overlapping runs on
|
|
7
|
+
// one node reject with "reading 'firstChild'".
|
|
8
|
+
let renderQueue = Promise.resolve()
|
|
9
|
+
|
|
10
|
+
/** Renders a Mermaid diagram from a chart definition string */
|
|
11
|
+
export default function MermaidRenderer(props: { children: string }) {
|
|
12
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const el = ref.current
|
|
16
|
+
if (!el) return
|
|
17
|
+
|
|
18
|
+
const render = () => {
|
|
19
|
+
renderQueue = renderQueue
|
|
20
|
+
.then(() => {
|
|
21
|
+
if (!el.isConnected) return
|
|
22
|
+
const isDark = document.documentElement.classList.contains("dark")
|
|
23
|
+
mermaid.initialize({
|
|
24
|
+
startOnLoad: false,
|
|
25
|
+
theme: isDark ? "dark" : "default",
|
|
26
|
+
})
|
|
27
|
+
el.textContent = props.children
|
|
28
|
+
el.removeAttribute("data-processed")
|
|
29
|
+
return mermaid.run({ nodes: [el] })
|
|
30
|
+
})
|
|
31
|
+
.catch(error => {
|
|
32
|
+
if (el.isConnected) console.error(error)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
render()
|
|
37
|
+
|
|
38
|
+
const observer = new MutationObserver(render)
|
|
39
|
+
observer.observe(document.documentElement, {
|
|
40
|
+
attributes: true,
|
|
41
|
+
attributeFilter: ["class"],
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return () => observer.disconnect()
|
|
45
|
+
}, [props.children])
|
|
46
|
+
|
|
47
|
+
return <div ref={ref} className="not-prose mermaid flex justify-center" />
|
|
48
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Check, Clipboard } from "lucide-react"
|
|
2
|
+
import { useState } from "react"
|
|
3
|
+
import { useMessages } from "../hooks/messages.ts"
|
|
4
|
+
|
|
5
|
+
type Manager = "npm" | "pnpm" | "yarn" | "bun"
|
|
6
|
+
|
|
7
|
+
const managers: Manager[] = ["npm", "pnpm", "yarn", "bun"]
|
|
8
|
+
|
|
9
|
+
interface PackageTabsProps {
|
|
10
|
+
npm: string
|
|
11
|
+
pnpm: string
|
|
12
|
+
yarn: string
|
|
13
|
+
bun: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Tabbed code block showing a command for each package manager */
|
|
17
|
+
export function PackageTabs(props: PackageTabsProps) {
|
|
18
|
+
const [active, setActive] = useState<Manager>("npm")
|
|
19
|
+
const [copied, setCopied] = useState(false)
|
|
20
|
+
const t = useMessages()
|
|
21
|
+
|
|
22
|
+
const commands: Record<Manager, string> = {
|
|
23
|
+
npm: props.npm,
|
|
24
|
+
pnpm: props.pnpm,
|
|
25
|
+
yarn: props.yarn,
|
|
26
|
+
bun: props.bun,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const handleCopy = () => {
|
|
30
|
+
navigator.clipboard.writeText(commands[active])
|
|
31
|
+
setCopied(true)
|
|
32
|
+
setTimeout(() => setCopied(false), 2000)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<figure className="not-prose my-4 relative rounded-card border shadow-sm overflow-hidden text-sm bg-sidebar dark:bg-sidebar">
|
|
37
|
+
<div className="flex items-center border-b">
|
|
38
|
+
<div className="flex flex-1 items-center">
|
|
39
|
+
{managers.map(pm => (
|
|
40
|
+
<button
|
|
41
|
+
key={pm}
|
|
42
|
+
type="button"
|
|
43
|
+
onClick={() => setActive(pm)}
|
|
44
|
+
className={`px-3 py-2 text-xs font-medium transition-colors ${
|
|
45
|
+
pm === active
|
|
46
|
+
? "text-foreground border-b-2 border-foreground"
|
|
47
|
+
: "text-muted-foreground hover:text-foreground"
|
|
48
|
+
}`}
|
|
49
|
+
>
|
|
50
|
+
{pm}
|
|
51
|
+
</button>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
onClick={handleCopy}
|
|
57
|
+
className="me-2 p-1 rounded-control text-muted-foreground hover:text-foreground transition-colors"
|
|
58
|
+
aria-label={t("code.copy")}
|
|
59
|
+
>
|
|
60
|
+
{copied ? <Check className="size-3.5" /> : <Clipboard className="size-3.5" />}
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="text-[0.8125rem] py-3.5 overflow-auto max-h-[600px]">
|
|
64
|
+
<pre className="min-w-full w-max">
|
|
65
|
+
<code className="flex flex-col">
|
|
66
|
+
<span className="px-4">{commands[active]}</span>
|
|
67
|
+
</code>
|
|
68
|
+
</pre>
|
|
69
|
+
</div>
|
|
70
|
+
</figure>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useEffect, useRef, type ComponentProps } from "react"
|
|
2
|
+
import {
|
|
3
|
+
Table,
|
|
4
|
+
TableHeader,
|
|
5
|
+
TableBody,
|
|
6
|
+
TableRow,
|
|
7
|
+
TableHead,
|
|
8
|
+
TableCell,
|
|
9
|
+
} from "../elements/table.tsx"
|
|
10
|
+
|
|
11
|
+
/** Wraps markdown tables with click-to-sort column headers */
|
|
12
|
+
export function SortableTable(props: ComponentProps<"table">) {
|
|
13
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const container = ref.current
|
|
17
|
+
if (!container) return
|
|
18
|
+
|
|
19
|
+
let activeCol = -1
|
|
20
|
+
let ascending = true
|
|
21
|
+
|
|
22
|
+
const headers = container.querySelectorAll<HTMLElement>("th")
|
|
23
|
+
const cleanups: (() => void)[] = []
|
|
24
|
+
|
|
25
|
+
headers.forEach((th, colIndex) => {
|
|
26
|
+
th.style.cursor = "pointer"
|
|
27
|
+
th.classList.add("select-none")
|
|
28
|
+
|
|
29
|
+
const handler = () => {
|
|
30
|
+
if (activeCol === colIndex) {
|
|
31
|
+
ascending = !ascending
|
|
32
|
+
} else {
|
|
33
|
+
activeCol = colIndex
|
|
34
|
+
ascending = true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const tbody = container.querySelector("tbody")
|
|
38
|
+
if (!tbody) return
|
|
39
|
+
|
|
40
|
+
const rows = Array.from(tbody.querySelectorAll("tr"))
|
|
41
|
+
rows.sort((a, b) => {
|
|
42
|
+
const aVal = a.children[colIndex]?.textContent?.trim() ?? ""
|
|
43
|
+
const bVal = b.children[colIndex]?.textContent?.trim() ?? ""
|
|
44
|
+
const aNum = Number(aVal)
|
|
45
|
+
const bNum = Number(bVal)
|
|
46
|
+
const cmp =
|
|
47
|
+
aVal !== "" && bVal !== "" && !isNaN(aNum) && !isNaN(bNum)
|
|
48
|
+
? aNum - bNum
|
|
49
|
+
: aVal.localeCompare(bVal)
|
|
50
|
+
return ascending ? cmp : -cmp
|
|
51
|
+
})
|
|
52
|
+
for (const row of rows) tbody.appendChild(row)
|
|
53
|
+
|
|
54
|
+
headers.forEach((h, i) => {
|
|
55
|
+
const indicator = h.querySelector("[data-sort]")
|
|
56
|
+
if (indicator) indicator.remove()
|
|
57
|
+
if (i === colIndex) {
|
|
58
|
+
const span = document.createElement("span")
|
|
59
|
+
span.dataset.sort = ""
|
|
60
|
+
span.className = "ml-1 text-muted-foreground"
|
|
61
|
+
span.textContent = ascending ? "↑" : "↓"
|
|
62
|
+
h.appendChild(span)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
th.addEventListener("click", handler)
|
|
68
|
+
cleanups.push(() => th.removeEventListener("click", handler))
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return () => cleanups.forEach(fn => fn())
|
|
72
|
+
}, [])
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div ref={ref} className="not-prose">
|
|
76
|
+
<Table {...props} />
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export { TableHeader, TableBody, TableRow, TableHead, TableCell }
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Children, isValidElement, useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
/** Tabbed container component for grouping content tabs with optional sync across groups */
|
|
4
|
+
export function Tabs(props: { sync?: string; children: React.ReactNode }) {
|
|
5
|
+
const children = Children.toArray(props.children)
|
|
6
|
+
const titles = children.map(child =>
|
|
7
|
+
isValidElement<{ title: string }>(child) ? child.props.title : "",
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
const getInitialIndex = () => {
|
|
11
|
+
if (!props.sync) return 0
|
|
12
|
+
const saved =
|
|
13
|
+
typeof window !== "undefined"
|
|
14
|
+
? localStorage.getItem("livemark-tab-" + props.sync)
|
|
15
|
+
: null
|
|
16
|
+
if (!saved) return 0
|
|
17
|
+
const idx = titles.indexOf(saved)
|
|
18
|
+
return idx >= 0 ? idx : 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const [active, setActive] = useState(getInitialIndex)
|
|
22
|
+
|
|
23
|
+
const handleSelect = (i: number) => {
|
|
24
|
+
setActive(i)
|
|
25
|
+
if (props.sync) {
|
|
26
|
+
localStorage.setItem("livemark-tab-" + props.sync, titles[i]!)
|
|
27
|
+
window.dispatchEvent(
|
|
28
|
+
new CustomEvent("livemark-tab-sync", {
|
|
29
|
+
detail: { sync: props.sync, title: titles[i] },
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!props.sync) return
|
|
37
|
+
const handler = (e: Event) => {
|
|
38
|
+
const { sync, title } = (e as CustomEvent<{ sync: string; title: string }>).detail
|
|
39
|
+
if (sync !== props.sync) return
|
|
40
|
+
const idx = titles.indexOf(title)
|
|
41
|
+
if (idx >= 0) setActive(idx)
|
|
42
|
+
}
|
|
43
|
+
window.addEventListener("livemark-tab-sync", handler)
|
|
44
|
+
return () => window.removeEventListener("livemark-tab-sync", handler)
|
|
45
|
+
}, [props.sync, titles])
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="not-prose my-4 rounded-card border bg-card">
|
|
49
|
+
<div className="flex border-b">
|
|
50
|
+
{titles.map((title, i) => (
|
|
51
|
+
<button
|
|
52
|
+
key={title}
|
|
53
|
+
type="button"
|
|
54
|
+
onClick={() => handleSelect(i)}
|
|
55
|
+
className={`px-4 py-2 text-sm font-medium transition-colors cursor-pointer ${
|
|
56
|
+
i === active
|
|
57
|
+
? "text-foreground border-b-2 border-primary -mb-px"
|
|
58
|
+
: "text-muted-foreground hover:text-foreground"
|
|
59
|
+
}`}
|
|
60
|
+
>
|
|
61
|
+
{title}
|
|
62
|
+
</button>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
{children.map((child, i) => (
|
|
66
|
+
<div
|
|
67
|
+
key={titles[i]}
|
|
68
|
+
className={i === active ? "p-4 prose dark:prose-invert max-w-none" : "hidden"}
|
|
69
|
+
>
|
|
70
|
+
{child}
|
|
71
|
+
</div>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useTocItems } from "../helpers/toc-context.ts"
|
|
2
|
+
import { useMessages } from "../hooks/messages.ts"
|
|
3
|
+
|
|
4
|
+
/** Renders the page's table of contents inline within the content */
|
|
5
|
+
export function Toc(props: { maxLevel?: string }) {
|
|
6
|
+
const allItems = useTocItems()
|
|
7
|
+
const t = useMessages()
|
|
8
|
+
const maxDepth = props.maxLevel ? Number(props.maxLevel) : 4
|
|
9
|
+
const items = allItems.filter(item => item.depth <= maxDepth)
|
|
10
|
+
|
|
11
|
+
if (items.length === 0) return null
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<nav className="not-prose my-8 rounded-card border border-border bg-card p-6">
|
|
15
|
+
<p className="text-base font-semibold mb-4">{t("toc.inline")}</p>
|
|
16
|
+
<ul className="text-base space-y-2.5">
|
|
17
|
+
{items.map(item => (
|
|
18
|
+
<li key={item.url} style={{ paddingLeft: `${(item.depth - 2) * 1.25}rem` }}>
|
|
19
|
+
<a
|
|
20
|
+
href={item.url}
|
|
21
|
+
className="text-muted-foreground hover:text-primary transition-colors"
|
|
22
|
+
>
|
|
23
|
+
{item.title}
|
|
24
|
+
</a>
|
|
25
|
+
</li>
|
|
26
|
+
))}
|
|
27
|
+
</ul>
|
|
28
|
+
</nav>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { parseYoutubeId } from "../helpers/youtube-id.ts"
|
|
2
|
+
import { useMessages } from "../hooks/messages.ts"
|
|
3
|
+
/** Renders a responsive YouTube iframe embed */
|
|
4
|
+
export function Video(props: { url: string }) {
|
|
5
|
+
const t = useMessages()
|
|
6
|
+
const id = parseYoutubeId(props.url)
|
|
7
|
+
if (!id) {
|
|
8
|
+
return (
|
|
9
|
+
<p>
|
|
10
|
+
<a href={props.url}>{props.url}</a>
|
|
11
|
+
</p>
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="not-prose my-4 overflow-hidden rounded-card border">
|
|
17
|
+
<div className="aspect-video">
|
|
18
|
+
<iframe
|
|
19
|
+
className="h-full w-full"
|
|
20
|
+
src={`https://www.youtube.com/embed/${id}`}
|
|
21
|
+
title={t("media.youtube")}
|
|
22
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
23
|
+
allowFullScreen
|
|
24
|
+
/>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|