@brett_lamy/docstream 0.3.0 → 0.3.2
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 +36 -80
- package/package.json +18 -8
- package/src/docs/DocsRenderer.tsx +6 -1
- package/src/env.d.ts +4 -0
- package/src/gitbook/ast.ts +6 -0
- package/src/gitbook/parse.ts +23 -1
- package/src/gitbook/serialize.ts +12 -2
- package/src/index.ts +2 -0
- package/src/styles.css +16 -0
- package/src/viz/VizEmbed.tsx +42 -0
- package/src/viz/index.ts +4 -0
package/README.md
CHANGED
|
@@ -10,11 +10,10 @@ GitBook-aware markdown rendering for React applications and AI streaming surface
|
|
|
10
10
|
- Streaming-friendly `GitbookStreamdown` component inspired by `vercel/streamdown`.
|
|
11
11
|
- Parser and serializer for round-tripping supported GitBook syntax.
|
|
12
12
|
- Syntax-highlighted code blocks through `lowlight`.
|
|
13
|
-
- Optional live React/JSX/TSX previews powered by the almost-node runtime.
|
|
14
|
-
- A complete virtual-filesystem helper for multi-file React demos.
|
|
15
13
|
- GitBook block support for hints, tabs, expandables, steppers, embeds, content refs, columns, figures, tables, math, dividers, updates, and OpenAPI operations.
|
|
16
|
-
- Embed blocks render YouTube/direct video URLs through the built-in `VideoEmbed` and public Loop QA task, journey, exploration, or test-run URLs through `ReplayPreview`.
|
|
17
14
|
- CSS exported as a stable package entrypoint so host apps can theme with CSS variables or shadcn-style design tokens.
|
|
15
|
+
- Attribute-aware direct video embeds for muted, looping inline clips in long-form posts.
|
|
16
|
+
- Optional `VizEmbed` integration for mounting deterministic `@brett_lamy/viz-engine` scenes in a document.
|
|
18
17
|
|
|
19
18
|
## Installation
|
|
20
19
|
|
|
@@ -70,62 +69,6 @@ export function Preview() {
|
|
|
70
69
|
}
|
|
71
70
|
```
|
|
72
71
|
|
|
73
|
-
## Run React and JSX demos
|
|
74
|
-
|
|
75
|
-
Install `@agent-wasm/core` in the host app and serve its service worker through
|
|
76
|
-
the Vite plugin:
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
import { almostnodePlugin } from "@agent-wasm/core/vite"
|
|
80
|
-
|
|
81
|
-
export default defineConfig({
|
|
82
|
-
plugins: [almostnodePlugin()],
|
|
83
|
-
})
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
`ReactDemo` creates the complete file tree in almost-node, starts its Vite
|
|
87
|
-
server, and renders the resulting app in a preview iframe. The file map can
|
|
88
|
-
contain any number of files:
|
|
89
|
-
|
|
90
|
-
```tsx
|
|
91
|
-
import { ReactDemo } from "@brett_lamy/docstream"
|
|
92
|
-
|
|
93
|
-
const files = {
|
|
94
|
-
"/src/main.jsx": `
|
|
95
|
-
import React from "react"
|
|
96
|
-
import { createRoot } from "react-dom/client"
|
|
97
|
-
import App from "./App.jsx"
|
|
98
|
-
createRoot(document.getElementById("root")).render(<App />)
|
|
99
|
-
`,
|
|
100
|
-
"/src/App.jsx": `
|
|
101
|
-
import Card from "./Card.jsx"
|
|
102
|
-
export default function App() { return <Card title="Hello" /> }
|
|
103
|
-
`,
|
|
104
|
-
"/src/Card.jsx": `
|
|
105
|
-
export default function Card({ title }) { return <button>{title}</button> }
|
|
106
|
-
`,
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
<ReactDemo files={files} entry="/src/main.jsx" />
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
For lower-level integrations, `createAlmostNodeFilesystem(files)` creates a
|
|
113
|
-
`VirtualFS`, while `createAlmostNodeWorkspace(files)` creates a container and
|
|
114
|
-
populates its complete filesystem so callers can run shell commands, npm, or
|
|
115
|
-
Vite themselves.
|
|
116
|
-
|
|
117
|
-
Single-file live code blocks opt in explicitly so ordinary documentation
|
|
118
|
-
snippets remain inert:
|
|
119
|
-
|
|
120
|
-
````md
|
|
121
|
-
{% code language="jsx" live="true" entry="/src/main.jsx" %}
|
|
122
|
-
```jsx
|
|
123
|
-
import { createRoot } from "react-dom/client"
|
|
124
|
-
createRoot(document.getElementById("root")).render(<h1>Hello</h1>)
|
|
125
|
-
```
|
|
126
|
-
{% endcode %}
|
|
127
|
-
````
|
|
128
|
-
|
|
129
72
|
## Markdown Helper Component
|
|
130
73
|
|
|
131
74
|
`MarkdownContent` parses and renders a markdown string in one step:
|
|
@@ -138,25 +81,6 @@ export function Preview({ markdown }: { markdown: string }) {
|
|
|
138
81
|
}
|
|
139
82
|
```
|
|
140
83
|
|
|
141
|
-
## Video and Replay QA embeds
|
|
142
|
-
|
|
143
|
-
`embed` blocks accept direct `.mp4`, `.webm`, and `.ogg` assets, YouTube URLs,
|
|
144
|
-
and public Loop QA project URLs. Loop QA project URLs are converted to the
|
|
145
|
-
chrome-free `/p/...` route automatically:
|
|
146
|
-
|
|
147
|
-
```md
|
|
148
|
-
{% embed url="https://loop-qa.replay.io/projects/project-id/journeys/journey-id" /%}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
The replay component is also available directly when a document needs a
|
|
152
|
-
custom source or native rrweb events:
|
|
153
|
-
|
|
154
|
-
```tsx
|
|
155
|
-
import { ReplayPreview } from "@brett_lamy/docstream"
|
|
156
|
-
|
|
157
|
-
<ReplayPreview source="https://loop-qa.replay.io/projects/project-id/tasks/task-id" />
|
|
158
|
-
```
|
|
159
|
-
|
|
160
84
|
## GitBook Syntax
|
|
161
85
|
|
|
162
86
|
The parser supports normal Markdown plus GitBook-style block tags.
|
|
@@ -217,6 +141,40 @@ Call the API.
|
|
|
217
141
|
|
|
218
142
|
When a spec cannot be resolved, the renderer displays a fallback asking for an OpenAPI spec URL.
|
|
219
143
|
|
|
144
|
+
### Inline video clips
|
|
145
|
+
|
|
146
|
+
Direct media embeds can opt into browser-safe inline playback. `muted` is
|
|
147
|
+
important when `autoplay` is enabled:
|
|
148
|
+
|
|
149
|
+
```md
|
|
150
|
+
{% embed url="/generated/example/clips/offsets.mp4"
|
|
151
|
+
title="A reader resumes from its bookmark"
|
|
152
|
+
autoplay="true" loop="true" muted="true" controls="false" %}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The attributes round-trip through `parseMarkdown` and `serializeMarkdown` and
|
|
156
|
+
are rendered as a native `<video playsInline>` element.
|
|
157
|
+
|
|
158
|
+
### VizEngine scenes
|
|
159
|
+
|
|
160
|
+
Install the optional peer dependency when a document needs a live, seekable
|
|
161
|
+
scene rather than a rendered clip:
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
npm install @brett_lamy/docstream @brett_lamy/viz-engine
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
import { VizEmbed } from "@brett_lamy/docstream/viz"
|
|
169
|
+
import "@brett_lamy/docstream/styles.css"
|
|
170
|
+
|
|
171
|
+
<VizEmbed scene={scene} title="The same event, replayed into two projections" />
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`VizEmbed` keeps the scene's timeline deterministic and delegates the clock to
|
|
175
|
+
VizEngine, so a reader can pause or scrub the same explanation used to produce
|
|
176
|
+
the short clip.
|
|
177
|
+
|
|
220
178
|
## Assets and OpenAPI Specs
|
|
221
179
|
|
|
222
180
|
Relative image and OpenAPI spec paths can be resolved against an asset base:
|
|
@@ -236,8 +194,6 @@ You can also resolve paths yourself with `resolveAsset`.
|
|
|
236
194
|
- `GitbookStreamdown`: Parses and renders markdown for read-only stream output.
|
|
237
195
|
- `DocsRenderer`: Renders a parsed `DocumentNode`.
|
|
238
196
|
- `MarkdownContent`: Parses and renders a markdown string.
|
|
239
|
-
- `ReplayPreview` / `ReplayEmbed`: Embeds public Loop QA pages or plays CORS-enabled rrweb event data.
|
|
240
|
-
- `VideoEmbed`: Renders direct video assets or hosted video players.
|
|
241
197
|
- `OpenApiOperation`: Renders a parsed OpenAPI operation block.
|
|
242
198
|
|
|
243
199
|
### Parser and Serializer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brett_lamy/docstream",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "GitBook-aware readonly markdown and AI stream renderer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
"types": "./src/video/index.ts",
|
|
38
38
|
"import": "./src/video/index.ts"
|
|
39
39
|
},
|
|
40
|
+
"./viz": {
|
|
41
|
+
"types": "./src/viz/index.ts",
|
|
42
|
+
"import": "./src/viz/index.ts"
|
|
43
|
+
},
|
|
40
44
|
"./playground": {
|
|
41
45
|
"types": "./src/playground/index.ts",
|
|
42
46
|
"import": "./src/playground/index.ts"
|
|
@@ -50,15 +54,21 @@
|
|
|
50
54
|
"mermaid": "^11.15.0",
|
|
51
55
|
"rrweb": "2.0.0-alpha.20",
|
|
52
56
|
"streamdown": "^2.5.0",
|
|
53
|
-
"yaml": "^2.9.0"
|
|
57
|
+
"yaml": "^2.9.0",
|
|
58
|
+
"@brett_lamy/viz-engine": "^0.2.0"
|
|
54
59
|
},
|
|
55
60
|
"peerDependencies": {
|
|
56
|
-
"
|
|
57
|
-
"@
|
|
61
|
+
"@agent-wasm/core": ">=0.4.0",
|
|
62
|
+
"@brett_lamy/viz-engine": ">=0.2.0",
|
|
63
|
+
"react": ">=18"
|
|
64
|
+
},
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "git+https://github.com/BLamy/docstream.git"
|
|
58
68
|
},
|
|
59
|
-
"
|
|
60
|
-
"@agent-wasm/core":
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@agent-wasm/core": "^0.4.0",
|
|
71
|
+
"@types/react": "^18.3.3",
|
|
72
|
+
"typescript": "^5.5.4"
|
|
63
73
|
}
|
|
64
74
|
}
|
|
@@ -187,7 +187,12 @@ function BlockView({ block }: { block: Block }) {
|
|
|
187
187
|
<VideoEmbed
|
|
188
188
|
src={src ?? block.url}
|
|
189
189
|
className="docs-embed"
|
|
190
|
-
title={block.url}
|
|
190
|
+
title={block.title ?? block.url}
|
|
191
|
+
{...(block.autoplay === undefined ? {} : { autoplay: block.autoplay })}
|
|
192
|
+
{...(block.loop === undefined ? {} : { loop: block.loop })}
|
|
193
|
+
{...(block.muted === undefined ? {} : { muted: block.muted })}
|
|
194
|
+
{...(block.controls === undefined ? {} : { controls: block.controls })}
|
|
195
|
+
{...(block.poster === undefined ? {} : { poster: resolveAsset(block.poster) })}
|
|
191
196
|
/>
|
|
192
197
|
) : (
|
|
193
198
|
<a className="docs-content-ref" href={block.url} target="_blank" rel="noreferrer">
|
package/src/env.d.ts
ADDED
package/src/gitbook/ast.ts
CHANGED
|
@@ -85,6 +85,12 @@ export interface StepperNode {
|
|
|
85
85
|
export interface EmbedNode {
|
|
86
86
|
type: "embed"
|
|
87
87
|
url: string
|
|
88
|
+
title?: string
|
|
89
|
+
autoplay?: boolean
|
|
90
|
+
loop?: boolean
|
|
91
|
+
muted?: boolean
|
|
92
|
+
controls?: boolean
|
|
93
|
+
poster?: string
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
export interface ContentRefNode {
|
package/src/gitbook/parse.ts
CHANGED
|
@@ -66,6 +66,11 @@ function parseAttrs(raw: string | undefined): Record<string, string> {
|
|
|
66
66
|
return attrs
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
function booleanAttr(attrs: Record<string, string>, key: string): boolean | undefined {
|
|
70
|
+
if (!(key in attrs)) return undefined
|
|
71
|
+
return attrs[key] !== "false"
|
|
72
|
+
}
|
|
73
|
+
|
|
69
74
|
interface TemplateTag {
|
|
70
75
|
name: string
|
|
71
76
|
attrs: Record<string, string>
|
|
@@ -197,7 +202,24 @@ export function parseBlocks(lines: string[]): Block[] {
|
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
if (tag.name === "embed") {
|
|
200
|
-
blocks.push({
|
|
205
|
+
blocks.push({
|
|
206
|
+
type: "embed",
|
|
207
|
+
url: tag.attrs.url ?? "",
|
|
208
|
+
...(tag.attrs.title ? { title: tag.attrs.title } : {}),
|
|
209
|
+
...(tag.attrs.poster ? { poster: tag.attrs.poster } : {}),
|
|
210
|
+
...(booleanAttr(tag.attrs, "autoplay") === undefined
|
|
211
|
+
? {}
|
|
212
|
+
: { autoplay: booleanAttr(tag.attrs, "autoplay") }),
|
|
213
|
+
...(booleanAttr(tag.attrs, "loop") === undefined
|
|
214
|
+
? {}
|
|
215
|
+
: { loop: booleanAttr(tag.attrs, "loop") }),
|
|
216
|
+
...(booleanAttr(tag.attrs, "muted") === undefined
|
|
217
|
+
? {}
|
|
218
|
+
: { muted: booleanAttr(tag.attrs, "muted") }),
|
|
219
|
+
...(booleanAttr(tag.attrs, "controls") === undefined
|
|
220
|
+
? {}
|
|
221
|
+
: { controls: booleanAttr(tag.attrs, "controls") }),
|
|
222
|
+
})
|
|
201
223
|
i++
|
|
202
224
|
// tolerate optional {% endembed %}
|
|
203
225
|
if (i < lines.length && templateTag(lines[i])?.name === "endembed") i++
|
package/src/gitbook/serialize.ts
CHANGED
|
@@ -60,8 +60,18 @@ function serializeBlock(b: Block): string {
|
|
|
60
60
|
})
|
|
61
61
|
.join("\n\n")}\n{% endstepper %}`
|
|
62
62
|
|
|
63
|
-
case "embed":
|
|
64
|
-
|
|
63
|
+
case "embed": {
|
|
64
|
+
const attrs = [
|
|
65
|
+
` url="${b.url}"`,
|
|
66
|
+
b.title ? ` title="${b.title}"` : "",
|
|
67
|
+
b.poster ? ` poster="${b.poster}"` : "",
|
|
68
|
+
b.autoplay === undefined ? "" : ` autoplay="${b.autoplay}"`,
|
|
69
|
+
b.loop === undefined ? "" : ` loop="${b.loop}"`,
|
|
70
|
+
b.muted === undefined ? "" : ` muted="${b.muted}"`,
|
|
71
|
+
b.controls === undefined ? "" : ` controls="${b.controls}"`,
|
|
72
|
+
].join("")
|
|
73
|
+
return `{% embed${attrs} %}`
|
|
74
|
+
}
|
|
65
75
|
|
|
66
76
|
case "content-ref":
|
|
67
77
|
return `{% content-ref url="${b.url}" %}\n${serializeInline(b.children)}\n{% endcontent-ref %}`
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type {
|
|
|
12
12
|
export { isReplayQaUrl, normalizeReplayEmbedUrl } from "./replay"
|
|
13
13
|
export { VideoEmbed } from "./video"
|
|
14
14
|
export type { VideoEmbedProps } from "./video"
|
|
15
|
+
export { VizEmbed } from "./viz"
|
|
16
|
+
export type { VizEmbedProps } from "./viz"
|
|
15
17
|
export { OpenApiOperation } from "./openapi/OpenApiOperation"
|
|
16
18
|
export {
|
|
17
19
|
ReactCodePreview,
|
package/src/styles.css
CHANGED
|
@@ -122,6 +122,22 @@
|
|
|
122
122
|
margin-top: 8px;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
.docstream-viz {
|
|
126
|
+
margin: 1.5rem 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.docstream-viz__player {
|
|
130
|
+
overflow: hidden;
|
|
131
|
+
border: 1px solid var(--gb-border);
|
|
132
|
+
border-radius: 12px;
|
|
133
|
+
background: var(--gb-muted);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.docstream-viz figcaption {
|
|
137
|
+
margin-top: 8px;
|
|
138
|
+
color: var(--gb-muted-foreground);
|
|
139
|
+
}
|
|
140
|
+
|
|
125
141
|
.docs-hint {
|
|
126
142
|
display: flex;
|
|
127
143
|
gap: 12px;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { PlayerAudio, VideoScene } from "@brett_lamy/viz-engine"
|
|
2
|
+
import { VizPlayer } from "@brett_lamy/viz-engine"
|
|
3
|
+
|
|
4
|
+
export interface VizEmbedProps {
|
|
5
|
+
scene: VideoScene
|
|
6
|
+
audio?: PlayerAudio
|
|
7
|
+
title?: string
|
|
8
|
+
className?: string
|
|
9
|
+
autoplay?: boolean
|
|
10
|
+
loop?: boolean
|
|
11
|
+
showCaptions?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Mount a deterministic VizEngine scene inside a Docstream document. This is
|
|
16
|
+
* intentionally a component API rather than a markdown escape hatch: callers
|
|
17
|
+
* choose the scene and its evidence, while Docstream supplies document chrome
|
|
18
|
+
* and accessible figure semantics.
|
|
19
|
+
*/
|
|
20
|
+
export function VizEmbed({
|
|
21
|
+
scene,
|
|
22
|
+
audio,
|
|
23
|
+
title,
|
|
24
|
+
className,
|
|
25
|
+
autoplay = true,
|
|
26
|
+
loop = true,
|
|
27
|
+
showCaptions = false,
|
|
28
|
+
}: VizEmbedProps) {
|
|
29
|
+
return (
|
|
30
|
+
<figure className={`docstream-viz ${className ?? ""}`.trim()}>
|
|
31
|
+
<VizPlayer
|
|
32
|
+
scene={scene}
|
|
33
|
+
{...(audio === undefined ? {} : { audio })}
|
|
34
|
+
autoplay={autoplay}
|
|
35
|
+
loop={loop}
|
|
36
|
+
showCaptions={showCaptions}
|
|
37
|
+
className="docstream-viz__player"
|
|
38
|
+
/>
|
|
39
|
+
{title && <figcaption>{title}</figcaption>}
|
|
40
|
+
</figure>
|
|
41
|
+
)
|
|
42
|
+
}
|
package/src/viz/index.ts
ADDED