@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
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from "@tiptap/core"
|
|
2
|
+
import { CodeBlockLowlight } from "@tiptap/extension-code-block-lowlight"
|
|
3
|
+
import { common, createLowlight } from "lowlight"
|
|
4
|
+
import {
|
|
5
|
+
NodeViewContent,
|
|
6
|
+
NodeViewWrapper,
|
|
7
|
+
ReactNodeViewRenderer,
|
|
8
|
+
type NodeViewProps,
|
|
9
|
+
} from "@tiptap/react"
|
|
10
|
+
import {
|
|
11
|
+
AlertTriangle,
|
|
12
|
+
CheckCircle2,
|
|
13
|
+
ChevronDown,
|
|
14
|
+
Info,
|
|
15
|
+
Link2,
|
|
16
|
+
Plus,
|
|
17
|
+
X,
|
|
18
|
+
XCircle,
|
|
19
|
+
} from "lucide-react"
|
|
20
|
+
|
|
21
|
+
import { OpenApiOperation, resolveAsset, type HintStyle } from "@brett_lamy/docstream"
|
|
22
|
+
|
|
23
|
+
// ---------- Hint ----------
|
|
24
|
+
|
|
25
|
+
const HINT_META: Record<HintStyle, { icon: typeof Info; cls: string }> = {
|
|
26
|
+
info: { icon: Info, cls: "gb-hint-info" },
|
|
27
|
+
success: { icon: CheckCircle2, cls: "gb-hint-success" },
|
|
28
|
+
warning: { icon: AlertTriangle, cls: "gb-hint-warning" },
|
|
29
|
+
danger: { icon: XCircle, cls: "gb-hint-danger" },
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function HintView({ node, updateAttributes, editor }: NodeViewProps) {
|
|
33
|
+
const style = (node.attrs.style as HintStyle) ?? "info"
|
|
34
|
+
const { icon: Icon, cls } = HINT_META[style] ?? HINT_META.info
|
|
35
|
+
return (
|
|
36
|
+
<NodeViewWrapper className={`gb-hint ${cls}`}>
|
|
37
|
+
<div className="gb-hint-gutter" contentEditable={false}>
|
|
38
|
+
<Icon className="size-4" />
|
|
39
|
+
{editor.isEditable && (
|
|
40
|
+
<select
|
|
41
|
+
className="gb-hint-style"
|
|
42
|
+
value={style}
|
|
43
|
+
onChange={(e) => updateAttributes({ style: e.target.value })}
|
|
44
|
+
>
|
|
45
|
+
{Object.keys(HINT_META).map((s) => (
|
|
46
|
+
<option key={s} value={s}>
|
|
47
|
+
{s}
|
|
48
|
+
</option>
|
|
49
|
+
))}
|
|
50
|
+
</select>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
<NodeViewContent className="gb-hint-body" />
|
|
54
|
+
</NodeViewWrapper>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const GbHint = Node.create({
|
|
59
|
+
name: "gbHint",
|
|
60
|
+
group: "block",
|
|
61
|
+
content: "block+",
|
|
62
|
+
defining: true,
|
|
63
|
+
addAttributes() {
|
|
64
|
+
return { style: { default: "info" } }
|
|
65
|
+
},
|
|
66
|
+
parseHTML() {
|
|
67
|
+
return [{ tag: "div[data-gb-hint]" }]
|
|
68
|
+
},
|
|
69
|
+
renderHTML({ HTMLAttributes, node }) {
|
|
70
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-hint": node.attrs.style }), 0]
|
|
71
|
+
},
|
|
72
|
+
addNodeView() {
|
|
73
|
+
return ReactNodeViewRenderer(HintView)
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// ---------- Tabs ----------
|
|
78
|
+
|
|
79
|
+
function TabsView({ node, editor, getPos, updateAttributes }: NodeViewProps) {
|
|
80
|
+
const active = Math.min(node.attrs.active ?? 0, node.childCount - 1)
|
|
81
|
+
const titles: string[] = []
|
|
82
|
+
node.forEach((child) => titles.push(child.attrs.title))
|
|
83
|
+
|
|
84
|
+
const childPos = (index: number) => {
|
|
85
|
+
let pos = getPos()! + 1
|
|
86
|
+
for (let k = 0; k < index; k++) pos += node.child(k).nodeSize
|
|
87
|
+
return pos
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const renameTab = (index: number, title: string) => {
|
|
91
|
+
editor.view.dispatch(
|
|
92
|
+
editor.state.tr.setNodeAttribute(childPos(index), "title", title)
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const addTab = () => {
|
|
97
|
+
const type = editor.schema.nodes.gbTab
|
|
98
|
+
const tab = type.create({ title: `Tab ${node.childCount + 1}` }, [
|
|
99
|
+
editor.schema.nodes.paragraph.create(),
|
|
100
|
+
])
|
|
101
|
+
editor.view.dispatch(editor.state.tr.insert(childPos(node.childCount), tab))
|
|
102
|
+
updateAttributes({ active: node.childCount })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const removeTab = (index: number) => {
|
|
106
|
+
if (node.childCount <= 1) return
|
|
107
|
+
const pos = childPos(index)
|
|
108
|
+
editor.view.dispatch(editor.state.tr.delete(pos, pos + node.child(index).nodeSize))
|
|
109
|
+
updateAttributes({ active: Math.max(0, active - (index <= active ? 1 : 0)) })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<NodeViewWrapper className="gb-tabs">
|
|
114
|
+
<div className="gb-tabs-header" contentEditable={false}>
|
|
115
|
+
{titles.map((t, idx) => (
|
|
116
|
+
<div
|
|
117
|
+
key={idx}
|
|
118
|
+
className={`gb-tabs-tab ${idx === active ? "gb-tabs-tab-active" : ""}`}
|
|
119
|
+
onClick={() => updateAttributes({ active: idx })}
|
|
120
|
+
>
|
|
121
|
+
{editor.isEditable && idx === active ? (
|
|
122
|
+
<input
|
|
123
|
+
className="gb-tabs-title-input"
|
|
124
|
+
value={t}
|
|
125
|
+
size={Math.max(t.length, 3)}
|
|
126
|
+
onChange={(e) => renameTab(idx, e.target.value)}
|
|
127
|
+
/>
|
|
128
|
+
) : (
|
|
129
|
+
<span>{t}</span>
|
|
130
|
+
)}
|
|
131
|
+
{editor.isEditable && titles.length > 1 && (
|
|
132
|
+
<button className="gb-icon-btn" onClick={() => removeTab(idx)} title="Remove tab">
|
|
133
|
+
<X className="size-3" />
|
|
134
|
+
</button>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
))}
|
|
138
|
+
{editor.isEditable && (
|
|
139
|
+
<button className="gb-icon-btn" onClick={addTab} title="Add tab">
|
|
140
|
+
<Plus className="size-3.5" />
|
|
141
|
+
</button>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
<NodeViewContent className="gb-tabs-content" data-active={active} />
|
|
145
|
+
</NodeViewWrapper>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const GbTabs = Node.create({
|
|
150
|
+
name: "gbTabs",
|
|
151
|
+
group: "block",
|
|
152
|
+
content: "gbTab+",
|
|
153
|
+
defining: true,
|
|
154
|
+
isolating: true,
|
|
155
|
+
addAttributes() {
|
|
156
|
+
return { active: { default: 0, rendered: false } }
|
|
157
|
+
},
|
|
158
|
+
parseHTML() {
|
|
159
|
+
return [{ tag: "div[data-gb-tabs]" }]
|
|
160
|
+
},
|
|
161
|
+
renderHTML({ HTMLAttributes }) {
|
|
162
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-tabs": "" }), 0]
|
|
163
|
+
},
|
|
164
|
+
addNodeView() {
|
|
165
|
+
return ReactNodeViewRenderer(TabsView)
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
export const GbTab = Node.create({
|
|
170
|
+
name: "gbTab",
|
|
171
|
+
content: "block+",
|
|
172
|
+
defining: true,
|
|
173
|
+
isolating: true,
|
|
174
|
+
addAttributes() {
|
|
175
|
+
return { title: { default: "Tab" } }
|
|
176
|
+
},
|
|
177
|
+
parseHTML() {
|
|
178
|
+
return [{ tag: "div[data-gb-tab]" }]
|
|
179
|
+
},
|
|
180
|
+
renderHTML({ HTMLAttributes, node }) {
|
|
181
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-tab": node.attrs.title, class: "gb-tab" }), 0]
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// ---------- Expandable ----------
|
|
186
|
+
|
|
187
|
+
function ExpandableView({ node, updateAttributes, editor }: NodeViewProps) {
|
|
188
|
+
return (
|
|
189
|
+
<NodeViewWrapper className="gb-expandable">
|
|
190
|
+
<div className="gb-expandable-summary" contentEditable={false}>
|
|
191
|
+
<ChevronDown className="size-4" />
|
|
192
|
+
{editor.isEditable ? (
|
|
193
|
+
<input
|
|
194
|
+
className="gb-inline-input"
|
|
195
|
+
value={node.attrs.summary}
|
|
196
|
+
placeholder="Expandable title…"
|
|
197
|
+
onChange={(e) => updateAttributes({ summary: e.target.value })}
|
|
198
|
+
/>
|
|
199
|
+
) : (
|
|
200
|
+
<span>{node.attrs.summary}</span>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
<NodeViewContent className="gb-expandable-body" />
|
|
204
|
+
</NodeViewWrapper>
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const GbExpandable = Node.create({
|
|
209
|
+
name: "gbExpandable",
|
|
210
|
+
group: "block",
|
|
211
|
+
content: "block+",
|
|
212
|
+
defining: true,
|
|
213
|
+
addAttributes() {
|
|
214
|
+
return { summary: { default: "" } }
|
|
215
|
+
},
|
|
216
|
+
parseHTML() {
|
|
217
|
+
return [{ tag: "div[data-gb-expandable]" }]
|
|
218
|
+
},
|
|
219
|
+
renderHTML({ HTMLAttributes }) {
|
|
220
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-expandable": "" }), 0]
|
|
221
|
+
},
|
|
222
|
+
addNodeView() {
|
|
223
|
+
return ReactNodeViewRenderer(ExpandableView)
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// ---------- Stepper ----------
|
|
228
|
+
|
|
229
|
+
export const GbStepper = Node.create({
|
|
230
|
+
name: "gbStepper",
|
|
231
|
+
group: "block",
|
|
232
|
+
content: "gbStep+",
|
|
233
|
+
defining: true,
|
|
234
|
+
parseHTML() {
|
|
235
|
+
return [{ tag: "div[data-gb-stepper]" }]
|
|
236
|
+
},
|
|
237
|
+
renderHTML({ HTMLAttributes }) {
|
|
238
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-stepper": "", class: "gb-stepper" }), 0]
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
function StepView({ node, updateAttributes, editor, getPos }: NodeViewProps) {
|
|
243
|
+
let index = 0
|
|
244
|
+
try {
|
|
245
|
+
const $pos = editor.state.doc.resolve(getPos()!)
|
|
246
|
+
index = $pos.index($pos.depth)
|
|
247
|
+
} catch {
|
|
248
|
+
/* position can be momentarily stale during edits */
|
|
249
|
+
}
|
|
250
|
+
return (
|
|
251
|
+
<NodeViewWrapper className="gb-step">
|
|
252
|
+
<div className="gb-step-rail" contentEditable={false}>
|
|
253
|
+
<div className="gb-step-badge">{index + 1}</div>
|
|
254
|
+
<div className="gb-step-line" />
|
|
255
|
+
</div>
|
|
256
|
+
<div className="gb-step-main">
|
|
257
|
+
<div contentEditable={false}>
|
|
258
|
+
{editor.isEditable ? (
|
|
259
|
+
<input
|
|
260
|
+
className="gb-inline-input gb-step-title"
|
|
261
|
+
value={node.attrs.title}
|
|
262
|
+
placeholder="Step title…"
|
|
263
|
+
onChange={(e) => updateAttributes({ title: e.target.value })}
|
|
264
|
+
/>
|
|
265
|
+
) : (
|
|
266
|
+
<div className="gb-step-title">{node.attrs.title}</div>
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
<NodeViewContent className="gb-step-body" />
|
|
270
|
+
</div>
|
|
271
|
+
</NodeViewWrapper>
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export const GbStep = Node.create({
|
|
276
|
+
name: "gbStep",
|
|
277
|
+
content: "block+",
|
|
278
|
+
defining: true,
|
|
279
|
+
isolating: true,
|
|
280
|
+
addAttributes() {
|
|
281
|
+
return { title: { default: "" } }
|
|
282
|
+
},
|
|
283
|
+
parseHTML() {
|
|
284
|
+
return [{ tag: "div[data-gb-step]" }]
|
|
285
|
+
},
|
|
286
|
+
renderHTML({ HTMLAttributes }) {
|
|
287
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-step": "" }), 0]
|
|
288
|
+
},
|
|
289
|
+
addNodeView() {
|
|
290
|
+
return ReactNodeViewRenderer(StepView)
|
|
291
|
+
},
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
// ---------- Embed ----------
|
|
295
|
+
|
|
296
|
+
function embedPreview(url: string): string | null {
|
|
297
|
+
const yt = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/)
|
|
298
|
+
if (yt) return `https://www.youtube.com/embed/${yt[1]}`
|
|
299
|
+
return null
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function EmbedView({ node, updateAttributes, editor }: NodeViewProps) {
|
|
303
|
+
const url = node.attrs.url as string
|
|
304
|
+
const preview = embedPreview(url)
|
|
305
|
+
return (
|
|
306
|
+
<NodeViewWrapper className="gb-embed" contentEditable={false}>
|
|
307
|
+
{preview ? (
|
|
308
|
+
<iframe className="gb-embed-frame" src={preview} title={url} allowFullScreen />
|
|
309
|
+
) : (
|
|
310
|
+
<a className="gb-embed-link" href={url} target="_blank" rel="noreferrer">
|
|
311
|
+
<Link2 className="size-4" /> {url || "Embed URL…"}
|
|
312
|
+
</a>
|
|
313
|
+
)}
|
|
314
|
+
{editor.isEditable && (
|
|
315
|
+
<input
|
|
316
|
+
className="gb-inline-input gb-embed-input"
|
|
317
|
+
value={url}
|
|
318
|
+
placeholder="https://…"
|
|
319
|
+
onChange={(e) => updateAttributes({ url: e.target.value })}
|
|
320
|
+
/>
|
|
321
|
+
)}
|
|
322
|
+
</NodeViewWrapper>
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export const GbEmbed = Node.create({
|
|
327
|
+
name: "gbEmbed",
|
|
328
|
+
group: "block",
|
|
329
|
+
atom: true,
|
|
330
|
+
addAttributes() {
|
|
331
|
+
return { url: { default: "" } }
|
|
332
|
+
},
|
|
333
|
+
parseHTML() {
|
|
334
|
+
return [{ tag: "div[data-gb-embed]" }]
|
|
335
|
+
},
|
|
336
|
+
renderHTML({ HTMLAttributes, node }) {
|
|
337
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-embed": node.attrs.url })]
|
|
338
|
+
},
|
|
339
|
+
addNodeView() {
|
|
340
|
+
return ReactNodeViewRenderer(EmbedView)
|
|
341
|
+
},
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
// ---------- Content ref ----------
|
|
345
|
+
|
|
346
|
+
function ContentRefView({ node, updateAttributes, editor }: NodeViewProps) {
|
|
347
|
+
return (
|
|
348
|
+
<NodeViewWrapper className="gb-content-ref" contentEditable={false}>
|
|
349
|
+
<Link2 className="size-4 shrink-0" />
|
|
350
|
+
{editor.isEditable ? (
|
|
351
|
+
<>
|
|
352
|
+
<input
|
|
353
|
+
className="gb-inline-input"
|
|
354
|
+
value={node.attrs.label}
|
|
355
|
+
placeholder="Label…"
|
|
356
|
+
onChange={(e) => updateAttributes({ label: e.target.value })}
|
|
357
|
+
/>
|
|
358
|
+
<input
|
|
359
|
+
className="gb-inline-input gb-content-ref-url"
|
|
360
|
+
value={node.attrs.url}
|
|
361
|
+
placeholder="page.md or https://…"
|
|
362
|
+
onChange={(e) => updateAttributes({ url: e.target.value })}
|
|
363
|
+
/>
|
|
364
|
+
</>
|
|
365
|
+
) : (
|
|
366
|
+
<span>
|
|
367
|
+
{node.attrs.label} <span className="gb-content-ref-url">{node.attrs.url}</span>
|
|
368
|
+
</span>
|
|
369
|
+
)}
|
|
370
|
+
</NodeViewWrapper>
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export const GbContentRef = Node.create({
|
|
375
|
+
name: "gbContentRef",
|
|
376
|
+
group: "block",
|
|
377
|
+
atom: true,
|
|
378
|
+
addAttributes() {
|
|
379
|
+
return { url: { default: "" }, label: { default: "" } }
|
|
380
|
+
},
|
|
381
|
+
parseHTML() {
|
|
382
|
+
return [{ tag: "div[data-gb-content-ref]" }]
|
|
383
|
+
},
|
|
384
|
+
renderHTML({ HTMLAttributes, node }) {
|
|
385
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-content-ref": node.attrs.url })]
|
|
386
|
+
},
|
|
387
|
+
addNodeView() {
|
|
388
|
+
return ReactNodeViewRenderer(ContentRefView)
|
|
389
|
+
},
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
// ---------- Columns ----------
|
|
393
|
+
|
|
394
|
+
export const GbColumns = Node.create({
|
|
395
|
+
name: "gbColumns",
|
|
396
|
+
group: "block",
|
|
397
|
+
content: "gbColumn{2,}",
|
|
398
|
+
defining: true,
|
|
399
|
+
parseHTML() {
|
|
400
|
+
return [{ tag: "div[data-gb-columns]" }]
|
|
401
|
+
},
|
|
402
|
+
renderHTML({ HTMLAttributes }) {
|
|
403
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-columns": "", class: "gb-columns" }), 0]
|
|
404
|
+
},
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
export const GbColumn = Node.create({
|
|
408
|
+
name: "gbColumn",
|
|
409
|
+
content: "block+",
|
|
410
|
+
defining: true,
|
|
411
|
+
isolating: true,
|
|
412
|
+
parseHTML() {
|
|
413
|
+
return [{ tag: "div[data-gb-column]" }]
|
|
414
|
+
},
|
|
415
|
+
renderHTML({ HTMLAttributes }) {
|
|
416
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-column": "", class: "gb-column" }), 0]
|
|
417
|
+
},
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
// ---------- Figure ----------
|
|
421
|
+
|
|
422
|
+
function FigureView({ node, updateAttributes, editor }: NodeViewProps) {
|
|
423
|
+
const { src, alt, caption } = node.attrs
|
|
424
|
+
return (
|
|
425
|
+
<NodeViewWrapper className="gb-figure" contentEditable={false}>
|
|
426
|
+
{src ? (
|
|
427
|
+
<img src={resolveAsset(src)} alt={alt} className="gb-figure-img" />
|
|
428
|
+
) : (
|
|
429
|
+
<div className="gb-figure-placeholder">No image</div>
|
|
430
|
+
)}
|
|
431
|
+
{editor.isEditable ? (
|
|
432
|
+
<div className="gb-figure-fields">
|
|
433
|
+
<input
|
|
434
|
+
className="gb-inline-input"
|
|
435
|
+
value={src}
|
|
436
|
+
placeholder="Image URL…"
|
|
437
|
+
onChange={(e) => updateAttributes({ src: e.target.value })}
|
|
438
|
+
/>
|
|
439
|
+
<input
|
|
440
|
+
className="gb-inline-input"
|
|
441
|
+
value={caption}
|
|
442
|
+
placeholder="Caption…"
|
|
443
|
+
onChange={(e) => updateAttributes({ caption: e.target.value })}
|
|
444
|
+
/>
|
|
445
|
+
</div>
|
|
446
|
+
) : (
|
|
447
|
+
caption && <figcaption className="gb-figure-caption">{caption}</figcaption>
|
|
448
|
+
)}
|
|
449
|
+
</NodeViewWrapper>
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export const GbFigure = Node.create({
|
|
454
|
+
name: "gbFigure",
|
|
455
|
+
group: "block",
|
|
456
|
+
atom: true,
|
|
457
|
+
addAttributes() {
|
|
458
|
+
return { src: { default: "" }, alt: { default: "" }, caption: { default: "" } }
|
|
459
|
+
},
|
|
460
|
+
parseHTML() {
|
|
461
|
+
return [{ tag: "div[data-gb-figure]" }]
|
|
462
|
+
},
|
|
463
|
+
renderHTML({ HTMLAttributes, node }) {
|
|
464
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-figure": node.attrs.src })]
|
|
465
|
+
},
|
|
466
|
+
addNodeView() {
|
|
467
|
+
return ReactNodeViewRenderer(FigureView)
|
|
468
|
+
},
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
// ---------- Math ----------
|
|
472
|
+
|
|
473
|
+
function MathView({ node, updateAttributes, editor }: NodeViewProps) {
|
|
474
|
+
return (
|
|
475
|
+
<NodeViewWrapper className="gb-math" contentEditable={false}>
|
|
476
|
+
<div className="gb-math-label">TeX</div>
|
|
477
|
+
{editor.isEditable ? (
|
|
478
|
+
<textarea
|
|
479
|
+
className="gb-math-input"
|
|
480
|
+
value={node.attrs.formula}
|
|
481
|
+
rows={Math.max(1, String(node.attrs.formula).split("\n").length)}
|
|
482
|
+
onChange={(e) => updateAttributes({ formula: e.target.value })}
|
|
483
|
+
/>
|
|
484
|
+
) : (
|
|
485
|
+
<pre className="gb-math-formula">{node.attrs.formula}</pre>
|
|
486
|
+
)}
|
|
487
|
+
</NodeViewWrapper>
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export const GbMath = Node.create({
|
|
492
|
+
name: "gbMath",
|
|
493
|
+
group: "block",
|
|
494
|
+
atom: true,
|
|
495
|
+
addAttributes() {
|
|
496
|
+
return { formula: { default: "" } }
|
|
497
|
+
},
|
|
498
|
+
parseHTML() {
|
|
499
|
+
return [{ tag: "div[data-gb-math]" }]
|
|
500
|
+
},
|
|
501
|
+
renderHTML({ HTMLAttributes, node }) {
|
|
502
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-math": "" }), node.attrs.formula]
|
|
503
|
+
},
|
|
504
|
+
addNodeView() {
|
|
505
|
+
return ReactNodeViewRenderer(MathView)
|
|
506
|
+
},
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
// ---------- Code block with GitBook title/lineNumbers ----------
|
|
510
|
+
|
|
511
|
+
function CodeView({ node, updateAttributes, editor }: NodeViewProps) {
|
|
512
|
+
return (
|
|
513
|
+
<NodeViewWrapper className="gb-code">
|
|
514
|
+
<div className="gb-code-header" contentEditable={false}>
|
|
515
|
+
{editor.isEditable ? (
|
|
516
|
+
<>
|
|
517
|
+
<input
|
|
518
|
+
className="gb-inline-input gb-code-title"
|
|
519
|
+
value={node.attrs.title ?? ""}
|
|
520
|
+
placeholder="Title (optional)"
|
|
521
|
+
onChange={(e) => updateAttributes({ title: e.target.value || null })}
|
|
522
|
+
/>
|
|
523
|
+
<input
|
|
524
|
+
className="gb-inline-input gb-code-lang"
|
|
525
|
+
value={node.attrs.language ?? ""}
|
|
526
|
+
placeholder="lang"
|
|
527
|
+
onChange={(e) => updateAttributes({ language: e.target.value || null })}
|
|
528
|
+
/>
|
|
529
|
+
<label className="gb-code-linenos">
|
|
530
|
+
<input
|
|
531
|
+
type="checkbox"
|
|
532
|
+
checked={!!node.attrs.lineNumbers}
|
|
533
|
+
onChange={(e) => updateAttributes({ lineNumbers: e.target.checked })}
|
|
534
|
+
/>
|
|
535
|
+
#
|
|
536
|
+
</label>
|
|
537
|
+
</>
|
|
538
|
+
) : (
|
|
539
|
+
<>
|
|
540
|
+
{node.attrs.title && <span className="gb-code-title">{node.attrs.title}</span>}
|
|
541
|
+
{node.attrs.language && <span className="gb-code-lang">{node.attrs.language}</span>}
|
|
542
|
+
</>
|
|
543
|
+
)}
|
|
544
|
+
</div>
|
|
545
|
+
<pre className={node.attrs.lineNumbers ? "gb-code-numbered" : ""}>
|
|
546
|
+
<NodeViewContent {...({ as: "code" } as object)} />
|
|
547
|
+
</pre>
|
|
548
|
+
</NodeViewWrapper>
|
|
549
|
+
)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export const GbCodeBlock = CodeBlockLowlight.extend({
|
|
553
|
+
addAttributes() {
|
|
554
|
+
return {
|
|
555
|
+
...this.parent?.(),
|
|
556
|
+
title: { default: null },
|
|
557
|
+
lineNumbers: { default: false },
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
addNodeView() {
|
|
561
|
+
return ReactNodeViewRenderer(CodeView)
|
|
562
|
+
},
|
|
563
|
+
}).configure({ lowlight: createLowlight(common) })
|
|
564
|
+
|
|
565
|
+
// ---------- Inline image (GitHub badge style) ----------
|
|
566
|
+
|
|
567
|
+
function InlineImageView({ node }: NodeViewProps) {
|
|
568
|
+
const { src, alt, width, height, link } = node.attrs
|
|
569
|
+
const img = (
|
|
570
|
+
<img
|
|
571
|
+
src={resolveAsset(src)}
|
|
572
|
+
alt={alt}
|
|
573
|
+
className="gb-inline-img"
|
|
574
|
+
style={{ width: width || undefined, height: height || undefined }}
|
|
575
|
+
/>
|
|
576
|
+
)
|
|
577
|
+
return (
|
|
578
|
+
<NodeViewWrapper as="span" className="gb-inline-img-wrap" contentEditable={false}>
|
|
579
|
+
{link ? <a href={link}>{img}</a> : img}
|
|
580
|
+
</NodeViewWrapper>
|
|
581
|
+
)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export const GbInlineImage = Node.create({
|
|
585
|
+
name: "gbInlineImage",
|
|
586
|
+
group: "inline",
|
|
587
|
+
inline: true,
|
|
588
|
+
atom: true,
|
|
589
|
+
addAttributes() {
|
|
590
|
+
return {
|
|
591
|
+
src: { default: "" },
|
|
592
|
+
alt: { default: "" },
|
|
593
|
+
width: { default: "" },
|
|
594
|
+
height: { default: "" },
|
|
595
|
+
link: { default: "" },
|
|
596
|
+
}
|
|
597
|
+
},
|
|
598
|
+
parseHTML() {
|
|
599
|
+
return [{ tag: "img[data-gb-inline-img]" }]
|
|
600
|
+
},
|
|
601
|
+
renderHTML({ HTMLAttributes, node }) {
|
|
602
|
+
return ["img", mergeAttributes(HTMLAttributes, { "data-gb-inline-img": "", src: node.attrs.src })]
|
|
603
|
+
},
|
|
604
|
+
addNodeView() {
|
|
605
|
+
return ReactNodeViewRenderer(InlineImageView)
|
|
606
|
+
},
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
// ---------- Updates (changelog) ----------
|
|
610
|
+
|
|
611
|
+
export const GbUpdates = Node.create({
|
|
612
|
+
name: "gbUpdates",
|
|
613
|
+
group: "block",
|
|
614
|
+
content: "gbUpdate+",
|
|
615
|
+
defining: true,
|
|
616
|
+
addAttributes() {
|
|
617
|
+
return { format: { default: null } }
|
|
618
|
+
},
|
|
619
|
+
parseHTML() {
|
|
620
|
+
return [{ tag: "div[data-gb-updates]" }]
|
|
621
|
+
},
|
|
622
|
+
renderHTML({ HTMLAttributes }) {
|
|
623
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-updates": "", class: "gb-updates" }), 0]
|
|
624
|
+
},
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
function UpdateView({ node, updateAttributes, editor }: NodeViewProps) {
|
|
628
|
+
return (
|
|
629
|
+
<NodeViewWrapper className="gb-update">
|
|
630
|
+
<div className="gb-update-date" contentEditable={false}>
|
|
631
|
+
{editor.isEditable ? (
|
|
632
|
+
<input
|
|
633
|
+
type="date"
|
|
634
|
+
className="gb-inline-input"
|
|
635
|
+
value={node.attrs.date}
|
|
636
|
+
onChange={(e) => updateAttributes({ date: e.target.value })}
|
|
637
|
+
/>
|
|
638
|
+
) : (
|
|
639
|
+
<span>{node.attrs.date}</span>
|
|
640
|
+
)}
|
|
641
|
+
</div>
|
|
642
|
+
<NodeViewContent className="gb-update-body" />
|
|
643
|
+
</NodeViewWrapper>
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
export const GbUpdate = Node.create({
|
|
648
|
+
name: "gbUpdate",
|
|
649
|
+
content: "block+",
|
|
650
|
+
defining: true,
|
|
651
|
+
isolating: true,
|
|
652
|
+
addAttributes() {
|
|
653
|
+
return { date: { default: "" } }
|
|
654
|
+
},
|
|
655
|
+
parseHTML() {
|
|
656
|
+
return [{ tag: "div[data-gb-update]" }]
|
|
657
|
+
},
|
|
658
|
+
renderHTML({ HTMLAttributes, node }) {
|
|
659
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-update": node.attrs.date }), 0]
|
|
660
|
+
},
|
|
661
|
+
addNodeView() {
|
|
662
|
+
return ReactNodeViewRenderer(UpdateView)
|
|
663
|
+
},
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
// ---------- OpenAPI operation ----------
|
|
667
|
+
|
|
668
|
+
function OpenapiView({ node, updateAttributes, editor }: NodeViewProps) {
|
|
669
|
+
const { spec, path, method, specUrl } = node.attrs
|
|
670
|
+
return (
|
|
671
|
+
<NodeViewWrapper className="gb-openapi" contentEditable={false}>
|
|
672
|
+
{editor.isEditable && (
|
|
673
|
+
<div className="gb-openapi-fields">
|
|
674
|
+
<input
|
|
675
|
+
className="gb-inline-input"
|
|
676
|
+
value={spec}
|
|
677
|
+
placeholder="spec name"
|
|
678
|
+
onChange={(e) => updateAttributes({ spec: e.target.value })}
|
|
679
|
+
/>
|
|
680
|
+
<input
|
|
681
|
+
className="gb-inline-input"
|
|
682
|
+
value={method}
|
|
683
|
+
placeholder="get"
|
|
684
|
+
onChange={(e) => updateAttributes({ method: e.target.value })}
|
|
685
|
+
/>
|
|
686
|
+
<input
|
|
687
|
+
className="gb-inline-input gb-openapi-path"
|
|
688
|
+
value={path}
|
|
689
|
+
placeholder="/path"
|
|
690
|
+
onChange={(e) => updateAttributes({ path: e.target.value })}
|
|
691
|
+
/>
|
|
692
|
+
<input
|
|
693
|
+
className="gb-inline-input gb-openapi-url"
|
|
694
|
+
value={specUrl}
|
|
695
|
+
placeholder="https://…/openapi.yaml"
|
|
696
|
+
onChange={(e) => updateAttributes({ specUrl: e.target.value })}
|
|
697
|
+
/>
|
|
698
|
+
</div>
|
|
699
|
+
)}
|
|
700
|
+
<OpenApiOperation specUrl={specUrl} path={path} method={method || "get"} />
|
|
701
|
+
</NodeViewWrapper>
|
|
702
|
+
)
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
export const GbOpenapi = Node.create({
|
|
706
|
+
name: "gbOpenapi",
|
|
707
|
+
group: "block",
|
|
708
|
+
atom: true,
|
|
709
|
+
addAttributes() {
|
|
710
|
+
return {
|
|
711
|
+
spec: { default: "" },
|
|
712
|
+
path: { default: "" },
|
|
713
|
+
method: { default: "get" },
|
|
714
|
+
specUrl: { default: "" },
|
|
715
|
+
label: { default: "" },
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
parseHTML() {
|
|
719
|
+
return [{ tag: "div[data-gb-openapi]" }]
|
|
720
|
+
},
|
|
721
|
+
renderHTML({ HTMLAttributes, node }) {
|
|
722
|
+
return ["div", mergeAttributes(HTMLAttributes, { "data-gb-openapi": node.attrs.path })]
|
|
723
|
+
},
|
|
724
|
+
addNodeView() {
|
|
725
|
+
return ReactNodeViewRenderer(OpenapiView)
|
|
726
|
+
},
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
export const gitbookNodes = [
|
|
730
|
+
GbHint,
|
|
731
|
+
GbTabs,
|
|
732
|
+
GbTab,
|
|
733
|
+
GbExpandable,
|
|
734
|
+
GbStepper,
|
|
735
|
+
GbStep,
|
|
736
|
+
GbEmbed,
|
|
737
|
+
GbContentRef,
|
|
738
|
+
GbColumns,
|
|
739
|
+
GbColumn,
|
|
740
|
+
GbFigure,
|
|
741
|
+
GbMath,
|
|
742
|
+
GbUpdates,
|
|
743
|
+
GbUpdate,
|
|
744
|
+
GbOpenapi,
|
|
745
|
+
GbInlineImage,
|
|
746
|
+
]
|