@brett_lamy/docstream-editor 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -46
- package/package.json +6 -2
- package/src/editor/convert.ts +23 -4
- package/src/editor/nodes.tsx +29 -2
package/README.md
CHANGED
|
@@ -11,8 +11,6 @@ TipTap editor components for Docstream GitBook-style markdown documents.
|
|
|
11
11
|
- Conversion helpers between the Docstream AST and TipTap JSON.
|
|
12
12
|
- Slash menu for inserting supported blocks.
|
|
13
13
|
- Code block support through `lowlight`.
|
|
14
|
-
- Live React/JSX/TSX code blocks backed by almost-node.
|
|
15
|
-
- `ReactDemoEditor` for editing and running a complete multi-file project.
|
|
16
14
|
- Table, task list, heading, quote, and inline formatting support.
|
|
17
15
|
- Preserves Docstream/GitBook block semantics when converting back to markdown.
|
|
18
16
|
|
|
@@ -102,53 +100,9 @@ The editor supports common ProseMirror/TipTap content plus GitBook-flavored bloc
|
|
|
102
100
|
- Columns
|
|
103
101
|
- Figures and images
|
|
104
102
|
- OpenAPI operation placeholders
|
|
105
|
-
- Video and public Replay QA previews inside embed blocks
|
|
106
103
|
|
|
107
104
|
Some blocks are intentionally represented as structured nodes rather than fully bespoke editing controls. They are preserved through parse, edit, and serialize flows so the document can continue to round-trip as GitBook-style markdown.
|
|
108
105
|
|
|
109
|
-
### Video and Replay QA embeds
|
|
110
|
-
|
|
111
|
-
Insert an embed block from the slash menu, then enter a direct video URL or a
|
|
112
|
-
public Loop QA project URL. The editor renders video assets through Docstream's
|
|
113
|
-
`VideoEmbed` and converts Loop QA project paths to their chrome-free public
|
|
114
|
-
preview route. The serialized markdown remains portable:
|
|
115
|
-
|
|
116
|
-
```md
|
|
117
|
-
{% embed url="https://loop-qa.replay.io/projects/project-id/tasks/task-id" /%}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### Live React code
|
|
121
|
-
|
|
122
|
-
Code blocks can opt into a live preview by setting `live="true"`. The block
|
|
123
|
-
must be a complete entry module (including its `createRoot` call); ordinary
|
|
124
|
-
code snippets remain read-only:
|
|
125
|
-
|
|
126
|
-
````md
|
|
127
|
-
{% code language="jsx" live="true" entry="/src/main.jsx" %}
|
|
128
|
-
```jsx
|
|
129
|
-
import { createRoot } from "react-dom/client"
|
|
130
|
-
createRoot(document.getElementById("root")).render(<h1>Hello</h1>)
|
|
131
|
-
```
|
|
132
|
-
{% endcode %}
|
|
133
|
-
````
|
|
134
|
-
|
|
135
|
-
For multi-file demos, use the standalone editor component:
|
|
136
|
-
|
|
137
|
-
```tsx
|
|
138
|
-
import { ReactDemoEditor } from "@brett_lamy/docstream-editor"
|
|
139
|
-
|
|
140
|
-
<ReactDemoEditor
|
|
141
|
-
files={{
|
|
142
|
-
"/src/main.jsx": mainSource,
|
|
143
|
-
"/src/App.jsx": appSource,
|
|
144
|
-
}}
|
|
145
|
-
entry="/src/main.jsx"
|
|
146
|
-
/>
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
The host app must install `@agent-wasm/core` and expose its service worker with
|
|
150
|
-
`almostnodePlugin()` from `@agent-wasm/core/vite`.
|
|
151
|
-
|
|
152
106
|
## Slash Menu
|
|
153
107
|
|
|
154
108
|
The editor includes a slash menu extension for inserting supported block structures. Type `/` in an empty paragraph to open block insertion options.
|
|
@@ -218,3 +172,9 @@ import { GitbookStreamdown } from "@brett_lamy/docstream"
|
|
|
218
172
|
|
|
219
173
|
<GitbookStreamdown markdown={markdown} isStreaming={false} />
|
|
220
174
|
```
|
|
175
|
+
|
|
176
|
+
Embed nodes preserve the Docstream media attributes (`title`, `poster`,
|
|
177
|
+
`autoplay`, `loop`, `muted`, and `controls`) when converting between the
|
|
178
|
+
editable TipTap document and GitBook markdown. This makes short, muted,
|
|
179
|
+
autoplaying clips round-trip through the editor without losing their playback
|
|
180
|
+
contract.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brett_lamy/docstream-editor",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "TipTap editor for Docstream GitBook-style markdown documents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"./styles.css": "./src/styles.css"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@brett_lamy/docstream": "0.3.
|
|
23
|
+
"@brett_lamy/docstream": "0.3.2",
|
|
24
24
|
"lowlight": "^3.3.0",
|
|
25
25
|
"lucide-react": "^1.17.0"
|
|
26
26
|
},
|
|
@@ -35,5 +35,9 @@
|
|
|
35
35
|
"@tiptap/starter-kit": "^3.26.1",
|
|
36
36
|
"@tiptap/suggestion": "^3.26.1",
|
|
37
37
|
"react": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/BLamy/docstream-editor.git"
|
|
38
42
|
}
|
|
39
43
|
}
|
package/src/editor/convert.ts
CHANGED
|
@@ -98,8 +98,16 @@ function blockToPM(b: Block): PMNode {
|
|
|
98
98
|
content: blocksPM(s.children),
|
|
99
99
|
})),
|
|
100
100
|
}
|
|
101
|
-
case "embed":
|
|
102
|
-
|
|
101
|
+
case "embed": {
|
|
102
|
+
const attrs: Record<string, string | boolean> = { url: b.url }
|
|
103
|
+
if (b.title !== undefined) attrs.title = b.title
|
|
104
|
+
if (b.poster !== undefined) attrs.poster = b.poster
|
|
105
|
+
if (b.autoplay !== undefined) attrs.autoplay = b.autoplay
|
|
106
|
+
if (b.loop !== undefined) attrs.loop = b.loop
|
|
107
|
+
if (b.muted !== undefined) attrs.muted = b.muted
|
|
108
|
+
if (b.controls !== undefined) attrs.controls = b.controls
|
|
109
|
+
return { type: "gbEmbed", attrs }
|
|
110
|
+
}
|
|
103
111
|
case "content-ref":
|
|
104
112
|
return { type: "gbContentRef", attrs: { url: b.url, label: plainText(b.children) } }
|
|
105
113
|
case "columns":
|
|
@@ -255,8 +263,19 @@ function pmToBlock(n: PMNode): Block | null {
|
|
|
255
263
|
children: pmToBlocks(s.content),
|
|
256
264
|
})),
|
|
257
265
|
}
|
|
258
|
-
case "gbEmbed":
|
|
259
|
-
|
|
266
|
+
case "gbEmbed": {
|
|
267
|
+
const embed: Extract<Block, { type: "embed" }> = {
|
|
268
|
+
type: "embed",
|
|
269
|
+
url: String(n.attrs?.url ?? ""),
|
|
270
|
+
}
|
|
271
|
+
if (typeof n.attrs?.title === "string") embed.title = n.attrs.title
|
|
272
|
+
if (typeof n.attrs?.poster === "string") embed.poster = n.attrs.poster
|
|
273
|
+
if (typeof n.attrs?.autoplay === "boolean") embed.autoplay = n.attrs.autoplay
|
|
274
|
+
if (typeof n.attrs?.loop === "boolean") embed.loop = n.attrs.loop
|
|
275
|
+
if (typeof n.attrs?.muted === "boolean") embed.muted = n.attrs.muted
|
|
276
|
+
if (typeof n.attrs?.controls === "boolean") embed.controls = n.attrs.controls
|
|
277
|
+
return embed
|
|
278
|
+
}
|
|
260
279
|
case "gbContentRef":
|
|
261
280
|
return {
|
|
262
281
|
type: "content-ref",
|
package/src/editor/nodes.tsx
CHANGED
|
@@ -306,15 +306,34 @@ function isDirectVideo(url: string): boolean {
|
|
|
306
306
|
return /\.(?:mp4|webm|ogg)(?:[?#]|$)/i.test(url)
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
function booleanAttribute(value: unknown): boolean | undefined {
|
|
310
|
+
return typeof value === "boolean" ? value : undefined
|
|
311
|
+
}
|
|
312
|
+
|
|
309
313
|
function EmbedView({ node, updateAttributes, editor }: NodeViewProps) {
|
|
310
314
|
const url = node.attrs.url as string
|
|
311
315
|
const preview = embedPreview(url)
|
|
316
|
+
const title = typeof node.attrs.title === "string" && node.attrs.title ? node.attrs.title : url
|
|
317
|
+
const poster = typeof node.attrs.poster === "string" ? node.attrs.poster : undefined
|
|
318
|
+
const autoplay = booleanAttribute(node.attrs.autoplay)
|
|
319
|
+
const loop = booleanAttribute(node.attrs.loop)
|
|
320
|
+
const muted = booleanAttribute(node.attrs.muted)
|
|
321
|
+
const controls = booleanAttribute(node.attrs.controls)
|
|
312
322
|
return (
|
|
313
323
|
<NodeViewWrapper className="gb-embed" contentEditable={false}>
|
|
314
324
|
{isReplayQaUrl(url) ? (
|
|
315
325
|
<ReplayPreview source={url} title="Replay preview" />
|
|
316
326
|
) : preview || isDirectVideo(url) ? (
|
|
317
|
-
<VideoEmbed
|
|
327
|
+
<VideoEmbed
|
|
328
|
+
src={preview ?? url}
|
|
329
|
+
className="gb-embed-frame"
|
|
330
|
+
title={title}
|
|
331
|
+
{...(autoplay === undefined ? {} : { autoplay })}
|
|
332
|
+
{...(loop === undefined ? {} : { loop })}
|
|
333
|
+
{...(muted === undefined ? {} : { muted })}
|
|
334
|
+
{...(controls === undefined ? {} : { controls })}
|
|
335
|
+
{...(poster === undefined ? {} : { poster })}
|
|
336
|
+
/>
|
|
318
337
|
) : (
|
|
319
338
|
<a className="gb-embed-link" href={url} target="_blank" rel="noreferrer">
|
|
320
339
|
<Link2 className="size-4" /> {url || "Embed URL…"}
|
|
@@ -337,7 +356,15 @@ export const GbEmbed = Node.create({
|
|
|
337
356
|
group: "block",
|
|
338
357
|
atom: true,
|
|
339
358
|
addAttributes() {
|
|
340
|
-
return {
|
|
359
|
+
return {
|
|
360
|
+
url: { default: "" },
|
|
361
|
+
title: { default: null },
|
|
362
|
+
autoplay: { default: null },
|
|
363
|
+
loop: { default: null },
|
|
364
|
+
muted: { default: null },
|
|
365
|
+
controls: { default: null },
|
|
366
|
+
poster: { default: null },
|
|
367
|
+
}
|
|
341
368
|
},
|
|
342
369
|
parseHTML() {
|
|
343
370
|
return [{ tag: "div[data-gb-embed]" }]
|