@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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { Block, DocumentNode, Inline, ListItemNode } from "./ast"
|
|
2
|
+
import { serializeInline } from "./inline"
|
|
3
|
+
|
|
4
|
+
export function serializeMarkdown(doc: DocumentNode): string {
|
|
5
|
+
return serializeBlocks(doc.children).trimEnd() + "\n"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function serializeBlocks(blocks: Block[]): string {
|
|
9
|
+
return blocks.map(serializeBlock).join("\n\n")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function indent(s: string, pad: string): string {
|
|
13
|
+
return s
|
|
14
|
+
.split("\n")
|
|
15
|
+
.map((l) => (l ? pad + l : l))
|
|
16
|
+
.join("\n")
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function serializeBlock(b: Block): string {
|
|
20
|
+
switch (b.type) {
|
|
21
|
+
case "paragraph":
|
|
22
|
+
return serializeInline(b.children)
|
|
23
|
+
|
|
24
|
+
case "heading": {
|
|
25
|
+
const inline = serializeInline(b.children)
|
|
26
|
+
return inline ? `${"#".repeat(b.level)} ${inline}` : "#".repeat(b.level)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
case "code": {
|
|
30
|
+
const fence = "```" + (b.language ?? "")
|
|
31
|
+
const body = `${fence}\n${b.code}\n\`\`\``
|
|
32
|
+
if (b.title || b.lineNumbers) {
|
|
33
|
+
const attrs = [
|
|
34
|
+
b.title ? ` title="${b.title}"` : "",
|
|
35
|
+
b.lineNumbers ? ` lineNumbers="true"` : "",
|
|
36
|
+
].join("")
|
|
37
|
+
return `{% code${attrs} %}\n${body}\n{% endcode %}`
|
|
38
|
+
}
|
|
39
|
+
return body
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
case "hint":
|
|
43
|
+
return `{% hint style="${b.style}" %}\n${serializeBlocks(b.children)}\n{% endhint %}`
|
|
44
|
+
|
|
45
|
+
case "tabs":
|
|
46
|
+
return `{% tabs %}\n${b.tabs
|
|
47
|
+
.map((t) => `{% tab title="${t.title}" %}\n${serializeBlocks(t.children)}\n{% endtab %}`)
|
|
48
|
+
.join("\n\n")}\n{% endtabs %}`
|
|
49
|
+
|
|
50
|
+
case "expandable":
|
|
51
|
+
return `<details>\n\n<summary>${b.summary}</summary>\n\n${serializeBlocks(b.children)}\n\n</details>`
|
|
52
|
+
|
|
53
|
+
case "stepper":
|
|
54
|
+
return `{% stepper %}\n${b.steps
|
|
55
|
+
.map((s) => {
|
|
56
|
+
const title = s.title ? `### ${s.title}\n\n` : ""
|
|
57
|
+
return `{% step %}\n${title}${serializeBlocks(s.children)}\n{% endstep %}`
|
|
58
|
+
})
|
|
59
|
+
.join("\n\n")}\n{% endstepper %}`
|
|
60
|
+
|
|
61
|
+
case "embed":
|
|
62
|
+
return `{% embed url="${b.url}" %}`
|
|
63
|
+
|
|
64
|
+
case "content-ref":
|
|
65
|
+
return `{% content-ref url="${b.url}" %}\n${serializeInline(b.children)}\n{% endcontent-ref %}`
|
|
66
|
+
|
|
67
|
+
case "columns":
|
|
68
|
+
return `{% columns %}\n${b.columns
|
|
69
|
+
.map((c) => `{% column %}\n${serializeBlocks(c.children)}\n{% endcolumn %}`)
|
|
70
|
+
.join("\n\n")}\n{% endcolumns %}`
|
|
71
|
+
|
|
72
|
+
case "figure": {
|
|
73
|
+
const alt = b.alt ? ` alt="${b.alt}"` : ' alt=""'
|
|
74
|
+
const cap = b.caption ? `<figcaption><p>${b.caption}</p></figcaption>` : "<figcaption></figcaption>"
|
|
75
|
+
return `<figure><img src="${b.src}"${alt}>${cap}</figure>`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case "list":
|
|
79
|
+
return b.items.map((item, idx) => serializeListItem(item, b.ordered, b.task, idx)).join("\n")
|
|
80
|
+
|
|
81
|
+
case "blockquote":
|
|
82
|
+
return serializeBlocks(b.children)
|
|
83
|
+
.split("\n")
|
|
84
|
+
.map((l) => (l ? `> ${l}` : ">"))
|
|
85
|
+
.join("\n")
|
|
86
|
+
|
|
87
|
+
case "divider":
|
|
88
|
+
return "---"
|
|
89
|
+
|
|
90
|
+
case "table": {
|
|
91
|
+
if (b.view) {
|
|
92
|
+
const cellHtml = (c: Inline[]) => inlineToHtml(c)
|
|
93
|
+
const head = `<thead><tr>${b.header.map((c) => `<th>${cellHtml(c)}</th>`).join("")}</tr></thead>`
|
|
94
|
+
const body = `<tbody>${b.rows
|
|
95
|
+
.map((r) => `<tr>${r.map((c) => `<td>${cellHtml(c)}</td>`).join("")}</tr>`)
|
|
96
|
+
.join("")}</tbody>`
|
|
97
|
+
return `<table data-view="${b.view}">${head}${body}</table>`
|
|
98
|
+
}
|
|
99
|
+
const row = (cells: Inline[][]) => `| ${cells.map((c) => serializeInline(c)).join(" | ")} |`
|
|
100
|
+
const sep = `| ${b.header.map(() => "---").join(" | ")} |`
|
|
101
|
+
return [row(b.header), sep, ...b.rows.map(row)].join("\n")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case "math":
|
|
105
|
+
return `$$\n${b.formula}\n$$`
|
|
106
|
+
|
|
107
|
+
case "updates":
|
|
108
|
+
return `{% updates${b.format ? ` format="${b.format}"` : ""} %}\n${b.updates
|
|
109
|
+
.map(
|
|
110
|
+
(u) =>
|
|
111
|
+
`{% update date="${u.date}" %}\n${serializeBlocks(u.children)}\n{% endupdate %}`
|
|
112
|
+
)
|
|
113
|
+
.join("\n\n")}\n{% endupdates %}`
|
|
114
|
+
|
|
115
|
+
case "openapi-operation": {
|
|
116
|
+
const attrs = [
|
|
117
|
+
b.spec ? ` spec="${b.spec}"` : "",
|
|
118
|
+
b.path ? ` path="${b.path}"` : "",
|
|
119
|
+
b.method ? ` method="${b.method}"` : "",
|
|
120
|
+
].join("")
|
|
121
|
+
const inner = b.specUrl ? `\n[${b.label || b.spec || "OpenAPI"}](${b.specUrl})` : ""
|
|
122
|
+
return `{% openapi-operation${attrs} %}${inner}\n{% endopenapi-operation %}`
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function inlineToHtml(nodes: Inline[]): string {
|
|
128
|
+
return nodes
|
|
129
|
+
.map((n) => {
|
|
130
|
+
if (n.type === "image") {
|
|
131
|
+
const img = `<img src="${n.src}"${n.alt ? ` alt="${n.alt}"` : ""}>`
|
|
132
|
+
return n.link ? `<a href="${n.link}">${img}</a>` : img
|
|
133
|
+
}
|
|
134
|
+
let s = n.text
|
|
135
|
+
if (n.code) s = `<code>${s}</code>`
|
|
136
|
+
if (n.bold) s = `<strong>${s}</strong>`
|
|
137
|
+
if (n.italic) s = `<em>${s}</em>`
|
|
138
|
+
if (n.link) s = `<a href="${n.link}">${s}</a>`
|
|
139
|
+
return s
|
|
140
|
+
})
|
|
141
|
+
.join("")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function serializeListItem(item: ListItemNode, ordered: boolean, task: boolean, idx: number): string {
|
|
145
|
+
const bullet = ordered ? `${idx + 1}.` : "-"
|
|
146
|
+
const check = task ? `[${item.checked ? "x" : " "}] ` : ""
|
|
147
|
+
const [first, ...rest] = item.children
|
|
148
|
+
const firstText = first?.type === "paragraph" ? serializeInline(first.children) : first ? serializeBlock(first) : ""
|
|
149
|
+
const restText = rest.length ? "\n" + indent(serializeBlocks(rest), " ") : ""
|
|
150
|
+
return `${bullet} ${check}${firstText}${restText}`
|
|
151
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { GitbookStreamdown } from "./streamdown"
|
|
2
|
+
export type { GitbookStreamdownProps } from "./streamdown"
|
|
3
|
+
export { DocsRenderer, MarkdownContent } from "./docs/DocsRenderer"
|
|
4
|
+
export { OpenApiOperation } from "./openapi/OpenApiOperation"
|
|
5
|
+
export { resolveAsset, setAssetBase } from "./assets"
|
|
6
|
+
export { parseMarkdown, parseBlocks } from "./gitbook/parse"
|
|
7
|
+
export { serializeBlocks, serializeMarkdown } from "./gitbook/serialize"
|
|
8
|
+
export { parseInline, plainText, refDefinitions, serializeInline } from "./gitbook/inline"
|
|
9
|
+
export { Streamdown } from "streamdown"
|
|
10
|
+
export type { StreamdownProps } from "streamdown"
|
|
11
|
+
export type * from "./gitbook/ast"
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import { parse as parseYaml } from "yaml"
|
|
3
|
+
|
|
4
|
+
import { MarkdownContent } from "../docs/DocsRenderer"
|
|
5
|
+
import { HighlightedCode } from "../docs/HighlightedCode"
|
|
6
|
+
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
|
+
|
|
9
|
+
const specCache = new Map<string, Promise<any>>()
|
|
10
|
+
|
|
11
|
+
function loadSpec(url: string): Promise<any> {
|
|
12
|
+
if (!specCache.has(url)) {
|
|
13
|
+
specCache.set(
|
|
14
|
+
url,
|
|
15
|
+
fetch(url)
|
|
16
|
+
.then((r) => {
|
|
17
|
+
if (!r.ok) throw new Error(`Failed to load spec (${r.status})`)
|
|
18
|
+
return r.text()
|
|
19
|
+
})
|
|
20
|
+
.then((text) => (text.trimStart().startsWith("{") ? JSON.parse(text) : parseYaml(text)))
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
return specCache.get(url)!
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveRef(spec: any, node: any): any {
|
|
27
|
+
if (node && typeof node === "object" && typeof node.$ref === "string") {
|
|
28
|
+
const parts = node.$ref.replace(/^#\//, "").split("/")
|
|
29
|
+
let cur = spec
|
|
30
|
+
for (const p of parts) cur = cur?.[p]
|
|
31
|
+
return resolveRef(spec, cur)
|
|
32
|
+
}
|
|
33
|
+
return node
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function exampleFromSchema(spec: any, schema: any, depth = 0): any {
|
|
37
|
+
schema = resolveRef(spec, schema)
|
|
38
|
+
if (!schema || depth > 4) return null
|
|
39
|
+
if (schema.example !== undefined) return schema.example
|
|
40
|
+
if (schema.type === "array") return [exampleFromSchema(spec, schema.items, depth + 1)]
|
|
41
|
+
if (schema.type === "object" || schema.properties) {
|
|
42
|
+
const obj: Record<string, any> = {}
|
|
43
|
+
for (const [k, v] of Object.entries<any>(schema.properties ?? {})) {
|
|
44
|
+
obj[k] = exampleFromSchema(spec, v, depth + 1)
|
|
45
|
+
}
|
|
46
|
+
return obj
|
|
47
|
+
}
|
|
48
|
+
if (schema.enum?.length) return schema.enum[0]
|
|
49
|
+
switch (schema.type) {
|
|
50
|
+
case "integer":
|
|
51
|
+
case "number":
|
|
52
|
+
return 1
|
|
53
|
+
case "boolean":
|
|
54
|
+
return true
|
|
55
|
+
default:
|
|
56
|
+
return "string"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function SchemaFields({ spec, schema }: { spec: any; schema: any }) {
|
|
61
|
+
schema = resolveRef(spec, schema)
|
|
62
|
+
if (schema?.type === "array") schema = resolveRef(spec, schema.items)
|
|
63
|
+
const props = Object.entries<any>(schema?.properties ?? {})
|
|
64
|
+
if (!props.length) return null
|
|
65
|
+
const required: string[] = schema.required ?? []
|
|
66
|
+
return (
|
|
67
|
+
<div className="oas-fields">
|
|
68
|
+
{props.map(([name, raw]) => {
|
|
69
|
+
const p = resolveRef(spec, raw)
|
|
70
|
+
return (
|
|
71
|
+
<div key={name} className="oas-field">
|
|
72
|
+
<div>
|
|
73
|
+
<code className="oas-field-name">{name}</code>{" "}
|
|
74
|
+
<span className="oas-field-type">
|
|
75
|
+
{p.type ?? "object"}
|
|
76
|
+
{p.format ? ` · ${p.format}` : ""}
|
|
77
|
+
</span>{" "}
|
|
78
|
+
<span className={required.includes(name) ? "oas-required" : "oas-optional"}>
|
|
79
|
+
{required.includes(name) ? "required" : "optional"}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
{p.description && <p className="oas-field-desc">{p.description}</p>}
|
|
83
|
+
</div>
|
|
84
|
+
)
|
|
85
|
+
})}
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function OpenApiOperation({
|
|
91
|
+
specUrl,
|
|
92
|
+
path,
|
|
93
|
+
method,
|
|
94
|
+
}: {
|
|
95
|
+
specUrl: string
|
|
96
|
+
path: string
|
|
97
|
+
method: string
|
|
98
|
+
}) {
|
|
99
|
+
const [spec, setSpec] = useState<any>(null)
|
|
100
|
+
const [error, setError] = useState<string | null>(null)
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (!specUrl) return
|
|
104
|
+
setSpec(null)
|
|
105
|
+
setError(null)
|
|
106
|
+
loadSpec(specUrl)
|
|
107
|
+
.then(setSpec)
|
|
108
|
+
.catch((e) => setError(String(e.message ?? e)))
|
|
109
|
+
}, [specUrl])
|
|
110
|
+
|
|
111
|
+
if (!specUrl) return <div className="oas oas-empty">Set an OpenAPI spec URL…</div>
|
|
112
|
+
if (error) return <div className="oas oas-empty">⚠︎ {error}</div>
|
|
113
|
+
if (!spec) return <div className="oas oas-empty">Loading OpenAPI spec…</div>
|
|
114
|
+
|
|
115
|
+
const op = spec.paths?.[path]?.[method.toLowerCase()]
|
|
116
|
+
if (!op) {
|
|
117
|
+
return (
|
|
118
|
+
<div className="oas oas-empty">
|
|
119
|
+
Operation {method.toUpperCase()} {path} not found in spec.
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const server = spec.servers?.[0]?.url ?? ""
|
|
125
|
+
const security: any[] = op.security ?? spec.security ?? []
|
|
126
|
+
const schemes = Object.entries<any>(spec.components?.securitySchemes ?? {}).filter(([name]) =>
|
|
127
|
+
security.some((s) => name in s)
|
|
128
|
+
)
|
|
129
|
+
const responses = Object.entries<any>(op.responses ?? {})
|
|
130
|
+
const okResponse = responses.find(([code]) => code.startsWith("2"))?.[1]
|
|
131
|
+
const okSchema = okResponse?.content?.["application/json"]?.schema
|
|
132
|
+
const example = okSchema ? exampleFromSchema(spec, okSchema) : null
|
|
133
|
+
|
|
134
|
+
const curl = [
|
|
135
|
+
`curl -L ${server}${path} \\`,
|
|
136
|
+
...(schemes.length ? [` -H 'Authorization: Bearer YOUR_API_KEY'`] : []),
|
|
137
|
+
].join("\n")
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div className="oas">
|
|
141
|
+
<h3 className="oas-title">{op.summary ?? `${method.toUpperCase()} ${path}`}</h3>
|
|
142
|
+
<div className="oas-endpoint">
|
|
143
|
+
<span className={`oas-method oas-method-${method.toLowerCase()}`}>
|
|
144
|
+
{method.toUpperCase()}
|
|
145
|
+
</span>
|
|
146
|
+
{server && <span className="oas-server">{server}</span>}
|
|
147
|
+
<code className="oas-path">{path}</code>
|
|
148
|
+
</div>
|
|
149
|
+
{op.description && (
|
|
150
|
+
<div className="oas-desc">
|
|
151
|
+
<MarkdownContent markdown={op.description} />
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
{schemes.length > 0 && (
|
|
156
|
+
<section className="oas-section">
|
|
157
|
+
<h4>Authorizations</h4>
|
|
158
|
+
{schemes.map(([name, s]) => (
|
|
159
|
+
<div key={name} className="oas-field">
|
|
160
|
+
<div>
|
|
161
|
+
<code className="oas-field-name">{s.name ?? "Authorization"}</code>{" "}
|
|
162
|
+
<span className="oas-field-type">{s.type}</span>{" "}
|
|
163
|
+
<span className="oas-required">required</span>
|
|
164
|
+
</div>
|
|
165
|
+
{s.description && <p className="oas-field-desc">{s.description}</p>}
|
|
166
|
+
</div>
|
|
167
|
+
))}
|
|
168
|
+
</section>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{(op.parameters?.length ?? 0) > 0 && (
|
|
172
|
+
<section className="oas-section">
|
|
173
|
+
<h4>Parameters</h4>
|
|
174
|
+
{op.parameters.map((raw: any, i: number) => {
|
|
175
|
+
const p = resolveRef(spec, raw)
|
|
176
|
+
return (
|
|
177
|
+
<div key={i} className="oas-field">
|
|
178
|
+
<div>
|
|
179
|
+
<code className="oas-field-name">{p.name}</code>{" "}
|
|
180
|
+
<span className="oas-field-type">
|
|
181
|
+
{p.schema?.type ?? "string"} · {p.in}
|
|
182
|
+
</span>{" "}
|
|
183
|
+
<span className={p.required ? "oas-required" : "oas-optional"}>
|
|
184
|
+
{p.required ? "required" : "optional"}
|
|
185
|
+
</span>
|
|
186
|
+
</div>
|
|
187
|
+
{p.description && <p className="oas-field-desc">{p.description}</p>}
|
|
188
|
+
</div>
|
|
189
|
+
)
|
|
190
|
+
})}
|
|
191
|
+
</section>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
<section className="oas-section">
|
|
195
|
+
<h4>Responses</h4>
|
|
196
|
+
{responses.map(([code, raw]) => {
|
|
197
|
+
const r = resolveRef(spec, raw)
|
|
198
|
+
const schema = r.content?.["application/json"]?.schema
|
|
199
|
+
return (
|
|
200
|
+
<details key={code} className="oas-response" open={code.startsWith("2")}>
|
|
201
|
+
<summary>
|
|
202
|
+
<span className={`oas-code oas-code-${code[0]}`}>{code}</span>
|
|
203
|
+
<span>{r.description}</span>
|
|
204
|
+
</summary>
|
|
205
|
+
{schema && <SchemaFields spec={spec} schema={schema} />}
|
|
206
|
+
</details>
|
|
207
|
+
)
|
|
208
|
+
})}
|
|
209
|
+
</section>
|
|
210
|
+
|
|
211
|
+
<section className="oas-section oas-samples">
|
|
212
|
+
<div className="oas-sample">
|
|
213
|
+
<div className="oas-sample-head">
|
|
214
|
+
<span className={`oas-method oas-method-${method.toLowerCase()}`}>
|
|
215
|
+
{method.toUpperCase()}
|
|
216
|
+
</span>
|
|
217
|
+
<code>{path}</code>
|
|
218
|
+
<span className="oas-sample-label">cURL</span>
|
|
219
|
+
</div>
|
|
220
|
+
<pre>
|
|
221
|
+
<HighlightedCode code={curl} language="bash" />
|
|
222
|
+
</pre>
|
|
223
|
+
</div>
|
|
224
|
+
{example !== null && (
|
|
225
|
+
<div className="oas-sample">
|
|
226
|
+
<div className="oas-sample-head">
|
|
227
|
+
<span className="oas-code oas-code-2">200</span>
|
|
228
|
+
<span>{okResponse?.description}</span>
|
|
229
|
+
</div>
|
|
230
|
+
<pre>
|
|
231
|
+
<HighlightedCode code={JSON.stringify(example, null, 2)} language="json" />
|
|
232
|
+
</pre>
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
</section>
|
|
236
|
+
</div>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useMemo } from "react"
|
|
2
|
+
import { DocsRenderer } from "./docs/DocsRenderer"
|
|
3
|
+
import { parseMarkdown } from "./gitbook/parse"
|
|
4
|
+
|
|
5
|
+
export interface GitbookStreamdownProps {
|
|
6
|
+
children?: string
|
|
7
|
+
className?: string
|
|
8
|
+
isAnimating?: boolean
|
|
9
|
+
isStreaming?: boolean
|
|
10
|
+
markdown?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function GitbookStreamdown({
|
|
14
|
+
children,
|
|
15
|
+
className,
|
|
16
|
+
isAnimating,
|
|
17
|
+
isStreaming = false,
|
|
18
|
+
markdown,
|
|
19
|
+
}: GitbookStreamdownProps) {
|
|
20
|
+
const content = markdown ?? children ?? ""
|
|
21
|
+
const gitbookDoc = useMemo(() => parseMarkdown(content), [content])
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
aria-busy={isAnimating ?? isStreaming}
|
|
26
|
+
className={className}
|
|
27
|
+
data-docstream=""
|
|
28
|
+
data-streaming={isStreaming ? "" : undefined}
|
|
29
|
+
>
|
|
30
|
+
<DocsRenderer doc={gitbookDoc} />
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|