@jotx-labs/adapters 2.2.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.
@@ -0,0 +1,1020 @@
1
+ "use strict";
2
+ /**
3
+ * jotx 2.0 TiptapAdapter - Bridge between @jotx/core and Tiptap editor
4
+ *
5
+ * Converts:
6
+ * - Core BlockNode (jotx 2.0 format) ↔ Tiptap JSON
7
+ *
8
+ * All text content uses properties.text (consistent with jotx 2.0 syntax)
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.TiptapAdapter = void 0;
12
+ const core_1 = require("@jotx/core");
13
+ /**
14
+ * jotx 2.0 TiptapAdapter - Updated for consistent property-based syntax
15
+ */
16
+ class TiptapAdapter {
17
+ /**
18
+ * Parse Markdown-style formatting in text and convert to Tiptap content with marks
19
+ * Handles: **bold**, *italic*, ~~strike~~, `code`, [link](url), ==highlight==
20
+ */
21
+ parseFormattedText(text) {
22
+ if (!text)
23
+ return [];
24
+ const nodes = [];
25
+ let pos = 0;
26
+ // Regex patterns for inline formatting (order matters!)
27
+ // Using .+? (non-greedy) instead of [^x]+ to allow nested formatting
28
+ const patterns = [
29
+ { type: 'code', regex: /`([^`]+)`/g, marks: ['code'] },
30
+ { type: 'jotxlink', regex: /\[\[([^\]]+)\]\]/g, marks: [] }, // [[target]] -> jotxlink node
31
+ { type: 'link', regex: /\[([^\]]+)\]\(([^)]+)\)/g, marks: [] }, // Special handling
32
+ { type: 'bold', regex: /\*\*(.+?)\*\*/g, marks: ['bold'] }, // Changed [^*]+ to .+?
33
+ { type: 'italic', regex: /\*(.+?)\*/g, marks: ['italic'] }, // Changed [^*]+ to .+?
34
+ { type: 'strike', regex: /~~(.+?)~~/g, marks: ['strike'] }, // Changed [^~]+ to .+?
35
+ { type: 'highlight', regex: /==(.+?)==/g, marks: ['highlight'] }, // Changed [^=]+ to .+?
36
+ { type: 'textStyle', regex: /<span style="color:([^"]+)">(.+?)<\/span>/g, marks: [] }
37
+ ];
38
+ const matches = [];
39
+ for (const pattern of patterns) {
40
+ const regex = new RegExp(pattern.regex.source, 'g');
41
+ let match;
42
+ while ((match = regex.exec(text)) !== null) {
43
+ if (pattern.type === 'link') {
44
+ matches.push({
45
+ start: match.index,
46
+ end: match.index + match[0].length,
47
+ text: match[1], // Link text
48
+ marks: [{ type: 'link', attrs: { href: match[2] } }]
49
+ });
50
+ }
51
+ else if (pattern.type === 'jotxlink') {
52
+ // [[target|text]] or [[target]]
53
+ const content = match[1];
54
+ const parts = content.split('|');
55
+ const target = parts[0];
56
+ const text = parts[1] || target;
57
+ // We represent inline wikilink as a 'jotxlink' NODE, not a mark.
58
+ // Note: This requires the schema to allow 'jotxlink' inline.
59
+ matches.push({
60
+ start: match.index,
61
+ end: match.index + match[0].length,
62
+ text: text,
63
+ marks: [], // No marks, it's a node (handled later? No, this array is matches with marks)
64
+ // Wait, parseFormattedText returns TiptapNode[].
65
+ // I need to intercept this in the loop below.
66
+ // I'll attach a special 'nodeType' property to the match object if possible?
67
+ // The Match interface doesn't support it.
68
+ // I'll update Match interface? No, I'll use 'marks' to signal it.
69
+ // Actually, I can add a custom type to marks: { type: 'jotxlink', attrs: { target } }
70
+ // But 'jotxlink' is a node type, not mark type.
71
+ // If I use a mark, I have to ensure Tiptap has a mark for it.
72
+ // User wants 'jotxlink' block.
73
+ // If it's inline, it must be inline node.
74
+ // I'll cheat: Use a special mark 'jotxlink_mark' and convert it to node in the loop?
75
+ // Or better: Modify Match interface.
76
+ });
77
+ // Wait, I can't modify Match interface easily it's inside the method.
78
+ // Let's look at the existing code again relative to where I am inserting.
79
+ // I will use a dummy mark { type: 'INTERNAL_JOTXLINK', attrs: { target } }
80
+ // and then in the loop convert it to a node.
81
+ matches.push({
82
+ start: match.index,
83
+ end: match.index + match[0].length,
84
+ text: text,
85
+ marks: [{ type: 'INTERNAL_JOTXLINK', attrs: { target } }]
86
+ });
87
+ }
88
+ else if (pattern.type === 'textStyle') {
89
+ matches.push({
90
+ start: match.index,
91
+ end: match.index + match[0].length,
92
+ text: match[2], // Content
93
+ marks: [{ type: 'textStyle', attrs: { color: match[1] } }]
94
+ });
95
+ }
96
+ else {
97
+ matches.push({
98
+ start: match.index,
99
+ end: match.index + match[0].length,
100
+ text: match[1], // Content inside marks
101
+ marks: pattern.marks.map(type => ({ type }))
102
+ });
103
+ }
104
+ }
105
+ }
106
+ // Sort matches by start position, then by length (longer first for preference)
107
+ matches.sort((a, b) => {
108
+ if (a.start !== b.start)
109
+ return a.start - b.start;
110
+ return (b.end - b.start) - (a.end - a.start); // Prefer longer matches
111
+ });
112
+ // Remove overlapping matches (keep first/longer match)
113
+ const filteredMatches = [];
114
+ for (const match of matches) {
115
+ const overlaps = filteredMatches.some(existing => (match.start >= existing.start && match.start < existing.end) ||
116
+ (match.end > existing.start && match.end <= existing.end) ||
117
+ (match.start <= existing.start && match.end >= existing.end));
118
+ if (!overlaps) {
119
+ filteredMatches.push(match);
120
+ }
121
+ }
122
+ // Sort filtered matches by start position
123
+ filteredMatches.sort((a, b) => a.start - b.start);
124
+ // Build content nodes
125
+ let lastPos = 0;
126
+ for (const match of filteredMatches) {
127
+ // Add plain text before this match
128
+ if (match.start > lastPos) {
129
+ const plainText = text.substring(lastPos, match.start);
130
+ if (plainText) {
131
+ nodes.push({ type: 'text', text: plainText });
132
+ }
133
+ }
134
+ // Check for internal jotxlink mark
135
+ const jotxLinkMark = match.marks.find(m => m.type === 'INTERNAL_JOTXLINK');
136
+ if (jotxLinkMark) {
137
+ nodes.push({
138
+ type: 'jotxlink',
139
+ attrs: {
140
+ target: jotxLinkMark.attrs?.target,
141
+ text: match.text
142
+ }
143
+ });
144
+ }
145
+ else {
146
+ // Add formatted text
147
+ nodes.push({
148
+ type: 'text',
149
+ text: match.text,
150
+ marks: match.marks
151
+ });
152
+ }
153
+ lastPos = match.end;
154
+ }
155
+ // Add remaining plain text
156
+ if (lastPos < text.length) {
157
+ const plainText = text.substring(lastPos);
158
+ if (plainText) {
159
+ nodes.push({ type: 'text', text: plainText });
160
+ }
161
+ }
162
+ // If no matches found, return plain text
163
+ if (nodes.length === 0 && text) {
164
+ nodes.push({ type: 'text', text });
165
+ }
166
+ return nodes;
167
+ }
168
+ /**
169
+ * Convert a single BlockNode to Tiptap node
170
+ * Reads from properties.text (jotx 2.0 format) and parses Markdown-style formatting
171
+ */
172
+ blockToTiptap(block) {
173
+ const text = block.properties?.text || '';
174
+ switch (block.type) {
175
+ // ========== TEXT BLOCKS ==========
176
+ case 'heading':
177
+ const level = block.properties?.level ? parseInt(String(block.properties.level)) : 1;
178
+ return {
179
+ type: 'heading',
180
+ attrs: { level, blockId: block.id },
181
+ content: this.parseFormattedText(text)
182
+ };
183
+ case 'list':
184
+ const listType = block.properties?.listtype === 'numbered' || block.properties?.type === 'numbered' ? 'orderedList' : 'bulletList';
185
+ return {
186
+ type: listType,
187
+ attrs: { blockId: block.id },
188
+ content: (block.children || []).map(b => this.blockToTiptap(b))
189
+ };
190
+ case 'listitem':
191
+ return {
192
+ type: 'listItem',
193
+ attrs: { blockId: block.id },
194
+ content: [{
195
+ type: 'paragraph',
196
+ content: this.parseFormattedText(text)
197
+ }, ...(block.children || []).map(b => this.blockToTiptap(b))]
198
+ };
199
+ case 'checklist':
200
+ return {
201
+ type: 'taskList',
202
+ attrs: { blockId: block.id },
203
+ content: (block.children || []).map(b => this.blockToTiptap(b))
204
+ };
205
+ case 'taskitem':
206
+ return {
207
+ type: 'taskItem',
208
+ attrs: {
209
+ checked: block.properties?.checked === true || block.properties?.checked === 'true',
210
+ blockId: block.id
211
+ },
212
+ content: [{
213
+ type: 'paragraph',
214
+ content: this.parseFormattedText(text)
215
+ }, ...(block.children || []).map(b => this.blockToTiptap(b))]
216
+ };
217
+ case 'paragraph':
218
+ return {
219
+ type: 'paragraph',
220
+ attrs: { blockId: block.id },
221
+ content: this.parseFormattedText(text)
222
+ };
223
+ case 'quote':
224
+ return {
225
+ type: 'blockquote',
226
+ attrs: { blockId: block.id },
227
+ content: [{
228
+ type: 'paragraph',
229
+ content: this.parseFormattedText(text)
230
+ }]
231
+ };
232
+ // ========== CODE & MEDIA BLOCKS ==========
233
+ case 'code':
234
+ return {
235
+ type: 'codeBlock',
236
+ attrs: {
237
+ language: block.properties?.language || 'plaintext',
238
+ blockId: block.id
239
+ },
240
+ content: [{ type: 'text', text }]
241
+ };
242
+ case 'codereference':
243
+ return {
244
+ type: 'codeReference',
245
+ attrs: {
246
+ path: block.properties?.path || '',
247
+ lines: block.properties?.lines || '',
248
+ language: block.properties?.language || 'typescript',
249
+ blockId: block.id
250
+ },
251
+ content: text ? [{ type: 'text', text }] : []
252
+ };
253
+ case 'math':
254
+ return {
255
+ type: 'math',
256
+ attrs: {
257
+ src: block.properties?.src || '',
258
+ display: block.properties?.display || 'block',
259
+ blockId: block.id
260
+ }
261
+ };
262
+ case 'mermaid':
263
+ return {
264
+ type: 'mermaid',
265
+ attrs: {
266
+ src: block.properties?.src || '',
267
+ showSource: block.properties?.showsource === 'true',
268
+ blockId: block.id
269
+ }
270
+ };
271
+ case 'chart':
272
+ return {
273
+ type: 'chart',
274
+ attrs: {
275
+ chartType: block.properties?.charttype || 'bar',
276
+ json: block.properties?.json || '',
277
+ showSource: block.properties?.showsource === 'true',
278
+ blockId: block.id
279
+ }
280
+ };
281
+ case 'datetime':
282
+ return {
283
+ type: 'datetime',
284
+ attrs: {
285
+ value: block.properties?.value || '',
286
+ mode: block.properties?.mode || 'datetime',
287
+ blockId: block.id
288
+ }
289
+ };
290
+ case 'floatimage':
291
+ return {
292
+ type: 'floatImageBlock',
293
+ attrs: {
294
+ src: block.properties?.src || '',
295
+ alt: block.properties?.alt || '',
296
+ width: block.properties?.width ? parseInt(block.properties.width) : 400,
297
+ height: block.properties?.height || 'auto',
298
+ float: block.properties?.float || 'left',
299
+ caption: block.properties?.caption || '',
300
+ title: block.properties?.title || '',
301
+ description: block.properties?.description || '',
302
+ image: block.properties?.image || '',
303
+ siteName: block.properties?.sitename || '',
304
+ blockId: block.id
305
+ },
306
+ content: (block.children || []).map(child => this.blockToTiptap(child))
307
+ };
308
+ case 'jotxlink':
309
+ return {
310
+ type: 'jotxlink',
311
+ attrs: {
312
+ target: block.properties?.target || '',
313
+ text: block.properties?.text || '',
314
+ blockId: block.id
315
+ }
316
+ };
317
+ case 'blockref':
318
+ return {
319
+ type: 'blockref',
320
+ attrs: {
321
+ ref: block.properties?.ref || '',
322
+ blockId: block.id
323
+ }
324
+ };
325
+ // ========== MISSING BLOCKS (Added for Roundtrip Fix) ==========
326
+ case 'video':
327
+ const srcOrUrl = block.properties?.src || block.properties?.url || '';
328
+ return {
329
+ type: 'videoBlock',
330
+ attrs: {
331
+ src: srcOrUrl,
332
+ url: srcOrUrl,
333
+ title: block.properties?.title || '',
334
+ type: block.properties?.type || 'youtube',
335
+ width: block.properties?.width || 800,
336
+ height: block.properties?.height || 450,
337
+ controls: block.properties?.controls === 'true' ? 'true' : 'false',
338
+ autoplay: block.properties?.autoplay === 'true' ? 'true' : 'false',
339
+ muted: block.properties?.muted === 'true' ? 'true' : 'false',
340
+ loop: block.properties?.loop === 'true' ? 'true' : 'false',
341
+ blockId: block.id
342
+ }
343
+ };
344
+ case 'link':
345
+ return {
346
+ type: 'linkBlock',
347
+ attrs: {
348
+ href: block.properties?.url || block.properties?.href || '',
349
+ blockId: block.id
350
+ },
351
+ content: [{ type: 'text', text: block.properties?.text || '' }]
352
+ };
353
+ case 'divider':
354
+ return {
355
+ type: 'horizontalRule',
356
+ attrs: { blockId: block.id }
357
+ };
358
+ case 'attach':
359
+ return {
360
+ type: 'attach',
361
+ attrs: {
362
+ path: block.properties?.path || '',
363
+ text: block.properties?.text || block.properties?.name || '',
364
+ blockId: block.id
365
+ },
366
+ content: (block.children || []).map(child => this.blockToTiptap(child))
367
+ };
368
+ case 'image':
369
+ return {
370
+ type: 'imageBlock',
371
+ attrs: {
372
+ src: block.properties?.src || '',
373
+ alt: block.properties?.alt || '',
374
+ caption: block.properties?.caption || '',
375
+ width: block.properties?.width ? parseInt(block.properties.width) : 800,
376
+ align: block.properties?.align || 'center',
377
+ blockId: block.id
378
+ }
379
+ };
380
+ // ========== PROPERTIES TABLE (2-column key-value table) ==========
381
+ case 'properties':
382
+ // Properties table has automatic "Key", "Value", and "Type" headers
383
+ const propsHeaderRow = {
384
+ type: 'tableRow',
385
+ content: [
386
+ {
387
+ type: 'tableHeader',
388
+ content: [{
389
+ type: 'paragraph',
390
+ content: [{
391
+ type: 'text',
392
+ text: 'Key',
393
+ marks: [{ type: 'bold' }]
394
+ }]
395
+ }]
396
+ },
397
+ {
398
+ type: 'tableHeader',
399
+ content: [{
400
+ type: 'paragraph',
401
+ content: [{
402
+ type: 'text',
403
+ text: 'Value',
404
+ marks: [{ type: 'bold' }]
405
+ }]
406
+ }]
407
+ },
408
+ {
409
+ type: 'tableHeader',
410
+ content: [{
411
+ type: 'paragraph',
412
+ content: [{
413
+ type: 'text',
414
+ text: 'Type',
415
+ marks: [{ type: 'bold' }]
416
+ }]
417
+ }]
418
+ }
419
+ ]
420
+ };
421
+ // Convert property rows
422
+ const propRows = (block.children || []).map(prop => ({
423
+ type: 'tableRow',
424
+ attrs: { blockId: prop.id },
425
+ content: [
426
+ {
427
+ type: 'tableCell',
428
+ content: [{
429
+ type: 'paragraph',
430
+ content: prop.properties?.key ? [{
431
+ type: 'text',
432
+ text: prop.properties.key
433
+ }] : []
434
+ }]
435
+ },
436
+ {
437
+ type: 'tableCell',
438
+ content: [{
439
+ type: 'paragraph',
440
+ content: prop.properties?.value ? [{
441
+ type: 'text',
442
+ text: prop.properties.value
443
+ }] : []
444
+ }]
445
+ },
446
+ {
447
+ type: 'tableCell',
448
+ content: [{
449
+ type: 'paragraph',
450
+ content: prop.properties?.type ? [{
451
+ type: 'text',
452
+ text: prop.properties.type
453
+ }] : [{
454
+ type: 'text',
455
+ text: 'text'
456
+ }]
457
+ }]
458
+ }
459
+ ]
460
+ }));
461
+ return {
462
+ type: 'table',
463
+ attrs: { blockId: block.id },
464
+ content: [propsHeaderRow, ...propRows]
465
+ };
466
+ // ========== CONTAINER BLOCKS (Added for Roundtrip Fix) ==========
467
+ case 'toggle':
468
+ return {
469
+ type: 'toggle',
470
+ attrs: {
471
+ title: block.properties?.title || 'Toggle',
472
+ collapsed: block.properties?.collapsed === 'true',
473
+ blockId: block.id
474
+ },
475
+ content: (block.children || []).map(child => this.blockToTiptap(child))
476
+ };
477
+ case 'section':
478
+ return {
479
+ type: 'section',
480
+ attrs: {
481
+ title: block.properties?.title || 'Section',
482
+ collapsed: block.properties?.collapsed === 'true',
483
+ blockId: block.id
484
+ },
485
+ content: (block.children || []).map(child => this.blockToTiptap(child))
486
+ };
487
+ case 'callout':
488
+ // Callout in AST has 'text' property, but Tiptap node expects content children (paragraph)
489
+ return {
490
+ type: 'callout',
491
+ attrs: {
492
+ variant: block.properties?.variant || 'info',
493
+ blockId: block.id
494
+ },
495
+ content: [{
496
+ type: 'paragraph',
497
+ content: [{ type: 'text', text: block.properties?.text || '' }]
498
+ }]
499
+ };
500
+ // ========== TABLE (complex nested structure) ==========
501
+ case 'table':
502
+ // Parse column names from the columns property
503
+ const columnNames = (block.properties?.columns || '')
504
+ .split(',')
505
+ .map((c) => c.trim())
506
+ .filter(Boolean);
507
+ // Create header row with bold, capitalized column names
508
+ const headerRow = {
509
+ type: 'tableRow',
510
+ content: columnNames.map((name) => ({
511
+ type: 'tableHeader',
512
+ content: [{
513
+ type: 'paragraph',
514
+ content: [{
515
+ type: 'text',
516
+ text: name.charAt(0).toUpperCase() + name.slice(1),
517
+ marks: [{ type: 'bold' }]
518
+ }]
519
+ }]
520
+ }))
521
+ };
522
+ // Convert data rows
523
+ const dataRows = (block.children || []).map(row => ({
524
+ type: 'tableRow',
525
+ attrs: { blockId: row.id },
526
+ content: (row.children || []).map(cell => ({
527
+ type: 'tableCell',
528
+ attrs: { blockId: cell.id },
529
+ content: (cell.children || []).map(b => this.blockToTiptap(b))
530
+ }))
531
+ }));
532
+ return {
533
+ type: 'table',
534
+ attrs: { blockId: block.id },
535
+ content: [headerRow, ...dataRows]
536
+ };
537
+ default:
538
+ // Fallback: render as paragraph
539
+ return {
540
+ type: 'paragraph',
541
+ attrs: { blockId: block.id },
542
+ content: text ? [{ type: 'text', text }] : []
543
+ };
544
+ }
545
+ }
546
+ /**
547
+ * Convert Tiptap node to BlockNode
548
+ * Writes to properties.text (jotx 2.0 format)
549
+ * (Reverse direction - for saving edits from Tiptap)
550
+ */
551
+ tiptapToBlock(node, index) {
552
+ // Preserve existing ID or generate new one
553
+ const blockId = node.attrs?.blockId || (0, core_1.generateBlockId)();
554
+ const attrs = node.attrs || {};
555
+ const content = node.content || [];
556
+ switch (node.type) {
557
+ // ========== TEXT BLOCKS ==========
558
+ case 'heading':
559
+ const level = attrs.level || 1;
560
+ return {
561
+ id: blockId,
562
+ type: 'heading',
563
+ properties: {
564
+ text: this.extractText(node),
565
+ level: level
566
+ },
567
+ children: undefined
568
+ };
569
+ case 'paragraph':
570
+ return {
571
+ id: blockId,
572
+ type: 'paragraph',
573
+ properties: {
574
+ text: this.extractText(node)
575
+ },
576
+ children: undefined
577
+ };
578
+ case 'blockquote':
579
+ return {
580
+ id: blockId,
581
+ type: 'quote',
582
+ properties: {
583
+ text: this.extractText(node)
584
+ },
585
+ children: undefined
586
+ };
587
+ // ========== CODE & MEDIA BLOCKS ==========
588
+ case 'codeBlock':
589
+ return {
590
+ id: blockId,
591
+ type: 'code',
592
+ properties: {
593
+ language: attrs.language || 'plaintext',
594
+ text: this.extractText(node)
595
+ },
596
+ children: undefined
597
+ };
598
+ case 'math':
599
+ return {
600
+ id: blockId,
601
+ type: 'math',
602
+ properties: {
603
+ src: attrs.src || this.extractText(node),
604
+ display: attrs.display || 'block'
605
+ },
606
+ children: undefined
607
+ };
608
+ case 'image':
609
+ case 'imageBlock':
610
+ return {
611
+ id: blockId,
612
+ type: 'image',
613
+ properties: {
614
+ src: this.convertWebviewUriToRelativePath(attrs.src || ''),
615
+ alt: attrs.alt || '',
616
+ width: attrs.width || 800,
617
+ height: attrs.height || 'auto',
618
+ align: attrs.align || 'center',
619
+ caption: attrs.caption || ''
620
+ },
621
+ children: undefined
622
+ };
623
+ case 'floatImageBlock':
624
+ // Float image is now a CONTAINER with content children
625
+ return {
626
+ id: blockId,
627
+ type: 'floatimage',
628
+ properties: {
629
+ src: this.convertWebviewUriToRelativePath(attrs.src || ''),
630
+ alt: attrs.alt || '',
631
+ width: attrs.width || 400,
632
+ height: attrs.height || 'auto',
633
+ float: attrs.float || 'left',
634
+ caption: attrs.caption || '',
635
+ title: attrs.title || '',
636
+ description: attrs.description || '',
637
+ image: attrs.image || '',
638
+ sitename: attrs.siteName || ''
639
+ },
640
+ children: content.map(child => this.tiptapToBlock(child)) || []
641
+ };
642
+ case 'videoBlock':
643
+ return {
644
+ id: blockId,
645
+ type: 'video',
646
+ properties: {
647
+ type: attrs.type || 'youtube',
648
+ src: this.convertWebviewUriToRelativePath(attrs.src || attrs.url || ''),
649
+ title: attrs.title || '',
650
+ width: attrs.width || 800,
651
+ height: attrs.height || 450,
652
+ controls: attrs.controls || 'true',
653
+ autoplay: attrs.autoplay || 'false',
654
+ muted: attrs.muted || 'false',
655
+ loop: attrs.loop || 'false'
656
+ },
657
+ children: undefined
658
+ };
659
+ case 'codeReference':
660
+ return {
661
+ id: blockId,
662
+ type: 'codereference',
663
+ properties: {
664
+ path: attrs.path || '',
665
+ lines: attrs.lines || '',
666
+ language: attrs.language || 'typescript',
667
+ text: this.extractText(node)
668
+ },
669
+ children: undefined
670
+ };
671
+ // ========== CONTAINER BLOCKS ==========
672
+ case 'callout':
673
+ return {
674
+ id: blockId,
675
+ type: 'callout',
676
+ properties: {
677
+ variant: attrs.variant || 'info',
678
+ text: this.extractText(node)
679
+ },
680
+ children: undefined
681
+ };
682
+ case 'toggle':
683
+ return {
684
+ id: blockId,
685
+ type: 'toggle',
686
+ properties: {
687
+ title: attrs.title || 'Toggle',
688
+ collapsed: attrs.collapsed ? 'true' : 'false'
689
+ },
690
+ children: content.map((child, i) => this.tiptapToBlock(child))
691
+ };
692
+ case 'section':
693
+ return {
694
+ id: blockId,
695
+ type: 'section',
696
+ properties: {
697
+ title: attrs.title || 'Section',
698
+ collapsed: attrs.collapsed ? 'true' : 'false'
699
+ },
700
+ children: content.map((child, i) => this.tiptapToBlock(child))
701
+ };
702
+ // ========== LIST BLOCKS (write to children in jotx 2.0) ==========
703
+ case 'bulletList':
704
+ case 'orderedList':
705
+ return {
706
+ id: blockId,
707
+ type: 'list',
708
+ properties: {
709
+ listtype: node.type === 'orderedList' ? 'numbered' : 'bulleted'
710
+ },
711
+ children: content.map(child => this.tiptapToBlock(child))
712
+ };
713
+ case 'listItem':
714
+ // Segregate content: first paragraph is 'text', rest are children
715
+ let itemText = '';
716
+ const childBlocks = [];
717
+ let hasFoundText = false;
718
+ if (content) {
719
+ for (const child of content) {
720
+ // The first paragraph is considered the item's main text
721
+ if (!hasFoundText && child.type === 'paragraph') {
722
+ itemText = this.extractText(child);
723
+ hasFoundText = true;
724
+ }
725
+ else {
726
+ // Any other blocks (sub-lists, etc.) are children
727
+ childBlocks.push(this.tiptapToBlock(child));
728
+ }
729
+ }
730
+ }
731
+ return {
732
+ type: 'listitem',
733
+ id: blockId,
734
+ properties: {
735
+ text: itemText
736
+ },
737
+ children: childBlocks.length > 0 ? childBlocks : undefined
738
+ };
739
+ case 'taskList':
740
+ return {
741
+ type: 'checklist',
742
+ id: blockId,
743
+ properties: {},
744
+ children: content.map(child => this.tiptapToBlock(child))
745
+ };
746
+ case 'taskItem':
747
+ // Same logic as listItem
748
+ let taskText = '';
749
+ const taskChildren = [];
750
+ let hasFoundTaskText = false;
751
+ if (content) {
752
+ for (const child of content) {
753
+ if (!hasFoundTaskText && child.type === 'paragraph') {
754
+ taskText = this.extractText(child);
755
+ hasFoundTaskText = true;
756
+ }
757
+ else {
758
+ taskChildren.push(this.tiptapToBlock(child));
759
+ }
760
+ }
761
+ }
762
+ return {
763
+ type: 'taskitem',
764
+ id: blockId,
765
+ properties: {
766
+ checked: attrs.checked ? 'true' : 'false',
767
+ text: taskText
768
+ },
769
+ children: taskChildren.length > 0 ? taskChildren : undefined
770
+ };
771
+ // ========== SEPARATOR & EMBED BLOCKS ==========
772
+ case 'horizontalRule':
773
+ return {
774
+ id: blockId,
775
+ type: 'divider',
776
+ properties: {},
777
+ children: undefined
778
+ };
779
+ case 'linkBlock':
780
+ return {
781
+ id: blockId,
782
+ type: 'link',
783
+ properties: {
784
+ text: this.extractText(node)
785
+ },
786
+ children: undefined
787
+ };
788
+ case 'attach':
789
+ return {
790
+ id: blockId,
791
+ type: 'attach',
792
+ properties: {
793
+ path: attrs.path || '',
794
+ text: this.extractText(node)
795
+ },
796
+ children: content.map(child => this.tiptapToBlock(child))
797
+ };
798
+ // ========== RICH / INTERACTIVE BLOCKS ==========
799
+ case 'mermaid':
800
+ return {
801
+ id: blockId,
802
+ type: 'mermaid',
803
+ properties: {
804
+ src: attrs.src || this.extractText(node),
805
+ showsource: attrs.showSource ? 'true' : 'false'
806
+ },
807
+ children: undefined
808
+ };
809
+ case 'chart':
810
+ return {
811
+ id: blockId,
812
+ type: 'chart',
813
+ properties: {
814
+ charttype: attrs.chartType || 'bar',
815
+ json: attrs.json || this.extractText(node),
816
+ showsource: attrs.showSource ? 'true' : 'false'
817
+ },
818
+ children: undefined
819
+ };
820
+ case 'datetime':
821
+ return {
822
+ id: blockId,
823
+ type: 'datetime',
824
+ properties: {
825
+ value: attrs.value || '',
826
+ mode: attrs.mode || 'datetime',
827
+ timezone: attrs.timezone || ''
828
+ },
829
+ children: undefined
830
+ };
831
+ case 'jotxlink':
832
+ return {
833
+ id: blockId,
834
+ type: 'jotxlink',
835
+ properties: {
836
+ target: attrs.target || '',
837
+ text: attrs.text || ''
838
+ },
839
+ children: undefined
840
+ };
841
+ case 'blockref':
842
+ return {
843
+ id: blockId,
844
+ type: 'blockref',
845
+ properties: {
846
+ ref: attrs.ref || ''
847
+ },
848
+ children: undefined
849
+ };
850
+ // ========== TABLE (complex nested structure) ==========
851
+ case 'table':
852
+ const allRows = content;
853
+ const headerRow = allRows[0];
854
+ const dataRows = allRows.slice(1); // Skip header row
855
+ // Extract column names from header row
856
+ const columnNames = (headerRow?.content || []).map(cell => {
857
+ // Extract text from the first paragraph in the cell
858
+ const firstPara = cell.content?.[0];
859
+ const text = firstPara?.content?.[0]?.text || '';
860
+ return text.trim();
861
+ }).filter(Boolean);
862
+ // Check if this is a properties table (Key, Value, Type columns)
863
+ const isPropertiesTable = columnNames.length === 3 &&
864
+ columnNames[0] === 'Key' &&
865
+ columnNames[1] === 'Value' &&
866
+ columnNames[2] === 'Type';
867
+ if (isPropertiesTable) {
868
+ // Convert to properties block
869
+ const props = dataRows.map((rowNode, i) => {
870
+ const cells = rowNode.content || [];
871
+ const keyCell = cells[0];
872
+ const valueCell = cells[1];
873
+ const typeCell = cells[2];
874
+ // Extract text from cells
875
+ const key = keyCell?.content?.[0]?.content?.[0]?.text || '';
876
+ const value = valueCell?.content?.[0]?.content?.[0]?.text || '';
877
+ const type = typeCell?.content?.[0]?.content?.[0]?.text || 'text';
878
+ return {
879
+ type: 'property',
880
+ id: rowNode.attrs?.blockId || `prop_${i + 1}`,
881
+ properties: {
882
+ key,
883
+ value,
884
+ type
885
+ },
886
+ children: undefined
887
+ };
888
+ });
889
+ return {
890
+ id: blockId,
891
+ type: 'properties',
892
+ properties: {},
893
+ children: props
894
+ };
895
+ }
896
+ // Regular table
897
+ const columns = columnNames.join(', ');
898
+ // Convert data rows to jotx 2.0 structure (rows/cells as children)
899
+ const rows = dataRows.map((rowNode, i) => ({
900
+ type: 'row',
901
+ id: rowNode.attrs?.blockId || `row_${i + 1}`,
902
+ properties: {},
903
+ children: (rowNode.content || []).map((cellNode, j) => ({
904
+ type: 'cell',
905
+ id: cellNode.attrs?.blockId || `cell_${j + 1}`,
906
+ properties: {},
907
+ children: (cellNode.content || []).map((b) => this.tiptapToBlock(b))
908
+ }))
909
+ }));
910
+ return {
911
+ id: blockId,
912
+ type: 'table',
913
+ properties: {
914
+ columns
915
+ },
916
+ children: rows
917
+ };
918
+ default:
919
+ // Fallback: convert to paragraph
920
+ return {
921
+ id: blockId,
922
+ type: 'paragraph',
923
+ properties: {
924
+ text: this.extractText(node)
925
+ },
926
+ children: undefined
927
+ };
928
+ }
929
+ }
930
+ /**
931
+ * Convert Tiptap document to BlockNodes
932
+ */
933
+ tiptapToBlocks(doc) {
934
+ return (doc.content || []).map((node) => this.tiptapToBlock(node));
935
+ }
936
+ /**
937
+ * Extract text content from Tiptap node WITH formatting (Markdown-style)
938
+ * Converts Tiptap marks to Markdown syntax for storage in jotx files
939
+ */
940
+ extractText(node) {
941
+ // Direct text node with marks
942
+ if (node.text !== undefined) {
943
+ let text = node.text;
944
+ // Apply marks in the correct order (innermost to outermost)
945
+ if (node.marks && node.marks.length > 0) {
946
+ // Sort marks for consistent nesting: code > link > strong > em > strike > highlight
947
+ const sortedMarks = [...node.marks].sort((a, b) => {
948
+ const order = { code: 1, link: 2, bold: 3, strong: 3, italic: 4, em: 4, strike: 5, highlight: 6 };
949
+ return (order[a.type] || 99) - (order[b.type] || 99);
950
+ });
951
+ for (const mark of sortedMarks) {
952
+ switch (mark.type) {
953
+ case 'code':
954
+ text = `\`${text}\``;
955
+ break;
956
+ case 'link':
957
+ const href = mark.attrs?.href || '';
958
+ text = `[${text}](${href})`;
959
+ break;
960
+ case 'bold':
961
+ case 'strong':
962
+ text = `**${text}**`;
963
+ break;
964
+ case 'italic':
965
+ case 'em':
966
+ text = `*${text}*`;
967
+ break;
968
+ case 'strike':
969
+ text = `~~${text}~~`;
970
+ break;
971
+ case 'highlight':
972
+ text = `==${text}==`;
973
+ break;
974
+ }
975
+ }
976
+ }
977
+ return text;
978
+ }
979
+ // Recurse through content
980
+ if (!node.content)
981
+ return '';
982
+ return node.content.map(child => this.extractText(child)).join('');
983
+ }
984
+ /**
985
+ * Convert webview URI back to relative path for serialization
986
+ * Examples:
987
+ * vscode-webview://.../ attachments/image.png → attachments/image.png
988
+ * attachments/image.png → attachments/image.png (no change)
989
+ * https://... → https://... (no change for external URLs)
990
+ */
991
+ convertWebviewUriToRelativePath(uri) {
992
+ if (!uri)
993
+ return '';
994
+ // Already a relative path? Return as-is
995
+ if (uri.startsWith('attachments/'))
996
+ return uri;
997
+ // External URL (http/https)? Return as-is
998
+ if (uri.startsWith('http://') || uri.startsWith('https://'))
999
+ return uri;
1000
+ // Webview URI? Extract filename from path
1001
+ if (uri.includes('vscode-webview://') || uri.includes('vscode-webview-resource://')) {
1002
+ // Extract the last part after /attachments/
1003
+ const match = uri.match(/\/attachments\/([^?#]+)/);
1004
+ if (match) {
1005
+ return `attachments/${decodeURIComponent(match[1])}`;
1006
+ }
1007
+ }
1008
+ // Fallback: if it looks like a file path with attachments, extract it
1009
+ if (uri.includes('/attachments/')) {
1010
+ const parts = uri.split('/attachments/');
1011
+ if (parts.length > 1) {
1012
+ return `attachments/${decodeURIComponent(parts[parts.length - 1]).split('?')[0]}`;
1013
+ }
1014
+ }
1015
+ // Can't parse - return original
1016
+ return uri;
1017
+ }
1018
+ }
1019
+ exports.TiptapAdapter = TiptapAdapter;
1020
+ //# sourceMappingURL=TiptapAdapter.js.map