@brett_lamy/docstream 1.3.0 → 1.5.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.
Files changed (228) hide show
  1. package/README.md +79 -5
  2. package/dist/assets.d.ts +3 -0
  3. package/dist/assets.d.ts.map +1 -0
  4. package/dist/assets.js +20 -0
  5. package/dist/assets.js.map +1 -0
  6. package/dist/demo/DemoViewer.d.ts +78 -0
  7. package/dist/demo/DemoViewer.d.ts.map +1 -0
  8. package/dist/demo/DemoViewer.js +467 -0
  9. package/dist/demo/DemoViewer.js.map +1 -0
  10. package/dist/demo/clipboard.d.ts +70 -0
  11. package/dist/demo/clipboard.d.ts.map +1 -0
  12. package/dist/demo/clipboard.js +187 -0
  13. package/dist/demo/clipboard.js.map +1 -0
  14. package/dist/demo/context.d.ts +20 -0
  15. package/dist/demo/context.d.ts.map +1 -0
  16. package/dist/demo/context.js +31 -0
  17. package/dist/demo/context.js.map +1 -0
  18. package/dist/demo/copy-demo.d.ts +23 -0
  19. package/dist/demo/copy-demo.d.ts.map +1 -0
  20. package/dist/demo/copy-demo.js +81 -0
  21. package/dist/demo/copy-demo.js.map +1 -0
  22. package/dist/demo/glob.d.ts +60 -0
  23. package/dist/demo/glob.d.ts.map +1 -0
  24. package/dist/demo/glob.js +158 -0
  25. package/dist/demo/glob.js.map +1 -0
  26. package/dist/demo/index.d.ts +16 -0
  27. package/dist/demo/index.d.ts.map +1 -0
  28. package/dist/demo/index.js +9 -0
  29. package/dist/demo/index.js.map +1 -0
  30. package/dist/demo/markdown.d.ts +60 -0
  31. package/dist/demo/markdown.d.ts.map +1 -0
  32. package/dist/demo/markdown.js +181 -0
  33. package/dist/demo/markdown.js.map +1 -0
  34. package/dist/demo/surface.d.ts +11 -0
  35. package/dist/demo/surface.d.ts.map +1 -0
  36. package/dist/demo/surface.js +69 -0
  37. package/dist/demo/surface.js.map +1 -0
  38. package/dist/demo/types.d.ts +91 -0
  39. package/dist/demo/types.d.ts.map +1 -0
  40. package/dist/demo/types.js +2 -0
  41. package/dist/demo/types.js.map +1 -0
  42. package/dist/demo/vfs.d.ts +51 -0
  43. package/dist/demo/vfs.d.ts.map +1 -0
  44. package/dist/demo/vfs.js +156 -0
  45. package/dist/demo/vfs.js.map +1 -0
  46. package/dist/docs/CollapsibleCode.d.ts +14 -0
  47. package/dist/docs/CollapsibleCode.d.ts.map +1 -0
  48. package/dist/docs/CollapsibleCode.js +19 -0
  49. package/dist/docs/CollapsibleCode.js.map +1 -0
  50. package/dist/docs/DocsRenderer.d.ts +55 -0
  51. package/dist/docs/DocsRenderer.d.ts.map +1 -0
  52. package/dist/docs/DocsRenderer.js +279 -0
  53. package/dist/docs/DocsRenderer.js.map +1 -0
  54. package/dist/docs/HighlightedCode.d.ts +6 -0
  55. package/dist/docs/HighlightedCode.d.ts.map +1 -0
  56. package/dist/docs/HighlightedCode.js +40 -0
  57. package/dist/docs/HighlightedCode.js.map +1 -0
  58. package/dist/docs/Mermaid.d.ts +7 -0
  59. package/dist/docs/Mermaid.d.ts.map +1 -0
  60. package/dist/docs/Mermaid.js +39 -0
  61. package/dist/docs/Mermaid.js.map +1 -0
  62. package/dist/docs/PageActions.d.ts +33 -0
  63. package/dist/docs/PageActions.d.ts.map +1 -0
  64. package/dist/docs/PageActions.js +115 -0
  65. package/dist/docs/PageActions.js.map +1 -0
  66. package/dist/docs/controls.d.ts +29 -0
  67. package/dist/docs/controls.d.ts.map +1 -0
  68. package/dist/docs/controls.js +46 -0
  69. package/dist/docs/controls.js.map +1 -0
  70. package/dist/docs/copy.d.ts +24 -0
  71. package/dist/docs/copy.d.ts.map +1 -0
  72. package/dist/docs/copy.js +62 -0
  73. package/dist/docs/copy.js.map +1 -0
  74. package/dist/docs/math.d.ts +3 -0
  75. package/dist/docs/math.d.ts.map +1 -0
  76. package/dist/docs/math.js +12 -0
  77. package/dist/docs/math.js.map +1 -0
  78. package/dist/docs/reference.d.ts +27 -0
  79. package/dist/docs/reference.d.ts.map +1 -0
  80. package/dist/docs/reference.js +73 -0
  81. package/dist/docs/reference.js.map +1 -0
  82. package/dist/docs/tabs-sync.d.ts +5 -0
  83. package/dist/docs/tabs-sync.d.ts.map +1 -0
  84. package/dist/docs/tabs-sync.js +58 -0
  85. package/dist/docs/tabs-sync.js.map +1 -0
  86. package/dist/gitbook/ast.d.ts +506 -0
  87. package/dist/gitbook/ast.d.ts.map +1 -0
  88. package/dist/gitbook/ast.js +8 -0
  89. package/dist/gitbook/ast.js.map +1 -0
  90. package/dist/gitbook/attrs.d.ts +14 -0
  91. package/dist/gitbook/attrs.d.ts.map +1 -0
  92. package/dist/gitbook/attrs.js +36 -0
  93. package/dist/gitbook/attrs.js.map +1 -0
  94. package/dist/gitbook/flatten.d.ts +14 -0
  95. package/dist/gitbook/flatten.d.ts.map +1 -0
  96. package/dist/gitbook/flatten.js +45 -0
  97. package/dist/gitbook/flatten.js.map +1 -0
  98. package/dist/gitbook/index.d.ts +10 -0
  99. package/dist/gitbook/index.d.ts.map +1 -0
  100. package/dist/gitbook/index.js +8 -0
  101. package/dist/gitbook/index.js.map +1 -0
  102. package/dist/gitbook/inline.d.ts +28 -0
  103. package/dist/gitbook/inline.d.ts.map +1 -0
  104. package/dist/gitbook/inline.js +956 -0
  105. package/dist/gitbook/inline.js.map +1 -0
  106. package/dist/gitbook/outline.d.ts +16 -0
  107. package/dist/gitbook/outline.d.ts.map +1 -0
  108. package/dist/gitbook/outline.js +44 -0
  109. package/dist/gitbook/outline.js.map +1 -0
  110. package/dist/gitbook/package-managers.d.ts +12 -0
  111. package/dist/gitbook/package-managers.d.ts.map +1 -0
  112. package/dist/gitbook/package-managers.js +93 -0
  113. package/dist/gitbook/package-managers.js.map +1 -0
  114. package/dist/gitbook/parse.d.ts +44 -0
  115. package/dist/gitbook/parse.d.ts.map +1 -0
  116. package/dist/gitbook/parse.js +1113 -0
  117. package/dist/gitbook/parse.js.map +1 -0
  118. package/dist/gitbook/serialize.d.ts +23 -0
  119. package/dist/gitbook/serialize.d.ts.map +1 -0
  120. package/dist/gitbook/serialize.js +498 -0
  121. package/dist/gitbook/serialize.js.map +1 -0
  122. package/dist/index.d.ts +34 -0
  123. package/dist/index.d.ts.map +1 -0
  124. package/dist/index.js +24 -0
  125. package/dist/index.js.map +1 -0
  126. package/dist/openapi/OpenApiOperation.d.ts +6 -0
  127. package/dist/openapi/OpenApiOperation.d.ts.map +1 -0
  128. package/dist/openapi/OpenApiOperation.js +112 -0
  129. package/dist/openapi/OpenApiOperation.js.map +1 -0
  130. package/dist/playground/InlineDemoPreview.d.ts +24 -0
  131. package/dist/playground/InlineDemoPreview.d.ts.map +1 -0
  132. package/dist/playground/InlineDemoPreview.js +48 -0
  133. package/dist/playground/InlineDemoPreview.js.map +1 -0
  134. package/dist/playground/PlaygroundStreamdown.d.ts +16 -0
  135. package/dist/playground/PlaygroundStreamdown.d.ts.map +1 -0
  136. package/dist/playground/PlaygroundStreamdown.js +22 -0
  137. package/dist/playground/PlaygroundStreamdown.js.map +1 -0
  138. package/dist/playground/ReactCodePreview.d.ts +64 -0
  139. package/dist/playground/ReactCodePreview.d.ts.map +1 -0
  140. package/dist/playground/ReactCodePreview.js +108 -0
  141. package/dist/playground/ReactCodePreview.js.map +1 -0
  142. package/dist/playground/filesystem.d.ts +27 -0
  143. package/dist/playground/filesystem.d.ts.map +1 -0
  144. package/dist/playground/filesystem.js +65 -0
  145. package/dist/playground/filesystem.js.map +1 -0
  146. package/dist/playground/index.d.ts +9 -0
  147. package/dist/playground/index.d.ts.map +1 -0
  148. package/dist/playground/index.js +7 -0
  149. package/dist/playground/index.js.map +1 -0
  150. package/dist/playground/project.d.ts +57 -0
  151. package/dist/playground/project.d.ts.map +1 -0
  152. package/dist/playground/project.js +137 -0
  153. package/dist/playground/project.js.map +1 -0
  154. package/dist/replay/NativeReplay.d.ts +9 -0
  155. package/dist/replay/NativeReplay.d.ts.map +1 -0
  156. package/dist/replay/NativeReplay.js +173 -0
  157. package/dist/replay/NativeReplay.js.map +1 -0
  158. package/dist/replay/ReplayPreview.d.ts +33 -0
  159. package/dist/replay/ReplayPreview.d.ts.map +1 -0
  160. package/dist/replay/ReplayPreview.js +91 -0
  161. package/dist/replay/ReplayPreview.js.map +1 -0
  162. package/dist/replay/index.d.ts +5 -0
  163. package/dist/replay/index.d.ts.map +1 -0
  164. package/dist/replay/index.js +4 -0
  165. package/dist/replay/index.js.map +1 -0
  166. package/dist/replay/sanitize.d.ts +4 -0
  167. package/dist/replay/sanitize.d.ts.map +1 -0
  168. package/dist/replay/sanitize.js +86 -0
  169. package/dist/replay/sanitize.js.map +1 -0
  170. package/dist/replay/url.d.ts +8 -0
  171. package/dist/replay/url.d.ts.map +1 -0
  172. package/dist/replay/url.js +41 -0
  173. package/dist/replay/url.js.map +1 -0
  174. package/dist/source/SourcePreview.d.ts +13 -0
  175. package/dist/source/SourcePreview.d.ts.map +1 -0
  176. package/dist/source/SourcePreview.js +47 -0
  177. package/dist/source/SourcePreview.js.map +1 -0
  178. package/dist/source/client.d.ts +7 -0
  179. package/dist/source/client.d.ts.map +1 -0
  180. package/dist/source/client.js +52 -0
  181. package/dist/source/client.js.map +1 -0
  182. package/dist/source/index.d.ts +6 -0
  183. package/dist/source/index.d.ts.map +1 -0
  184. package/dist/source/index.js +3 -0
  185. package/dist/source/index.js.map +1 -0
  186. package/dist/source/types.d.ts +33 -0
  187. package/dist/source/types.d.ts.map +1 -0
  188. package/dist/source/types.js +2 -0
  189. package/dist/source/types.js.map +1 -0
  190. package/dist/streamdown.d.ts +19 -0
  191. package/dist/streamdown.d.ts.map +1 -0
  192. package/dist/streamdown.js +15 -0
  193. package/dist/streamdown.js.map +1 -0
  194. package/dist/styles.css +2441 -0
  195. package/dist/video/VideoEmbed.d.ts +17 -0
  196. package/dist/video/VideoEmbed.d.ts.map +1 -0
  197. package/dist/video/VideoEmbed.js +12 -0
  198. package/dist/video/VideoEmbed.js.map +1 -0
  199. package/dist/video/index.d.ts +3 -0
  200. package/dist/video/index.d.ts.map +1 -0
  201. package/dist/video/index.js +2 -0
  202. package/dist/video/index.js.map +1 -0
  203. package/dist/vite/index.d.ts +21 -0
  204. package/dist/vite/index.d.ts.map +1 -0
  205. package/dist/vite/index.js +137 -0
  206. package/dist/vite/index.js.map +1 -0
  207. package/dist/viz/VizEmbed.d.ts +18 -0
  208. package/dist/viz/VizEmbed.d.ts.map +1 -0
  209. package/dist/viz/VizEmbed.js +12 -0
  210. package/dist/viz/VizEmbed.js.map +1 -0
  211. package/dist/viz/index.d.ts +4 -0
  212. package/dist/viz/index.d.ts.map +1 -0
  213. package/dist/viz/index.js +3 -0
  214. package/dist/viz/index.js.map +1 -0
  215. package/package.json +65 -32
  216. package/src/demo/DemoViewer.tsx +56 -11
  217. package/src/demo/clipboard.ts +228 -0
  218. package/src/demo/context.ts +7 -0
  219. package/src/demo/copy-demo.tsx +102 -0
  220. package/src/demo/index.ts +29 -2
  221. package/src/demo/vfs.ts +197 -0
  222. package/src/docs/DocsRenderer.tsx +13 -4
  223. package/src/docs/Mermaid.tsx +5 -3
  224. package/src/index.ts +19 -0
  225. package/src/replay/ReplayPreview.tsx +0 -1
  226. package/src/streamdown.tsx +2 -0
  227. package/src/styles.css +91 -0
  228. package/src/replay/styles.css +0 -87
@@ -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
+ }
@@ -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)
@@ -0,0 +1,102 @@
1
+ /* "Copy demo": writes the docstream clipboard (Markdown with the demo's files inline, plus the
2
+ `application/x-docstream+json` payload) through react-aria's `useClipboard`, so pasting into
3
+ a docstream editor recreates the demo with its files. A button click has no copy event of
4
+ its own, so the button focuses itself and runs the browser's copy command; react-aria's
5
+ listener then fills the event's DataTransfer. Where that is unavailable, the Markdown is
6
+ copied as plain text (editors still parse its inline-file demos). */
7
+ import { useEffect, useRef, useState, type DOMAttributes, type MutableRefObject } from "react"
8
+ import { useClipboard } from "react-aria"
9
+ import { CopyIcon, copyText } from "../docs/copy"
10
+ import { docstreamClipboardItems, type DocstreamClipboard } from "./clipboard"
11
+
12
+ export interface DocstreamCopy<T extends HTMLElement> {
13
+ ref: MutableRefObject<T | null>
14
+ /** react-aria `useClipboard`'s focus handlers: Cmd/Ctrl+C on the focused element copies too. */
15
+ props: DOMAttributes<T>
16
+ /** Copies; resolves false when there was nothing to copy. */
17
+ copy: () => Promise<boolean>
18
+ }
19
+
20
+ /**
21
+ * Copies a `DocstreamClipboard` (all three representations) from a click. Spread `props` on
22
+ * the triggering element (a focusable one, e.g. a `<button>`) and give it `ref`.
23
+ */
24
+ export function useDocstreamCopy<T extends HTMLElement = HTMLButtonElement>(produce: () => DocstreamClipboard | null): DocstreamCopy<T> {
25
+ const ref = useRef<T | null>(null)
26
+ const latest = useRef(produce)
27
+ latest.current = produce
28
+ const wrote = useRef(false)
29
+ const { clipboardProps } = useClipboard({
30
+ getItems: () => {
31
+ const clip = latest.current()
32
+ return clip ? docstreamClipboardItems(clip) : []
33
+ },
34
+ onCopy: () => {
35
+ wrote.current = true
36
+ },
37
+ })
38
+
39
+ const copy = async (): Promise<boolean> => {
40
+ const clip = latest.current()
41
+ if (!clip) return false
42
+ const el = ref.current
43
+ wrote.current = false
44
+ if (el && typeof document !== "undefined" && typeof document.execCommand === "function") {
45
+ // The copy event targets the selection when there is one; clear it so it targets
46
+ // this (focused) element and nothing else — an editor around us — claims the event.
47
+ const selection = typeof window !== "undefined" ? window.getSelection() : null
48
+ const ranges: Range[] = []
49
+ if (selection) for (let i = 0; i < selection.rangeCount; i++) ranges.push(selection.getRangeAt(i))
50
+ try {
51
+ if (document.activeElement !== el) el.focus({ preventScroll: true })
52
+ selection?.removeAllRanges()
53
+ document.execCommand("copy")
54
+ } catch {
55
+ // fall through to the plain-text copy
56
+ } finally {
57
+ if (selection && ranges.length && !selection.rangeCount) for (const range of ranges) selection.addRange(range)
58
+ }
59
+ }
60
+ if (!wrote.current) await copyText(clip.text)
61
+ return true
62
+ }
63
+
64
+ return { ref, props: clipboardProps as DOMAttributes<T>, copy }
65
+ }
66
+
67
+ export interface CopyDemoButtonProps {
68
+ /** The clipboard to write, computed on click. `null` disables the copy. */
69
+ clipboard: () => DocstreamClipboard | null
70
+ label?: string
71
+ className?: string
72
+ }
73
+
74
+ /** Icon button: copies the demo (Markdown + files + docstream payload) and swaps to a tick. */
75
+ export function CopyDemoButton({ clipboard, label = "Copy demo", className }: CopyDemoButtonProps) {
76
+ const { ref, props, copy } = useDocstreamCopy(clipboard)
77
+ const [copied, setCopied] = useState(false)
78
+ const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
79
+ useEffect(() => () => clearTimeout(timer.current), [])
80
+ return (
81
+ <button
82
+ {...props}
83
+ ref={ref}
84
+ type="button"
85
+ className={className ? `docs-copy ${className}` : "docs-copy"}
86
+ onClick={() => {
87
+ void copy().then((ok) => {
88
+ if (!ok) return
89
+ setCopied(true)
90
+ clearTimeout(timer.current)
91
+ timer.current = setTimeout(() => setCopied(false), 1600)
92
+ })
93
+ }}
94
+ aria-label={copied ? "Copied" : label}
95
+ title={copied ? "Copied" : `${label} (Markdown with its files — paste into a docstream editor)`}
96
+ data-copied={copied || undefined}
97
+ data-docstream-copy-demo=""
98
+ >
99
+ <CopyIcon copied={copied} />
100
+ </button>
101
+ )
102
+ }
package/src/demo/index.ts CHANGED
@@ -1,5 +1,32 @@
1
- export { DocstreamDemoContext, DocstreamDemoRuntimeContext, useDemoResolver, useDemoRuntime } from "./context"
2
- export { DemoBlock, DemoFullscreen, DemoViewer, preloadDemos } from "./DemoViewer"
1
+ export { DocstreamDemoContext, DocstreamDemoRuntimeContext, DocstreamDemoVFSContext, useDemoResolver, useDemoRuntime } from "./context"
2
+ export { DemoBlock, DemoFullscreen, DemoViewer, peekDemoSource, prefetchDemoSources, preloadDemos } from "./DemoViewer"
3
+ export {
4
+ DOCSTREAM_CLIPBOARD_TYPE,
5
+ collectDemoNodes,
6
+ createDocstreamClipboard,
7
+ createDocstreamClipboardFromMarkdown,
8
+ demoNodeMeta,
9
+ docstreamClipboardItems,
10
+ docstreamPayloadFromMarkdown,
11
+ hasDocstreamClipboard,
12
+ mapDemoNodes,
13
+ parseDocstreamClipboard,
14
+ readDocstreamClipboard,
15
+ readDocstreamClipboardItems,
16
+ writeDocstreamClipboard,
17
+ } from "./clipboard"
18
+ export type {
19
+ CreateDocstreamClipboardOptions,
20
+ DemoSourceSnapshot,
21
+ DocstreamClipboard,
22
+ DocstreamClipboardItem,
23
+ DocstreamClipboardPayload,
24
+ DocstreamDropItem,
25
+ } from "./clipboard"
26
+ export { CopyDemoButton, useDocstreamCopy } from "./copy-demo"
27
+ export type { CopyDemoButtonProps, DocstreamCopy } from "./copy-demo"
28
+ export { createDemoVFS, useDemoVFS, useDemoVFSContext, useDemoVFSEntry, useDemoVFSFiles } from "./vfs"
29
+ export type { DemoVFS, DemoVFSChange, DemoVFSEntry, DemoVFSResolverOptions } from "./vfs"
3
30
  export type { DemoFullscreenProps, DemoViewerProps } from "./DemoViewer"
4
31
  export {
5
32
  createGlobDemoResolver,
@@ -0,0 +1,197 @@
1
+ /* The demo virtual file system: demo files held in memory by `src`, for demos no host
2
+ resolver knows — typically ones pasted from another docstream page or editor.
3
+
4
+ - `DemoViewer` reads it from context (`DocstreamDemoVFSContext`, or a renderer's `demoVFS`):
5
+ a block with no inline files whose `src` is in the VFS shows the VFS files exactly like
6
+ inline files (the host resolver still wins when it knows `src`; the Preview runs through
7
+ the `demoRuntime`, else the Code view opens with the "needs runtime" note).
8
+ - `vfs.resolver({ resolver, runtime })` is the same thing as a `DemoResolver`, layered over
9
+ a host resolver: `files` / `meta` from the host when it knows `src`, else from the VFS;
10
+ `load` from the host, else a component that runs the VFS files through the runtime.
11
+ - One VFS may be shared by several editors / renderers; `subscribe` reports changes so a
12
+ host can persist pasted files. */
13
+ import { useContext, useRef, useSyncExternalStore } from "react"
14
+ import { DocstreamDemoVFSContext } from "./context"
15
+ import type { DemoComponentProps, DemoFile, DemoMeta, DemoResolver, InlineDemoRuntime } from "./types"
16
+
17
+ export interface DemoVFSEntry {
18
+ /** Entry first (or `meta.entry`), paths relative to the demo folder. */
19
+ files: DemoFile[]
20
+ /** `meta.json`-style defaults, usually the `{% demo %}` tag's attributes. */
21
+ meta?: DemoMeta
22
+ }
23
+
24
+ export interface DemoVFSChange {
25
+ src: string
26
+ /** The new entry; `undefined` when `src` was removed. */
27
+ entry: DemoVFSEntry | undefined
28
+ }
29
+
30
+ export interface DemoVFSResolverOptions {
31
+ /** The host resolver, consulted first. */
32
+ resolver?: DemoResolver
33
+ /** Runs VFS files for `load(src)`. Without one, `load` rejects for VFS-only demos. */
34
+ runtime?: InlineDemoRuntime
35
+ /** Extra npm dependencies handed to the runtime. */
36
+ dependencies?: Record<string, string>
37
+ }
38
+
39
+ export interface DemoVFS {
40
+ get(src: string): DemoVFSEntry | undefined
41
+ has(src: string): boolean
42
+ /** Every `src` held, in insertion order. */
43
+ list(): string[]
44
+ /** Adds or replaces `src`. A no-op (and no change event) when nothing differs. */
45
+ set(src: string, files: DemoFile[], meta?: DemoMeta): void
46
+ delete(src: string): boolean
47
+ clear(): void
48
+ /** Everything held, as `{ [src]: entry }`. The object is stable until the next change. */
49
+ snapshot(): Readonly<Record<string, DemoVFSEntry>>
50
+ /** Files only, as `{ [src]: files }` (what a host usually persists). Stable until the next change. */
51
+ files(): Readonly<Record<string, DemoFile[]>>
52
+ subscribe(listener: (change: DemoVFSChange) => void): () => void
53
+ /** This VFS as a `DemoResolver` layered over a host resolver. */
54
+ resolver(options?: DemoVFSResolverOptions): DemoResolver
55
+ }
56
+
57
+ const RUNTIME_NOTE = "Live preview needs the playground runtime. The code is shown instead."
58
+
59
+ function cleanFiles(files: DemoFile[]): DemoFile[] {
60
+ return files
61
+ .filter((f) => f && typeof f.path === "string" && typeof f.content === "string")
62
+ .map((f) => ({ path: f.path, content: f.content, ...(f.language ? { language: f.language } : {}) }))
63
+ }
64
+
65
+ function entryFirst(files: DemoFile[], entry: string | undefined): DemoFile[] {
66
+ const index = entry ? files.findIndex((f) => f.path === entry) : -1
67
+ return index > 0 ? [files[index], ...files.slice(0, index), ...files.slice(index + 1)] : files
68
+ }
69
+
70
+ const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b)
71
+
72
+ /** A new, empty (or pre-filled) demo VFS. */
73
+ export function createDemoVFS(initial?: Record<string, DemoFile[] | DemoVFSEntry>): DemoVFS {
74
+ const entries = new Map<string, DemoVFSEntry>()
75
+ const listeners = new Set<(change: DemoVFSChange) => void>()
76
+ let snap: Record<string, DemoVFSEntry> | null = null
77
+ let fileSnap: Record<string, DemoFile[]> | null = null
78
+
79
+ const put = (src: string, files: DemoFile[], meta?: DemoMeta) => {
80
+ const entry: DemoVFSEntry = { files: cleanFiles(files), ...(meta && Object.keys(meta).length ? { meta } : {}) }
81
+ const prev = entries.get(src)
82
+ if (prev && same(prev, entry)) return undefined
83
+ entries.set(src, entry)
84
+ return entry
85
+ }
86
+ const emit = (change: DemoVFSChange) => {
87
+ snap = null
88
+ fileSnap = null
89
+ for (const listener of Array.from(listeners)) listener(change)
90
+ }
91
+
92
+ for (const [src, value] of Object.entries(initial ?? {})) {
93
+ if (Array.isArray(value)) put(src, value)
94
+ else if (value) put(src, value.files, value.meta)
95
+ }
96
+
97
+ const vfs: DemoVFS = {
98
+ get: (src) => entries.get(src),
99
+ has: (src) => entries.has(src),
100
+ list: () => Array.from(entries.keys()),
101
+ set(src, files, meta) {
102
+ const entry = put(src, files, meta)
103
+ if (entry) emit({ src, entry })
104
+ },
105
+ delete(src) {
106
+ if (!entries.delete(src)) return false
107
+ emit({ src, entry: undefined })
108
+ return true
109
+ },
110
+ clear() {
111
+ for (const src of Array.from(entries.keys())) vfs.delete(src)
112
+ },
113
+ snapshot() {
114
+ if (!snap) snap = Object.fromEntries(entries)
115
+ return snap
116
+ },
117
+ files() {
118
+ if (!fileSnap) fileSnap = Object.fromEntries(Array.from(entries, ([src, e]) => [src, e.files]))
119
+ return fileSnap
120
+ },
121
+ subscribe(listener) {
122
+ listeners.add(listener)
123
+ return () => {
124
+ listeners.delete(listener)
125
+ }
126
+ },
127
+ resolver: (options) => layeredResolver(vfs, options ?? {}),
128
+ }
129
+ return vfs
130
+ }
131
+
132
+ function layeredResolver(vfs: DemoVFS, { resolver: host, runtime, dependencies }: DemoVFSResolverOptions): DemoResolver {
133
+ // Whether the host should be asked first: it lists `src`, or it can't list at all.
134
+ const hostFirst = (src: string) => !!host && (!host.list || host.list().includes(src) || !vfs.has(src))
135
+ const fromVfs = (src: string) => {
136
+ const entry = vfs.get(src)
137
+ if (!entry) throw new Error(`Unknown demo: ${src}`)
138
+ return entry
139
+ }
140
+ async function pick<T>(src: string, viaHost: (host: DemoResolver) => Promise<T>, viaVfs: (entry: DemoVFSEntry) => T): Promise<T> {
141
+ if (hostFirst(src)) {
142
+ try {
143
+ return await viaHost(host!)
144
+ } catch (error) {
145
+ if (!vfs.has(src)) throw error
146
+ }
147
+ }
148
+ return viaVfs(fromVfs(src))
149
+ }
150
+ const layered: DemoResolver = {
151
+ list: () => Array.from(new Set([...(host?.list?.() ?? []), ...vfs.list()])),
152
+ meta: (src) => pick(src, (h) => h.meta(src), (e) => e.meta),
153
+ files: (src) => pick(src, (h) => h.files(src), (e) => entryFirst(e.files, e.meta?.entry)),
154
+ load: (src) =>
155
+ pick(src, (h) => h.load(src), (e) => {
156
+ if (!runtime) throw new Error(RUNTIME_NOTE)
157
+ const files = entryFirst(e.files, e.meta?.entry)
158
+ const title = e.meta?.title ?? (src.split("/").pop() || "Demo")
159
+ const VfsDemo = ({ variant }: DemoComponentProps) =>
160
+ runtime({ src, title, files, entry: files[0]?.path ?? "index.tsx", ...(variant ? { variant } : {}), ...(dependencies ? { dependencies } : {}) })
161
+ VfsDemo.displayName = `VfsDemo(${src})`
162
+ return VfsDemo as never
163
+ }),
164
+ }
165
+ if (host?.href) layered.href = (src, options) => (hostFirst(src) ? host.href!(src, options) : undefined)
166
+ return layered
167
+ }
168
+
169
+ /**
170
+ * A demo VFS owned by the calling component (created once). Pass it as `demoVFS` to several
171
+ * editors / renderers to share pasted demo files between them.
172
+ */
173
+ export function useDemoVFS(initial?: Record<string, DemoFile[] | DemoVFSEntry>): DemoVFS {
174
+ const ref = useRef<DemoVFS | null>(null)
175
+ if (!ref.current) ref.current = createDemoVFS(initial)
176
+ return ref.current
177
+ }
178
+
179
+ /** The VFS in context (a renderer's or editor's `demoVFS`), unless `override` is given. */
180
+ export function useDemoVFSContext(override?: DemoVFS): DemoVFS | undefined {
181
+ const inherited = useContext(DocstreamDemoVFSContext)
182
+ return override ?? inherited
183
+ }
184
+
185
+ const EMPTY: Readonly<Record<string, DemoFile[]>> = Object.freeze({})
186
+ const noop = () => () => {}
187
+
188
+ /** `{ [src]: files }` of a VFS, re-rendering on change. */
189
+ export function useDemoVFSFiles(vfs: DemoVFS | undefined): Readonly<Record<string, DemoFile[]>> {
190
+ return useSyncExternalStore(vfs ? vfs.subscribe : noop, () => (vfs ? vfs.files() : EMPTY), () => (vfs ? vfs.files() : EMPTY))
191
+ }
192
+
193
+ /** One VFS entry, re-rendering when it changes. */
194
+ export function useDemoVFSEntry(vfs: DemoVFS | undefined, src: string): DemoVFSEntry | undefined {
195
+ const snapshot = useSyncExternalStore(vfs ? vfs.subscribe : noop, () => vfs?.snapshot(), () => vfs?.snapshot())
196
+ return src ? snapshot?.[src] : undefined
197
+ }
@@ -16,7 +16,8 @@ import { slugify } from "../gitbook/outline"
16
16
  import { resolveAsset } from "../assets"
17
17
  import { parseMarkdown } from "../gitbook/parse"
18
18
  import { serializeMarkdown } from "../gitbook/serialize"
19
- import { DocstreamDemoContext, DocstreamDemoRuntimeContext } from "../demo/context"
19
+ import { DocstreamDemoContext, DocstreamDemoRuntimeContext, DocstreamDemoVFSContext } from "../demo/context"
20
+ import type { DemoVFS } from "../demo/vfs"
20
21
  import { DemoBlock } from "../demo/DemoViewer"
21
22
  import type { DemoResolver, InlineDemoRuntime } from "../demo/types"
22
23
  import { CopyButton } from "./copy"
@@ -592,6 +593,11 @@ export interface DocRenderOptions {
592
593
  demoRuntime?: InlineDemoRuntime
593
594
  /** Extra npm dependencies inline demos may import (`{ "@brett_lamy/ui": "^1.2.0" }`). */
594
595
  demoDependencies?: Record<string, string>
596
+ /**
597
+ * Demo files held in memory by `src` (`createDemoVFS` / `useDemoVFS`), e.g. demos pasted
598
+ * into an editor. Blocks without inline files use them when no resolver knows `src`.
599
+ */
600
+ demoVFS?: DemoVFS
595
601
  /**
596
602
  * Render "Copy page ▾" (Copy / View as Markdown, Open in ChatGPT / Claude) above the content.
597
603
  * `true` uses the defaults; an object passes options to `DocPageActions`.
@@ -599,8 +605,9 @@ export interface DocRenderOptions {
599
605
  pageActions?: boolean | DocPageActionsOptions
600
606
  }
601
607
 
602
- function withDemoContext({ demoResolver, demoRuntime, demoDependencies }: DocRenderOptions, children: ReactNode) {
608
+ function withDemoContext({ demoResolver, demoRuntime, demoDependencies, demoVFS }: DocRenderOptions, children: ReactNode) {
603
609
  let out = children
610
+ if (demoVFS) out = <DocstreamDemoVFSContext.Provider value={demoVFS}>{out}</DocstreamDemoVFSContext.Provider>
604
611
  if (demoRuntime || demoDependencies) {
605
612
  out = <DemoRuntimeProvider runtime={demoRuntime} dependencies={demoDependencies}>{out}</DemoRuntimeProvider>
606
613
  }
@@ -640,12 +647,13 @@ export function MarkdownContent({
640
647
  demoResolver,
641
648
  demoRuntime,
642
649
  demoDependencies,
650
+ demoVFS,
643
651
  pageActions,
644
652
  className,
645
653
  }: { markdown: string; className?: string } & Renderers & DocRenderOptions) {
646
654
  const doc = useMemo(() => parseMarkdown(markdown), [markdown])
647
655
  return withDemoContext(
648
- { demoResolver, demoRuntime, demoDependencies },
656
+ { demoResolver, demoRuntime, demoDependencies, demoVFS },
649
657
  <div data-docstream="" className={className}>
650
658
  <PageActionsBar markdown={markdown} options={pageActions} />
651
659
  <Blocks blocks={doc.children} liveRenderer={liveRenderer} sourceRenderer={sourceRenderer} />
@@ -661,11 +669,12 @@ export function DocsRenderer({
661
669
  demoResolver,
662
670
  demoRuntime,
663
671
  demoDependencies,
672
+ demoVFS,
664
673
  pageActions,
665
674
  markdown,
666
675
  }: { doc: DocumentNode; /** Source for page actions; defaults to `serializeMarkdown(doc)`. */ markdown?: string } & Renderers & DocRenderOptions) {
667
676
  return withDemoContext(
668
- { demoResolver, demoRuntime, demoDependencies },
677
+ { demoResolver, demoRuntime, demoDependencies, demoVFS },
669
678
  <article className="docs-article">
670
679
  <PageActionsBar markdown={markdown ?? (() => serializeMarkdown(doc))} options={pageActions} />
671
680
  <Blocks blocks={doc.children} liveRenderer={liveRenderer} sourceRenderer={sourceRenderer} />
@@ -1,4 +1,4 @@
1
- import { useEffect, useId, useRef, useState, type CSSProperties } from "react"
1
+ import { useEffect, useId, useState, type CSSProperties } from "react"
2
2
  import { HighlightedCode } from "./HighlightedCode"
3
3
 
4
4
  let seq = 0
@@ -17,7 +17,6 @@ export function Mermaid({ code, collapsedCodeLines = 3, expandedCodeLines = 30 }
17
17
  const [svg, setSvg] = useState<string | null>(null)
18
18
  const [error, setError] = useState<string | null>(null)
19
19
  const [codeExpanded, setCodeExpanded] = useState(false)
20
- const idRef = useRef(`mermaid-${++seq}`)
21
20
  const codePanelId = useId()
22
21
  const collapsedLines = positiveLineCount(collapsedCodeLines, 3)
23
22
  const expandedLines = Math.max(collapsedLines, positiveLineCount(expandedCodeLines, 30))
@@ -27,7 +26,10 @@ export function Mermaid({ code, collapsedCodeLines = 3, expandedCodeLines = 30 }
27
26
  import("mermaid")
28
27
  .then(async ({ default: mermaid }) => {
29
28
  mermaid.initialize({ startOnLoad: false, securityLevel: "strict" })
30
- const { svg } = await mermaid.render(idRef.current, code)
29
+ // A fresh id per render: mermaid.render removes any element with the id it is given,
30
+ // so reusing one (StrictMode's double effect, a code change) deleted the diagram an
31
+ // earlier render had already mounted.
32
+ const { svg } = await mermaid.render(`mermaid-${++seq}`, code)
31
33
  if (!cancelled) setSvg(svg)
32
34
  })
33
35
  .catch((e) => {