@brett_lamy/docstream 1.1.1 → 1.2.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 +147 -9
- package/package.json +3 -1
- package/src/demo/DemoViewer.tsx +213 -46
- package/src/demo/context.ts +17 -1
- package/src/demo/glob.ts +12 -4
- package/src/demo/index.ts +15 -5
- package/src/demo/markdown.ts +138 -41
- package/src/demo/types.ts +46 -2
- package/src/docs/DocsRenderer.tsx +147 -34
- package/src/docs/PageActions.tsx +9 -5
- package/src/docs/controls.tsx +4 -1
- package/src/gitbook/ast.ts +63 -4
- package/src/gitbook/flatten.ts +47 -0
- package/src/gitbook/index.ts +6 -2
- package/src/gitbook/inline.ts +5 -2
- package/src/gitbook/outline.ts +45 -0
- package/src/gitbook/package-managers.ts +98 -0
- package/src/gitbook/parse.ts +179 -8
- package/src/gitbook/serialize.ts +40 -4
- package/src/index.ts +23 -1
- package/src/playground/InlineDemoPreview.tsx +77 -0
- package/src/playground/PlaygroundStreamdown.tsx +12 -2
- package/src/playground/ReactCodePreview.tsx +67 -36
- package/src/playground/filesystem.ts +83 -1
- package/src/playground/index.ts +7 -2
- package/src/streamdown.tsx +6 -2
- package/src/styles.css +117 -8
package/src/index.ts
CHANGED
|
@@ -13,25 +13,35 @@ export {
|
|
|
13
13
|
DemoFullscreen,
|
|
14
14
|
DemoViewer,
|
|
15
15
|
DocstreamDemoContext,
|
|
16
|
+
DocstreamDemoRuntimeContext,
|
|
16
17
|
createGlobDemoResolver,
|
|
17
18
|
defaultDemoMarkdown,
|
|
19
|
+
inlineDemoMarkdown,
|
|
20
|
+
inlineDemoNode,
|
|
18
21
|
demoComponentFrom,
|
|
19
22
|
demoFromSearch,
|
|
20
23
|
demoHref,
|
|
24
|
+
preloadDemos,
|
|
21
25
|
resolveDemosToMarkdown,
|
|
22
26
|
sortDemoFiles,
|
|
27
|
+
toInlineFiles,
|
|
23
28
|
useDemoResolver,
|
|
29
|
+
useDemoRuntime,
|
|
24
30
|
} from "./demo"
|
|
25
31
|
export type {
|
|
26
32
|
DemoComponent,
|
|
27
33
|
DemoComponentProps,
|
|
28
34
|
DemoFile,
|
|
29
35
|
DemoFullscreenProps,
|
|
36
|
+
DemoMarkdownFormat,
|
|
30
37
|
DemoMeta,
|
|
31
38
|
DemoResolver,
|
|
39
|
+
DemoRuntimeOptions,
|
|
32
40
|
DemoVariant,
|
|
33
41
|
DemoViewerProps,
|
|
34
42
|
GlobDemoResolverOptions,
|
|
43
|
+
InlineDemoRuntime,
|
|
44
|
+
InlineDemoRuntimeProps,
|
|
35
45
|
ResolveDemosOptions,
|
|
36
46
|
ResolvedDemo,
|
|
37
47
|
} from "./demo"
|
|
@@ -52,8 +62,12 @@ export { VizEmbed } from "./viz"
|
|
|
52
62
|
export type { VizEmbedProps } from "./viz"
|
|
53
63
|
export { OpenApiOperation } from "./openapi/OpenApiOperation"
|
|
54
64
|
export {
|
|
65
|
+
InlineDemoPreview,
|
|
55
66
|
ReactCodePreview,
|
|
56
67
|
ReactDemo,
|
|
68
|
+
almostNodeDemoRuntime,
|
|
69
|
+
createAlmostNodeDemoRuntime,
|
|
70
|
+
createInlineDemoProject,
|
|
57
71
|
createReactDemoFiles,
|
|
58
72
|
createAlmostNodeFilesystem,
|
|
59
73
|
createAlmostNodeWorkspace,
|
|
@@ -65,6 +79,10 @@ export type {
|
|
|
65
79
|
AlmostNodeFilesystemOptions,
|
|
66
80
|
AlmostNodeWorkspace,
|
|
67
81
|
AlmostNodeWorkspaceOptions,
|
|
82
|
+
AlmostNodeDemoRuntimeOptions,
|
|
83
|
+
InlineDemoPreviewProps,
|
|
84
|
+
InlineDemoProject,
|
|
85
|
+
InlineDemoProjectOptions,
|
|
68
86
|
ReactCodePreviewProps,
|
|
69
87
|
ReactDemoProps,
|
|
70
88
|
} from "./playground"
|
|
@@ -79,7 +97,7 @@ export type {
|
|
|
79
97
|
SourceReferenceClient,
|
|
80
98
|
} from "./source"
|
|
81
99
|
export { parseDemoVariants, parseMarkdown, parseBlocks, trimPartialInlineToken } from "./gitbook/parse"
|
|
82
|
-
export { serializeBlocks, serializeMarkdown } from "./gitbook/serialize"
|
|
100
|
+
export { fenceFor, serializeBlocks, serializeDemoFile, serializeMarkdown } from "./gitbook/serialize"
|
|
83
101
|
export {
|
|
84
102
|
footnoteDefinitions,
|
|
85
103
|
parseInline,
|
|
@@ -91,3 +109,7 @@ export {
|
|
|
91
109
|
export { Streamdown } from "streamdown"
|
|
92
110
|
export type { StreamdownProps } from "streamdown"
|
|
93
111
|
export type * from "./gitbook/ast"
|
|
112
|
+
export { PACKAGE_MANAGERS, packageManagerCommands } from "./gitbook/package-managers"
|
|
113
|
+
export { flattenBlocks, flattenForPlainMarkdown } from "./gitbook/flatten"
|
|
114
|
+
export { documentOutline, slugify } from "./gitbook/outline"
|
|
115
|
+
export type { OutlineEntry } from "./gitbook/outline"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* Runs a `{% demo %}` block's inline files in almost-node — the Preview of a pasted,
|
|
2
|
+
self-contained demo when no resolver knows it. Just the iframe: the DemoViewer around it
|
|
3
|
+
supplies the header, variants, viewports, Code view and fullscreen. */
|
|
4
|
+
import { useMemo, type ReactNode } from "react"
|
|
5
|
+
|
|
6
|
+
import type { DemoFile, InlineDemoRuntime, InlineDemoRuntimeProps } from "../demo/types"
|
|
7
|
+
import { createInlineDemoProject, type AlmostNodeWorkspaceOptions } from "./filesystem"
|
|
8
|
+
import { useAlmostNodeServer } from "./ReactCodePreview"
|
|
9
|
+
|
|
10
|
+
export interface InlineDemoPreviewProps extends InlineDemoRuntimeProps {
|
|
11
|
+
/** Preferred virtual port. almost-node picks the next free one if taken. */
|
|
12
|
+
port?: number
|
|
13
|
+
/** iframe sandbox value (same-origin lets the almost-node service worker route it). */
|
|
14
|
+
sandbox?: string
|
|
15
|
+
/** Extra options for `createAlmostNodeWorkspace` (e.g. `basePath` for static hosting). */
|
|
16
|
+
workspaceOptions?: Partial<AlmostNodeWorkspaceOptions>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function filesKey(files: DemoFile[], entry: string, dependencies: Record<string, string> | undefined) {
|
|
20
|
+
return JSON.stringify([entry, dependencies ?? null, files.map((f) => [f.path, f.content])])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function InlineDemoPreview({
|
|
24
|
+
title,
|
|
25
|
+
files,
|
|
26
|
+
entry,
|
|
27
|
+
variant,
|
|
28
|
+
dependencies,
|
|
29
|
+
port = 4173,
|
|
30
|
+
sandbox = "allow-scripts allow-same-origin allow-forms allow-modals",
|
|
31
|
+
workspaceOptions,
|
|
32
|
+
}: InlineDemoPreviewProps) {
|
|
33
|
+
// Content-keyed so re-renders (variant switches, parent state) never restart Vite.
|
|
34
|
+
const key = filesKey(files, entry, dependencies)
|
|
35
|
+
const project = useMemo(
|
|
36
|
+
() => {
|
|
37
|
+
try {
|
|
38
|
+
return { value: createInlineDemoProject(files, { entry, dependencies }) }
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return { error: error instanceof Error ? error.message : String(error) }
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
44
|
+
[key],
|
|
45
|
+
)
|
|
46
|
+
const { state, url, error } = useAlmostNodeServer({
|
|
47
|
+
files: project.value?.files ?? {},
|
|
48
|
+
port,
|
|
49
|
+
run: project.value ? 1 : 0,
|
|
50
|
+
workspaceOptions,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const message = project.error ?? error
|
|
54
|
+
if (message) return <pre className="docs-demo-error" role="alert">{message}</pre>
|
|
55
|
+
if (state !== "ready" || !url) {
|
|
56
|
+
return <div className="docs-demo-runtime-status" role="status">Starting almost-node…</div>
|
|
57
|
+
}
|
|
58
|
+
const src = project.value?.wrapped && variant !== undefined ? `${url}?variant=${encodeURIComponent(variant)}` : url
|
|
59
|
+
return <iframe key={src} title={title} className="docs-demo-runtime-frame" src={src} sandbox={sandbox} />
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AlmostNodeDemoRuntimeOptions {
|
|
63
|
+
port?: number
|
|
64
|
+
sandbox?: string
|
|
65
|
+
workspaceOptions?: Partial<AlmostNodeWorkspaceOptions>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* An `InlineDemoRuntime` backed by almost-node (`@agent-wasm/core`, loaded lazily on first
|
|
70
|
+
* use). Pass it as `demoRuntime`; `PlaygroundStreamdown` uses one with default options.
|
|
71
|
+
*/
|
|
72
|
+
export function createAlmostNodeDemoRuntime(options: AlmostNodeDemoRuntimeOptions = {}): InlineDemoRuntime {
|
|
73
|
+
return (props): ReactNode => <InlineDemoPreview {...props} {...options} />
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The default almost-node runtime (no base path, port 4173+). */
|
|
77
|
+
export const almostNodeDemoRuntime: InlineDemoRuntime = createAlmostNodeDemoRuntime()
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { useMemo } from "react"
|
|
1
|
+
import { useContext, useMemo } from "react"
|
|
2
2
|
|
|
3
3
|
import { DocsRenderer, type DocRenderOptions, type LivePreviewRenderer, type SourceReferenceRenderer } from "../docs/DocsRenderer"
|
|
4
4
|
import { parseMarkdown, trimPartialInlineToken } from "../gitbook/parse"
|
|
5
|
+
import { DocstreamDemoRuntimeContext } from "../demo/context"
|
|
6
|
+
import { almostNodeDemoRuntime } from "./InlineDemoPreview"
|
|
5
7
|
import { ReactCodePreview } from "./ReactCodePreview"
|
|
6
8
|
|
|
7
9
|
export interface PlaygroundStreamdownProps extends DocRenderOptions {
|
|
@@ -16,7 +18,8 @@ export interface PlaygroundStreamdownProps extends DocRenderOptions {
|
|
|
16
18
|
/**
|
|
17
19
|
* The full renderer variant. It keeps the optional almost-node playground
|
|
18
20
|
* behind an explicit import boundary; markdown-only consumers should use the
|
|
19
|
-
* `@brett_lamy/docstream/streamdown` entry instead.
|
|
21
|
+
* `@brett_lamy/docstream/streamdown` entry instead. Live code fences and
|
|
22
|
+
* `{% demo %}` blocks with inline files (and no resolver) run in almost-node.
|
|
20
23
|
*/
|
|
21
24
|
export function PlaygroundStreamdown({
|
|
22
25
|
children,
|
|
@@ -26,9 +29,14 @@ export function PlaygroundStreamdown({
|
|
|
26
29
|
markdown,
|
|
27
30
|
sourceRenderer,
|
|
28
31
|
demoResolver,
|
|
32
|
+
demoRuntime,
|
|
33
|
+
demoDependencies,
|
|
29
34
|
pageActions,
|
|
30
35
|
}: PlaygroundStreamdownProps) {
|
|
31
36
|
const content = markdown ?? children ?? ""
|
|
37
|
+
// Inline-file demos run in almost-node unless the host (or an outer renderer) chose a runtime.
|
|
38
|
+
const inheritedRuntime = useContext(DocstreamDemoRuntimeContext).runtime
|
|
39
|
+
const runtime = demoRuntime ?? inheritedRuntime ?? almostNodeDemoRuntime
|
|
32
40
|
const doc = useMemo(
|
|
33
41
|
() => parseMarkdown(isStreaming ? trimPartialInlineToken(content) : content),
|
|
34
42
|
[content, isStreaming]
|
|
@@ -56,6 +64,8 @@ export function PlaygroundStreamdown({
|
|
|
56
64
|
liveRenderer={liveRenderer}
|
|
57
65
|
sourceRenderer={sourceRenderer}
|
|
58
66
|
demoResolver={demoResolver}
|
|
67
|
+
demoRuntime={runtime}
|
|
68
|
+
demoDependencies={demoDependencies}
|
|
59
69
|
pageActions={isStreaming ? false : pageActions}
|
|
60
70
|
/>
|
|
61
71
|
</div>
|
|
@@ -53,7 +53,7 @@ export interface ReactDemoProps {
|
|
|
53
53
|
|
|
54
54
|
export type ReactCodePreviewProps = ReactDemoProps
|
|
55
55
|
|
|
56
|
-
type PreviewState = "idle" | "starting" | "ready" | "error"
|
|
56
|
+
export type PreviewState = "idle" | "starting" | "ready" | "error"
|
|
57
57
|
|
|
58
58
|
const DETACHED_SERVER_ENV = { ALMOSTNODE_DETACH_DEV_SERVERS: "1" }
|
|
59
59
|
|
|
@@ -79,43 +79,30 @@ function sourceText(files: AlmostNodeFiles, requestedPath: string): string | nul
|
|
|
79
79
|
return typeof file.content === "string" ? file.content : new TextDecoder().decode(file.content)
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
export interface AlmostNodeServerOptions {
|
|
83
|
+
/** The complete project (see `createReactDemoFiles`). Keep its identity stable between renders. */
|
|
84
|
+
files: AlmostNodeFiles
|
|
85
|
+
port?: number
|
|
86
|
+
/** Start when > 0; bump to restart. 0 stops (and disposes) the server. */
|
|
87
|
+
run: number
|
|
88
|
+
workspaceOptions?: Partial<AlmostNodeWorkspaceOptions>
|
|
89
|
+
onReady?: (url: string) => void
|
|
90
|
+
onError?: (error: Error) => void
|
|
91
|
+
}
|
|
84
92
|
|
|
85
|
-
export
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
height = 360,
|
|
95
|
-
codeFile,
|
|
96
|
-
collapsedCodeLines = 3,
|
|
97
|
-
expandedCodeLines = 30,
|
|
98
|
-
title = "Live React preview",
|
|
99
|
-
autoStart = true,
|
|
100
|
-
sandbox = "allow-scripts allow-same-origin allow-forms allow-modals",
|
|
101
|
-
workspaceOptions,
|
|
102
|
-
className,
|
|
103
|
-
onReady,
|
|
104
|
-
onError,
|
|
105
|
-
}: ReactDemoProps) {
|
|
106
|
-
const inline = preview !== undefined
|
|
107
|
-
const [run, setRun] = useState(autoStart && !inline ? 1 : 0)
|
|
108
|
-
const [state, setState] = useState<PreviewState>(inline ? "ready" : autoStart ? "starting" : "idle")
|
|
93
|
+
export interface AlmostNodeServerState {
|
|
94
|
+
state: PreviewState
|
|
95
|
+
url: string | null
|
|
96
|
+
error: string | null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Boots `vite` for `files` in an almost-node container; disposes it on change/unmount. */
|
|
100
|
+
export function useAlmostNodeServer({ files, port = 4173, run, workspaceOptions, onReady, onError }: AlmostNodeServerOptions): AlmostNodeServerState {
|
|
101
|
+
const [state, setState] = useState<PreviewState>(run ? "starting" : "idle")
|
|
109
102
|
const [url, setUrl] = useState<string | null>(null)
|
|
110
103
|
const [error, setError] = useState<string | null>(null)
|
|
111
|
-
const displayedCodeFile = codeFile ?? entry
|
|
112
|
-
const code = useMemo(
|
|
113
|
-
() => codeProp ?? sourceText(files, displayedCodeFile),
|
|
114
|
-
[codeProp, displayedCodeFile, files],
|
|
115
|
-
)
|
|
116
104
|
|
|
117
105
|
useEffect(() => {
|
|
118
|
-
if (inline) return
|
|
119
106
|
if (!run) {
|
|
120
107
|
setState("idle")
|
|
121
108
|
setUrl(null)
|
|
@@ -132,8 +119,7 @@ export function ReactDemo({
|
|
|
132
119
|
|
|
133
120
|
const start = async () => {
|
|
134
121
|
try {
|
|
135
|
-
|
|
136
|
-
workspace = await createAlmostNodeWorkspace(projectFiles, {
|
|
122
|
+
workspace = await createAlmostNodeWorkspace(files, {
|
|
137
123
|
...workspaceOptions,
|
|
138
124
|
env: { ...DETACHED_SERVER_ENV, ...workspaceOptions?.env },
|
|
139
125
|
onServerReady: (serverPort, serverUrl) => {
|
|
@@ -173,7 +159,52 @@ export function ReactDemo({
|
|
|
173
159
|
cancelled = true
|
|
174
160
|
workspace?.dispose()
|
|
175
161
|
}
|
|
176
|
-
}, [
|
|
162
|
+
}, [files, onError, onReady, port, run, workspaceOptions])
|
|
163
|
+
|
|
164
|
+
return { state, url, error }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Render a multi-file React/JSX/TSX project through almost-node's Vite server. */
|
|
168
|
+
const NO_FILES: AlmostNodeFiles = {}
|
|
169
|
+
|
|
170
|
+
export function ReactDemo({
|
|
171
|
+
files = NO_FILES,
|
|
172
|
+
preview,
|
|
173
|
+
code: codeProp,
|
|
174
|
+
language,
|
|
175
|
+
status,
|
|
176
|
+
actions,
|
|
177
|
+
entry = "/src/main.jsx",
|
|
178
|
+
port = 4173,
|
|
179
|
+
height = 360,
|
|
180
|
+
codeFile,
|
|
181
|
+
collapsedCodeLines = 3,
|
|
182
|
+
expandedCodeLines = 30,
|
|
183
|
+
title = "Live React preview",
|
|
184
|
+
autoStart = true,
|
|
185
|
+
sandbox = "allow-scripts allow-same-origin allow-forms allow-modals",
|
|
186
|
+
workspaceOptions,
|
|
187
|
+
className,
|
|
188
|
+
onReady,
|
|
189
|
+
onError,
|
|
190
|
+
}: ReactDemoProps) {
|
|
191
|
+
const inline = preview !== undefined
|
|
192
|
+
const [run, setRun] = useState(autoStart && !inline ? 1 : 0)
|
|
193
|
+
const projectFiles = useMemo(() => createReactDemoFiles(files, { entry }), [files, entry])
|
|
194
|
+
const { state: serverState, url, error } = useAlmostNodeServer({
|
|
195
|
+
files: projectFiles,
|
|
196
|
+
port,
|
|
197
|
+
run: inline ? 0 : run,
|
|
198
|
+
workspaceOptions,
|
|
199
|
+
onReady,
|
|
200
|
+
onError,
|
|
201
|
+
})
|
|
202
|
+
const state: PreviewState = inline ? "ready" : serverState
|
|
203
|
+
const displayedCodeFile = codeFile ?? entry
|
|
204
|
+
const code = useMemo(
|
|
205
|
+
() => codeProp ?? sourceText(files, displayedCodeFile),
|
|
206
|
+
[codeProp, displayedCodeFile, files],
|
|
207
|
+
)
|
|
177
208
|
|
|
178
209
|
const wrapperClass = className ? `docs-react-demo ${className}` : "docs-react-demo"
|
|
179
210
|
const running = state === "starting"
|
|
@@ -154,6 +154,8 @@ export async function createAlmostNodeWorkspace(
|
|
|
154
154
|
|
|
155
155
|
export interface ReactDemoFilesOptions {
|
|
156
156
|
entry?: string
|
|
157
|
+
/** Extra npm dependencies for the generated package.json (ignored when the files bring one). */
|
|
158
|
+
dependencies?: Record<string, string>
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
/**
|
|
@@ -174,7 +176,7 @@ export function createReactDemoFiles(
|
|
|
174
176
|
name: "docstream-react-demo",
|
|
175
177
|
private: true,
|
|
176
178
|
type: "module",
|
|
177
|
-
dependencies: { react: "^19.0.0", "react-dom": "^19.0.0" },
|
|
179
|
+
dependencies: { react: "^19.0.0", "react-dom": "^19.0.0", ...options.dependencies },
|
|
178
180
|
},
|
|
179
181
|
null,
|
|
180
182
|
2,
|
|
@@ -200,3 +202,83 @@ export function createReactDemoFiles(
|
|
|
200
202
|
|
|
201
203
|
return map
|
|
202
204
|
}
|
|
205
|
+
|
|
206
|
+
export interface InlineDemoProjectOptions {
|
|
207
|
+
/** Entry file among `files` (relative path). Defaults to the first file. */
|
|
208
|
+
entry?: string
|
|
209
|
+
/** Extra npm dependencies (merged over a `package.json` among the files, if any). */
|
|
210
|
+
dependencies?: Record<string, string>
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface InlineDemoProject {
|
|
214
|
+
/** Complete almost-node project: the demo under `/src/demo/`, a wrapper entry, package.json, index.html. */
|
|
215
|
+
files: Record<string, AlmostNodeFileContent>
|
|
216
|
+
/** The Vite entry to hand to `createReactDemoFiles` / `ReactDemo`. */
|
|
217
|
+
entry: string
|
|
218
|
+
/** Whether the entry is wrapped (so `?variant=` reaches the component as its `variant` prop). */
|
|
219
|
+
wrapped: boolean
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Folder the inline demo's files are written to inside the almost-node project. */
|
|
223
|
+
export const INLINE_DEMO_DIR = "/src/demo"
|
|
224
|
+
const WRAPPER_ENTRY = "/src/__docstream_demo_entry.js"
|
|
225
|
+
|
|
226
|
+
function selfMounting(source: string): boolean {
|
|
227
|
+
return /\bcreateRoot\s*\(|\bReactDOM\.render\s*\(|\bhydrateRoot\s*\(/.test(source)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Turn a `{% demo %}` block's inline files into a runnable almost-node project.
|
|
232
|
+
*
|
|
233
|
+
* The files go under `/src/demo/` (relative imports between them keep working). A tiny
|
|
234
|
+
* wrapper entry imports the entry file, takes its default export (or the first exported
|
|
235
|
+
* function), and renders it with `variant` read from the page's `?variant=` query — so
|
|
236
|
+
* switching variants just reloads the iframe, no new Vite server. An entry that mounts
|
|
237
|
+
* itself (`createRoot(…)`) is used directly instead, and receives no `variant`.
|
|
238
|
+
*
|
|
239
|
+
* A `package.json` among the files contributes its `dependencies`; `options.dependencies`
|
|
240
|
+
* are merged over them. Bare imports resolve through almost-node (installed packages, else
|
|
241
|
+
* esm.sh at the major version declared here), with `react`/`react-dom` provided.
|
|
242
|
+
*/
|
|
243
|
+
export function createInlineDemoProject(
|
|
244
|
+
files: readonly { path: string; content: string }[],
|
|
245
|
+
options: InlineDemoProjectOptions = {},
|
|
246
|
+
): InlineDemoProject {
|
|
247
|
+
const map: Record<string, AlmostNodeFileContent> = {}
|
|
248
|
+
let packageDeps: Record<string, string> = {}
|
|
249
|
+
for (const file of files) {
|
|
250
|
+
const relative = file.path.replace(/^\.?\/+/, "")
|
|
251
|
+
if (relative === "package.json") {
|
|
252
|
+
try {
|
|
253
|
+
const pkg = JSON.parse(file.content) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
|
|
254
|
+
packageDeps = { ...pkg.devDependencies, ...pkg.dependencies }
|
|
255
|
+
} catch {
|
|
256
|
+
// Not JSON: keep it as a plain file.
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
map[normalizeAlmostNodePath(relative, INLINE_DEMO_DIR)] = file.content
|
|
260
|
+
}
|
|
261
|
+
const entryFile = files.find((f) => f.path === options.entry) ?? files[0]
|
|
262
|
+
if (!entryFile) throw new Error("An inline demo needs at least one file")
|
|
263
|
+
const entryPath = normalizeAlmostNodePath(entryFile.path.replace(/^\.?\/+/, ""), INLINE_DEMO_DIR)
|
|
264
|
+
const dependencies = { ...packageDeps, ...options.dependencies }
|
|
265
|
+
const wrapped = !selfMounting(entryFile.content)
|
|
266
|
+
if (wrapped) {
|
|
267
|
+
const specifier = `.${entryPath.slice("/src".length)}`
|
|
268
|
+
map[WRAPPER_ENTRY] = `import { createElement } from "react"
|
|
269
|
+
import { createRoot } from "react-dom/client"
|
|
270
|
+
import * as demo from ${JSON.stringify(specifier)}
|
|
271
|
+
|
|
272
|
+
const Component = demo.default ?? Object.values(demo).find((value) => typeof value === "function")
|
|
273
|
+
const variant = new URLSearchParams(location.search).get("variant")
|
|
274
|
+
const props = variant === null ? {} : { variant }
|
|
275
|
+
createRoot(document.getElementById("root")).render(
|
|
276
|
+
Component
|
|
277
|
+
? createElement(Component, props)
|
|
278
|
+
: createElement("pre", null, ${JSON.stringify(`${entryFile.path} has no default export to render.`)}),
|
|
279
|
+
)
|
|
280
|
+
`
|
|
281
|
+
}
|
|
282
|
+
const entry = wrapped ? WRAPPER_ENTRY : entryPath
|
|
283
|
+
return { files: createReactDemoFiles(map, { entry, dependencies }), entry, wrapped }
|
|
284
|
+
}
|
package/src/playground/index.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
export { ReactCodePreview, ReactDemo } from "./ReactCodePreview"
|
|
2
|
-
export type { ReactCodePreviewProps, ReactDemoProps } from "./ReactCodePreview"
|
|
1
|
+
export { ReactCodePreview, ReactDemo, useAlmostNodeServer } from "./ReactCodePreview"
|
|
2
|
+
export type { AlmostNodeServerOptions, AlmostNodeServerState, PreviewState, ReactCodePreviewProps, ReactDemoProps } from "./ReactCodePreview"
|
|
3
|
+
export { InlineDemoPreview, almostNodeDemoRuntime, createAlmostNodeDemoRuntime } from "./InlineDemoPreview"
|
|
4
|
+
export type { AlmostNodeDemoRuntimeOptions, InlineDemoPreviewProps } from "./InlineDemoPreview"
|
|
3
5
|
export {
|
|
4
6
|
createAlmostNodeFilesystem,
|
|
5
7
|
createAlmostNodeWorkspace,
|
|
8
|
+
createInlineDemoProject,
|
|
6
9
|
createReactDemoFiles,
|
|
7
10
|
normalizeAlmostNodeFiles,
|
|
8
11
|
normalizeAlmostNodePath,
|
|
@@ -15,5 +18,7 @@ export type {
|
|
|
15
18
|
AlmostNodeFilesystemOptions,
|
|
16
19
|
AlmostNodeWorkspace,
|
|
17
20
|
AlmostNodeWorkspaceOptions,
|
|
21
|
+
InlineDemoProject,
|
|
22
|
+
InlineDemoProjectOptions,
|
|
18
23
|
ReactDemoFilesOptions,
|
|
19
24
|
} from "./filesystem"
|
package/src/streamdown.tsx
CHANGED
|
@@ -8,8 +8,8 @@ export { CitationSources, DocstreamRefContext, InlineReference } from "./docs/re
|
|
|
8
8
|
export { DocPageActions } from "./docs/PageActions"
|
|
9
9
|
export type { DocPageAction, DocPageActionsOptions, DocPageActionsProps } from "./docs/PageActions"
|
|
10
10
|
export type { DocRenderOptions } from "./docs/DocsRenderer"
|
|
11
|
-
export { DocstreamDemoContext, DemoFullscreen, DemoViewer } from "./demo"
|
|
12
|
-
export type { DemoFile, DemoMeta, DemoResolver, DemoVariant } from "./demo"
|
|
11
|
+
export { DocstreamDemoContext, DocstreamDemoRuntimeContext, DemoFullscreen, DemoViewer } from "./demo"
|
|
12
|
+
export type { DemoFile, DemoMeta, DemoResolver, DemoVariant, InlineDemoRuntime, InlineDemoRuntimeProps } from "./demo"
|
|
13
13
|
export type { CitationPreview, CitationPreviewResolver, DocstreamRefContextValue } from "./docs/reference"
|
|
14
14
|
|
|
15
15
|
export interface GitbookStreamdownProps extends DocRenderOptions {
|
|
@@ -31,6 +31,8 @@ export function GitbookStreamdown({
|
|
|
31
31
|
liveRenderer,
|
|
32
32
|
sourceRenderer,
|
|
33
33
|
demoResolver,
|
|
34
|
+
demoRuntime,
|
|
35
|
+
demoDependencies,
|
|
34
36
|
pageActions,
|
|
35
37
|
}: GitbookStreamdownProps) {
|
|
36
38
|
const content = markdown ?? children ?? ""
|
|
@@ -52,6 +54,8 @@ export function GitbookStreamdown({
|
|
|
52
54
|
liveRenderer={liveRenderer}
|
|
53
55
|
sourceRenderer={sourceRenderer}
|
|
54
56
|
demoResolver={demoResolver}
|
|
57
|
+
demoRuntime={demoRuntime}
|
|
58
|
+
demoDependencies={demoDependencies}
|
|
55
59
|
pageActions={isStreaming ? false : pageActions}
|
|
56
60
|
/>
|
|
57
61
|
</div>
|
package/src/styles.css
CHANGED
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
--gb-pill-shadow: 0 1px 2px rgba(15, 15, 35, 0.14), 0 0 0 0.5px rgba(15, 15, 35, 0.1);
|
|
25
25
|
--gb-hover: rgba(128, 128, 128, 0.12);
|
|
26
26
|
--gb-menu-shadow: 0 14px 34px -10px rgba(15, 15, 35, 0.28), 0 2px 6px rgba(15, 15, 35, 0.08);
|
|
27
|
+
/* The terminal glyph in command boxes. Its own tokens (not --gb-panel-foreground), so a host
|
|
28
|
+
that sets panel colors to currentColor still gets a visible glyph. */
|
|
29
|
+
--gb-term-bg: rgba(128, 128, 128, 0.2);
|
|
30
|
+
--gb-term-fg: var(--gb-muted-foreground);
|
|
27
31
|
|
|
28
32
|
/* Semantic hint palette — solid color plus precomputed tints. Matches the
|
|
29
33
|
editor palette in @brett_lamy/docstream-editor/styles.css. */
|
|
@@ -148,6 +152,10 @@
|
|
|
148
152
|
margin-block: 0.75em;
|
|
149
153
|
}
|
|
150
154
|
|
|
155
|
+
:is([data-docstream], .docs-article) [data-docstream-blocks] > .docs-tabs-section {
|
|
156
|
+
margin-block: 1.6em 0.9em;
|
|
157
|
+
}
|
|
158
|
+
|
|
151
159
|
:is([data-docstream], .docs-article) a {
|
|
152
160
|
color: var(--gb-primary);
|
|
153
161
|
text-decoration: underline;
|
|
@@ -1370,13 +1378,14 @@
|
|
|
1370
1378
|
margin: -3px 0;
|
|
1371
1379
|
}
|
|
1372
1380
|
|
|
1381
|
+
/* Untitled code blocks: no header bar, just the code and a copy button in its top-right corner. */
|
|
1373
1382
|
.docs-code-copy-float {
|
|
1374
1383
|
position: absolute;
|
|
1375
|
-
top:
|
|
1376
|
-
right:
|
|
1384
|
+
top: 9px;
|
|
1385
|
+
right: 9px;
|
|
1377
1386
|
z-index: 1;
|
|
1378
|
-
background:
|
|
1379
|
-
opacity: 0;
|
|
1387
|
+
background: transparent;
|
|
1388
|
+
opacity: 0.75;
|
|
1380
1389
|
}
|
|
1381
1390
|
|
|
1382
1391
|
.docs-code:hover .docs-code-copy-float,
|
|
@@ -1506,9 +1515,45 @@
|
|
|
1506
1515
|
width: 20px;
|
|
1507
1516
|
height: 20px;
|
|
1508
1517
|
border-radius: 5px;
|
|
1509
|
-
background: var(--gb-
|
|
1510
|
-
color: var(--gb-
|
|
1511
|
-
|
|
1518
|
+
background: var(--gb-term-bg, rgba(128, 128, 128, 0.2));
|
|
1519
|
+
color: var(--gb-term-fg, #71717a);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
/* ---- Titled tabs (`{% tabs title="Installation" %}`): heading + pill switch, then the tab ---- */
|
|
1523
|
+
|
|
1524
|
+
.docs-tabs-section-head {
|
|
1525
|
+
display: flex;
|
|
1526
|
+
flex-wrap: wrap;
|
|
1527
|
+
align-items: center;
|
|
1528
|
+
justify-content: space-between;
|
|
1529
|
+
gap: 12px;
|
|
1530
|
+
margin-bottom: 12px;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
:is([data-docstream], .docs-article) .docs-tabs-section-head .docs-tabs-section-title {
|
|
1534
|
+
margin: 0;
|
|
1535
|
+
padding-bottom: 0;
|
|
1536
|
+
border-bottom: 0;
|
|
1537
|
+
scroll-margin-top: 80px;
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
.docs-tabs-section-body {
|
|
1541
|
+
display: flex;
|
|
1542
|
+
flex-direction: column;
|
|
1543
|
+
gap: 10px;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
.docs-tabs-section-body > [data-docstream-blocks] > * {
|
|
1547
|
+
margin-block: 0 10px;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
.docs-tabs-section-body > [data-docstream-blocks] > :last-child {
|
|
1551
|
+
margin-bottom: 0;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
.docs-tabs-section-body > [data-docstream-blocks] > p {
|
|
1555
|
+
color: var(--gb-muted-foreground);
|
|
1556
|
+
font-size: 0.93em;
|
|
1512
1557
|
}
|
|
1513
1558
|
|
|
1514
1559
|
.docs-tabs-code-title {
|
|
@@ -1900,9 +1945,12 @@
|
|
|
1900
1945
|
opacity: 1;
|
|
1901
1946
|
}
|
|
1902
1947
|
|
|
1948
|
+
/* Block-level demo roots fill the width (place-items: center would shrink them to their
|
|
1949
|
+
content); content is centred vertically, and intrinsically sized roots horizontally. */
|
|
1903
1950
|
.docs-demo-canvas {
|
|
1904
1951
|
display: grid;
|
|
1905
|
-
|
|
1952
|
+
align-content: center;
|
|
1953
|
+
justify-items: stretch;
|
|
1906
1954
|
box-sizing: border-box;
|
|
1907
1955
|
min-height: 100%;
|
|
1908
1956
|
padding: 28px 24px;
|
|
@@ -1910,9 +1958,14 @@
|
|
|
1910
1958
|
}
|
|
1911
1959
|
|
|
1912
1960
|
.docs-demo-canvas > * {
|
|
1961
|
+
min-width: 0;
|
|
1913
1962
|
max-width: 100%;
|
|
1914
1963
|
}
|
|
1915
1964
|
|
|
1965
|
+
.docs-demo-canvas > :where(button, a, img, svg, video, canvas, input, select, textarea, label, span, code) {
|
|
1966
|
+
justify-self: center;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1916
1969
|
.docs-demo-canvas-bleed {
|
|
1917
1970
|
display: block;
|
|
1918
1971
|
place-items: normal;
|
|
@@ -1924,6 +1977,45 @@
|
|
|
1924
1977
|
max-width: none;
|
|
1925
1978
|
}
|
|
1926
1979
|
|
|
1980
|
+
/* Inline-file demos: the runtime's iframe fills the frame. */
|
|
1981
|
+
.docs-demo-canvas-runtime {
|
|
1982
|
+
display: flex;
|
|
1983
|
+
flex-direction: column;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
.docs-demo-canvas-runtime > * {
|
|
1987
|
+
flex: 1 1 auto;
|
|
1988
|
+
min-height: 0;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
.docs-demo-runtime-frame {
|
|
1992
|
+
display: block;
|
|
1993
|
+
width: 100%;
|
|
1994
|
+
height: 100%;
|
|
1995
|
+
min-height: 120px;
|
|
1996
|
+
border: 0;
|
|
1997
|
+
background: #fff;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
.docs-demo-runtime-status {
|
|
2001
|
+
display: grid;
|
|
2002
|
+
place-items: center;
|
|
2003
|
+
height: 100%;
|
|
2004
|
+
min-height: 120px;
|
|
2005
|
+
color: var(--gb-muted-foreground);
|
|
2006
|
+
font-size: 12.5px;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
/* No runtime for an inline-file demo (or still streaming): a one-line note, then the code. */
|
|
2010
|
+
.docs-demo-note {
|
|
2011
|
+
margin: 0;
|
|
2012
|
+
padding: 8px 14px;
|
|
2013
|
+
border-bottom: 1px solid var(--gb-border);
|
|
2014
|
+
color: var(--gb-muted-foreground);
|
|
2015
|
+
font-size: 12.5px;
|
|
2016
|
+
line-height: 1.5;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
1927
2019
|
.docs-demo-loading {
|
|
1928
2020
|
display: grid;
|
|
1929
2021
|
place-items: center;
|
|
@@ -2150,10 +2242,27 @@
|
|
|
2150
2242
|
}
|
|
2151
2243
|
|
|
2152
2244
|
.docs-demo-single-preview {
|
|
2245
|
+
display: grid;
|
|
2153
2246
|
min-height: 120px;
|
|
2154
2247
|
background: var(--background, var(--gb-panel));
|
|
2155
2248
|
}
|
|
2156
2249
|
|
|
2250
|
+
.docs-demo-status {
|
|
2251
|
+
flex-shrink: 0;
|
|
2252
|
+
padding: 3px 8px;
|
|
2253
|
+
border-radius: 999px;
|
|
2254
|
+
background: var(--gb-pill-track);
|
|
2255
|
+
color: var(--gb-muted-foreground);
|
|
2256
|
+
font-size: 11.5px;
|
|
2257
|
+
font-weight: 600;
|
|
2258
|
+
line-height: 1.2;
|
|
2259
|
+
white-space: nowrap;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
.docs-demo-variants-sized .docs-pill {
|
|
2263
|
+
flex: 1 1 0;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2157
2266
|
/* Fullscreen overlay + deep link */
|
|
2158
2267
|
|
|
2159
2268
|
.docs-demo-dialog {
|