@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.
Files changed (105) hide show
  1. package/dist/EditorContext.d.ts +65 -1
  2. package/dist/EditorContext.d.ts.map +1 -1
  3. package/dist/EditorContext.js +31 -4
  4. package/dist/EditorContext.js.map +1 -1
  5. package/dist/EditorShell.d.ts +101 -2
  6. package/dist/EditorShell.d.ts.map +1 -1
  7. package/dist/EditorShell.js +20 -8
  8. package/dist/EditorShell.js.map +1 -1
  9. package/dist/ImageNodeView.d.ts.map +1 -1
  10. package/dist/ImageNodeView.js +12 -2
  11. package/dist/ImageNodeView.js.map +1 -1
  12. package/dist/MediaBin.d.ts.map +1 -1
  13. package/dist/MediaBin.js +16 -1
  14. package/dist/MediaBin.js.map +1 -1
  15. package/dist/MentionExtension.d.ts +22 -0
  16. package/dist/MentionExtension.d.ts.map +1 -0
  17. package/dist/MentionExtension.js +242 -0
  18. package/dist/MentionExtension.js.map +1 -0
  19. package/dist/PreviewPanel.d.ts +3 -8
  20. package/dist/PreviewPanel.d.ts.map +1 -1
  21. package/dist/PreviewPanel.js +4 -282
  22. package/dist/PreviewPanel.js.map +1 -1
  23. package/dist/RawEditor.d.ts +8 -1
  24. package/dist/RawEditor.d.ts.map +1 -1
  25. package/dist/RawEditor.js +167 -30
  26. package/dist/RawEditor.js.map +1 -1
  27. package/dist/TemplateAnnotation.d.ts.map +1 -1
  28. package/dist/TemplateAnnotation.js +4 -2
  29. package/dist/TemplateAnnotation.js.map +1 -1
  30. package/dist/Toolbar.d.ts +7 -1
  31. package/dist/Toolbar.d.ts.map +1 -1
  32. package/dist/Toolbar.js +57 -18
  33. package/dist/Toolbar.js.map +1 -1
  34. package/dist/Tooltip.d.ts +10 -0
  35. package/dist/Tooltip.d.ts.map +1 -0
  36. package/dist/Tooltip.js +104 -0
  37. package/dist/Tooltip.js.map +1 -0
  38. package/dist/ViewSwitcher.d.ts +1 -1
  39. package/dist/ViewSwitcher.d.ts.map +1 -1
  40. package/dist/ViewSwitcher.js +10 -4
  41. package/dist/ViewSwitcher.js.map +1 -1
  42. package/dist/WysiwygEditor.d.ts +13 -2
  43. package/dist/WysiwygEditor.d.ts.map +1 -1
  44. package/dist/WysiwygEditor.js +239 -4
  45. package/dist/WysiwygEditor.js.map +1 -1
  46. package/dist/__tests__/detectMarkdown.test.d.ts +2 -0
  47. package/dist/__tests__/detectMarkdown.test.d.ts.map +1 -0
  48. package/dist/__tests__/detectMarkdown.test.js +69 -0
  49. package/dist/__tests__/detectMarkdown.test.js.map +1 -0
  50. package/dist/__tests__/fileKind.test.d.ts +2 -0
  51. package/dist/__tests__/fileKind.test.d.ts.map +1 -0
  52. package/dist/__tests__/fileKind.test.js +81 -0
  53. package/dist/__tests__/fileKind.test.js.map +1 -0
  54. package/dist/__tests__/tiptapBridge.test.js +36 -0
  55. package/dist/__tests__/tiptapBridge.test.js.map +1 -1
  56. package/dist/buildPreviewDoc.d.ts +22 -0
  57. package/dist/buildPreviewDoc.d.ts.map +1 -0
  58. package/dist/buildPreviewDoc.js +212 -0
  59. package/dist/buildPreviewDoc.js.map +1 -0
  60. package/dist/detectMarkdown.d.ts +20 -0
  61. package/dist/detectMarkdown.d.ts.map +1 -0
  62. package/dist/detectMarkdown.js +61 -0
  63. package/dist/detectMarkdown.js.map +1 -0
  64. package/dist/fileKind.d.ts +30 -0
  65. package/dist/fileKind.d.ts.map +1 -0
  66. package/dist/fileKind.js +123 -0
  67. package/dist/fileKind.js.map +1 -0
  68. package/dist/hooks/useFileDrop.d.ts.map +1 -1
  69. package/dist/hooks/useFileDrop.js +9 -7
  70. package/dist/hooks/useFileDrop.js.map +1 -1
  71. package/dist/index.d.ts +5 -1
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +6 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/mediaDragMime.d.ts +17 -0
  76. package/dist/mediaDragMime.d.ts.map +1 -0
  77. package/dist/mediaDragMime.js +22 -0
  78. package/dist/mediaDragMime.js.map +1 -0
  79. package/dist/tiptapBridge.d.ts.map +1 -1
  80. package/dist/tiptapBridge.js +58 -2
  81. package/dist/tiptapBridge.js.map +1 -1
  82. package/package.json +9 -7
  83. package/src/EditorContext.tsx +106 -3
  84. package/src/EditorShell.tsx +195 -15
  85. package/src/ImageNodeView.tsx +15 -2
  86. package/src/MediaBin.tsx +23 -1
  87. package/src/MentionExtension.tsx +258 -0
  88. package/src/PreviewPanel.tsx +5 -333
  89. package/src/RawEditor.tsx +193 -37
  90. package/src/TemplateAnnotation.ts +4 -2
  91. package/src/Toolbar.tsx +111 -48
  92. package/src/Tooltip.tsx +124 -0
  93. package/src/ViewSwitcher.tsx +15 -5
  94. package/src/WysiwygEditor.tsx +270 -5
  95. package/src/__tests__/detectMarkdown.test.ts +88 -0
  96. package/src/__tests__/fileKind.test.ts +96 -0
  97. package/src/__tests__/tiptapBridge.test.ts +44 -0
  98. package/src/buildPreviewDoc.ts +254 -0
  99. package/src/detectMarkdown.ts +62 -0
  100. package/src/fileKind.ts +134 -0
  101. package/src/hooks/useFileDrop.ts +10 -6
  102. package/src/index.ts +14 -0
  103. package/src/mediaDragMime.ts +32 -0
  104. package/src/styles/editor.css +214 -8
  105. package/src/tiptapBridge.ts +66 -2
@@ -5,8 +5,20 @@
5
5
  /* ─── Shell ──────────────────────────────────────────── */
6
6
 
7
7
  .squisq-editor-shell {
8
- font-family:
9
- -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
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::after {
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: 180px;
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: #eeecea;
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 0 8px rgba(0, 0, 0, 0.06);
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
+ }
@@ -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('- ' + htmlToInline(item[1].replace(/<\/?p>/g, '')));
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}. ` + htmlToInline(item[1].replace(/<\/?p>/g, '')));
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: ![alt](src) — 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