@brett_lamy/docstream-editor 0.1.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/package.json +37 -0
- package/src/editor/Editor.tsx +135 -0
- package/src/editor/convert.ts +333 -0
- package/src/editor/nodes.tsx +746 -0
- package/src/editor/slash-menu.tsx +309 -0
- package/src/index.ts +4 -0
- package/src/styles.css +1 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brett_lamy/docstream-editor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TipTap editor for Docstream GitBook-style markdown documents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"sideEffects": [
|
|
12
|
+
"**/*.css"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./src/index.ts",
|
|
17
|
+
"import": "./src/index.ts"
|
|
18
|
+
},
|
|
19
|
+
"./styles.css": "./src/styles.css"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@tiptap/core": "^3.26.1",
|
|
23
|
+
"@tiptap/extension-code-block-lowlight": "^3.26.1",
|
|
24
|
+
"@tiptap/extension-list": "^3.26.1",
|
|
25
|
+
"@tiptap/extension-placeholder": "^3.26.1",
|
|
26
|
+
"@tiptap/extension-table": "^3.26.1",
|
|
27
|
+
"@tiptap/react": "^3.26.1",
|
|
28
|
+
"@tiptap/starter-kit": "^3.26.1",
|
|
29
|
+
"@tiptap/suggestion": "^3.26.1",
|
|
30
|
+
"@brett_lamy/docstream": "0.1.0",
|
|
31
|
+
"lowlight": "^3.3.0",
|
|
32
|
+
"lucide-react": "^1.17.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": ">=18"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react"
|
|
2
|
+
import { EditorContent, useEditor, type Editor as TiptapEditor } from "@tiptap/react"
|
|
3
|
+
import StarterKit from "@tiptap/starter-kit"
|
|
4
|
+
import Placeholder from "@tiptap/extension-placeholder"
|
|
5
|
+
import { TaskItem, TaskList } from "@tiptap/extension-list"
|
|
6
|
+
import { Table, TableCell, TableHeader, TableRow } from "@tiptap/extension-table"
|
|
7
|
+
import {
|
|
8
|
+
Bold,
|
|
9
|
+
Code as CodeIcon,
|
|
10
|
+
Italic,
|
|
11
|
+
Link2,
|
|
12
|
+
SlashSquare,
|
|
13
|
+
Strikethrough,
|
|
14
|
+
} from "lucide-react"
|
|
15
|
+
|
|
16
|
+
import { parseMarkdown, serializeMarkdown } from "@brett_lamy/docstream"
|
|
17
|
+
import { astToTiptap, tiptapToAst, type PMNode } from "./convert"
|
|
18
|
+
import { GbCodeBlock, gitbookNodes } from "./nodes"
|
|
19
|
+
import { SlashMenu } from "./slash-menu"
|
|
20
|
+
|
|
21
|
+
export interface GitbookEditorProps {
|
|
22
|
+
markdown: string
|
|
23
|
+
onChange: (markdown: string) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Carries GitBook's data-view (e.g. "cards") through the editor untouched.
|
|
27
|
+
const GbTable = Table.extend({
|
|
28
|
+
addAttributes() {
|
|
29
|
+
return { ...this.parent?.(), view: { default: null } }
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
function ToolbarButton({
|
|
34
|
+
onClick,
|
|
35
|
+
active,
|
|
36
|
+
title,
|
|
37
|
+
children,
|
|
38
|
+
}: {
|
|
39
|
+
onClick: () => void
|
|
40
|
+
active?: boolean
|
|
41
|
+
title: string
|
|
42
|
+
children: React.ReactNode
|
|
43
|
+
}) {
|
|
44
|
+
return (
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
title={title}
|
|
48
|
+
className={`gb-tool ${active ? "gb-tool-active" : ""}`}
|
|
49
|
+
onMouseDown={(e) => {
|
|
50
|
+
e.preventDefault()
|
|
51
|
+
onClick()
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</button>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Toolbar({ editor }: { editor: TiptapEditor }) {
|
|
60
|
+
const chain = () => editor.chain().focus()
|
|
61
|
+
return (
|
|
62
|
+
<div className="gb-toolbar">
|
|
63
|
+
<ToolbarButton title="Bold" active={editor.isActive("bold")} onClick={() => chain().toggleBold().run()}>
|
|
64
|
+
<Bold className="size-4" />
|
|
65
|
+
</ToolbarButton>
|
|
66
|
+
<ToolbarButton title="Italic" active={editor.isActive("italic")} onClick={() => chain().toggleItalic().run()}>
|
|
67
|
+
<Italic className="size-4" />
|
|
68
|
+
</ToolbarButton>
|
|
69
|
+
<ToolbarButton title="Strike" active={editor.isActive("strike")} onClick={() => chain().toggleStrike().run()}>
|
|
70
|
+
<Strikethrough className="size-4" />
|
|
71
|
+
</ToolbarButton>
|
|
72
|
+
<ToolbarButton title="Inline code" active={editor.isActive("code")} onClick={() => chain().toggleCode().run()}>
|
|
73
|
+
<CodeIcon className="size-4" />
|
|
74
|
+
</ToolbarButton>
|
|
75
|
+
<ToolbarButton
|
|
76
|
+
title="Link"
|
|
77
|
+
active={editor.isActive("link")}
|
|
78
|
+
onClick={() => {
|
|
79
|
+
const url = window.prompt("URL")
|
|
80
|
+
if (url) chain().setLink({ href: url }).run()
|
|
81
|
+
else chain().unsetLink().run()
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<Link2 className="size-4" />
|
|
85
|
+
</ToolbarButton>
|
|
86
|
+
<span className="gb-toolbar-hint">
|
|
87
|
+
<SlashSquare className="size-3.5" /> Type <kbd>/</kbd> for blocks
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function GitbookEditor({ markdown, onChange }: GitbookEditorProps) {
|
|
94
|
+
// Tracks the markdown the editor itself produced, so external updates
|
|
95
|
+
// (file switches) reset content but our own onChange echoes don't.
|
|
96
|
+
const lastEmitted = useRef<string | null>(null)
|
|
97
|
+
|
|
98
|
+
const editor = useEditor({
|
|
99
|
+
extensions: [
|
|
100
|
+
StarterKit.configure({ codeBlock: false }),
|
|
101
|
+
GbCodeBlock,
|
|
102
|
+
TaskList,
|
|
103
|
+
TaskItem.configure({ nested: true }),
|
|
104
|
+
GbTable.configure({ resizable: false }),
|
|
105
|
+
TableRow,
|
|
106
|
+
TableHeader,
|
|
107
|
+
TableCell,
|
|
108
|
+
Placeholder.configure({ placeholder: "Write, or type / to insert a block…" }),
|
|
109
|
+
SlashMenu,
|
|
110
|
+
...gitbookNodes,
|
|
111
|
+
],
|
|
112
|
+
content: astToTiptap(parseMarkdown(markdown)),
|
|
113
|
+
onUpdate({ editor }) {
|
|
114
|
+
const md = serializeMarkdown(tiptapToAst(editor.getJSON() as PMNode))
|
|
115
|
+
lastEmitted.current = md
|
|
116
|
+
onChange(md)
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!editor) return
|
|
122
|
+
if (markdown === lastEmitted.current) return
|
|
123
|
+
lastEmitted.current = markdown
|
|
124
|
+
editor.commands.setContent(astToTiptap(parseMarkdown(markdown)), { emitUpdate: false })
|
|
125
|
+
}, [editor, markdown])
|
|
126
|
+
|
|
127
|
+
if (!editor) return null
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className="gb">
|
|
131
|
+
<Toolbar editor={editor} />
|
|
132
|
+
<EditorContent editor={editor} className="gb-content" />
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { plainText, type Block, type DocumentNode, type Inline, type ListItemNode } from "@brett_lamy/docstream"
|
|
2
|
+
|
|
3
|
+
// TipTap/ProseMirror JSON shape (loosely typed on purpose)
|
|
4
|
+
export interface PMNode {
|
|
5
|
+
type: string
|
|
6
|
+
attrs?: Record<string, unknown>
|
|
7
|
+
content?: PMNode[]
|
|
8
|
+
marks?: Array<{ type: string; attrs?: Record<string, unknown> }>
|
|
9
|
+
text?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ---------- AST → TipTap ----------
|
|
13
|
+
|
|
14
|
+
function inlineToPM(nodes: Inline[]): PMNode[] {
|
|
15
|
+
return nodes
|
|
16
|
+
.filter((n) => n.type === "image" || n.text.length > 0)
|
|
17
|
+
.map((n) => {
|
|
18
|
+
if (n.type === "image") {
|
|
19
|
+
return {
|
|
20
|
+
type: "gbInlineImage",
|
|
21
|
+
attrs: {
|
|
22
|
+
src: n.src,
|
|
23
|
+
alt: n.alt ?? "",
|
|
24
|
+
width: n.width ?? "",
|
|
25
|
+
height: n.height ?? "",
|
|
26
|
+
link: n.link ?? "",
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const marks: PMNode["marks"] = []
|
|
31
|
+
if (n.bold) marks.push({ type: "bold" })
|
|
32
|
+
if (n.italic) marks.push({ type: "italic" })
|
|
33
|
+
if (n.strike) marks.push({ type: "strike" })
|
|
34
|
+
if (n.code) marks.push({ type: "code" })
|
|
35
|
+
if (n.link) marks.push({ type: "link", attrs: { href: n.link } })
|
|
36
|
+
return { type: "text", text: n.text, ...(marks.length ? { marks } : {}) }
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const paragraphPM = (children: Inline[]): PMNode => {
|
|
41
|
+
const content = inlineToPM(children)
|
|
42
|
+
return content.length ? { type: "paragraph", content } : { type: "paragraph" }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Containers require block+ content; empty GitBook blocks get a placeholder paragraph.
|
|
46
|
+
const blocksPM = (children: Block[]): PMNode[] =>
|
|
47
|
+
children.length ? children.map(blockToPM) : [{ type: "paragraph" }]
|
|
48
|
+
|
|
49
|
+
function listItemToPM(item: ListItemNode, task: boolean): PMNode {
|
|
50
|
+
return {
|
|
51
|
+
type: task ? "taskItem" : "listItem",
|
|
52
|
+
...(task ? { attrs: { checked: !!item.checked } } : {}),
|
|
53
|
+
content: item.children.map(blockToPM),
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function blockToPM(b: Block): PMNode {
|
|
58
|
+
switch (b.type) {
|
|
59
|
+
case "paragraph":
|
|
60
|
+
return paragraphPM(b.children)
|
|
61
|
+
case "heading":
|
|
62
|
+
return { type: "heading", attrs: { level: b.level }, content: inlineToPM(b.children) }
|
|
63
|
+
case "code":
|
|
64
|
+
return {
|
|
65
|
+
type: "codeBlock",
|
|
66
|
+
attrs: { language: b.language, title: b.title, lineNumbers: b.lineNumbers },
|
|
67
|
+
content: b.code ? [{ type: "text", text: b.code }] : [],
|
|
68
|
+
}
|
|
69
|
+
case "hint":
|
|
70
|
+
return { type: "gbHint", attrs: { style: b.style }, content: blocksPM(b.children) }
|
|
71
|
+
case "tabs":
|
|
72
|
+
return {
|
|
73
|
+
type: "gbTabs",
|
|
74
|
+
content: b.tabs.map((t) => ({
|
|
75
|
+
type: "gbTab",
|
|
76
|
+
attrs: { title: t.title },
|
|
77
|
+
content: blocksPM(t.children),
|
|
78
|
+
})),
|
|
79
|
+
}
|
|
80
|
+
case "expandable":
|
|
81
|
+
return {
|
|
82
|
+
type: "gbExpandable",
|
|
83
|
+
attrs: { summary: b.summary },
|
|
84
|
+
content: blocksPM(b.children),
|
|
85
|
+
}
|
|
86
|
+
case "stepper":
|
|
87
|
+
return {
|
|
88
|
+
type: "gbStepper",
|
|
89
|
+
content: b.steps.map((s) => ({
|
|
90
|
+
type: "gbStep",
|
|
91
|
+
attrs: { title: s.title },
|
|
92
|
+
content: blocksPM(s.children),
|
|
93
|
+
})),
|
|
94
|
+
}
|
|
95
|
+
case "embed":
|
|
96
|
+
return { type: "gbEmbed", attrs: { url: b.url } }
|
|
97
|
+
case "content-ref":
|
|
98
|
+
return { type: "gbContentRef", attrs: { url: b.url, label: plainText(b.children) } }
|
|
99
|
+
case "columns":
|
|
100
|
+
return {
|
|
101
|
+
type: "gbColumns",
|
|
102
|
+
content: b.columns.map((c) => ({ type: "gbColumn", content: blocksPM(c.children) })),
|
|
103
|
+
}
|
|
104
|
+
case "figure":
|
|
105
|
+
return { type: "gbFigure", attrs: { src: b.src, alt: b.alt, caption: b.caption } }
|
|
106
|
+
case "list":
|
|
107
|
+
return {
|
|
108
|
+
type: b.task ? "taskList" : b.ordered ? "orderedList" : "bulletList",
|
|
109
|
+
content: b.items.map((item) => listItemToPM(item, b.task)),
|
|
110
|
+
}
|
|
111
|
+
case "blockquote":
|
|
112
|
+
return { type: "blockquote", content: blocksPM(b.children) }
|
|
113
|
+
case "divider":
|
|
114
|
+
return { type: "horizontalRule" }
|
|
115
|
+
case "table":
|
|
116
|
+
return {
|
|
117
|
+
type: "table",
|
|
118
|
+
...(b.view ? { attrs: { view: b.view } } : {}),
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: "tableRow",
|
|
122
|
+
content: b.header.map((cell) => ({
|
|
123
|
+
type: "tableHeader",
|
|
124
|
+
content: [paragraphPM(cell)],
|
|
125
|
+
})),
|
|
126
|
+
},
|
|
127
|
+
...b.rows.map((row) => ({
|
|
128
|
+
type: "tableRow",
|
|
129
|
+
content: row.map((cell) => ({ type: "tableCell", content: [paragraphPM(cell)] })),
|
|
130
|
+
})),
|
|
131
|
+
],
|
|
132
|
+
}
|
|
133
|
+
case "math":
|
|
134
|
+
return { type: "gbMath", attrs: { formula: b.formula } }
|
|
135
|
+
case "updates":
|
|
136
|
+
return {
|
|
137
|
+
type: "gbUpdates",
|
|
138
|
+
attrs: { format: b.format },
|
|
139
|
+
content: b.updates.map((u) => ({
|
|
140
|
+
type: "gbUpdate",
|
|
141
|
+
attrs: { date: u.date },
|
|
142
|
+
content: blocksPM(u.children),
|
|
143
|
+
})),
|
|
144
|
+
}
|
|
145
|
+
case "openapi-operation":
|
|
146
|
+
return {
|
|
147
|
+
type: "gbOpenapi",
|
|
148
|
+
attrs: {
|
|
149
|
+
spec: b.spec,
|
|
150
|
+
path: b.path,
|
|
151
|
+
method: b.method,
|
|
152
|
+
specUrl: b.specUrl,
|
|
153
|
+
label: b.label,
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function astToTiptap(doc: DocumentNode): PMNode {
|
|
160
|
+
const content = doc.children.map(blockToPM)
|
|
161
|
+
return { type: "doc", content: content.length ? content : [{ type: "paragraph" }] }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------- TipTap → AST ----------
|
|
165
|
+
|
|
166
|
+
function pmTextToInline(nodes: PMNode[] | undefined): Inline[] {
|
|
167
|
+
if (!nodes) return []
|
|
168
|
+
return nodes
|
|
169
|
+
.filter((n) => (n.type === "text" && n.text) || n.type === "gbInlineImage")
|
|
170
|
+
.map((n): Inline => {
|
|
171
|
+
if (n.type === "gbInlineImage") {
|
|
172
|
+
const a = n.attrs ?? {}
|
|
173
|
+
return {
|
|
174
|
+
type: "image",
|
|
175
|
+
src: String(a.src ?? ""),
|
|
176
|
+
...(a.alt ? { alt: String(a.alt) } : {}),
|
|
177
|
+
...(a.width ? { width: String(a.width) } : {}),
|
|
178
|
+
...(a.height ? { height: String(a.height) } : {}),
|
|
179
|
+
...(a.link ? { link: String(a.link) } : {}),
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const inline: Inline = { type: "text", text: n.text! }
|
|
183
|
+
for (const mark of n.marks ?? []) {
|
|
184
|
+
if (mark.type === "bold") inline.bold = true
|
|
185
|
+
if (mark.type === "italic") inline.italic = true
|
|
186
|
+
if (mark.type === "strike") inline.strike = true
|
|
187
|
+
if (mark.type === "code") inline.code = true
|
|
188
|
+
if (mark.type === "link") inline.link = String(mark.attrs?.href ?? "")
|
|
189
|
+
}
|
|
190
|
+
return inline
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function pmCellToInline(cell: PMNode): Inline[] {
|
|
195
|
+
// table cells contain paragraphs; flatten the first one
|
|
196
|
+
return pmTextToInline(cell.content?.[0]?.content)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function pmToBlock(n: PMNode): Block | null {
|
|
200
|
+
switch (n.type) {
|
|
201
|
+
case "paragraph": {
|
|
202
|
+
const children = pmTextToInline(n.content)
|
|
203
|
+
if (!children.length) return null // drop empty paragraphs from markdown
|
|
204
|
+
return { type: "paragraph", children }
|
|
205
|
+
}
|
|
206
|
+
case "heading":
|
|
207
|
+
return {
|
|
208
|
+
type: "heading",
|
|
209
|
+
level: (Number(n.attrs?.level) || 1) as 1 | 2 | 3 | 4 | 5 | 6,
|
|
210
|
+
children: pmTextToInline(n.content),
|
|
211
|
+
}
|
|
212
|
+
case "codeBlock":
|
|
213
|
+
return {
|
|
214
|
+
type: "code",
|
|
215
|
+
language: (n.attrs?.language as string) || null,
|
|
216
|
+
title: (n.attrs?.title as string) || null,
|
|
217
|
+
lineNumbers: !!n.attrs?.lineNumbers,
|
|
218
|
+
code: n.content?.map((c) => c.text ?? "").join("") ?? "",
|
|
219
|
+
}
|
|
220
|
+
case "gbHint":
|
|
221
|
+
return {
|
|
222
|
+
type: "hint",
|
|
223
|
+
style: (n.attrs?.style as never) ?? "info",
|
|
224
|
+
children: pmToBlocks(n.content),
|
|
225
|
+
}
|
|
226
|
+
case "gbTabs":
|
|
227
|
+
return {
|
|
228
|
+
type: "tabs",
|
|
229
|
+
tabs: (n.content ?? []).map((t) => ({
|
|
230
|
+
type: "tab",
|
|
231
|
+
title: String(t.attrs?.title ?? "Tab"),
|
|
232
|
+
children: pmToBlocks(t.content),
|
|
233
|
+
})),
|
|
234
|
+
}
|
|
235
|
+
case "gbExpandable":
|
|
236
|
+
return {
|
|
237
|
+
type: "expandable",
|
|
238
|
+
summary: String(n.attrs?.summary ?? ""),
|
|
239
|
+
children: pmToBlocks(n.content),
|
|
240
|
+
}
|
|
241
|
+
case "gbStepper":
|
|
242
|
+
return {
|
|
243
|
+
type: "stepper",
|
|
244
|
+
steps: (n.content ?? []).map((s) => ({
|
|
245
|
+
type: "step",
|
|
246
|
+
title: String(s.attrs?.title ?? ""),
|
|
247
|
+
children: pmToBlocks(s.content),
|
|
248
|
+
})),
|
|
249
|
+
}
|
|
250
|
+
case "gbEmbed":
|
|
251
|
+
return { type: "embed", url: String(n.attrs?.url ?? "") }
|
|
252
|
+
case "gbContentRef":
|
|
253
|
+
return {
|
|
254
|
+
type: "content-ref",
|
|
255
|
+
url: String(n.attrs?.url ?? ""),
|
|
256
|
+
children: [{ type: "text", text: String(n.attrs?.label ?? "") }],
|
|
257
|
+
}
|
|
258
|
+
case "gbColumns":
|
|
259
|
+
return {
|
|
260
|
+
type: "columns",
|
|
261
|
+
columns: (n.content ?? []).map((c) => ({ type: "column", children: pmToBlocks(c.content) })),
|
|
262
|
+
}
|
|
263
|
+
case "gbFigure":
|
|
264
|
+
return {
|
|
265
|
+
type: "figure",
|
|
266
|
+
src: String(n.attrs?.src ?? ""),
|
|
267
|
+
alt: String(n.attrs?.alt ?? ""),
|
|
268
|
+
caption: String(n.attrs?.caption ?? ""),
|
|
269
|
+
}
|
|
270
|
+
case "gbMath":
|
|
271
|
+
return { type: "math", formula: String(n.attrs?.formula ?? "") }
|
|
272
|
+
case "gbUpdates":
|
|
273
|
+
return {
|
|
274
|
+
type: "updates",
|
|
275
|
+
format: (n.attrs?.format as string) || null,
|
|
276
|
+
updates: (n.content ?? []).map((u) => ({
|
|
277
|
+
type: "update",
|
|
278
|
+
date: String(u.attrs?.date ?? ""),
|
|
279
|
+
children: pmToBlocks(u.content),
|
|
280
|
+
})),
|
|
281
|
+
}
|
|
282
|
+
case "gbOpenapi":
|
|
283
|
+
return {
|
|
284
|
+
type: "openapi-operation",
|
|
285
|
+
spec: String(n.attrs?.spec ?? ""),
|
|
286
|
+
path: String(n.attrs?.path ?? ""),
|
|
287
|
+
method: String(n.attrs?.method ?? ""),
|
|
288
|
+
specUrl: String(n.attrs?.specUrl ?? ""),
|
|
289
|
+
label: String(n.attrs?.label ?? ""),
|
|
290
|
+
}
|
|
291
|
+
case "bulletList":
|
|
292
|
+
case "orderedList":
|
|
293
|
+
case "taskList": {
|
|
294
|
+
const task = n.type === "taskList"
|
|
295
|
+
return {
|
|
296
|
+
type: "list",
|
|
297
|
+
ordered: n.type === "orderedList",
|
|
298
|
+
task,
|
|
299
|
+
items: (n.content ?? []).map((item) => {
|
|
300
|
+
const base: ListItemNode = { type: "listItem", children: pmToBlocks(item.content) }
|
|
301
|
+
if (task) base.checked = !!item.attrs?.checked
|
|
302
|
+
if (!base.children.length) base.children = [{ type: "paragraph", children: [] }]
|
|
303
|
+
return base
|
|
304
|
+
}),
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
case "blockquote":
|
|
308
|
+
return { type: "blockquote", children: pmToBlocks(n.content) }
|
|
309
|
+
case "horizontalRule":
|
|
310
|
+
return { type: "divider" }
|
|
311
|
+
case "table": {
|
|
312
|
+
const rows = n.content ?? []
|
|
313
|
+
const [head, ...body] = rows
|
|
314
|
+
const view = n.attrs?.view as string | undefined
|
|
315
|
+
return {
|
|
316
|
+
type: "table",
|
|
317
|
+
header: (head?.content ?? []).map(pmCellToInline),
|
|
318
|
+
rows: body.map((r) => (r.content ?? []).map(pmCellToInline)),
|
|
319
|
+
...(view ? { view } : {}),
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
default:
|
|
323
|
+
return null
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function pmToBlocks(nodes: PMNode[] | undefined): Block[] {
|
|
328
|
+
return (nodes ?? []).map(pmToBlock).filter((b): b is Block => b !== null)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function tiptapToAst(doc: PMNode): DocumentNode {
|
|
332
|
+
return { type: "doc", children: pmToBlocks(doc.content) }
|
|
333
|
+
}
|