@brett_lamy/docstream 0.1.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/package.json +32 -0
- package/src/assets.ts +20 -0
- package/src/docs/DocsRenderer.tsx +311 -0
- package/src/docs/HighlightedCode.tsx +71 -0
- package/src/docs/Mermaid.tsx +37 -0
- package/src/gitbook/ast.ts +194 -0
- package/src/gitbook/inline.ts +202 -0
- package/src/gitbook/parse.ts +594 -0
- package/src/gitbook/serialize.ts +151 -0
- package/src/index.ts +11 -0
- package/src/openapi/OpenApiOperation.tsx +238 -0
- package/src/streamdown.tsx +33 -0
- package/src/styles.css +727 -0
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brett_lamy/docstream",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "GitBook-aware readonly markdown and AI stream renderer.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"sideEffects": [
|
|
12
|
+
"**/*.css"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./src/index.ts",
|
|
17
|
+
"import": "./src/index.ts"
|
|
18
|
+
},
|
|
19
|
+
"./styles.css": "./src/styles.css"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"hast-util-to-html": "^9.0.5",
|
|
23
|
+
"lowlight": "^3.3.0",
|
|
24
|
+
"lucide-react": "^1.17.0",
|
|
25
|
+
"mermaid": "^11.15.0",
|
|
26
|
+
"streamdown": "^2.5.0",
|
|
27
|
+
"yaml": "^2.9.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": ">=18"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/assets.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Resolves repo-relative image paths (e.g. "readme-assets/logo.svg") against
|
|
2
|
+
// the raw.githubusercontent.com URL of the file being edited. The AST keeps
|
|
3
|
+
// the original relative src; only rendering resolves it.
|
|
4
|
+
|
|
5
|
+
let assetBase: string | null = null
|
|
6
|
+
|
|
7
|
+
export function setAssetBase(repo: string | null, branch: string | null, filePath: string | null) {
|
|
8
|
+
if (!repo || !branch || !filePath) {
|
|
9
|
+
assetBase = null
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
const dir = filePath.includes("/") ? filePath.slice(0, filePath.lastIndexOf("/") + 1) : ""
|
|
13
|
+
assetBase = `https://raw.githubusercontent.com/${repo}/${branch}/${dir}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveAsset(src: string): string {
|
|
17
|
+
if (!src || /^(https?:|data:|blob:)/i.test(src)) return src
|
|
18
|
+
if (!assetBase) return src
|
|
19
|
+
return assetBase + src.replace(/^\.\//, "")
|
|
20
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { useState, type ReactNode } from "react"
|
|
2
|
+
import {
|
|
3
|
+
AlertTriangle,
|
|
4
|
+
CheckCircle2,
|
|
5
|
+
ChevronRight,
|
|
6
|
+
File,
|
|
7
|
+
Info,
|
|
8
|
+
XCircle,
|
|
9
|
+
} from "lucide-react"
|
|
10
|
+
|
|
11
|
+
import type { Block, DocumentNode, Inline } from "../gitbook/ast"
|
|
12
|
+
import { resolveAsset } from "../assets"
|
|
13
|
+
import { parseMarkdown } from "../gitbook/parse"
|
|
14
|
+
import { OpenApiOperation } from "../openapi/OpenApiOperation"
|
|
15
|
+
import { Mermaid } from "./Mermaid"
|
|
16
|
+
import { HighlightedCode } from "./HighlightedCode"
|
|
17
|
+
|
|
18
|
+
function InlineText({ nodes }: { nodes: Inline[] }) {
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
{nodes.map((n, i) => {
|
|
22
|
+
if (n.type === "image") {
|
|
23
|
+
const img = (
|
|
24
|
+
<img
|
|
25
|
+
src={resolveAsset(n.src)}
|
|
26
|
+
alt={n.alt ?? ""}
|
|
27
|
+
className="docs-inline-img"
|
|
28
|
+
style={{ width: n.width, height: n.height }}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
return n.link ? (
|
|
32
|
+
<a key={i} href={n.link} target="_blank" rel="noreferrer" className="docs-inline-img-link">
|
|
33
|
+
{img}
|
|
34
|
+
</a>
|
|
35
|
+
) : (
|
|
36
|
+
<span key={i}>{img}</span>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
let el: ReactNode = n.text
|
|
40
|
+
if (n.code) el = <code>{el}</code>
|
|
41
|
+
if (n.bold) el = <strong>{el}</strong>
|
|
42
|
+
if (n.italic) el = <em>{el}</em>
|
|
43
|
+
if (n.strike) el = <s>{el}</s>
|
|
44
|
+
if (n.link)
|
|
45
|
+
el = (
|
|
46
|
+
<a href={n.link} target="_blank" rel="noreferrer">
|
|
47
|
+
{el}
|
|
48
|
+
</a>
|
|
49
|
+
)
|
|
50
|
+
return <span key={i}>{el}</span>
|
|
51
|
+
})}
|
|
52
|
+
</>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const HINT_ICONS = {
|
|
57
|
+
info: Info,
|
|
58
|
+
success: CheckCircle2,
|
|
59
|
+
warning: AlertTriangle,
|
|
60
|
+
danger: XCircle,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function embedSrc(url: string): string | null {
|
|
64
|
+
const yt = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/)
|
|
65
|
+
return yt ? `https://www.youtube.com/embed/${yt[1]}` : null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Tabs({ block }: { block: Extract<Block, { type: "tabs" }> }) {
|
|
69
|
+
const [active, setActive] = useState(0)
|
|
70
|
+
return (
|
|
71
|
+
<div className="docs-tabs">
|
|
72
|
+
<div className="docs-tabs-header">
|
|
73
|
+
{block.tabs.map((t, i) => (
|
|
74
|
+
<button
|
|
75
|
+
key={i}
|
|
76
|
+
className={i === active ? "docs-tab-active" : ""}
|
|
77
|
+
onClick={() => setActive(i)}
|
|
78
|
+
>
|
|
79
|
+
{t.title}
|
|
80
|
+
</button>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
<div className="docs-tabs-body">
|
|
84
|
+
<Blocks blocks={block.tabs[active]?.children ?? []} />
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function BlockView({ block }: { block: Block }) {
|
|
91
|
+
switch (block.type) {
|
|
92
|
+
case "paragraph":
|
|
93
|
+
return (
|
|
94
|
+
<p>
|
|
95
|
+
<InlineText nodes={block.children} />
|
|
96
|
+
</p>
|
|
97
|
+
)
|
|
98
|
+
case "heading": {
|
|
99
|
+
const Tag = `h${block.level}` as const
|
|
100
|
+
return (
|
|
101
|
+
<Tag>
|
|
102
|
+
<InlineText nodes={block.children} />
|
|
103
|
+
</Tag>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
case "code":
|
|
107
|
+
if (block.language === "mermaid") return <Mermaid code={block.code} />
|
|
108
|
+
return (
|
|
109
|
+
<div className="docs-code">
|
|
110
|
+
{(block.title || block.language) && (
|
|
111
|
+
<div className="docs-code-header">
|
|
112
|
+
<span>{block.title}</span>
|
|
113
|
+
<span className="docs-code-lang">{block.language}</span>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
<pre className={block.lineNumbers ? "docs-code-numbered" : ""}>
|
|
117
|
+
<HighlightedCode
|
|
118
|
+
code={block.code}
|
|
119
|
+
language={block.language}
|
|
120
|
+
lineNumbers={block.lineNumbers}
|
|
121
|
+
/>
|
|
122
|
+
</pre>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
case "hint": {
|
|
126
|
+
const Icon = HINT_ICONS[block.style] ?? Info
|
|
127
|
+
return (
|
|
128
|
+
<div className={`docs-hint docs-hint-${block.style}`}>
|
|
129
|
+
<Icon className="docs-hint-icon" />
|
|
130
|
+
<div>
|
|
131
|
+
<Blocks blocks={block.children} />
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
case "tabs":
|
|
137
|
+
return <Tabs block={block} />
|
|
138
|
+
case "expandable":
|
|
139
|
+
return (
|
|
140
|
+
<details className="docs-expandable">
|
|
141
|
+
<summary>{block.summary}</summary>
|
|
142
|
+
<div className="docs-expandable-body">
|
|
143
|
+
<Blocks blocks={block.children} />
|
|
144
|
+
</div>
|
|
145
|
+
</details>
|
|
146
|
+
)
|
|
147
|
+
case "stepper":
|
|
148
|
+
return (
|
|
149
|
+
<div className="docs-stepper">
|
|
150
|
+
{block.steps.map((s, i) => (
|
|
151
|
+
<div key={i} className="docs-step">
|
|
152
|
+
<div className="docs-step-rail">
|
|
153
|
+
<div className="docs-step-badge">{i + 1}</div>
|
|
154
|
+
{i < block.steps.length - 1 && <div className="docs-step-line" />}
|
|
155
|
+
</div>
|
|
156
|
+
<div className="docs-step-main">
|
|
157
|
+
{s.title && <div className="docs-step-title">{s.title}</div>}
|
|
158
|
+
<Blocks blocks={s.children} />
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
))}
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
case "embed": {
|
|
165
|
+
const src = embedSrc(block.url)
|
|
166
|
+
return src ? (
|
|
167
|
+
<iframe className="docs-embed" src={src} title={block.url} allowFullScreen />
|
|
168
|
+
) : (
|
|
169
|
+
<a className="docs-content-ref" href={block.url} target="_blank" rel="noreferrer">
|
|
170
|
+
<File className="size-4" /> {block.url}
|
|
171
|
+
</a>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
case "content-ref":
|
|
175
|
+
return (
|
|
176
|
+
<a className="docs-content-ref" href={block.url}>
|
|
177
|
+
<File className="size-4" />
|
|
178
|
+
<span>
|
|
179
|
+
<InlineText nodes={block.children} />
|
|
180
|
+
</span>
|
|
181
|
+
<ChevronRight className="size-4 ml-auto" />
|
|
182
|
+
</a>
|
|
183
|
+
)
|
|
184
|
+
case "columns":
|
|
185
|
+
return (
|
|
186
|
+
<div className="docs-columns">
|
|
187
|
+
{block.columns.map((c, i) => (
|
|
188
|
+
<div key={i} className="docs-column">
|
|
189
|
+
<Blocks blocks={c.children} />
|
|
190
|
+
</div>
|
|
191
|
+
))}
|
|
192
|
+
</div>
|
|
193
|
+
)
|
|
194
|
+
case "figure":
|
|
195
|
+
return (
|
|
196
|
+
<figure className="docs-figure">
|
|
197
|
+
{block.src && <img src={resolveAsset(block.src)} alt={block.alt} />}
|
|
198
|
+
{block.caption && <figcaption>{block.caption}</figcaption>}
|
|
199
|
+
</figure>
|
|
200
|
+
)
|
|
201
|
+
case "list": {
|
|
202
|
+
const Tag = block.ordered ? "ol" : "ul"
|
|
203
|
+
return (
|
|
204
|
+
<Tag className={block.task ? "docs-tasklist" : undefined}>
|
|
205
|
+
{block.items.map((item, i) => (
|
|
206
|
+
<li key={i}>
|
|
207
|
+
{block.task && <input type="checkbox" checked={!!item.checked} readOnly />}
|
|
208
|
+
<Blocks blocks={item.children} inline />
|
|
209
|
+
</li>
|
|
210
|
+
))}
|
|
211
|
+
</Tag>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
case "blockquote":
|
|
215
|
+
return (
|
|
216
|
+
<blockquote>
|
|
217
|
+
<Blocks blocks={block.children} />
|
|
218
|
+
</blockquote>
|
|
219
|
+
)
|
|
220
|
+
case "divider":
|
|
221
|
+
return <hr />
|
|
222
|
+
case "table":
|
|
223
|
+
if (block.view === "cards") {
|
|
224
|
+
const cards = block.rows.length ? block.rows : [block.header]
|
|
225
|
+
return (
|
|
226
|
+
<div className="docs-cards">
|
|
227
|
+
{cards.map((row, i) => (
|
|
228
|
+
<div key={i} className="docs-card">
|
|
229
|
+
{row.map((cell, j) => (
|
|
230
|
+
<div key={j} className={j === 0 ? "docs-card-title" : "docs-card-body"}>
|
|
231
|
+
<InlineText nodes={cell} />
|
|
232
|
+
</div>
|
|
233
|
+
))}
|
|
234
|
+
</div>
|
|
235
|
+
))}
|
|
236
|
+
</div>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
return (
|
|
240
|
+
<table className="docs-table">
|
|
241
|
+
<thead>
|
|
242
|
+
<tr>
|
|
243
|
+
{block.header.map((cell, i) => (
|
|
244
|
+
<th key={i}>
|
|
245
|
+
<InlineText nodes={cell} />
|
|
246
|
+
</th>
|
|
247
|
+
))}
|
|
248
|
+
</tr>
|
|
249
|
+
</thead>
|
|
250
|
+
<tbody>
|
|
251
|
+
{block.rows.map((row, i) => (
|
|
252
|
+
<tr key={i}>
|
|
253
|
+
{row.map((cell, j) => (
|
|
254
|
+
<td key={j}>
|
|
255
|
+
<InlineText nodes={cell} />
|
|
256
|
+
</td>
|
|
257
|
+
))}
|
|
258
|
+
</tr>
|
|
259
|
+
))}
|
|
260
|
+
</tbody>
|
|
261
|
+
</table>
|
|
262
|
+
)
|
|
263
|
+
case "math":
|
|
264
|
+
return <pre className="docs-math">{block.formula}</pre>
|
|
265
|
+
case "updates":
|
|
266
|
+
return (
|
|
267
|
+
<div className="docs-updates">
|
|
268
|
+
{block.updates.map((u, i) => (
|
|
269
|
+
<div key={i} className="docs-update">
|
|
270
|
+
<div className="docs-update-date">{u.date}</div>
|
|
271
|
+
<div className="docs-update-body">
|
|
272
|
+
<Blocks blocks={u.children} />
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
))}
|
|
276
|
+
</div>
|
|
277
|
+
)
|
|
278
|
+
case "openapi-operation":
|
|
279
|
+
return (
|
|
280
|
+
<OpenApiOperation
|
|
281
|
+
specUrl={block.specUrl}
|
|
282
|
+
path={block.path}
|
|
283
|
+
method={block.method || "get"}
|
|
284
|
+
/>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function Blocks({ blocks, inline }: { blocks: Block[]; inline?: boolean }) {
|
|
290
|
+
return (
|
|
291
|
+
<div className={inline ? "docs-blocks-inline" : undefined} style={inline ? { display: "inline" } : undefined}>
|
|
292
|
+
{blocks.map((b, i) => (
|
|
293
|
+
<BlockView key={i} block={b} />
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Renders a markdown string inline — used for embedded markdown like
|
|
300
|
+
// OpenAPI descriptions, which can themselves contain GitBook blocks.
|
|
301
|
+
export function MarkdownContent({ markdown }: { markdown: string }) {
|
|
302
|
+
return <Blocks blocks={parseMarkdown(markdown).children} />
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function DocsRenderer({ doc }: { doc: DocumentNode }) {
|
|
306
|
+
return (
|
|
307
|
+
<article className="docs-article">
|
|
308
|
+
<Blocks blocks={doc.children} />
|
|
309
|
+
</article>
|
|
310
|
+
)
|
|
311
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useMemo } from "react"
|
|
2
|
+
import { common, createLowlight } from "lowlight"
|
|
3
|
+
import { toHtml } from "hast-util-to-html"
|
|
4
|
+
|
|
5
|
+
const lowlight = createLowlight(common)
|
|
6
|
+
|
|
7
|
+
const LANGUAGE_ALIASES: Record<string, string> = {
|
|
8
|
+
js: "javascript",
|
|
9
|
+
jsx: "javascript",
|
|
10
|
+
sh: "bash",
|
|
11
|
+
shell: "bash",
|
|
12
|
+
ts: "typescript",
|
|
13
|
+
tsx: "typescript",
|
|
14
|
+
yml: "yaml",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeLanguage(language: string | null) {
|
|
18
|
+
if (!language) return null
|
|
19
|
+
const normalized = language.toLowerCase()
|
|
20
|
+
return LANGUAGE_ALIASES[normalized] ?? normalized
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function highlightedHtml(code: string, language: string | null) {
|
|
24
|
+
const normalized = normalizeLanguage(language)
|
|
25
|
+
if (normalized && lowlight.registered(normalized)) {
|
|
26
|
+
try {
|
|
27
|
+
return toHtml(lowlight.highlight(normalized, code))
|
|
28
|
+
} catch {
|
|
29
|
+
/* fall through to plain */
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function HighlightedCode({
|
|
36
|
+
code,
|
|
37
|
+
language,
|
|
38
|
+
lineNumbers = false,
|
|
39
|
+
}: {
|
|
40
|
+
code: string
|
|
41
|
+
language: string | null
|
|
42
|
+
lineNumbers?: boolean
|
|
43
|
+
}) {
|
|
44
|
+
const lines = useMemo(() => code.split("\n"), [code])
|
|
45
|
+
const html = useMemo(() => {
|
|
46
|
+
if (lineNumbers) {
|
|
47
|
+
return lines.map((line) => highlightedHtml(line || " ", language))
|
|
48
|
+
}
|
|
49
|
+
return highlightedHtml(code, language)
|
|
50
|
+
}, [code, language, lineNumbers, lines])
|
|
51
|
+
|
|
52
|
+
if (lineNumbers) {
|
|
53
|
+
return (
|
|
54
|
+
<code className="hljs">
|
|
55
|
+
{(html as Array<string | null>).map((lineHtml, index) => (
|
|
56
|
+
<span key={index} className="docs-code-line">
|
|
57
|
+
<span className="docs-code-lineno">{index + 1}</span>
|
|
58
|
+
{lineHtml === null ? (
|
|
59
|
+
<span>{lines[index]}</span>
|
|
60
|
+
) : (
|
|
61
|
+
<span dangerouslySetInnerHTML={{ __html: lineHtml }} />
|
|
62
|
+
)}
|
|
63
|
+
</span>
|
|
64
|
+
))}
|
|
65
|
+
</code>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (html === null) return <code>{code}</code>
|
|
70
|
+
return <code className="hljs" dangerouslySetInnerHTML={{ __html: html as string }} />
|
|
71
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react"
|
|
2
|
+
|
|
3
|
+
let seq = 0
|
|
4
|
+
|
|
5
|
+
export function Mermaid({ code }: { code: string }) {
|
|
6
|
+
const [svg, setSvg] = useState<string | null>(null)
|
|
7
|
+
const [error, setError] = useState<string | null>(null)
|
|
8
|
+
const idRef = useRef(`mermaid-${++seq}`)
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
let cancelled = false
|
|
12
|
+
import("mermaid")
|
|
13
|
+
.then(async ({ default: mermaid }) => {
|
|
14
|
+
mermaid.initialize({ startOnLoad: false, securityLevel: "strict" })
|
|
15
|
+
const { svg } = await mermaid.render(idRef.current, code)
|
|
16
|
+
if (!cancelled) setSvg(svg)
|
|
17
|
+
})
|
|
18
|
+
.catch((e) => {
|
|
19
|
+
if (!cancelled) setError(String(e.message ?? e))
|
|
20
|
+
})
|
|
21
|
+
return () => {
|
|
22
|
+
cancelled = true
|
|
23
|
+
}
|
|
24
|
+
}, [code])
|
|
25
|
+
|
|
26
|
+
if (error) {
|
|
27
|
+
return (
|
|
28
|
+
<pre className="docs-mermaid-error">
|
|
29
|
+
mermaid error: {error}
|
|
30
|
+
{"\n"}
|
|
31
|
+
{code}
|
|
32
|
+
</pre>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
if (!svg) return <div className="docs-mermaid">Rendering diagram…</div>
|
|
36
|
+
return <div className="docs-mermaid" dangerouslySetInnerHTML={{ __html: svg }} />
|
|
37
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// AST for GitBook-flavored markdown. This is the single source of truth that
|
|
2
|
+
// the parser, serializer, TipTap converters, and docs renderer all share.
|
|
3
|
+
|
|
4
|
+
export type HintStyle = "info" | "success" | "warning" | "danger"
|
|
5
|
+
|
|
6
|
+
export interface TextNode {
|
|
7
|
+
type: "text"
|
|
8
|
+
text: string
|
|
9
|
+
bold?: boolean
|
|
10
|
+
italic?: boolean
|
|
11
|
+
strike?: boolean
|
|
12
|
+
code?: boolean
|
|
13
|
+
link?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Inline HTML image, optionally wrapped in a link — GitHub README badge style. */
|
|
17
|
+
export interface InlineImageNode {
|
|
18
|
+
type: "image"
|
|
19
|
+
src: string
|
|
20
|
+
alt?: string
|
|
21
|
+
width?: string
|
|
22
|
+
height?: string
|
|
23
|
+
link?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type Inline = TextNode | InlineImageNode
|
|
27
|
+
|
|
28
|
+
export interface ParagraphNode {
|
|
29
|
+
type: "paragraph"
|
|
30
|
+
children: Inline[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface HeadingNode {
|
|
34
|
+
type: "heading"
|
|
35
|
+
level: 1 | 2 | 3 | 4 | 5 | 6
|
|
36
|
+
children: Inline[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CodeBlockNode {
|
|
40
|
+
type: "code"
|
|
41
|
+
language: string | null
|
|
42
|
+
title: string | null
|
|
43
|
+
lineNumbers: boolean
|
|
44
|
+
code: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface HintNode {
|
|
48
|
+
type: "hint"
|
|
49
|
+
style: HintStyle
|
|
50
|
+
children: Block[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface TabNode {
|
|
54
|
+
type: "tab"
|
|
55
|
+
title: string
|
|
56
|
+
children: Block[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TabsNode {
|
|
60
|
+
type: "tabs"
|
|
61
|
+
tabs: TabNode[]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface ExpandableNode {
|
|
65
|
+
type: "expandable"
|
|
66
|
+
summary: string
|
|
67
|
+
children: Block[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface StepNode {
|
|
71
|
+
type: "step"
|
|
72
|
+
title: string
|
|
73
|
+
children: Block[]
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface StepperNode {
|
|
77
|
+
type: "stepper"
|
|
78
|
+
steps: StepNode[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface EmbedNode {
|
|
82
|
+
type: "embed"
|
|
83
|
+
url: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ContentRefNode {
|
|
87
|
+
type: "content-ref"
|
|
88
|
+
url: string
|
|
89
|
+
children: Inline[]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ColumnNode {
|
|
93
|
+
type: "column"
|
|
94
|
+
children: Block[]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ColumnsNode {
|
|
98
|
+
type: "columns"
|
|
99
|
+
columns: ColumnNode[]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface FigureNode {
|
|
103
|
+
type: "figure"
|
|
104
|
+
src: string
|
|
105
|
+
alt: string
|
|
106
|
+
caption: string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ListItemNode {
|
|
110
|
+
type: "listItem"
|
|
111
|
+
children: Block[]
|
|
112
|
+
checked?: boolean // present only in task lists
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ListNode {
|
|
116
|
+
type: "list"
|
|
117
|
+
ordered: boolean
|
|
118
|
+
task: boolean
|
|
119
|
+
items: ListItemNode[]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface BlockquoteNode {
|
|
123
|
+
type: "blockquote"
|
|
124
|
+
children: Block[]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface DividerNode {
|
|
128
|
+
type: "divider"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface TableNode {
|
|
132
|
+
type: "table"
|
|
133
|
+
header: Inline[][]
|
|
134
|
+
rows: Inline[][][]
|
|
135
|
+
/** GitBook table view, e.g. "cards" for <table data-view="cards"> */
|
|
136
|
+
view?: string
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface UpdateNode {
|
|
140
|
+
type: "update"
|
|
141
|
+
date: string
|
|
142
|
+
children: Block[]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface UpdatesNode {
|
|
146
|
+
type: "updates"
|
|
147
|
+
format: string | null
|
|
148
|
+
updates: UpdateNode[]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface OpenApiOperationNode {
|
|
152
|
+
type: "openapi-operation"
|
|
153
|
+
spec: string
|
|
154
|
+
path: string
|
|
155
|
+
method: string
|
|
156
|
+
specUrl: string
|
|
157
|
+
label: string
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface MathNode {
|
|
161
|
+
type: "math"
|
|
162
|
+
formula: string
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export type Block =
|
|
166
|
+
| ParagraphNode
|
|
167
|
+
| HeadingNode
|
|
168
|
+
| CodeBlockNode
|
|
169
|
+
| HintNode
|
|
170
|
+
| TabsNode
|
|
171
|
+
| ExpandableNode
|
|
172
|
+
| StepperNode
|
|
173
|
+
| EmbedNode
|
|
174
|
+
| ContentRefNode
|
|
175
|
+
| ColumnsNode
|
|
176
|
+
| FigureNode
|
|
177
|
+
| ListNode
|
|
178
|
+
| BlockquoteNode
|
|
179
|
+
| DividerNode
|
|
180
|
+
| TableNode
|
|
181
|
+
| MathNode
|
|
182
|
+
| UpdatesNode
|
|
183
|
+
| OpenApiOperationNode
|
|
184
|
+
|
|
185
|
+
export interface DocumentNode {
|
|
186
|
+
type: "doc"
|
|
187
|
+
children: Block[]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const text = (t: string, marks: Partial<Omit<TextNode, "type" | "text">> = {}): TextNode => ({
|
|
191
|
+
type: "text",
|
|
192
|
+
text: t,
|
|
193
|
+
...marks,
|
|
194
|
+
})
|