@bendyline/squisq-editor-react 1.2.1 → 1.3.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/dist/EditorContext.d.ts +65 -1
- package/dist/EditorContext.d.ts.map +1 -1
- package/dist/EditorContext.js +31 -4
- package/dist/EditorContext.js.map +1 -1
- package/dist/EditorShell.d.ts +101 -2
- package/dist/EditorShell.d.ts.map +1 -1
- package/dist/EditorShell.js +20 -8
- package/dist/EditorShell.js.map +1 -1
- package/dist/ImageNodeView.d.ts.map +1 -1
- package/dist/ImageNodeView.js +12 -2
- package/dist/ImageNodeView.js.map +1 -1
- package/dist/MediaBin.d.ts.map +1 -1
- package/dist/MediaBin.js +16 -1
- package/dist/MediaBin.js.map +1 -1
- package/dist/MentionExtension.d.ts +22 -0
- package/dist/MentionExtension.d.ts.map +1 -0
- package/dist/MentionExtension.js +242 -0
- package/dist/MentionExtension.js.map +1 -0
- package/dist/PreviewPanel.d.ts +3 -8
- package/dist/PreviewPanel.d.ts.map +1 -1
- package/dist/PreviewPanel.js +4 -282
- package/dist/PreviewPanel.js.map +1 -1
- package/dist/RawEditor.d.ts +8 -1
- package/dist/RawEditor.d.ts.map +1 -1
- package/dist/RawEditor.js +167 -30
- package/dist/RawEditor.js.map +1 -1
- package/dist/TemplateAnnotation.d.ts.map +1 -1
- package/dist/TemplateAnnotation.js +4 -2
- package/dist/TemplateAnnotation.js.map +1 -1
- package/dist/Toolbar.d.ts +7 -1
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar.js +57 -18
- package/dist/Toolbar.js.map +1 -1
- package/dist/Tooltip.d.ts +10 -0
- package/dist/Tooltip.d.ts.map +1 -0
- package/dist/Tooltip.js +104 -0
- package/dist/Tooltip.js.map +1 -0
- package/dist/ViewSwitcher.d.ts +1 -1
- package/dist/ViewSwitcher.d.ts.map +1 -1
- package/dist/ViewSwitcher.js +10 -4
- package/dist/ViewSwitcher.js.map +1 -1
- package/dist/WysiwygEditor.d.ts +13 -2
- package/dist/WysiwygEditor.d.ts.map +1 -1
- package/dist/WysiwygEditor.js +239 -4
- package/dist/WysiwygEditor.js.map +1 -1
- package/dist/__tests__/detectMarkdown.test.d.ts +2 -0
- package/dist/__tests__/detectMarkdown.test.d.ts.map +1 -0
- package/dist/__tests__/detectMarkdown.test.js +69 -0
- package/dist/__tests__/detectMarkdown.test.js.map +1 -0
- package/dist/__tests__/fileKind.test.d.ts +2 -0
- package/dist/__tests__/fileKind.test.d.ts.map +1 -0
- package/dist/__tests__/fileKind.test.js +81 -0
- package/dist/__tests__/fileKind.test.js.map +1 -0
- package/dist/__tests__/tiptapBridge.test.js +36 -0
- package/dist/__tests__/tiptapBridge.test.js.map +1 -1
- package/dist/buildPreviewDoc.d.ts +22 -0
- package/dist/buildPreviewDoc.d.ts.map +1 -0
- package/dist/buildPreviewDoc.js +212 -0
- package/dist/buildPreviewDoc.js.map +1 -0
- package/dist/detectMarkdown.d.ts +20 -0
- package/dist/detectMarkdown.d.ts.map +1 -0
- package/dist/detectMarkdown.js +61 -0
- package/dist/detectMarkdown.js.map +1 -0
- package/dist/fileKind.d.ts +30 -0
- package/dist/fileKind.d.ts.map +1 -0
- package/dist/fileKind.js +123 -0
- package/dist/fileKind.js.map +1 -0
- package/dist/hooks/useFileDrop.d.ts.map +1 -1
- package/dist/hooks/useFileDrop.js +9 -7
- package/dist/hooks/useFileDrop.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/mediaDragMime.d.ts +17 -0
- package/dist/mediaDragMime.d.ts.map +1 -0
- package/dist/mediaDragMime.js +22 -0
- package/dist/mediaDragMime.js.map +1 -0
- package/dist/tiptapBridge.d.ts.map +1 -1
- package/dist/tiptapBridge.js +58 -2
- package/dist/tiptapBridge.js.map +1 -1
- package/package.json +9 -7
- package/src/EditorContext.tsx +106 -3
- package/src/EditorShell.tsx +195 -15
- package/src/ImageNodeView.tsx +15 -2
- package/src/MediaBin.tsx +23 -1
- package/src/MentionExtension.tsx +258 -0
- package/src/PreviewPanel.tsx +5 -333
- package/src/RawEditor.tsx +193 -37
- package/src/TemplateAnnotation.ts +4 -2
- package/src/Toolbar.tsx +111 -48
- package/src/Tooltip.tsx +124 -0
- package/src/ViewSwitcher.tsx +15 -5
- package/src/WysiwygEditor.tsx +270 -5
- package/src/__tests__/detectMarkdown.test.ts +88 -0
- package/src/__tests__/fileKind.test.ts +96 -0
- package/src/__tests__/tiptapBridge.test.ts +44 -0
- package/src/buildPreviewDoc.ts +254 -0
- package/src/detectMarkdown.ts +62 -0
- package/src/fileKind.ts +134 -0
- package/src/hooks/useFileDrop.ts +10 -6
- package/src/index.ts +14 -0
- package/src/mediaDragMime.ts +32 -0
- package/src/styles/editor.css +214 -8
- package/src/tiptapBridge.ts +66 -2
package/src/styles/editor.css
CHANGED
|
@@ -5,8 +5,20 @@
|
|
|
5
5
|
/* ─── Shell ──────────────────────────────────────────── */
|
|
6
6
|
|
|
7
7
|
.squisq-editor-shell {
|
|
8
|
-
font
|
|
9
|
-
|
|
8
|
+
/* UX font applies to editor *chrome* — toolbar, tabs, buttons, status
|
|
9
|
+
bar. The actual editing surfaces (Tiptap, Monaco) keep their own
|
|
10
|
+
fonts. `--squisq-ux-font` is set when a consumer passes the `uxFont`
|
|
11
|
+
prop on EditorShell; unset, we fall back to the system stack. */
|
|
12
|
+
font-family: var(
|
|
13
|
+
--squisq-ux-font,
|
|
14
|
+
-apple-system,
|
|
15
|
+
BlinkMacSystemFont,
|
|
16
|
+
'Segoe UI',
|
|
17
|
+
'Noto Sans',
|
|
18
|
+
Helvetica,
|
|
19
|
+
Arial,
|
|
20
|
+
sans-serif
|
|
21
|
+
);
|
|
10
22
|
color: #1f2937;
|
|
11
23
|
background: #fff;
|
|
12
24
|
}
|
|
@@ -27,6 +39,25 @@
|
|
|
27
39
|
.squisq-view-switcher {
|
|
28
40
|
display: flex;
|
|
29
41
|
gap: 0;
|
|
42
|
+
container-type: inline-size;
|
|
43
|
+
container-name: squisq-view-switcher;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.squisq-view-tab-label {
|
|
47
|
+
display: inline-block;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.squisq-view-tab-label--short {
|
|
51
|
+
display: none;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@container squisq-view-switcher (max-width: 280px) {
|
|
55
|
+
.squisq-view-tab:has(.squisq-view-tab-label--short) .squisq-view-tab-label--long {
|
|
56
|
+
display: none;
|
|
57
|
+
}
|
|
58
|
+
.squisq-view-tab-label--short {
|
|
59
|
+
display: inline-block;
|
|
60
|
+
}
|
|
30
61
|
}
|
|
31
62
|
|
|
32
63
|
.squisq-view-tab {
|
|
@@ -63,6 +94,8 @@
|
|
|
63
94
|
padding: 0 12px 0 0;
|
|
64
95
|
gap: 2px;
|
|
65
96
|
background: rgba(0, 0, 0, 0.07);
|
|
97
|
+
container-type: inline-size;
|
|
98
|
+
container-name: squisq-toolbar;
|
|
66
99
|
}
|
|
67
100
|
|
|
68
101
|
/* ─── View Tabs (inside toolbar) ─────────────────────── */
|
|
@@ -101,7 +134,15 @@
|
|
|
101
134
|
border-color 0.15s;
|
|
102
135
|
}
|
|
103
136
|
|
|
104
|
-
.squisq-toolbar-view-tab
|
|
137
|
+
.squisq-toolbar-view-tab-label {
|
|
138
|
+
display: inline-block;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.squisq-toolbar-view-tab-label--short {
|
|
142
|
+
display: none;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.squisq-toolbar-view-tab-label::after {
|
|
105
146
|
content: attr(data-label);
|
|
106
147
|
display: block;
|
|
107
148
|
font-weight: 600;
|
|
@@ -110,6 +151,16 @@
|
|
|
110
151
|
visibility: hidden;
|
|
111
152
|
}
|
|
112
153
|
|
|
154
|
+
@container squisq-toolbar (max-width: 900px) {
|
|
155
|
+
.squisq-toolbar-view-tab:has(.squisq-toolbar-view-tab-label--short)
|
|
156
|
+
.squisq-toolbar-view-tab-label--long {
|
|
157
|
+
display: none;
|
|
158
|
+
}
|
|
159
|
+
.squisq-toolbar-view-tab-label--short {
|
|
160
|
+
display: inline-block;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
113
164
|
.squisq-toolbar-view-tab:hover {
|
|
114
165
|
color: #111827;
|
|
115
166
|
}
|
|
@@ -146,6 +197,7 @@
|
|
|
146
197
|
cursor: pointer;
|
|
147
198
|
font-size: 13px;
|
|
148
199
|
font-weight: 600;
|
|
200
|
+
white-space: nowrap;
|
|
149
201
|
transition:
|
|
150
202
|
background 0.12s,
|
|
151
203
|
color 0.12s;
|
|
@@ -193,18 +245,32 @@
|
|
|
193
245
|
|
|
194
246
|
.squisq-toolbar-overflow-menu {
|
|
195
247
|
position: absolute;
|
|
196
|
-
top: 100%;
|
|
197
248
|
right: 0;
|
|
198
249
|
z-index: 100;
|
|
199
|
-
min-width:
|
|
250
|
+
min-width: 220px;
|
|
251
|
+
max-width: 280px;
|
|
200
252
|
padding: 4px 0;
|
|
201
|
-
margin-top: 4px;
|
|
202
253
|
background: #fff;
|
|
203
254
|
border: 1px solid #e5e7eb;
|
|
204
255
|
border-radius: 6px;
|
|
205
256
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
206
257
|
}
|
|
207
258
|
|
|
259
|
+
/* Downward-opening (default): menu sits below the trigger. */
|
|
260
|
+
.squisq-toolbar-overflow-menu--down {
|
|
261
|
+
top: 100%;
|
|
262
|
+
margin-top: 4px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* Upward-opening: when the host clips the bottom of the toolbar (e.g. a
|
|
266
|
+
chat composer near the bottom of the viewport), the menu flips above
|
|
267
|
+
the trigger so the items stay visible. */
|
|
268
|
+
.squisq-toolbar-overflow-menu--up {
|
|
269
|
+
bottom: 100%;
|
|
270
|
+
margin-bottom: 4px;
|
|
271
|
+
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.12);
|
|
272
|
+
}
|
|
273
|
+
|
|
208
274
|
.squisq-toolbar-overflow-item {
|
|
209
275
|
display: flex;
|
|
210
276
|
align-items: center;
|
|
@@ -255,11 +321,14 @@
|
|
|
255
321
|
.squisq-toolbar-overflow-template {
|
|
256
322
|
gap: 6px;
|
|
257
323
|
padding: 6px 12px;
|
|
324
|
+
box-sizing: border-box;
|
|
325
|
+
max-width: 100%;
|
|
258
326
|
}
|
|
259
327
|
|
|
260
328
|
.squisq-toolbar-overflow-template select {
|
|
261
329
|
flex: 1;
|
|
262
330
|
min-width: 0;
|
|
331
|
+
max-width: 100%;
|
|
263
332
|
}
|
|
264
333
|
|
|
265
334
|
/* ─── Template Picker (toolbar) ──────────────────────── */
|
|
@@ -295,6 +364,32 @@
|
|
|
295
364
|
outline-offset: -1px;
|
|
296
365
|
}
|
|
297
366
|
|
|
367
|
+
/* ─── Tooltip (portal) ────────────────────────────────── */
|
|
368
|
+
|
|
369
|
+
.squisq-tooltip {
|
|
370
|
+
transform: translateX(-50%);
|
|
371
|
+
padding: 4px 8px;
|
|
372
|
+
font-size: 12px;
|
|
373
|
+
font-weight: 500;
|
|
374
|
+
color: #f9fafb;
|
|
375
|
+
background: #1f2937;
|
|
376
|
+
border-radius: 4px;
|
|
377
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
|
|
378
|
+
white-space: nowrap;
|
|
379
|
+
pointer-events: none;
|
|
380
|
+
z-index: 2000;
|
|
381
|
+
animation: squisq-tooltip-fade 0.1s ease-out;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@keyframes squisq-tooltip-fade {
|
|
385
|
+
from {
|
|
386
|
+
opacity: 0;
|
|
387
|
+
}
|
|
388
|
+
to {
|
|
389
|
+
opacity: 1;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
298
393
|
/* ─── Template Badge (WYSIWYG heading) ───────────────── */
|
|
299
394
|
|
|
300
395
|
.squisq-template-badge {
|
|
@@ -314,6 +409,10 @@
|
|
|
314
409
|
line-height: 1.6;
|
|
315
410
|
}
|
|
316
411
|
|
|
412
|
+
.squisq-template-badge::after {
|
|
413
|
+
content: attr(data-template);
|
|
414
|
+
}
|
|
415
|
+
|
|
317
416
|
/* ─── Status Bar ─────────────────────────────────────── */
|
|
318
417
|
|
|
319
418
|
.squisq-status-bar {
|
|
@@ -351,7 +450,7 @@
|
|
|
351
450
|
/* ─── WYSIWYG Editor ─────────────────────────────────── */
|
|
352
451
|
|
|
353
452
|
.squisq-wysiwyg-container {
|
|
354
|
-
background: #
|
|
453
|
+
background: #dcd8d0;
|
|
355
454
|
}
|
|
356
455
|
|
|
357
456
|
.squisq-wysiwyg-editor {
|
|
@@ -361,7 +460,7 @@
|
|
|
361
460
|
outline: none;
|
|
362
461
|
min-height: 100%;
|
|
363
462
|
background: #fff;
|
|
364
|
-
box-shadow: 0
|
|
463
|
+
box-shadow: 0 2px 14px rgba(0, 0, 0, 0.12);
|
|
365
464
|
}
|
|
366
465
|
|
|
367
466
|
.squisq-wysiwyg-editor h1 {
|
|
@@ -1229,3 +1328,110 @@
|
|
|
1229
1328
|
min-height: 120px;
|
|
1230
1329
|
}
|
|
1231
1330
|
}
|
|
1331
|
+
|
|
1332
|
+
/* ─── Full-width mode (opt-in via <EditorShell fullWidth />) ─────────
|
|
1333
|
+
*
|
|
1334
|
+
* Drops the centered 800px "page" column so the WYSIWYG surface fills
|
|
1335
|
+
* the host container. Used by hosts where the page metaphor doesn't fit
|
|
1336
|
+
* — chat composers, narrow side panels, dialog embeds. */
|
|
1337
|
+
.squisq-editor-shell[data-full-width='true'] .squisq-wysiwyg-editor {
|
|
1338
|
+
max-width: none;
|
|
1339
|
+
margin: 0;
|
|
1340
|
+
box-shadow: none;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
.squisq-editor-shell[data-full-width='true'] .squisq-wysiwyg-container {
|
|
1344
|
+
background: transparent;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
.squisq-editor-shell[data-theme='dark'][data-full-width='true'] .squisq-wysiwyg-container {
|
|
1348
|
+
background: transparent;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/* Thin-margins mode — drops the 16×24px page padding on the editing
|
|
1352
|
+
surface so the composer hugs its container (chat composers etc.). */
|
|
1353
|
+
.squisq-editor-shell[data-thin-margins='true'] .squisq-wysiwyg-editor {
|
|
1354
|
+
padding: 6px 10px;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/* ── @-mention chip + suggestion popover ─────────────────────────── */
|
|
1358
|
+
|
|
1359
|
+
/* Chip rendered in both WYSIWYG (Tiptap Node) and bridge-HTML surfaces. */
|
|
1360
|
+
.squisq-wysiwyg-editor .mention,
|
|
1361
|
+
.squisq-wysiwyg-editor span[data-mention] {
|
|
1362
|
+
display: inline-block;
|
|
1363
|
+
padding: 0 6px;
|
|
1364
|
+
border-radius: 10px;
|
|
1365
|
+
font-weight: 500;
|
|
1366
|
+
background: rgba(88, 101, 242, 0.18);
|
|
1367
|
+
color: #1a2a8a;
|
|
1368
|
+
line-height: 1.4;
|
|
1369
|
+
white-space: nowrap;
|
|
1370
|
+
cursor: default;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-wysiwyg-editor .mention,
|
|
1374
|
+
.squisq-editor-shell[data-theme='dark'] .squisq-wysiwyg-editor span[data-mention] {
|
|
1375
|
+
background: rgba(128, 140, 255, 0.22);
|
|
1376
|
+
color: #c8d0ff;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/* Suggestion popover appended to <body> so it escapes overflow clipping. */
|
|
1380
|
+
.squisq-mention-popover {
|
|
1381
|
+
min-width: 220px;
|
|
1382
|
+
max-width: 320px;
|
|
1383
|
+
max-height: 240px;
|
|
1384
|
+
overflow-y: auto;
|
|
1385
|
+
background: #fff;
|
|
1386
|
+
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
1387
|
+
border-radius: 6px;
|
|
1388
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
1389
|
+
padding: 4px;
|
|
1390
|
+
font-family: var(--squisq-ux-font, system-ui, sans-serif);
|
|
1391
|
+
font-size: 13px;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
.squisq-mention-popover .squisq-mention-item {
|
|
1395
|
+
display: flex;
|
|
1396
|
+
flex-direction: column;
|
|
1397
|
+
gap: 2px;
|
|
1398
|
+
align-items: flex-start;
|
|
1399
|
+
width: 100%;
|
|
1400
|
+
padding: 6px 8px;
|
|
1401
|
+
border: none;
|
|
1402
|
+
background: transparent;
|
|
1403
|
+
border-radius: 4px;
|
|
1404
|
+
cursor: pointer;
|
|
1405
|
+
text-align: left;
|
|
1406
|
+
color: inherit;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
.squisq-mention-popover .squisq-mention-item.is-selected,
|
|
1410
|
+
.squisq-mention-popover .squisq-mention-item:hover {
|
|
1411
|
+
background: rgba(88, 101, 242, 0.12);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
.squisq-mention-popover .squisq-mention-label {
|
|
1415
|
+
font-weight: 500;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
.squisq-mention-popover .squisq-mention-desc {
|
|
1419
|
+
font-size: 11px;
|
|
1420
|
+
color: rgba(0, 0, 0, 0.55);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
@media (prefers-color-scheme: dark) {
|
|
1424
|
+
.squisq-mention-popover {
|
|
1425
|
+
background: #1f2230;
|
|
1426
|
+
border-color: rgba(255, 255, 255, 0.14);
|
|
1427
|
+
color: #e5e7eb;
|
|
1428
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
|
|
1429
|
+
}
|
|
1430
|
+
.squisq-mention-popover .squisq-mention-item.is-selected,
|
|
1431
|
+
.squisq-mention-popover .squisq-mention-item:hover {
|
|
1432
|
+
background: rgba(128, 140, 255, 0.18);
|
|
1433
|
+
}
|
|
1434
|
+
.squisq-mention-popover .squisq-mention-desc {
|
|
1435
|
+
color: rgba(255, 255, 255, 0.6);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
package/src/tiptapBridge.ts
CHANGED
|
@@ -20,6 +20,11 @@ const RE_STRIKETHROUGH = /~~(.+?)~~/g;
|
|
|
20
20
|
const RE_INLINE_CODE = /`(.+?)`/g;
|
|
21
21
|
const RE_LINK = /\[(.+?)\]\((.+?)\)/g;
|
|
22
22
|
const RE_IMAGE = /!\[(.+?)\]\((.+?)\)/g;
|
|
23
|
+
// Mentions: `@[Display](scheme:id)` — scheme-part must start with a letter
|
|
24
|
+
// so plain `$100` or price-style parentheticals don't accidentally match.
|
|
25
|
+
// remark-stringify may round-trip the colon as `\:` — tolerate either.
|
|
26
|
+
const RE_MENTION = /@\[([^\]]+?)\]\(([a-z][a-z0-9+.-]*)\\?:([^)\s]+)\)/gi;
|
|
27
|
+
const RE_MENTION_TAG = /<span\b[^>]*?\bdata-mention\b[^>]*?>(?:<[^>]+>)*([^<]*)<\/span>/gi;
|
|
23
28
|
const RE_STRONG_TAG = /<strong>(.*?)<\/strong>/g;
|
|
24
29
|
const RE_B_TAG = /<b>(.*?)<\/b>/g;
|
|
25
30
|
const RE_EM_TAG = /<em>(.*?)<\/em>/g;
|
|
@@ -426,7 +431,7 @@ export function tiptapToMarkdown(html: string): string {
|
|
|
426
431
|
if (ulMatch) {
|
|
427
432
|
const items = ulMatch[1].matchAll(/<li>(.*?)<\/li>/gs);
|
|
428
433
|
for (const item of items) {
|
|
429
|
-
lines.push('- '
|
|
434
|
+
lines.push(...renderListItem('- ', item[1]));
|
|
430
435
|
}
|
|
431
436
|
lines.push('');
|
|
432
437
|
remaining = remaining.slice(ulMatch[0].length);
|
|
@@ -438,7 +443,7 @@ export function tiptapToMarkdown(html: string): string {
|
|
|
438
443
|
if (olMatch) {
|
|
439
444
|
const items = [...olMatch[1].matchAll(/<li>(.*?)<\/li>/gs)];
|
|
440
445
|
items.forEach((item, idx) => {
|
|
441
|
-
lines.push(`${idx + 1}.
|
|
446
|
+
lines.push(...renderListItem(`${idx + 1}. `, item[1]));
|
|
442
447
|
});
|
|
443
448
|
lines.push('');
|
|
444
449
|
remaining = remaining.slice(olMatch[0].length);
|
|
@@ -485,6 +490,41 @@ export function tiptapToMarkdown(html: string): string {
|
|
|
485
490
|
);
|
|
486
491
|
}
|
|
487
492
|
|
|
493
|
+
/**
|
|
494
|
+
* Render a list item's HTML content as one or more markdown lines.
|
|
495
|
+
* Handles `<p>` paragraph breaks (blank line) and `<br>` hard breaks
|
|
496
|
+
* (two trailing spaces). Continuation lines are indented to keep them
|
|
497
|
+
* inside the list item.
|
|
498
|
+
*/
|
|
499
|
+
function renderListItem(prefix: string, html: string): string[] {
|
|
500
|
+
const indent = ' '.repeat(prefix.length);
|
|
501
|
+
|
|
502
|
+
// Split on </p><p> to detect paragraph breaks within the item
|
|
503
|
+
const paragraphs = html
|
|
504
|
+
.split(/<\/p>\s*<p[^>]*>/i)
|
|
505
|
+
.map((p) => p.replace(/^<p[^>]*>/i, '').replace(/<\/p>\s*$/i, ''));
|
|
506
|
+
|
|
507
|
+
const result: string[] = [];
|
|
508
|
+
paragraphs.forEach((paragraph, pIdx) => {
|
|
509
|
+
const inline = htmlToInline(paragraph).trim();
|
|
510
|
+
if (!inline) return;
|
|
511
|
+
|
|
512
|
+
// Each <br> already became " \n" in htmlToInline; split on it now.
|
|
513
|
+
const subLines = inline.split('\n');
|
|
514
|
+
subLines.forEach((sub, sIdx) => {
|
|
515
|
+
if (pIdx === 0 && sIdx === 0) {
|
|
516
|
+
result.push(prefix + sub);
|
|
517
|
+
} else {
|
|
518
|
+
// Blank line separator between paragraphs (sIdx === 0 means new paragraph)
|
|
519
|
+
if (sIdx === 0) result.push('');
|
|
520
|
+
result.push(indent + sub);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
return result.length > 0 ? result : [prefix];
|
|
526
|
+
}
|
|
527
|
+
|
|
488
528
|
// ─── Table helpers ───────────────────────────────────────
|
|
489
529
|
|
|
490
530
|
/** Split a GFM table row into trimmed cell strings (strips outer pipes). */
|
|
@@ -545,6 +585,16 @@ function inlineToHtml(text: string): string {
|
|
|
545
585
|
// Images first:  — must be before links so the `!` prefix is consumed
|
|
546
586
|
result = result.replace(RE_IMAGE, '<img alt="$1" src="$2">');
|
|
547
587
|
|
|
588
|
+
// Mentions: @[Display](scheme:id) — must run before links so the
|
|
589
|
+
// bracket+paren isn't consumed as a regular link. The input here has
|
|
590
|
+
// already been run through escapeHtml at the top of this function, so
|
|
591
|
+
// the captured groups are safe to interpolate directly.
|
|
592
|
+
result = result.replace(
|
|
593
|
+
RE_MENTION,
|
|
594
|
+
(_match, label, kind, id) =>
|
|
595
|
+
`<span data-mention="true" data-kind="${kind}" data-id="${id}" data-label="${label}" class="mention">@${label}</span>`,
|
|
596
|
+
);
|
|
597
|
+
|
|
548
598
|
// Links: [text](url)
|
|
549
599
|
result = result.replace(RE_LINK, '<a href="$2">$1</a>');
|
|
550
600
|
|
|
@@ -555,6 +605,10 @@ function inlineToHtml(text: string): string {
|
|
|
555
605
|
function htmlToInline(html: string): string {
|
|
556
606
|
let result = html;
|
|
557
607
|
|
|
608
|
+
// Soft line breaks — convert <br> to GFM hard-break syntax (two trailing
|
|
609
|
+
// spaces + newline) before stripping tags so the newline survives.
|
|
610
|
+
result = result.replace(/<br\s*\/?>/gi, ' \n');
|
|
611
|
+
|
|
558
612
|
// Strong
|
|
559
613
|
result = result.replace(RE_STRONG_TAG, '**$1**');
|
|
560
614
|
result = result.replace(RE_B_TAG, '**$1**');
|
|
@@ -570,6 +624,16 @@ function htmlToInline(html: string): string {
|
|
|
570
624
|
// Code
|
|
571
625
|
result = result.replace(RE_CODE_TAG, '`$1`');
|
|
572
626
|
|
|
627
|
+
// Mentions — match before the link handler so the span isn't stripped
|
|
628
|
+
// out as an unknown tag. Pull kind + id out of the data attributes.
|
|
629
|
+
result = result.replace(RE_MENTION_TAG, (match, _inner) => {
|
|
630
|
+
const kind = /data-kind="([^"]*)"/i.exec(match)?.[1] ?? '';
|
|
631
|
+
const id = /data-id="([^"]*)"/i.exec(match)?.[1] ?? '';
|
|
632
|
+
const label = /data-label="([^"]*)"/i.exec(match)?.[1] ?? '';
|
|
633
|
+
if (!kind || !id || !label) return match;
|
|
634
|
+
return `@[${label}](${kind}:${id})`;
|
|
635
|
+
});
|
|
636
|
+
|
|
573
637
|
// Links
|
|
574
638
|
result = result.replace(RE_A_TAG, '[$2]($1)');
|
|
575
639
|
|