@brett_lamy/docstream 1.2.3 → 1.3.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/README.md +56 -10
- package/package.json +9 -3
- package/src/demo/markdown.ts +1 -1
- package/src/demo/types.ts +2 -2
- package/src/docs/DocsRenderer.tsx +52 -16
- package/src/docs/math.ts +3 -0
- package/src/gitbook/ast.ts +240 -43
- package/src/gitbook/inline.ts +542 -149
- package/src/gitbook/parse.ts +403 -165
- package/src/gitbook/serialize.ts +446 -126
- package/src/index.ts +10 -28
- package/src/playground/PlaygroundStreamdown.tsx +28 -61
- package/src/playground/filesystem.ts +10 -190
- package/src/playground/index.ts +4 -0
- package/src/playground/project.ts +194 -0
- package/src/replay/NativeReplay.tsx +236 -0
- package/src/replay/ReplayPreview.tsx +7 -225
- package/src/styles.css +0 -1
package/README.md
CHANGED
|
@@ -8,8 +8,12 @@ GitBook-aware markdown rendering for React applications and AI streaming surface
|
|
|
8
8
|
|
|
9
9
|
- React renderer for GitBook-style markdown blocks.
|
|
10
10
|
- Streaming-friendly `GitbookStreamdown` component inspired by `vercel/streamdown`.
|
|
11
|
-
- Parser and serializer for round-tripping supported GitBook syntax
|
|
12
|
-
|
|
11
|
+
- Parser and serializer for round-tripping supported GitBook syntax — byte-for-byte: `serializeMarkdown(parseMarkdown(md)) === md`
|
|
12
|
+
for Markdown and GitBook pages (the author's emphasis markers, escapes, link forms and reference definitions,
|
|
13
|
+
Markdown vs HTML images, blank lines between blocks, list markers, fences, table layout and tag lines as written).
|
|
14
|
+
Layout-only fields (`gap`, `raw`, `opening`, …) are recorded only where the source differs from the default
|
|
15
|
+
output, and are ignored once they no longer fit an edited node.
|
|
16
|
+
- Syntax-highlighted code blocks through `gpu-lexer`; Mermaid diagrams and KaTeX math load on first use.
|
|
13
17
|
- GitBook block support for hints, tabs, expandables, steppers, embeds, content refs, columns, figures, tables, math, dividers, updates, and OpenAPI operations.
|
|
14
18
|
- CSS exported as a stable package entrypoint so host apps can theme with CSS variables or shadcn-style design tokens.
|
|
15
19
|
- Attribute-aware direct video embeds for muted, looping inline clips in long-form posts.
|
|
@@ -19,10 +23,31 @@ GitBook-aware markdown rendering for React applications and AI streaming surface
|
|
|
19
23
|
## Installation
|
|
20
24
|
|
|
21
25
|
```sh
|
|
22
|
-
|
|
26
|
+
pnpm add @brett_lamy/docstream
|
|
27
|
+
# or: npm install @brett_lamy/docstream
|
|
23
28
|
```
|
|
24
29
|
|
|
25
|
-
React
|
|
30
|
+
React and React DOM are peer dependencies and must be provided by your app.
|
|
31
|
+
Nothing in docstream's dependency tree runs an install script, so
|
|
32
|
+
`pnpm add` succeeds with no build-script prompts (no `ERR_PNPM_IGNORED_BUILDS`).
|
|
33
|
+
Heavy renderers are loaded only when a page needs them: Mermaid for
|
|
34
|
+
` ```mermaid ` fences, KaTeX for math blocks, rrweb for replay event lists.
|
|
35
|
+
|
|
36
|
+
Two features are opt-in, each behind its own entry and an **optional** peer
|
|
37
|
+
dependency that the rest of the package never references:
|
|
38
|
+
|
|
39
|
+
| Entry | Install to use it | What it adds |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `@brett_lamy/docstream/playground` | `@agent-wasm/core` (almost-node, an in-browser Node runtime) | Live React code fences and runnable inline `{% demo %}` files |
|
|
42
|
+
| `@brett_lamy/docstream/viz` | `@brett_lamy/viz-engine` | `VizEmbed` |
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pnpm add @agent-wasm/core # only if you import @brett_lamy/docstream/playground
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Without the playground, live code fences render as highlighted code (or through
|
|
49
|
+
a `liveRenderer` you pass), and inline-file demos open on their Code view with a
|
|
50
|
+
short "needs the playground runtime" note.
|
|
26
51
|
|
|
27
52
|
## Basic Setup
|
|
28
53
|
|
|
@@ -46,11 +71,12 @@ If your TypeScript app checks CSS side-effect imports, include Vite's standard e
|
|
|
46
71
|
|
|
47
72
|
Use `GitbookStreamdown` when markdown may arrive incrementally from an AI stream. The component accepts either `markdown` or string children.
|
|
48
73
|
|
|
49
|
-
|
|
50
|
-
|
|
74
|
+
The package root's `GitbookStreamdown` is the same component as the
|
|
75
|
+
`@brett_lamy/docstream/streamdown` entry. For live code fences and runnable
|
|
76
|
+
inline demos, import the playground variant instead (requires `@agent-wasm/core`):
|
|
51
77
|
|
|
52
78
|
```tsx
|
|
53
|
-
import { GitbookStreamdown } from "@brett_lamy/docstream/
|
|
79
|
+
import { GitbookStreamdown } from "@brett_lamy/docstream/playground" // = PlaygroundStreamdown
|
|
54
80
|
```
|
|
55
81
|
|
|
56
82
|
```tsx
|
|
@@ -363,13 +389,15 @@ what it did before 1.2: a demo, then a code block. While streaming, a block whos
|
|
|
363
389
|
| inline files | no runtime (e.g. the `/streamdown` entry) | a one-line note; the viewer opens on Code | the inline files |
|
|
364
390
|
| `src` only | no resolver | a placeholder chip | — |
|
|
365
391
|
|
|
366
|
-
|
|
367
|
-
in almost-node by default.
|
|
392
|
+
`PlaygroundStreamdown` (also exported as `GitbookStreamdown` from
|
|
393
|
+
`@brett_lamy/docstream/playground`) runs inline demos in almost-node by default.
|
|
394
|
+
Elsewhere pass `demoRuntime` (and `liveRenderer` for live code fences):
|
|
368
395
|
|
|
369
396
|
```tsx
|
|
370
397
|
import { createAlmostNodeDemoRuntime } from "@brett_lamy/docstream/playground"
|
|
371
398
|
|
|
372
399
|
const demoRuntime = createAlmostNodeDemoRuntime({ workspaceOptions: { basePath: "/docs" } })
|
|
400
|
+
// live code fences: import { almostNodeLiveRenderer } from "@brett_lamy/docstream/playground"
|
|
373
401
|
|
|
374
402
|
<MarkdownContent markdown={page} demoResolver={demoResolver} demoRuntime={demoRuntime}
|
|
375
403
|
demoDependencies={{ "@brett_lamy/ui": "^1.2.0" }} />
|
|
@@ -542,7 +570,8 @@ You can also resolve paths yourself with `resolveAsset`.
|
|
|
542
570
|
- `resolveDemosToMarkdown(markdown, resolver, { format })`: Gives each `{% demo %}` its real files inline — renderable docstream by default, `format: "plain"` for LLM prompts (pure, async).
|
|
543
571
|
- `inlineDemoMarkdown(node, files, meta)`: One demo as a pasteable block-form `{% demo %}`.
|
|
544
572
|
- `preloadDemos(resolver, srcs)`: Settle resolver demos before the first render (SSR, static export).
|
|
545
|
-
- `createAlmostNodeDemoRuntime(options)` / `InlineDemoPreview`
|
|
573
|
+
- `createAlmostNodeDemoRuntime(options)` / `InlineDemoPreview` (`/playground`): Run inline demo files in almost-node. `createInlineDemoProject(files, { entry, dependencies })` (root and `/playground`) builds the runnable project as plain data.
|
|
574
|
+
- `PlaygroundStreamdown`, `almostNodeLiveRenderer`, `ReactDemo` / `ReactCodePreview`, `createAlmostNodeWorkspace`, `useAlmostNodeServer` (`/playground` only): the almost-node playground.
|
|
546
575
|
- `DocPageActions`: The "Copy page ▾" menu.
|
|
547
576
|
|
|
548
577
|
### Parser and Serializer
|
|
@@ -619,6 +648,23 @@ This release ships TypeScript and TSX source through ESM exports:
|
|
|
619
648
|
|
|
620
649
|
It is validated with Vite and modern TypeScript `moduleResolution: "Bundler"`. Plain Node.js, CommonJS, or tooling that does not transpile TypeScript in dependencies may need a future precompiled JS build.
|
|
621
650
|
|
|
651
|
+
## Migrating to 1.3
|
|
652
|
+
|
|
653
|
+
1.3 moves everything that touches almost-node (`@agent-wasm/core`) out of the
|
|
654
|
+
package root, so installing docstream no longer drags a WASM Node runtime and
|
|
655
|
+
native compressors into every app. `@agent-wasm/core` is now an optional peer.
|
|
656
|
+
|
|
657
|
+
| 1.2 (root import) | 1.3 |
|
|
658
|
+
| --- | --- |
|
|
659
|
+
| `GitbookStreamdown` (ran live code + inline demos in almost-node) | Root `GitbookStreamdown` renders them as code unless you pass `liveRenderer` / `demoRuntime`. For the old behaviour: `import { GitbookStreamdown } from "@brett_lamy/docstream/playground"` and install `@agent-wasm/core`. |
|
|
660
|
+
| `ReactCodePreview`, `ReactDemo`, `InlineDemoPreview`, `almostNodeDemoRuntime`, `createAlmostNodeDemoRuntime`, `createAlmostNodeFilesystem`, `createAlmostNodeWorkspace` and their option/prop types | `@brett_lamy/docstream/playground` |
|
|
661
|
+
| `VizEmbed`, `VizEmbedProps` | `@brett_lamy/docstream/viz`, and install `@brett_lamy/viz-engine` (now an optional peer, no longer a dependency) |
|
|
662
|
+
| `Streamdown`, `StreamdownProps` (re-export of `streamdown`) | Removed: install `streamdown` and import it directly |
|
|
663
|
+
|
|
664
|
+
`createInlineDemoProject`, `createReactDemoFiles` and the `AlmostNodeFile*` data
|
|
665
|
+
types stay on the root (they are plain data helpers). Math blocks now show their
|
|
666
|
+
TeX source for a moment while KaTeX loads.
|
|
667
|
+
|
|
622
668
|
## Related Package
|
|
623
669
|
|
|
624
670
|
Use `@brett_lamy/docstream-editor` when you need the editable TipTap experience for the same document model.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brett_lamy/docstream",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "GitBook-aware readonly markdown and AI stream renderer.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -78,23 +78,28 @@
|
|
|
78
78
|
"./styles.css": "./src/styles.css"
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
|
-
"@brett_lamy/viz-engine": ">=0.2.0",
|
|
82
81
|
"framer-motion": "^13.1.1",
|
|
83
82
|
"gpu-lexer": "^0.0.2",
|
|
84
83
|
"katex": "^0.18.5",
|
|
85
84
|
"lucide-react": "^1.17.0",
|
|
86
85
|
"mermaid": "^11.15.0",
|
|
87
86
|
"rrweb": "2.0.0-alpha.20",
|
|
88
|
-
"streamdown": "^2.5.0",
|
|
89
87
|
"yaml": "^2.9.0"
|
|
90
88
|
},
|
|
91
89
|
"peerDependencies": {
|
|
92
90
|
"@agent-wasm/core": ">=0.4.0",
|
|
93
91
|
"@brett_lamy/viz-engine": ">=0.2.0",
|
|
94
92
|
"react": ">=18",
|
|
93
|
+
"react-dom": ">=18",
|
|
95
94
|
"vite": ">=5"
|
|
96
95
|
},
|
|
97
96
|
"peerDependenciesMeta": {
|
|
97
|
+
"@agent-wasm/core": {
|
|
98
|
+
"optional": true
|
|
99
|
+
},
|
|
100
|
+
"@brett_lamy/viz-engine": {
|
|
101
|
+
"optional": true
|
|
102
|
+
},
|
|
98
103
|
"vite": {
|
|
99
104
|
"optional": true
|
|
100
105
|
}
|
|
@@ -105,6 +110,7 @@
|
|
|
105
110
|
},
|
|
106
111
|
"devDependencies": {
|
|
107
112
|
"@agent-wasm/core": "^0.4.0",
|
|
113
|
+
"@brett_lamy/viz-engine": "^0.2.1",
|
|
108
114
|
"@types/node": "^24.0.0",
|
|
109
115
|
"@types/react": "^18.3.3",
|
|
110
116
|
"typescript": "^5.5.4",
|
package/src/demo/markdown.ts
CHANGED
|
@@ -177,7 +177,7 @@ function inlineDemoKeepingTag(tagLine: string, node: DemoNode, files: DemoFile[]
|
|
|
177
177
|
const next = inlineDemoNode(node, files, meta)
|
|
178
178
|
const out = serializeBlocks([next])
|
|
179
179
|
const bare = (n: DemoNode): DemoNode => {
|
|
180
|
-
const { files: _files, open: _open, ...rest } = n
|
|
180
|
+
const { files: _files, open: _open, raw: _raw, ...rest } = n
|
|
181
181
|
return rest
|
|
182
182
|
}
|
|
183
183
|
if (serializeBlocks([bare(next)]) !== serializeBlocks([bare(node)])) return out
|
package/src/demo/types.ts
CHANGED
|
@@ -85,8 +85,8 @@ export interface InlineDemoRuntimeProps {
|
|
|
85
85
|
/**
|
|
86
86
|
* Renders the Preview of a `{% demo %}` block that carries its files inline and has
|
|
87
87
|
* no resolver behind it. `@brett_lamy/docstream/playground` provides an almost-node
|
|
88
|
-
* implementation (`createAlmostNodeDemoRuntime`), which `PlaygroundStreamdown`
|
|
89
|
-
* package root
|
|
88
|
+
* implementation (`createAlmostNodeDemoRuntime`), which that entry's `PlaygroundStreamdown`
|
|
89
|
+
* wires in by default. The package root never loads a runtime on its own.
|
|
90
90
|
*/
|
|
91
91
|
export type InlineDemoRuntime = (props: InlineDemoRuntimeProps) => ReactNode
|
|
92
92
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { useContext, useId, useMemo, useState, type ReactNode } from "react"
|
|
1
|
+
import { useContext, useEffect, useId, useMemo, useState, type ReactNode } from "react"
|
|
2
2
|
import { motion, useReducedMotion } from "framer-motion"
|
|
3
|
-
import "katex/dist/katex.min.css"
|
|
4
3
|
import {
|
|
5
4
|
AlertTriangle,
|
|
6
5
|
CheckCircle2,
|
|
@@ -24,16 +23,41 @@ import { CopyButton } from "./copy"
|
|
|
24
23
|
import { PillTabs, rovingKeyDown } from "./controls"
|
|
25
24
|
import { DocPageActions, type DocPageActionsOptions } from "./PageActions"
|
|
26
25
|
import { useSyncedTab } from "./tabs-sync"
|
|
27
|
-
import { ReplayPreview
|
|
26
|
+
import { ReplayPreview } from "../replay/ReplayPreview"
|
|
27
|
+
import { isReplayQaUrl } from "../replay/url"
|
|
28
28
|
import { VideoEmbed } from "../video"
|
|
29
29
|
import { OpenApiOperation } from "../openapi/OpenApiOperation"
|
|
30
30
|
import { Mermaid } from "./Mermaid"
|
|
31
31
|
import { HighlightedCode } from "./HighlightedCode"
|
|
32
32
|
import { CitationSources, InlineReference } from "./reference"
|
|
33
|
-
import { renderMathToHtml } from "./math"
|
|
34
33
|
|
|
34
|
+
type MathRenderer = (formula: string) => string
|
|
35
|
+
let mathRenderer: MathRenderer | null = null
|
|
36
|
+
let mathLoad: Promise<MathRenderer> | null = null
|
|
37
|
+
function loadMathRenderer(): Promise<MathRenderer> {
|
|
38
|
+
mathLoad ??= import("./math").then((m) => (mathRenderer = m.renderMathToHtml))
|
|
39
|
+
return mathLoad
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// KaTeX loads on first use; until then the TeX source shows in place.
|
|
35
43
|
function MathBlock({ formula }: { formula: string }) {
|
|
36
|
-
|
|
44
|
+
const [render, setRender] = useState<MathRenderer | null>(() => mathRenderer)
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (render) return
|
|
47
|
+
let live = true
|
|
48
|
+
loadMathRenderer().then((fn) => live && setRender(() => fn), () => {})
|
|
49
|
+
return () => {
|
|
50
|
+
live = false
|
|
51
|
+
}
|
|
52
|
+
}, [render])
|
|
53
|
+
if (!render) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="docs-math" data-pending="">
|
|
56
|
+
<code>{formula}</code>
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
return <div className="docs-math" dangerouslySetInnerHTML={{ __html: render(formula) }} />
|
|
37
61
|
}
|
|
38
62
|
|
|
39
63
|
export interface LivePreviewProps {
|
|
@@ -52,8 +76,12 @@ function InlineText({ nodes }: { nodes: Inline[] }) {
|
|
|
52
76
|
return (
|
|
53
77
|
<>
|
|
54
78
|
{nodes.map((n, i) => {
|
|
55
|
-
|
|
56
|
-
|
|
79
|
+
const emphasized = !!(n.bold || n.italic || n.strike)
|
|
80
|
+
let el: ReactNode
|
|
81
|
+
if (n.type === "reference") {
|
|
82
|
+
if (!emphasized && !n.link) return <InlineReference key={i} node={n} />
|
|
83
|
+
el = <InlineReference node={n} />
|
|
84
|
+
} else if (n.type === "image") {
|
|
57
85
|
const img = (
|
|
58
86
|
<img
|
|
59
87
|
src={resolveAsset(n.src)}
|
|
@@ -62,16 +90,20 @@ function InlineText({ nodes }: { nodes: Inline[] }) {
|
|
|
62
90
|
style={{ width: n.width, height: n.height }}
|
|
63
91
|
/>
|
|
64
92
|
)
|
|
65
|
-
|
|
93
|
+
const linked = n.link ? (
|
|
66
94
|
<a key={i} href={n.link} target="_blank" rel="noreferrer" className="docs-inline-img-link">
|
|
67
95
|
{img}
|
|
68
96
|
</a>
|
|
69
|
-
) :
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
) : null
|
|
98
|
+
if (!emphasized) return linked ?? <span key={i}>{img}</span>
|
|
99
|
+
// The image's link is its own anchor; emphasis wraps it.
|
|
100
|
+
el = linked ?? img
|
|
101
|
+
if (n.bold) el = <strong>{el}</strong>
|
|
102
|
+
if (n.italic) el = <em>{el}</em>
|
|
103
|
+
if (n.strike) el = <s>{el}</s>
|
|
104
|
+
return <span key={i}>{el}</span>
|
|
105
|
+
} else el = n.text
|
|
106
|
+
if (n.type === "text" && n.code) el = <code>{el}</code>
|
|
75
107
|
if (n.bold) el = <strong>{el}</strong>
|
|
76
108
|
if (n.italic) el = <em>{el}</em>
|
|
77
109
|
if (n.strike) el = <s>{el}</s>
|
|
@@ -523,6 +555,10 @@ function BlockView({ block, liveRenderer, sourceRenderer }: { block: Block } & R
|
|
|
523
555
|
method={block.method || "get"}
|
|
524
556
|
/>
|
|
525
557
|
)
|
|
558
|
+
// Reference definitions and verbatim-kept tags carry no content of their own.
|
|
559
|
+
case "definition":
|
|
560
|
+
case "raw":
|
|
561
|
+
return null
|
|
526
562
|
}
|
|
527
563
|
}
|
|
528
564
|
|
|
@@ -549,8 +585,8 @@ export interface DocRenderOptions {
|
|
|
549
585
|
demoResolver?: DemoResolver
|
|
550
586
|
/**
|
|
551
587
|
* Runs the Preview of `{% demo %}` blocks that carry their files inline and have no
|
|
552
|
-
* resolver behind them.
|
|
553
|
-
*
|
|
588
|
+
* resolver behind them. Nothing is supplied by default: pass `createAlmostNodeDemoRuntime()`
|
|
589
|
+
* from `@brett_lamy/docstream/playground` (or use its `PlaygroundStreamdown`) to run them.
|
|
554
590
|
* Without one, such demos show their code with a short note.
|
|
555
591
|
*/
|
|
556
592
|
demoRuntime?: InlineDemoRuntime
|
package/src/docs/math.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
/* KaTeX lives behind this module, which DocsRenderer loads with a dynamic import the
|
|
2
|
+
first time a page has a math block — pages without math never download KaTeX. */
|
|
1
3
|
import katex from "katex"
|
|
4
|
+
import "katex/dist/katex.min.css"
|
|
2
5
|
|
|
3
6
|
export function renderMathToHtml(formula: string): string {
|
|
4
7
|
return katex.renderToString(formula, {
|