@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.
- package/dist/editor/TiptapAdapter.d.ts +62 -0
- package/dist/editor/TiptapAdapter.d.ts.map +1 -0
- package/dist/editor/TiptapAdapter.js +1020 -0
- package/dist/editor/TiptapAdapter.js.map +1 -0
- package/dist/editor/index.d.ts +2 -0
- package/dist/editor/index.d.ts.map +1 -0
- package/dist/editor/index.js +18 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/platform/NodePlatform.d.ts +19 -0
- package/dist/platform/NodePlatform.d.ts.map +1 -0
- package/dist/platform/NodePlatform.js +99 -0
- package/dist/platform/NodePlatform.js.map +1 -0
- package/dist/platform/VSCodePlatform.d.ts +20 -0
- package/dist/platform/VSCodePlatform.d.ts.map +1 -0
- package/dist/platform/VSCodePlatform.js +135 -0
- package/dist/platform/VSCodePlatform.js.map +1 -0
- package/dist/platform/index.d.ts +3 -0
- package/dist/platform/index.d.ts.map +1 -0
- package/dist/platform/index.js +19 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/renderer/HTMLRenderer.d.ts +22 -0
- package/dist/renderer/HTMLRenderer.d.ts.map +1 -0
- package/dist/renderer/HTMLRenderer.js +154 -0
- package/dist/renderer/HTMLRenderer.js.map +1 -0
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +18 -0
- package/dist/renderer/index.js.map +1 -0
- package/package.json +30 -0
|
@@ -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
|