@brett_lamy/docstream 1.4.0 → 1.6.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 +90 -0
- package/dist/demo/DemoViewer.d.ts +14 -0
- package/dist/demo/DemoViewer.d.ts.map +1 -1
- package/dist/demo/DemoViewer.js +50 -9
- package/dist/demo/DemoViewer.js.map +1 -1
- package/dist/demo/clipboard.d.ts +70 -0
- package/dist/demo/clipboard.d.ts.map +1 -0
- package/dist/demo/clipboard.js +187 -0
- package/dist/demo/clipboard.js.map +1 -0
- package/dist/demo/context.d.ts +6 -0
- package/dist/demo/context.d.ts.map +1 -1
- package/dist/demo/context.js +6 -0
- package/dist/demo/context.js.map +1 -1
- package/dist/demo/copy-demo.d.ts +23 -0
- package/dist/demo/copy-demo.d.ts.map +1 -0
- package/dist/demo/copy-demo.js +81 -0
- package/dist/demo/copy-demo.js.map +1 -0
- package/dist/demo/index.d.ts +8 -2
- package/dist/demo/index.d.ts.map +1 -1
- package/dist/demo/index.js +5 -2
- package/dist/demo/index.js.map +1 -1
- package/dist/demo/vfs.d.ts +51 -0
- package/dist/demo/vfs.d.ts.map +1 -0
- package/dist/demo/vfs.js +156 -0
- package/dist/demo/vfs.js.map +1 -0
- package/dist/docs/DocsRenderer.d.ts +8 -2
- package/dist/docs/DocsRenderer.d.ts.map +1 -1
- package/dist/docs/DocsRenderer.js +8 -6
- package/dist/docs/DocsRenderer.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/replay/NativeReplay.d.ts +2 -2
- package/dist/replay/NativeReplay.d.ts.map +1 -1
- package/dist/replay/NativeReplay.js +3 -1
- package/dist/replay/NativeReplay.js.map +1 -1
- package/dist/replay/ReplayPreview.d.ts +3 -3
- package/dist/replay/ReplayPreview.d.ts.map +1 -1
- package/dist/replay/ReplayPreview.js.map +1 -1
- package/dist/replay/ReplayStage.d.ts +10 -0
- package/dist/replay/ReplayStage.d.ts.map +1 -0
- package/dist/replay/ReplayStage.js +19 -0
- package/dist/replay/ReplayStage.js.map +1 -0
- package/dist/replay/ReplayStageImpl.d.ts +5 -0
- package/dist/replay/ReplayStageImpl.d.ts.map +1 -0
- package/dist/replay/ReplayStageImpl.js +249 -0
- package/dist/replay/ReplayStageImpl.js.map +1 -0
- package/dist/replay/index.d.ts +6 -0
- package/dist/replay/index.d.ts.map +1 -1
- package/dist/replay/index.js +3 -0
- package/dist/replay/index.js.map +1 -1
- package/dist/replay/markers.d.ts +55 -0
- package/dist/replay/markers.d.ts.map +1 -0
- package/dist/replay/markers.js +177 -0
- package/dist/replay/markers.js.map +1 -0
- package/dist/replay/sanitize.d.ts +2 -2
- package/dist/replay/sanitize.d.ts.map +1 -1
- package/dist/replay/sanitize.js +1 -1
- package/dist/replay/sanitize.js.map +1 -1
- package/dist/replay/stage-types.d.ts +65 -0
- package/dist/replay/stage-types.d.ts.map +1 -0
- package/dist/replay/stage-types.js +2 -0
- package/dist/replay/stage-types.js.map +1 -0
- package/dist/replay/types.d.ts +39 -0
- package/dist/replay/types.d.ts.map +1 -0
- package/dist/replay/types.js +36 -0
- package/dist/replay/types.js.map +1 -0
- package/dist/streamdown.d.ts +1 -1
- package/dist/streamdown.d.ts.map +1 -1
- package/dist/streamdown.js +2 -2
- package/dist/streamdown.js.map +1 -1
- package/package.json +9 -2
- package/src/demo/DemoViewer.tsx +56 -11
- package/src/demo/clipboard.ts +228 -0
- package/src/demo/context.ts +7 -0
- package/src/demo/copy-demo.tsx +102 -0
- package/src/demo/index.ts +29 -2
- package/src/demo/vfs.ts +197 -0
- package/src/docs/DocsRenderer.tsx +13 -4
- package/src/index.ts +20 -0
- package/src/replay/NativeReplay.tsx +6 -2
- package/src/replay/ReplayPreview.tsx +6 -6
- package/src/replay/ReplayStage.tsx +28 -0
- package/src/replay/ReplayStageImpl.tsx +281 -0
- package/src/replay/index.ts +6 -0
- package/src/replay/markers.ts +213 -0
- package/src/replay/sanitize.ts +2 -3
- package/src/replay/stage-types.ts +63 -0
- package/src/replay/types.ts +46 -0
- package/src/streamdown.tsx +2 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import type { ReplayEvent } from './types.js';
|
|
3
|
+
/** Imperative control over a mounted `ReplayStage`. Times are ms from the recording's start. */
|
|
4
|
+
export interface ReplayStageHandle {
|
|
5
|
+
/** Play from `offset` (default: the current time; from 0 when at the end). */
|
|
6
|
+
play(offset?: number): void;
|
|
7
|
+
/** Pause, optionally jumping to `offset`. */
|
|
8
|
+
pause(offset?: number): void;
|
|
9
|
+
/** Jump to `ms`, keeping the play state. */
|
|
10
|
+
seek(ms: number): void;
|
|
11
|
+
toggle(): void;
|
|
12
|
+
getCurrentTime(): number;
|
|
13
|
+
getDuration(): number;
|
|
14
|
+
isPlaying(): boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface ReplayStageLayout {
|
|
17
|
+
/** Recorded viewport size (px). */
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
/** How much the viewport is scaled to fit the stage box. */
|
|
21
|
+
scale: number;
|
|
22
|
+
/** Where the scaled viewport sits inside the box. */
|
|
23
|
+
offsetX: number;
|
|
24
|
+
offsetY: number;
|
|
25
|
+
}
|
|
26
|
+
export interface ReplayStageProps {
|
|
27
|
+
/** rrweb events; appended events are fed to the running player. */
|
|
28
|
+
events: ReplayEvent[];
|
|
29
|
+
autoplay?: boolean;
|
|
30
|
+
loop?: boolean;
|
|
31
|
+
/** Playback rate (1 = real time). */
|
|
32
|
+
speed?: number;
|
|
33
|
+
/** Fast-forward idle stretches longer than 5s (rrweb `skipInactive`). Default true. */
|
|
34
|
+
skipInactive?: boolean;
|
|
35
|
+
/** Where the paused player starts (ms). */
|
|
36
|
+
initialTime?: number;
|
|
37
|
+
/**
|
|
38
|
+
* `width` (default): scale down to the box width, height follows the recording's aspect.
|
|
39
|
+
* `contain`: fill the box (give it a height) and letterbox the viewport inside it.
|
|
40
|
+
*/
|
|
41
|
+
fit?: 'width' | 'contain';
|
|
42
|
+
/** rrweb's own cursor. Hide it when drawing a pointer overlay. Default true. */
|
|
43
|
+
showMouse?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Rendered over the page, in recorded-viewport coordinates (a box of `width × height` px
|
|
46
|
+
* scaled with the page). `var(--replay-scale)` holds the scale, so an overlay can
|
|
47
|
+
* counter-scale elements that should keep their on-screen size.
|
|
48
|
+
*/
|
|
49
|
+
overlay?: ReactNode;
|
|
50
|
+
className?: string;
|
|
51
|
+
style?: CSSProperties;
|
|
52
|
+
/** Shown while the player chunk (rrweb) loads. */
|
|
53
|
+
fallback?: ReactNode;
|
|
54
|
+
onReady?(info: {
|
|
55
|
+
duration: number;
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
}): void;
|
|
59
|
+
/** Every frame while playing, and after each seek/pause. */
|
|
60
|
+
onTimeUpdate?(ms: number): void;
|
|
61
|
+
onPlayingChange?(playing: boolean): void;
|
|
62
|
+
onFinish?(): void;
|
|
63
|
+
onLayout?(layout: ReplayStageLayout): void;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=stage-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stage-types.d.ts","sourceRoot":"","sources":["../../src/replay/stage-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAE1C,gGAAgG;AAChG,MAAM,WAAW,iBAAiB;IAChC,8EAA8E;IAC9E,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,6CAA6C;IAC7C,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,4CAA4C;IAC5C,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,MAAM,IAAI,IAAI,CAAA;IACd,cAAc,IAAI,MAAM,CAAA;IACxB,WAAW,IAAI,MAAM,CAAA;IACrB,SAAS,IAAI,OAAO,CAAA;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAA;IACb,qDAAqD;IACrD,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,uFAAuF;IACvF,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,GAAG,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IACzB,gFAAgF;IAChF,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB,OAAO,CAAC,CAAC,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;IACzE,4DAA4D;IAC5D,YAAY,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,eAAe,CAAC,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACxC,QAAQ,CAAC,IAAI,IAAI,CAAA;IACjB,QAAQ,CAAC,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAA;CAC3C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stage-types.js","sourceRoot":"","sources":["../../src/replay/stage-types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** One recorded rrweb event. rrweb's own `eventWithTime` is assignable to it. */
|
|
2
|
+
export interface ReplayEvent {
|
|
3
|
+
type: number;
|
|
4
|
+
data: unknown;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
delay?: number;
|
|
7
|
+
}
|
|
8
|
+
/** rrweb `EventType`. */
|
|
9
|
+
export declare const ReplayEventType: {
|
|
10
|
+
readonly DomContentLoaded: 0;
|
|
11
|
+
readonly Load: 1;
|
|
12
|
+
readonly FullSnapshot: 2;
|
|
13
|
+
readonly IncrementalSnapshot: 3;
|
|
14
|
+
readonly Meta: 4;
|
|
15
|
+
readonly Custom: 5;
|
|
16
|
+
readonly Plugin: 6;
|
|
17
|
+
};
|
|
18
|
+
/** rrweb `IncrementalSource` (the subset the helpers read). */
|
|
19
|
+
export declare const ReplayIncrementalSource: {
|
|
20
|
+
readonly Mutation: 0;
|
|
21
|
+
readonly MouseMove: 1;
|
|
22
|
+
readonly MouseInteraction: 2;
|
|
23
|
+
readonly Scroll: 3;
|
|
24
|
+
readonly ViewportResize: 4;
|
|
25
|
+
readonly Input: 5;
|
|
26
|
+
readonly TouchMove: 6;
|
|
27
|
+
readonly Drag: 12;
|
|
28
|
+
};
|
|
29
|
+
/** rrweb `MouseInteractions` (the subset the helpers read). */
|
|
30
|
+
export declare const ReplayMouseInteraction: {
|
|
31
|
+
readonly MouseUp: 0;
|
|
32
|
+
readonly MouseDown: 1;
|
|
33
|
+
readonly Click: 2;
|
|
34
|
+
readonly ContextMenu: 3;
|
|
35
|
+
readonly DblClick: 4;
|
|
36
|
+
readonly TouchStart: 7;
|
|
37
|
+
readonly TouchEnd: 9;
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/replay/types.ts"],"names":[],"mappings":"AAKA,iFAAiF;AACjF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,yBAAyB;AACzB,eAAO,MAAM,eAAe;;;;;;;;CAQlB,CAAA;AAEV,+DAA+D;AAC/D,eAAO,MAAM,uBAAuB;;;;;;;;;CAS1B,CAAA;AAEV,+DAA+D;AAC/D,eAAO,MAAM,sBAAsB;;;;;;;;CAQzB,CAAA"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/* rrweb's event shape and the numeric enums the replay helpers read, declared locally so the
|
|
2
|
+
eager replay modules (and their published declarations) never import rrweb. rrweb is an
|
|
3
|
+
optional peer: only the lazily loaded player chunk (`ReplayStageImpl` / `NativeReplay`)
|
|
4
|
+
imports it, so hosts that never play native events don't need it installed. */
|
|
5
|
+
/** rrweb `EventType`. */
|
|
6
|
+
export const ReplayEventType = {
|
|
7
|
+
DomContentLoaded: 0,
|
|
8
|
+
Load: 1,
|
|
9
|
+
FullSnapshot: 2,
|
|
10
|
+
IncrementalSnapshot: 3,
|
|
11
|
+
Meta: 4,
|
|
12
|
+
Custom: 5,
|
|
13
|
+
Plugin: 6,
|
|
14
|
+
};
|
|
15
|
+
/** rrweb `IncrementalSource` (the subset the helpers read). */
|
|
16
|
+
export const ReplayIncrementalSource = {
|
|
17
|
+
Mutation: 0,
|
|
18
|
+
MouseMove: 1,
|
|
19
|
+
MouseInteraction: 2,
|
|
20
|
+
Scroll: 3,
|
|
21
|
+
ViewportResize: 4,
|
|
22
|
+
Input: 5,
|
|
23
|
+
TouchMove: 6,
|
|
24
|
+
Drag: 12,
|
|
25
|
+
};
|
|
26
|
+
/** rrweb `MouseInteractions` (the subset the helpers read). */
|
|
27
|
+
export const ReplayMouseInteraction = {
|
|
28
|
+
MouseUp: 0,
|
|
29
|
+
MouseDown: 1,
|
|
30
|
+
Click: 2,
|
|
31
|
+
ContextMenu: 3,
|
|
32
|
+
DblClick: 4,
|
|
33
|
+
TouchStart: 7,
|
|
34
|
+
TouchEnd: 9,
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/replay/types.ts"],"names":[],"mappings":"AAAA;;;iFAGiF;AAUjF,yBAAyB;AACzB,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,gBAAgB,EAAE,CAAC;IACnB,IAAI,EAAE,CAAC;IACP,YAAY,EAAE,CAAC;IACf,mBAAmB,EAAE,CAAC;IACtB,IAAI,EAAE,CAAC;IACP,MAAM,EAAE,CAAC;IACT,MAAM,EAAE,CAAC;CACD,CAAA;AAEV,+DAA+D;AAC/D,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,QAAQ,EAAE,CAAC;IACX,SAAS,EAAE,CAAC;IACZ,gBAAgB,EAAE,CAAC;IACnB,MAAM,EAAE,CAAC;IACT,cAAc,EAAE,CAAC;IACjB,KAAK,EAAE,CAAC;IACR,SAAS,EAAE,CAAC;IACZ,IAAI,EAAE,EAAE;CACA,CAAA;AAEV,+DAA+D;AAC/D,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,OAAO,EAAE,CAAC;IACV,SAAS,EAAE,CAAC;IACZ,KAAK,EAAE,CAAC;IACR,WAAW,EAAE,CAAC;IACd,QAAQ,EAAE,CAAC;IACX,UAAU,EAAE,CAAC;IACb,QAAQ,EAAE,CAAC;CACH,CAAA"}
|
package/dist/streamdown.d.ts
CHANGED
|
@@ -15,5 +15,5 @@ export interface GitbookStreamdownProps extends DocRenderOptions {
|
|
|
15
15
|
isStreaming?: boolean;
|
|
16
16
|
markdown?: string;
|
|
17
17
|
}
|
|
18
|
-
export declare function GitbookStreamdown({ children, className, isAnimating, isStreaming, markdown, liveRenderer, sourceRenderer, demoResolver, demoRuntime, demoDependencies, pageActions, }: GitbookStreamdownProps): import("react").JSX.Element;
|
|
18
|
+
export declare function GitbookStreamdown({ children, className, isAnimating, isStreaming, markdown, liveRenderer, sourceRenderer, demoResolver, demoRuntime, demoDependencies, demoVFS, pageActions, }: GitbookStreamdownProps): import("react").JSX.Element;
|
|
19
19
|
//# sourceMappingURL=streamdown.d.ts.map
|
package/dist/streamdown.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streamdown.d.ts","sourceRoot":"","sources":["../src/streamdown.tsx"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,gBAAgB,EAAE,KAAK,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AAKjI,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACnG,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC3D,OAAO,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACtG,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAA;AACtH,YAAY,EAAE,eAAe,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAE1G,MAAM,WAAW,sBAAuB,SAAQ,gBAAgB;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,mBAAmB,CAAA;IAClC,cAAc,CAAC,EAAE,uBAAuB,CAAA;IACxC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,SAAS,EACT,WAAW,EACX,WAAmB,EACnB,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,WAAW,GACZ,EAAE,sBAAsB,+
|
|
1
|
+
{"version":3,"file":"streamdown.d.ts","sourceRoot":"","sources":["../src/streamdown.tsx"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,gBAAgB,EAAE,KAAK,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AAKjI,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACnG,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC3D,OAAO,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACtG,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAA;AACtH,YAAY,EAAE,eAAe,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA;AAE1G,MAAM,WAAW,sBAAuB,SAAQ,gBAAgB;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,mBAAmB,CAAA;IAClC,cAAc,CAAC,EAAE,uBAAuB,CAAA;IACxC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,SAAS,EACT,WAAW,EACX,WAAmB,EACnB,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,OAAO,EACP,WAAW,GACZ,EAAE,sBAAsB,+BA2BxB"}
|
package/dist/streamdown.js
CHANGED
|
@@ -7,9 +7,9 @@ import { parseMarkdown, trimPartialInlineToken } from "./gitbook/parse.js";
|
|
|
7
7
|
export { CitationSources, DocstreamRefContext, InlineReference } from "./docs/reference.js";
|
|
8
8
|
export { DocPageActions } from "./docs/PageActions.js";
|
|
9
9
|
export { DocstreamDemoContext, DocstreamDemoRuntimeContext, DemoFullscreen, DemoViewer } from "./demo/index.js";
|
|
10
|
-
export function GitbookStreamdown({ children, className, isAnimating, isStreaming = false, markdown, liveRenderer, sourceRenderer, demoResolver, demoRuntime, demoDependencies, pageActions, }) {
|
|
10
|
+
export function GitbookStreamdown({ children, className, isAnimating, isStreaming = false, markdown, liveRenderer, sourceRenderer, demoResolver, demoRuntime, demoDependencies, demoVFS, pageActions, }) {
|
|
11
11
|
const content = markdown ?? children ?? "";
|
|
12
12
|
const gitbookDoc = useMemo(() => parseMarkdown(isStreaming ? trimPartialInlineToken(content) : content), [content, isStreaming]);
|
|
13
|
-
return (_jsx("div", { "aria-busy": isAnimating ?? isStreaming, className: className, "data-docstream": "", "data-streaming": isStreaming ? "" : undefined, children: _jsx(DocsRenderer, { doc: gitbookDoc, markdown: content, liveRenderer: liveRenderer, sourceRenderer: sourceRenderer, demoResolver: demoResolver, demoRuntime: demoRuntime, demoDependencies: demoDependencies, pageActions: isStreaming ? false : pageActions }) }));
|
|
13
|
+
return (_jsx("div", { "aria-busy": isAnimating ?? isStreaming, className: className, "data-docstream": "", "data-streaming": isStreaming ? "" : undefined, children: _jsx(DocsRenderer, { doc: gitbookDoc, markdown: content, liveRenderer: liveRenderer, sourceRenderer: sourceRenderer, demoResolver: demoResolver, demoRuntime: demoRuntime, demoDependencies: demoDependencies, demoVFS: demoVFS, pageActions: isStreaming ? false : pageActions }) }));
|
|
14
14
|
}
|
|
15
15
|
//# sourceMappingURL=streamdown.js.map
|
package/dist/streamdown.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"streamdown.js","sourceRoot":"","sources":["../src/streamdown.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,YAAY,EAAiF,MAAM,qBAAqB,CAAA;AACjI,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAEvE,uEAAuE;AACvE,yDAAyD;AACzD,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAGnD,OAAO,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AActG,MAAM,UAAU,iBAAiB,CAAC,EAChC,QAAQ,EACR,SAAS,EACT,WAAW,EACX,WAAW,GAAG,KAAK,EACnB,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,WAAW,GACY;IACvB,MAAM,OAAO,GAAG,QAAQ,IAAI,QAAQ,IAAI,EAAE,CAAA;IAC1C,MAAM,UAAU,GAAG,OAAO,CACxB,GAAG,EAAE,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAC5E,CAAC,OAAO,EAAE,WAAW,CAAC,CACvB,CAAA;IAED,OAAO,CACL,2BACa,WAAW,IAAI,WAAW,EACrC,SAAS,EAAE,SAAS,oBACL,EAAE,oBACD,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,YAE5C,KAAC,YAAY,IACX,GAAG,EAAE,UAAU,EACf,QAAQ,EAAE,OAAO,EACjB,YAAY,EAAE,YAAY,EAC1B,cAAc,EAAE,cAAc,EAC9B,YAAY,EAAE,YAAY,EAC1B,WAAW,EAAE,WAAW,EACxB,gBAAgB,EAAE,gBAAgB,EAClC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,GAC9C,GACE,CACP,CAAA;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"streamdown.js","sourceRoot":"","sources":["../src/streamdown.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,YAAY,EAAiF,MAAM,qBAAqB,CAAA;AACjI,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAEvE,uEAAuE;AACvE,yDAAyD;AACzD,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAGnD,OAAO,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AActG,MAAM,UAAU,iBAAiB,CAAC,EAChC,QAAQ,EACR,SAAS,EACT,WAAW,EACX,WAAW,GAAG,KAAK,EACnB,QAAQ,EACR,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,OAAO,EACP,WAAW,GACY;IACvB,MAAM,OAAO,GAAG,QAAQ,IAAI,QAAQ,IAAI,EAAE,CAAA;IAC1C,MAAM,UAAU,GAAG,OAAO,CACxB,GAAG,EAAE,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC,sBAAsB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAC5E,CAAC,OAAO,EAAE,WAAW,CAAC,CACvB,CAAA;IAED,OAAO,CACL,2BACa,WAAW,IAAI,WAAW,EACrC,SAAS,EAAE,SAAS,oBACL,EAAE,oBACD,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,YAE5C,KAAC,YAAY,IACX,GAAG,EAAE,UAAU,EACf,QAAQ,EAAE,OAAO,EACjB,YAAY,EAAE,YAAY,EAC1B,cAAc,EAAE,cAAc,EAC9B,YAAY,EAAE,YAAY,EAC1B,WAAW,EAAE,WAAW,EACxB,gBAAgB,EAAE,gBAAgB,EAClC,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,GAC9C,GACE,CACP,CAAA;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brett_lamy/docstream",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "GitBook-aware readonly markdown and AI stream renderer.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"katex": "^0.18.5",
|
|
113
113
|
"lucide-react": "^1.17.0",
|
|
114
114
|
"mermaid": "^11.15.0",
|
|
115
|
-
"
|
|
115
|
+
"react-aria": "^3.52.1",
|
|
116
116
|
"yaml": "^2.9.0"
|
|
117
117
|
},
|
|
118
118
|
"peerDependencies": {
|
|
@@ -120,6 +120,7 @@
|
|
|
120
120
|
"@brett_lamy/viz-engine": ">=0.2.0",
|
|
121
121
|
"react": ">=18",
|
|
122
122
|
"react-dom": ">=18",
|
|
123
|
+
"rrweb": "2.0.0-alpha.20",
|
|
123
124
|
"vite": ">=5"
|
|
124
125
|
},
|
|
125
126
|
"peerDependenciesMeta": {
|
|
@@ -131,6 +132,9 @@
|
|
|
131
132
|
},
|
|
132
133
|
"vite": {
|
|
133
134
|
"optional": true
|
|
135
|
+
},
|
|
136
|
+
"rrweb": {
|
|
137
|
+
"optional": true
|
|
134
138
|
}
|
|
135
139
|
},
|
|
136
140
|
"repository": {
|
|
@@ -142,6 +146,9 @@
|
|
|
142
146
|
"@brett_lamy/viz-engine": "^0.2.1",
|
|
143
147
|
"@types/node": "^24.0.0",
|
|
144
148
|
"@types/react": "^18.3.3",
|
|
149
|
+
"@types/react-dom": "^18.3.7",
|
|
150
|
+
"happy-dom": "^20.14.5",
|
|
151
|
+
"rrweb": "2.0.0-alpha.20",
|
|
145
152
|
"typescript": "^5.5.4",
|
|
146
153
|
"vite": "^7.0.0",
|
|
147
154
|
"vitest": "^3.2.4"
|
package/src/demo/DemoViewer.tsx
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
type KeyboardEvent as ReactKeyboardEvent,
|
|
25
25
|
type PointerEvent as ReactPointerEvent,
|
|
26
26
|
type ReactNode,
|
|
27
|
-
type
|
|
27
|
+
type Ref,
|
|
28
28
|
} from "react"
|
|
29
29
|
import { motion, useReducedMotion } from "framer-motion"
|
|
30
30
|
import { ExternalLink, FileCode2, Folder, Maximize2, Monitor, Smartphone, Tablet, X } from "lucide-react"
|
|
@@ -36,8 +36,10 @@ import { HighlightedCode } from "../docs/HighlightedCode"
|
|
|
36
36
|
import { CopyButton } from "../docs/copy"
|
|
37
37
|
import { PillTabs, Segmented, rovingKeyDown } from "../docs/controls"
|
|
38
38
|
import { useDemoResolver, useDemoRuntime } from "./context"
|
|
39
|
+
import { createDocstreamClipboard, type DemoSourceSnapshot, type DocstreamClipboard } from "./clipboard"
|
|
40
|
+
import { CopyDemoButton } from "./copy-demo"
|
|
39
41
|
import { languageForPath } from "./glob"
|
|
40
|
-
import {
|
|
42
|
+
import { useDemoVFSContext, useDemoVFSEntry, type DemoVFS } from "./vfs"
|
|
41
43
|
import { cssToSurface, surfaceStyle, surfaceToCss } from "./surface"
|
|
42
44
|
import type { DemoComponent, DemoFile, DemoMeta, DemoResolver, DemoVariant, InlineDemoRuntime } from "./types"
|
|
43
45
|
|
|
@@ -67,6 +69,11 @@ export interface DemoViewerProps {
|
|
|
67
69
|
runtime?: InlineDemoRuntime
|
|
68
70
|
/** Extra npm dependencies for the runtime, merged over `demoDependencies` from context. */
|
|
69
71
|
dependencies?: Record<string, string>
|
|
72
|
+
/**
|
|
73
|
+
* Demo files held in memory by `src` (pasted demos). Used, like inline files, when the block
|
|
74
|
+
* has none of its own and no resolver knows `src`. Defaults to the VFS in context (`demoVFS`).
|
|
75
|
+
*/
|
|
76
|
+
vfs?: DemoVFS
|
|
70
77
|
/** Class on the viewer's outer element. (The preview canvas's class is `canvasClassName`.) */
|
|
71
78
|
className?: string
|
|
72
79
|
/*
|
|
@@ -120,6 +127,32 @@ export async function preloadDemos(resolver: DemoResolver, srcs: string[]): Prom
|
|
|
120
127
|
)
|
|
121
128
|
}
|
|
122
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Fetch demos' meta and files (not their components) into the viewer's cache, so
|
|
132
|
+
* `peekDemoSource` can answer synchronously — e.g. when a copy event needs a demo's files.
|
|
133
|
+
*/
|
|
134
|
+
export async function prefetchDemoSources(resolver: DemoResolver, srcs: string[]): Promise<void> {
|
|
135
|
+
const cache = cacheFor(resolver)
|
|
136
|
+
const settle = async (key: string, run: () => Promise<unknown>) => {
|
|
137
|
+
if (cache.get(key)?.status === "ready") return
|
|
138
|
+
try {
|
|
139
|
+
cache.set(key, { status: "ready", value: await run() })
|
|
140
|
+
} catch (cause) {
|
|
141
|
+
cache.set(key, { status: "error", error: cause instanceof Error ? cause : new Error(String(cause)) })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
await Promise.all(srcs.flatMap((src) => [settle(`meta:${src}`, () => resolver.meta(src)), settle(`files:${src}`, () => resolver.files(src))]))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A demo's files (and meta) if the resolver has already delivered them (viewer, `preloadDemos`, `prefetchDemoSources`). */
|
|
148
|
+
export function peekDemoSource(resolver: DemoResolver, src: string): DemoSourceSnapshot | undefined {
|
|
149
|
+
const cache = settled.get(resolver)
|
|
150
|
+
const files = cache?.get(`files:${src}`)
|
|
151
|
+
if (files?.status !== "ready" || !(files.value as DemoFile[]).length) return undefined
|
|
152
|
+
const meta = cache?.get(`meta:${src}`)
|
|
153
|
+
return { files: files.value as DemoFile[], ...(meta?.status === "ready" && meta.value ? { meta: meta.value as DemoMeta } : {}) }
|
|
154
|
+
}
|
|
155
|
+
|
|
123
156
|
/** Resolves `run()` once `active`, remembering settled values so remounts don't flash. */
|
|
124
157
|
function useResolved<T>(resolver: DemoResolver | undefined, key: string, active: boolean, run: () => Promise<T>): Loaded<T> {
|
|
125
158
|
const cached = resolver ? (cacheFor(resolver).get(key) as Loaded<T> | undefined) : undefined
|
|
@@ -165,10 +198,11 @@ const useComponent = (resolver: DemoResolver | undefined, src: string, active: b
|
|
|
165
198
|
|
|
166
199
|
/** True once the element has come within `margin` of the viewport (stays true). */
|
|
167
200
|
function useNearViewport<T extends Element>(margin = "400px 0px") {
|
|
168
|
-
|
|
201
|
+
// A callback ref, so an element that appears later (a VFS demo arriving) is observed too.
|
|
202
|
+
const [el, setEl] = useState<T | null>(null)
|
|
203
|
+
const ref = useMemo(() => (node: T | null) => setEl(node), [])
|
|
169
204
|
const [near, setNear] = useState(false)
|
|
170
205
|
useEffect(() => {
|
|
171
|
-
const el = ref.current
|
|
172
206
|
if (!el || near) return
|
|
173
207
|
if (typeof IntersectionObserver === "undefined") {
|
|
174
208
|
setNear(true)
|
|
@@ -179,7 +213,7 @@ function useNearViewport<T extends Element>(margin = "400px 0px") {
|
|
|
179
213
|
}, { rootMargin: margin })
|
|
180
214
|
observer.observe(el)
|
|
181
215
|
return () => observer.disconnect()
|
|
182
|
-
}, [near, margin])
|
|
216
|
+
}, [el, near, margin])
|
|
183
217
|
return [ref, near] as const
|
|
184
218
|
}
|
|
185
219
|
|
|
@@ -594,7 +628,12 @@ export function DemoViewer(props: DemoViewerProps) {
|
|
|
594
628
|
const runtimeOptions = useDemoRuntime({ runtime: props.runtime, dependencies: props.dependencies })
|
|
595
629
|
const { src } = props
|
|
596
630
|
const [ref, near] = useNearViewport<HTMLElement>()
|
|
597
|
-
const
|
|
631
|
+
const vfs = useDemoVFSContext(props.vfs)
|
|
632
|
+
// Pasted demos: files held by the VFS stand in for inline files the block doesn't carry.
|
|
633
|
+
const vfsEntry = useDemoVFSEntry(vfs, props.files?.length ? "" : src)
|
|
634
|
+
const ownFiles = props.files?.length ? props.files : vfsEntry?.files
|
|
635
|
+
const ownEntry = props.files?.length ? props.entry : props.entry ?? vfsEntry?.meta?.entry
|
|
636
|
+
const inlineFiles = useMemo(() => orderInline(ownFiles, ownEntry), [ownFiles, ownEntry])
|
|
598
637
|
|
|
599
638
|
// A resolver is consulted unless the block brings its own files and the resolver
|
|
600
639
|
// says (via `list()`) that it does not know this src.
|
|
@@ -607,7 +646,7 @@ export function DemoViewer(props: DemoViewerProps) {
|
|
|
607
646
|
const mode: "resolver" | "inline" | "none" =
|
|
608
647
|
tryResolver && !(inlineFiles && resolverFailed) ? "resolver" : inlineFiles ? "inline" : "none"
|
|
609
648
|
|
|
610
|
-
const metaValue = mode === "resolver"
|
|
649
|
+
const metaValue = mode === "resolver" ? (meta.status === "ready" ? meta.value : undefined) : mode === "inline" && !props.files?.length ? vfsEntry?.meta : undefined
|
|
611
650
|
const files: Loaded<DemoFile[]> = mode === "inline" ? { status: "ready", value: inlineFiles! } : resolvedFiles
|
|
612
651
|
const title = props.title ?? metaValue?.title ?? (src.split("/").pop() || "Demo")
|
|
613
652
|
const description = props.description ?? metaValue?.description
|
|
@@ -669,7 +708,12 @@ export function DemoViewer(props: DemoViewerProps) {
|
|
|
669
708
|
...(props.surface ? { surface: typeof props.surface === "string" ? props.surface : surfaceToCss(props.surface) } : {}),
|
|
670
709
|
...(props.variantsWidth !== undefined && props.variantsWidth !== "" ? { variantsWidth: String(props.variantsWidth) } : {}),
|
|
671
710
|
}
|
|
672
|
-
|
|
711
|
+
// "Copy demo": the exact block (src-only stays src-only; inline files stay inline) plus its files.
|
|
712
|
+
const copyDemo = (): DocstreamClipboard | null => {
|
|
713
|
+
if (!fileList?.length) return null
|
|
714
|
+
const exact: DemoNode = props.files?.length ? { ...node, ...(props.entry ? { entry: props.entry } : {}), files: props.files.map((f) => ({ ...f })) } : node
|
|
715
|
+
return createDocstreamClipboard({ type: "doc", children: [exact] }, { lookup: () => ({ files: fileList, ...(metaValue ? { meta: metaValue } : {}) }) })
|
|
716
|
+
}
|
|
673
717
|
|
|
674
718
|
const dialog = (
|
|
675
719
|
<FullscreenDialog
|
|
@@ -702,6 +746,7 @@ export function DemoViewer(props: DemoViewerProps) {
|
|
|
702
746
|
<span className="docs-demo-spacer" />
|
|
703
747
|
<StatusBadge status={status} />
|
|
704
748
|
<VariantSwitch variants={variants} value={variant} onChange={setVariant} title={title} width={variantsWidth} />
|
|
749
|
+
{fileList?.length ? <CopyDemoButton clipboard={copyDemo} className="docs-demo-icon-btn" /> : null}
|
|
705
750
|
{runnable ? <FullscreenButton onOpen={() => setFullscreen(true)} title={title} /> : null}
|
|
706
751
|
</header>
|
|
707
752
|
{runnable ? (
|
|
@@ -766,7 +811,7 @@ export function DemoViewer(props: DemoViewerProps) {
|
|
|
766
811
|
}
|
|
767
812
|
|
|
768
813
|
function MultiFileViewer({ rootRef, className, baseId, mode, src, title, description, variants, variant, onVariant, files, preview, near, height, viewport, bleed, look, status, variantsWidth, href, onFullscreen, copyDemo, dialog }: {
|
|
769
|
-
rootRef:
|
|
814
|
+
rootRef: Ref<HTMLElement>
|
|
770
815
|
className: string
|
|
771
816
|
baseId: string
|
|
772
817
|
mode: "resolver" | "inline"
|
|
@@ -787,7 +832,7 @@ function MultiFileViewer({ rootRef, className, baseId, mode, src, title, descrip
|
|
|
787
832
|
variantsWidth: number | string | undefined
|
|
788
833
|
href: string | undefined
|
|
789
834
|
onFullscreen: () => void
|
|
790
|
-
copyDemo: (() =>
|
|
835
|
+
copyDemo: (() => DocstreamClipboard | null) | undefined
|
|
791
836
|
dialog: ReactNode
|
|
792
837
|
}) {
|
|
793
838
|
const runnable = preview.kind !== "unavailable"
|
|
@@ -835,7 +880,7 @@ function MultiFileViewer({ rootRef, className, baseId, mode, src, title, descrip
|
|
|
835
880
|
))}
|
|
836
881
|
</div>
|
|
837
882
|
) : null}
|
|
838
|
-
{copyDemo ? <
|
|
883
|
+
{copyDemo ? <CopyDemoButton clipboard={copyDemo} className="docs-demo-icon-btn" /> : null}
|
|
839
884
|
{runnable ? <FullscreenButton onOpen={onFullscreen} title={title} /> : null}
|
|
840
885
|
<NewTabLink href={href} title={title} />
|
|
841
886
|
</div>
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/* Copy / paste of docstream content that contains `{% demo %}` blocks.
|
|
2
|
+
|
|
3
|
+
A copy writes one clipboard item with three representations (react-aria `DragItem` shape,
|
|
4
|
+
so `useClipboard` / `useDrag` can write it; `writeDocstreamClipboard` writes it to a raw
|
|
5
|
+
`DataTransfer`):
|
|
6
|
+
|
|
7
|
+
- `text/plain` and `text/markdown`: renderable docstream Markdown — every demo carries its
|
|
8
|
+
files inline (`{% demo … %}` + titled fences + `{% enddemo %}`), resolver files included,
|
|
9
|
+
so pasting it anywhere (a docs page, an LLM, another editor) still renders;
|
|
10
|
+
- `application/x-docstream+json` (`DOCSTREAM_CLIPBOARD_TYPE`): a `DocstreamClipboardPayload`
|
|
11
|
+
— the exact Markdown (src-only demos stay src-only), plus every demo's files and meta by
|
|
12
|
+
`src`, so a docstream editor can restore the structure and register the files in its
|
|
13
|
+
`DemoVFS`. */
|
|
14
|
+
import type { DemoNode, DocumentNode } from "../gitbook/ast"
|
|
15
|
+
import { parseMarkdown } from "../gitbook/parse"
|
|
16
|
+
import { serializeMarkdown } from "../gitbook/serialize"
|
|
17
|
+
import { inlineDemoNode, toInlineFiles } from "./markdown"
|
|
18
|
+
import { cssToSurface } from "./surface"
|
|
19
|
+
import type { DemoFile, DemoMeta } from "./types"
|
|
20
|
+
|
|
21
|
+
export const DOCSTREAM_CLIPBOARD_TYPE = "application/x-docstream+json"
|
|
22
|
+
|
|
23
|
+
export interface DocstreamClipboardPayload {
|
|
24
|
+
/** Always `"docstream"`. */
|
|
25
|
+
kind: "docstream"
|
|
26
|
+
version: 1
|
|
27
|
+
/** The copied content as docstream Markdown, structure exactly as it was. */
|
|
28
|
+
markdown: string
|
|
29
|
+
/** Every demo's files by `src` (inline files, or the resolver's for src-only demos). */
|
|
30
|
+
files: Record<string, DemoFile[]>
|
|
31
|
+
/** `meta.json`-style defaults by `src` (the resolver's meta, else the tag's attributes). */
|
|
32
|
+
meta: Record<string, DemoMeta>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A demo's files (and meta) as the copying side knows them. */
|
|
36
|
+
export interface DemoSourceSnapshot {
|
|
37
|
+
files: DemoFile[]
|
|
38
|
+
meta?: DemoMeta
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface DocstreamClipboard {
|
|
42
|
+
payload: DocstreamClipboardPayload
|
|
43
|
+
/** The renderable Markdown (demos inlined) written as `text/plain` / `text/markdown`. */
|
|
44
|
+
text: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A react-aria `DragItem`: one representation per MIME type. */
|
|
48
|
+
export type DocstreamClipboardItem = Record<string, string>
|
|
49
|
+
|
|
50
|
+
/** Every `{% demo %}` node in a document (inside tabs, hints, lists …), in document order. */
|
|
51
|
+
export function collectDemoNodes(doc: DocumentNode): DemoNode[] {
|
|
52
|
+
const out: DemoNode[] = []
|
|
53
|
+
const walk = (value: unknown) => {
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
for (const item of value) walk(item)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
if (!value || typeof value !== "object") return
|
|
59
|
+
const node = value as { type?: unknown }
|
|
60
|
+
if (node.type === "demo") {
|
|
61
|
+
out.push(value as DemoNode)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
for (const child of Object.values(value)) if (child && typeof child === "object") walk(child)
|
|
65
|
+
}
|
|
66
|
+
walk(doc.children)
|
|
67
|
+
return out
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A copy of `doc` with every demo node replaced by `map(node)`. */
|
|
71
|
+
export function mapDemoNodes(doc: DocumentNode, map: (node: DemoNode) => DemoNode): DocumentNode {
|
|
72
|
+
const walk = (value: unknown): unknown => {
|
|
73
|
+
if (Array.isArray(value)) return value.map(walk)
|
|
74
|
+
if (!value || typeof value !== "object") return value
|
|
75
|
+
if ((value as { type?: unknown }).type === "demo") return map(value as DemoNode)
|
|
76
|
+
const out: Record<string, unknown> = {}
|
|
77
|
+
for (const [k, v] of Object.entries(value)) out[k] = v && typeof v === "object" ? walk(v) : v
|
|
78
|
+
return out
|
|
79
|
+
}
|
|
80
|
+
return walk(doc) as DocumentNode
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A demo tag's attributes as `meta.json`-style defaults. */
|
|
84
|
+
export function demoNodeMeta(node: DemoNode): DemoMeta {
|
|
85
|
+
const meta: DemoMeta = {}
|
|
86
|
+
if (node.title) meta.title = node.title
|
|
87
|
+
if (node.description) meta.description = node.description
|
|
88
|
+
if (node.height) meta.height = /^\d+(?:\.\d+)?$/.test(node.height) ? Number(node.height) : node.height
|
|
89
|
+
if (node.layout) meta.layout = node.layout
|
|
90
|
+
if (node.variants?.length) meta.variants = node.variants.map((v) => ({ ...v }))
|
|
91
|
+
if (node.entry) meta.entry = node.entry
|
|
92
|
+
if (node.status) meta.status = node.status
|
|
93
|
+
if (node.bleed !== undefined) meta.bleed = node.bleed
|
|
94
|
+
if (node.className) meta.className = node.className
|
|
95
|
+
if (node.surface) meta.surface = cssToSurface(node.surface)
|
|
96
|
+
if (node.variantsWidth) meta.variantsWidth = node.variantsWidth
|
|
97
|
+
return meta
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface CreateDocstreamClipboardOptions {
|
|
101
|
+
/**
|
|
102
|
+
* Files (and meta) for demos that don't carry theirs inline — the host resolver's, read
|
|
103
|
+
* synchronously (see `peekDemoSource`), or a `DemoVFS`'s. Demos it can't answer for stay
|
|
104
|
+
* src-only in both representations.
|
|
105
|
+
*/
|
|
106
|
+
lookup?: (node: DemoNode) => DemoSourceSnapshot | undefined
|
|
107
|
+
/** Markdown serializer. Defaults to docstream's `serializeMarkdown`. */
|
|
108
|
+
serialize?: (doc: DocumentNode) => string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The clipboard representations of a document (or a selection of one). */
|
|
112
|
+
export function createDocstreamClipboard(doc: DocumentNode, options: CreateDocstreamClipboardOptions = {}): DocstreamClipboard {
|
|
113
|
+
const serialize = options.serialize ?? serializeMarkdown
|
|
114
|
+
const files: Record<string, DemoFile[]> = {}
|
|
115
|
+
const meta: Record<string, DemoMeta> = {}
|
|
116
|
+
const inlined = mapDemoNodes(doc, (node) => {
|
|
117
|
+
if (node.files?.length) {
|
|
118
|
+
if (node.src) {
|
|
119
|
+
files[node.src] = toInlineFiles(node.files)
|
|
120
|
+
const tagMeta = demoNodeMeta(node)
|
|
121
|
+
if (Object.keys(tagMeta).length) meta[node.src] = tagMeta
|
|
122
|
+
}
|
|
123
|
+
return node
|
|
124
|
+
}
|
|
125
|
+
const found = node.src ? options.lookup?.(node) : undefined
|
|
126
|
+
if (!found?.files.length) return node
|
|
127
|
+
files[node.src] = toInlineFiles(found.files)
|
|
128
|
+
const merged = { ...found.meta, ...demoNodeMeta(node) }
|
|
129
|
+
if (Object.keys(merged).length) meta[node.src] = merged
|
|
130
|
+
return inlineDemoNode(node, found.files, found.meta)
|
|
131
|
+
})
|
|
132
|
+
return {
|
|
133
|
+
payload: { kind: "docstream", version: 1, markdown: serialize(doc), files, meta },
|
|
134
|
+
text: serialize(inlined),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Parse the Markdown of a copied docstream selection (or a single demo). */
|
|
139
|
+
export function createDocstreamClipboardFromMarkdown(markdown: string, options: CreateDocstreamClipboardOptions = {}): DocstreamClipboard {
|
|
140
|
+
const clip = createDocstreamClipboard(parseMarkdown(markdown), options)
|
|
141
|
+
// Keep the author's bytes for the exact representation.
|
|
142
|
+
return { ...clip, payload: { ...clip.payload, markdown } }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The clipboard as react-aria `DragItem`s (for `useClipboard`'s `getItems` / `useDrag`). */
|
|
146
|
+
export function docstreamClipboardItems(clip: DocstreamClipboard): DocstreamClipboardItem[] {
|
|
147
|
+
return [
|
|
148
|
+
{
|
|
149
|
+
"text/plain": clip.text,
|
|
150
|
+
"text/markdown": clip.text,
|
|
151
|
+
[DOCSTREAM_CLIPBOARD_TYPE]: JSON.stringify(clip.payload),
|
|
152
|
+
},
|
|
153
|
+
]
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Writes the representations straight onto a `DataTransfer` (a copy / cut / dragstart event's). */
|
|
157
|
+
export function writeDocstreamClipboard(data: Pick<DataTransfer, "setData">, clip: DocstreamClipboard): void {
|
|
158
|
+
for (const [type, value] of Object.entries(docstreamClipboardItems(clip)[0])) data.setData(type, value)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Validates a `DOCSTREAM_CLIPBOARD_TYPE` string. */
|
|
162
|
+
export function parseDocstreamClipboard(json: string | null | undefined): DocstreamClipboardPayload | null {
|
|
163
|
+
if (!json) return null
|
|
164
|
+
let raw: unknown
|
|
165
|
+
try {
|
|
166
|
+
raw = JSON.parse(json)
|
|
167
|
+
} catch {
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
if (!raw || typeof raw !== "object") return null
|
|
171
|
+
const value = raw as Partial<DocstreamClipboardPayload>
|
|
172
|
+
if (value.kind !== "docstream" || typeof value.markdown !== "string") return null
|
|
173
|
+
const files: Record<string, DemoFile[]> = {}
|
|
174
|
+
for (const [src, list] of Object.entries(value.files && typeof value.files === "object" ? value.files : {})) {
|
|
175
|
+
if (!Array.isArray(list)) continue
|
|
176
|
+
const clean = list
|
|
177
|
+
.filter((f): f is DemoFile => !!f && typeof f === "object" && typeof f.path === "string" && typeof f.content === "string")
|
|
178
|
+
.map((f) => ({ path: f.path, content: f.content, ...(typeof f.language === "string" && f.language ? { language: f.language } : {}) }))
|
|
179
|
+
if (clean.length) files[src] = clean
|
|
180
|
+
}
|
|
181
|
+
const meta: Record<string, DemoMeta> = {}
|
|
182
|
+
for (const [src, m] of Object.entries(value.meta && typeof value.meta === "object" ? value.meta : {})) {
|
|
183
|
+
if (m && typeof m === "object") meta[src] = m as DemoMeta
|
|
184
|
+
}
|
|
185
|
+
return { kind: "docstream", version: 1, markdown: value.markdown, files, meta }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function hasType(types: ArrayLike<string> | null | undefined, type: string) {
|
|
189
|
+
return !!types && Array.prototype.indexOf.call(types, type) >= 0
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** True when a `DataTransfer` carries a docstream payload. */
|
|
193
|
+
export function hasDocstreamClipboard(data: Pick<DataTransfer, "types"> | null | undefined): boolean {
|
|
194
|
+
return hasType(data?.types, DOCSTREAM_CLIPBOARD_TYPE)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The docstream payload on a paste / drop `DataTransfer`, if any. */
|
|
198
|
+
export function readDocstreamClipboard(data: Pick<DataTransfer, "getData" | "types"> | null | undefined): DocstreamClipboardPayload | null {
|
|
199
|
+
if (!data || !hasDocstreamClipboard(data)) return null
|
|
200
|
+
return parseDocstreamClipboard(data.getData(DOCSTREAM_CLIPBOARD_TYPE))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Minimal react-aria `DropItem` shape (text items). */
|
|
204
|
+
export interface DocstreamDropItem {
|
|
205
|
+
kind: string
|
|
206
|
+
types?: Set<string>
|
|
207
|
+
getText?: (type: string) => Promise<string>
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The docstream payload among react-aria `DropItem`s (`useClipboard`'s `onPaste`, `useDrop`). */
|
|
211
|
+
export async function readDocstreamClipboardItems(items: readonly DocstreamDropItem[]): Promise<DocstreamClipboardPayload | null> {
|
|
212
|
+
for (const item of items) {
|
|
213
|
+
if (item.kind === "text" && item.types?.has(DOCSTREAM_CLIPBOARD_TYPE) && item.getText) {
|
|
214
|
+
const payload = parseDocstreamClipboard(await item.getText(DOCSTREAM_CLIPBOARD_TYPE))
|
|
215
|
+
if (payload) return payload
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* A payload for Markdown that arrived without one (only `text/plain` / `text/markdown`):
|
|
223
|
+
* inline-file demos give their files by `src`.
|
|
224
|
+
*/
|
|
225
|
+
export function docstreamPayloadFromMarkdown(markdown: string): DocstreamClipboardPayload {
|
|
226
|
+
const { payload } = createDocstreamClipboardFromMarkdown(markdown)
|
|
227
|
+
return payload
|
|
228
|
+
}
|
package/src/demo/context.ts
CHANGED
|
@@ -27,3 +27,10 @@ export function useDemoRuntime(override?: DemoRuntimeOptions): DemoRuntimeOption
|
|
|
27
27
|
: undefined,
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Demo files held in memory by `src` (a `DemoVFS`), e.g. demos pasted from another page or
|
|
33
|
+
* editor. `DemoViewer` falls back to them when a block has no inline files; the host
|
|
34
|
+
* resolver still wins when it knows `src`. Provided by renderers' / editors' `demoVFS`.
|
|
35
|
+
*/
|
|
36
|
+
export const DocstreamDemoVFSContext = createContext<import("./vfs").DemoVFS | undefined>(undefined)
|