@brett_lamy/docstream 0.5.4 → 0.5.6
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/README.md +16 -0
- package/package.json +2 -1
- package/src/docs/DocsRenderer.tsx +20 -2
- package/src/docs/Mermaid.tsx +48 -13
- package/src/docs/math.ts +9 -0
- package/src/gitbook/ast.ts +4 -0
- package/src/gitbook/parse.ts +12 -0
- package/src/gitbook/serialize.ts +2 -0
- package/src/index.ts +2 -1
- package/src/playground/PlaygroundStreamdown.tsx +8 -2
- package/src/source/client.ts +19 -0
- package/src/source/index.ts +2 -1
- package/src/styles.css +33 -10
package/README.md
CHANGED
|
@@ -236,6 +236,21 @@ plugin validates the mount boundary, only edits existing files, and notifies
|
|
|
236
236
|
Vite after a save so the preview reloads through Vite's normal module pipeline.
|
|
237
237
|
Set `writable: false` on a mount when a documentation site should only render.
|
|
238
238
|
|
|
239
|
+
The Vite endpoints exist only during development. For a deployed static site,
|
|
240
|
+
bundle the referenced modules and use the read-only production client:
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
import { createBundledSourceClient } from "@brett_lamy/docstream"
|
|
244
|
+
import * as ButtonModule from "./components/Button"
|
|
245
|
+
|
|
246
|
+
const client = createBundledSourceClient({
|
|
247
|
+
"ui:Button.tsx": ButtonModule,
|
|
248
|
+
})
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Entries may also be lazy import functions. Keys can be `mount:path` (recommended)
|
|
252
|
+
or just `path` when names cannot collide.
|
|
253
|
+
|
|
239
254
|
## Assets and OpenAPI Specs
|
|
240
255
|
|
|
241
256
|
Relative image and OpenAPI spec paths can be resolved against an asset base:
|
|
@@ -258,6 +273,7 @@ You can also resolve paths yourself with `resolveAsset`.
|
|
|
258
273
|
- `OpenApiOperation`: Renders a parsed OpenAPI operation block.
|
|
259
274
|
- `SourcePreview`: Imports and renders a mounted component or CSF story export.
|
|
260
275
|
- `createViteSourceClient`: Reads, writes, and imports files exposed by the Vite plugin.
|
|
276
|
+
- `createBundledSourceClient`: Imports bundled source modules in read-only production builds.
|
|
261
277
|
|
|
262
278
|
### Parser and Serializer
|
|
263
279
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brett_lamy/docstream",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "GitBook-aware readonly markdown and AI stream renderer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"@brett_lamy/viz-engine": ">=0.2.0",
|
|
73
73
|
"framer-motion": "^13.1.1",
|
|
74
74
|
"hast-util-to-html": "^9.0.5",
|
|
75
|
+
"katex": "^0.18.5",
|
|
75
76
|
"lowlight": "^3.3.0",
|
|
76
77
|
"lucide-react": "^1.17.0",
|
|
77
78
|
"mermaid": "^11.15.0",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, type ReactNode } from "react"
|
|
2
2
|
import { LayoutGroup, motion } from "framer-motion"
|
|
3
|
+
import "katex/dist/katex.min.css"
|
|
3
4
|
import {
|
|
4
5
|
AlertTriangle,
|
|
5
6
|
CheckCircle2,
|
|
@@ -18,11 +19,18 @@ import { OpenApiOperation } from "../openapi/OpenApiOperation"
|
|
|
18
19
|
import { Mermaid } from "./Mermaid"
|
|
19
20
|
import { HighlightedCode } from "./HighlightedCode"
|
|
20
21
|
import { CitationSources, InlineReference } from "./reference"
|
|
22
|
+
import { renderMathToHtml } from "./math"
|
|
23
|
+
|
|
24
|
+
function MathBlock({ formula }: { formula: string }) {
|
|
25
|
+
return <div className="docs-math" dangerouslySetInnerHTML={{ __html: renderMathToHtml(formula) }} />
|
|
26
|
+
}
|
|
21
27
|
|
|
22
28
|
export interface LivePreviewProps {
|
|
23
29
|
files: Record<string, string>
|
|
24
30
|
entry: string
|
|
25
31
|
title: string
|
|
32
|
+
collapsedCodeLines?: number
|
|
33
|
+
expandedCodeLines?: number
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
export type LivePreviewRenderer = (props: LivePreviewProps) => ReactNode
|
|
@@ -140,13 +148,23 @@ function BlockView({ block, liveRenderer, sourceRenderer }: { block: Block } & R
|
|
|
140
148
|
)
|
|
141
149
|
}
|
|
142
150
|
case "code":
|
|
143
|
-
if (block.language === "mermaid")
|
|
151
|
+
if (block.language === "mermaid") {
|
|
152
|
+
return (
|
|
153
|
+
<Mermaid
|
|
154
|
+
code={block.code}
|
|
155
|
+
collapsedCodeLines={block.collapsedCodeLines}
|
|
156
|
+
expandedCodeLines={block.expandedCodeLines}
|
|
157
|
+
/>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
144
160
|
if (block.live && isReactLanguage(block.language) && liveRenderer) {
|
|
145
161
|
const entry = block.entry ?? "/src/main.jsx"
|
|
146
162
|
return liveRenderer({
|
|
147
163
|
files: { [entry]: block.code },
|
|
148
164
|
entry,
|
|
149
165
|
title: block.title ?? "Live React preview",
|
|
166
|
+
collapsedCodeLines: block.collapsedCodeLines,
|
|
167
|
+
expandedCodeLines: block.expandedCodeLines,
|
|
150
168
|
})
|
|
151
169
|
}
|
|
152
170
|
return (
|
|
@@ -327,7 +345,7 @@ function BlockView({ block, liveRenderer, sourceRenderer }: { block: Block } & R
|
|
|
327
345
|
</table>
|
|
328
346
|
)
|
|
329
347
|
case "math":
|
|
330
|
-
return <
|
|
348
|
+
return <MathBlock formula={block.formula} />
|
|
331
349
|
case "updates":
|
|
332
350
|
return (
|
|
333
351
|
<div className="docs-updates">
|
package/src/docs/Mermaid.tsx
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from "react"
|
|
1
|
+
import { useEffect, useId, useRef, useState, type CSSProperties } from "react"
|
|
2
|
+
import { HighlightedCode } from "./HighlightedCode"
|
|
2
3
|
|
|
3
4
|
let seq = 0
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
+
export interface MermaidProps {
|
|
7
|
+
code: string
|
|
8
|
+
collapsedCodeLines?: number
|
|
9
|
+
expandedCodeLines?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function positiveLineCount(value: number | undefined, fallback: number): number {
|
|
13
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Mermaid({ code, collapsedCodeLines = 3, expandedCodeLines = 30 }: MermaidProps) {
|
|
6
17
|
const [svg, setSvg] = useState<string | null>(null)
|
|
7
18
|
const [error, setError] = useState<string | null>(null)
|
|
19
|
+
const [codeExpanded, setCodeExpanded] = useState(false)
|
|
8
20
|
const idRef = useRef(`mermaid-${++seq}`)
|
|
21
|
+
const codePanelId = useId()
|
|
22
|
+
const collapsedLines = positiveLineCount(collapsedCodeLines, 3)
|
|
23
|
+
const expandedLines = Math.max(collapsedLines, positiveLineCount(expandedCodeLines, 30))
|
|
9
24
|
|
|
10
25
|
useEffect(() => {
|
|
11
26
|
let cancelled = false
|
|
@@ -23,15 +38,35 @@ export function Mermaid({ code }: { code: string }) {
|
|
|
23
38
|
}
|
|
24
39
|
}, [code])
|
|
25
40
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
mermaid error: {error}
|
|
30
|
-
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
return (
|
|
42
|
+
<section className="docs-mermaid-preview" data-docstream-mermaid="">
|
|
43
|
+
{error ? (
|
|
44
|
+
<pre className="docs-mermaid-error">mermaid error: {error}</pre>
|
|
45
|
+
) : svg ? (
|
|
46
|
+
<div className="docs-mermaid" dangerouslySetInnerHTML={{ __html: svg }} />
|
|
47
|
+
) : (
|
|
48
|
+
<div className="docs-mermaid">Rendering diagram…</div>
|
|
49
|
+
)}
|
|
50
|
+
<div className="docs-react-demo-code docs-mermaid-code">
|
|
51
|
+
<pre
|
|
52
|
+
id={codePanelId}
|
|
53
|
+
className={codeExpanded ? "docs-react-demo-code-body docs-react-demo-code-body-expanded" : "docs-react-demo-code-body"}
|
|
54
|
+
style={{
|
|
55
|
+
"--docs-react-demo-code-lines": codeExpanded ? expandedLines : collapsedLines,
|
|
56
|
+
} as CSSProperties}
|
|
57
|
+
>
|
|
58
|
+
<HighlightedCode code={code} language="mermaid" lineNumbers />
|
|
59
|
+
</pre>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
className="docs-react-demo-code-toggle"
|
|
63
|
+
aria-controls={codePanelId}
|
|
64
|
+
aria-expanded={codeExpanded}
|
|
65
|
+
onClick={() => setCodeExpanded((value) => !value)}
|
|
66
|
+
>
|
|
67
|
+
{codeExpanded ? "Hide Code" : "View Code"}
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
</section>
|
|
71
|
+
)
|
|
37
72
|
}
|
package/src/docs/math.ts
ADDED
package/src/gitbook/ast.ts
CHANGED
|
@@ -68,6 +68,10 @@ export interface CodeBlockNode {
|
|
|
68
68
|
live?: boolean
|
|
69
69
|
/** Project-relative entry file used by a live preview. */
|
|
70
70
|
entry?: string | null
|
|
71
|
+
/** Source lines visible before an expandable code preview is opened. */
|
|
72
|
+
collapsedCodeLines?: number
|
|
73
|
+
/** Maximum source lines visible before expanded code scrolls. */
|
|
74
|
+
expandedCodeLines?: number
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
export interface HintNode {
|
package/src/gitbook/parse.ts
CHANGED
|
@@ -71,6 +71,12 @@ function booleanAttr(attrs: Record<string, string>, key: string): boolean | unde
|
|
|
71
71
|
return attrs[key] !== "false"
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function positiveNumberAttr(attrs: Record<string, string>, key: string): number | undefined {
|
|
75
|
+
if (!(key in attrs)) return undefined
|
|
76
|
+
const value = Number(attrs[key])
|
|
77
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
74
80
|
interface TemplateTag {
|
|
75
81
|
name: string
|
|
76
82
|
attrs: Record<string, string>
|
|
@@ -447,6 +453,12 @@ export function parseBlocks(lines: string[]): Block[] {
|
|
|
447
453
|
code: code.join("\n"),
|
|
448
454
|
...(booleanAttr(attrs, "live") === undefined ? {} : { live: booleanAttr(attrs, "live") }),
|
|
449
455
|
...(attrs.entry ? { entry: attrs.entry } : {}),
|
|
456
|
+
...(positiveNumberAttr(attrs, "collapsedCodeLines") === undefined
|
|
457
|
+
? {}
|
|
458
|
+
: { collapsedCodeLines: positiveNumberAttr(attrs, "collapsedCodeLines") }),
|
|
459
|
+
...(positiveNumberAttr(attrs, "expandedCodeLines") === undefined
|
|
460
|
+
? {}
|
|
461
|
+
: { expandedCodeLines: positiveNumberAttr(attrs, "expandedCodeLines") }),
|
|
450
462
|
})
|
|
451
463
|
continue
|
|
452
464
|
}
|
package/src/gitbook/serialize.ts
CHANGED
|
@@ -39,6 +39,8 @@ function serializeBlock(b: Block): string {
|
|
|
39
39
|
!b.lineNumbers ? `lineNumbers="false"` : "",
|
|
40
40
|
b.live ? `live="true"` : "",
|
|
41
41
|
b.entry ? `entry="${b.entry}"` : "",
|
|
42
|
+
b.collapsedCodeLines ? `collapsedCodeLines="${b.collapsedCodeLines}"` : "",
|
|
43
|
+
b.expandedCodeLines ? `expandedCodeLines="${b.expandedCodeLines}"` : "",
|
|
42
44
|
].filter(Boolean)
|
|
43
45
|
const info = [b.language ?? "", ...attrs].filter(Boolean).join(" ")
|
|
44
46
|
return `\`\`\`${info}\n${b.code}\n\`\`\``
|
package/src/index.ts
CHANGED
|
@@ -36,8 +36,9 @@ export type {
|
|
|
36
36
|
ReactDemoProps,
|
|
37
37
|
} from "./playground"
|
|
38
38
|
export { resolveAsset, setAssetBase } from "./assets"
|
|
39
|
-
export { createViteSourceClient, SourcePreview } from "./source"
|
|
39
|
+
export { createBundledSourceClient, createViteSourceClient, SourcePreview } from "./source"
|
|
40
40
|
export type {
|
|
41
|
+
BundledSourceModule,
|
|
41
42
|
SourceFileSnapshot,
|
|
42
43
|
SourceModule,
|
|
43
44
|
SourcePreviewProps,
|
|
@@ -29,8 +29,14 @@ export function PlaygroundStreamdown({
|
|
|
29
29
|
() => parseMarkdown(isStreaming ? trimPartialInlineToken(content) : content),
|
|
30
30
|
[content, isStreaming]
|
|
31
31
|
)
|
|
32
|
-
const liveRenderer: LivePreviewRenderer = ({ files, entry, title }) => (
|
|
33
|
-
<ReactCodePreview
|
|
32
|
+
const liveRenderer: LivePreviewRenderer = ({ files, entry, title, collapsedCodeLines, expandedCodeLines }) => (
|
|
33
|
+
<ReactCodePreview
|
|
34
|
+
files={files}
|
|
35
|
+
entry={entry}
|
|
36
|
+
title={title}
|
|
37
|
+
collapsedCodeLines={collapsedCodeLines}
|
|
38
|
+
expandedCodeLines={expandedCodeLines}
|
|
39
|
+
/>
|
|
34
40
|
)
|
|
35
41
|
|
|
36
42
|
return (
|
package/src/source/client.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { SourceRefNode } from "../gitbook/ast"
|
|
2
2
|
import type { SourceFileSnapshot, SourceReferenceClient, SourceModule } from "./types"
|
|
3
3
|
|
|
4
|
+
export type BundledSourceModule = SourceModule | (() => SourceModule | Promise<SourceModule>)
|
|
5
|
+
|
|
4
6
|
function params(reference: SourceRefNode): URLSearchParams {
|
|
5
7
|
return new URLSearchParams({
|
|
6
8
|
mount: reference.mount,
|
|
@@ -38,3 +40,20 @@ export function createViteSourceClient(apiBase = "/@docstream"): SourceReference
|
|
|
38
40
|
},
|
|
39
41
|
}
|
|
40
42
|
}
|
|
43
|
+
|
|
44
|
+
/** Create a read-only source client for modules included in a production bundle. */
|
|
45
|
+
export function createBundledSourceClient(modules: Record<string, BundledSourceModule>): SourceReferenceClient {
|
|
46
|
+
return {
|
|
47
|
+
async read() {
|
|
48
|
+
throw new Error("Source text is unavailable in a bundled source client.")
|
|
49
|
+
},
|
|
50
|
+
async write() {
|
|
51
|
+
throw new Error("Bundled source previews are read-only.")
|
|
52
|
+
},
|
|
53
|
+
async importModule(reference) {
|
|
54
|
+
const entry = modules[`${reference.mount}:${reference.path}`] ?? modules[reference.path]
|
|
55
|
+
if (!entry) throw new Error(`No bundled source module found for ${reference.mount}:${reference.path}`)
|
|
56
|
+
return typeof entry === "function" ? entry() : entry
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/source/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { createViteSourceClient } from "./client"
|
|
1
|
+
export { createBundledSourceClient, createViteSourceClient } from "./client"
|
|
2
|
+
export type { BundledSourceModule } from "./client"
|
|
2
3
|
export { SourcePreview } from "./SourcePreview"
|
|
3
4
|
export type { SourcePreviewProps } from "./SourcePreview"
|
|
4
5
|
export type {
|
package/src/styles.css
CHANGED
|
@@ -123,15 +123,29 @@
|
|
|
123
123
|
padding-left: 1.5em;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
.docs-
|
|
126
|
+
.docs-tasklist {
|
|
127
127
|
list-style: none;
|
|
128
128
|
padding-left: 0.2em;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
.docs-
|
|
131
|
+
.docs-tasklist li {
|
|
132
132
|
display: flex;
|
|
133
133
|
gap: 8px;
|
|
134
|
-
align-items:
|
|
134
|
+
align-items: flex-start;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.docs-tasklist li > input {
|
|
138
|
+
flex: 0 0 auto;
|
|
139
|
+
margin-top: 0.45em;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.docs-tasklist .docs-blocks-inline {
|
|
143
|
+
flex: 1;
|
|
144
|
+
min-width: 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.docs-tasklist .docs-blocks-inline > p:first-child {
|
|
148
|
+
margin: 0;
|
|
135
149
|
}
|
|
136
150
|
|
|
137
151
|
.docs-article blockquote {
|
|
@@ -489,10 +503,12 @@
|
|
|
489
503
|
border-radius: 12px;
|
|
490
504
|
padding: 14px 16px;
|
|
491
505
|
background: var(--gb-muted);
|
|
492
|
-
font-family: ui-monospace, Menlo, monospace;
|
|
493
|
-
font-size: 13.5px;
|
|
494
506
|
text-align: center;
|
|
495
|
-
|
|
507
|
+
overflow-x: auto;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.docs-math .katex-display {
|
|
511
|
+
margin: 0;
|
|
496
512
|
}
|
|
497
513
|
|
|
498
514
|
.docs-updates {
|
|
@@ -547,12 +563,16 @@
|
|
|
547
563
|
margin-top: 4px;
|
|
548
564
|
}
|
|
549
565
|
|
|
566
|
+
.docs-mermaid-preview {
|
|
567
|
+
border: 1px solid var(--gb-border);
|
|
568
|
+
border-radius: 12px;
|
|
569
|
+
overflow: hidden;
|
|
570
|
+
}
|
|
571
|
+
|
|
550
572
|
.docs-mermaid {
|
|
551
573
|
display: flex;
|
|
552
574
|
justify-content: center;
|
|
553
575
|
padding: 16px;
|
|
554
|
-
border: 1px solid var(--gb-border);
|
|
555
|
-
border-radius: 12px;
|
|
556
576
|
background: var(--background, #ffffff);
|
|
557
577
|
}
|
|
558
578
|
|
|
@@ -561,14 +581,17 @@
|
|
|
561
581
|
}
|
|
562
582
|
|
|
563
583
|
.docs-mermaid-error {
|
|
584
|
+
margin: 0;
|
|
564
585
|
font-size: 12px;
|
|
565
586
|
color: var(--gb-danger);
|
|
566
|
-
border: 1px solid var(--gb-border);
|
|
567
|
-
border-radius: 8px;
|
|
568
587
|
padding: 10px;
|
|
569
588
|
white-space: pre-wrap;
|
|
570
589
|
}
|
|
571
590
|
|
|
591
|
+
.docs-mermaid-code {
|
|
592
|
+
border-top: 1px solid var(--gb-border);
|
|
593
|
+
}
|
|
594
|
+
|
|
572
595
|
.docs-inline-img {
|
|
573
596
|
display: inline-block;
|
|
574
597
|
vertical-align: middle;
|