@bendyline/squisq-editor-react 1.1.0 → 1.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/EditorContext.d.ts +6 -2
- package/dist/EditorContext.d.ts.map +1 -1
- package/dist/EditorContext.js +3 -1
- package/dist/EditorContext.js.map +1 -1
- package/dist/EditorShell.d.ts +11 -1
- package/dist/EditorShell.d.ts.map +1 -1
- package/dist/EditorShell.js +9 -7
- package/dist/EditorShell.js.map +1 -1
- package/dist/ImageNodeView.d.ts +15 -0
- package/dist/ImageNodeView.d.ts.map +1 -0
- package/dist/ImageNodeView.js +52 -0
- package/dist/ImageNodeView.js.map +1 -0
- package/dist/PreviewControls.d.ts +41 -0
- package/dist/PreviewControls.d.ts.map +1 -0
- package/dist/PreviewControls.js +201 -0
- package/dist/PreviewControls.js.map +1 -0
- package/dist/PreviewPanel.d.ts +7 -7
- package/dist/PreviewPanel.d.ts.map +1 -1
- package/dist/PreviewPanel.js +183 -199
- package/dist/PreviewPanel.js.map +1 -1
- package/dist/Toolbar.d.ts +8 -1
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar.js +145 -20
- package/dist/Toolbar.js.map +1 -1
- package/dist/WysiwygEditor.d.ts.map +1 -1
- package/dist/WysiwygEditor.js +3 -1
- package/dist/WysiwygEditor.js.map +1 -1
- package/dist/__tests__/tiptapBridge.test.d.ts +2 -0
- package/dist/__tests__/tiptapBridge.test.d.ts.map +1 -0
- package/dist/__tests__/tiptapBridge.test.js +241 -0
- package/dist/__tests__/tiptapBridge.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tiptapBridge.d.ts.map +1 -1
- package/dist/tiptapBridge.js +146 -5
- package/dist/tiptapBridge.js.map +1 -1
- package/package.json +5 -4
- package/src/EditorContext.tsx +8 -1
- package/src/EditorShell.tsx +71 -32
- package/src/ImageNodeView.tsx +70 -0
- package/src/PreviewControls.tsx +340 -0
- package/src/PreviewPanel.tsx +216 -287
- package/src/Toolbar.tsx +449 -17
- package/src/WysiwygEditor.tsx +3 -1
- package/src/__tests__/tiptapBridge.test.ts +290 -0
- package/src/index.ts +6 -0
- package/src/styles/editor.css +257 -16
- package/src/tiptapBridge.ts +164 -6
package/src/tiptapBridge.ts
CHANGED
|
@@ -39,9 +39,8 @@ const RE_STRIP_TAGS = /<[^>]+>/g;
|
|
|
39
39
|
export function markdownToTiptap(markdown: string): string {
|
|
40
40
|
if (!markdown.trim()) return '<p></p>';
|
|
41
41
|
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
const html = markdown;
|
|
42
|
+
// Normalize line endings — content from zip archives may use \r\n
|
|
43
|
+
const html = markdown.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
45
44
|
|
|
46
45
|
// Process blocks line by line for accurate conversion
|
|
47
46
|
const lines = html.split('\n');
|
|
@@ -52,6 +51,8 @@ export function markdownToTiptap(markdown: string): string {
|
|
|
52
51
|
let inList = false;
|
|
53
52
|
let listItems: string[] = [];
|
|
54
53
|
let listType: 'ul' | 'ol' | 'task' = 'ul';
|
|
54
|
+
let inTable = false;
|
|
55
|
+
let tableLines: string[] = [];
|
|
55
56
|
|
|
56
57
|
const flushList = () => {
|
|
57
58
|
if (inList && listItems.length > 0) {
|
|
@@ -63,6 +64,62 @@ export function markdownToTiptap(markdown: string): string {
|
|
|
63
64
|
}
|
|
64
65
|
};
|
|
65
66
|
|
|
67
|
+
const flushTable = () => {
|
|
68
|
+
if (!inTable || tableLines.length === 0) {
|
|
69
|
+
inTable = false;
|
|
70
|
+
tableLines = [];
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Validate: need at least 2 lines and second must be a separator
|
|
75
|
+
const separatorCells = tableLines.length >= 2 ? parseTableCells(tableLines[1]) : [];
|
|
76
|
+
const isSeparator =
|
|
77
|
+
separatorCells.length > 0 && separatorCells.every((cell) => /^:?-+:?$/.test(cell.trim()));
|
|
78
|
+
|
|
79
|
+
if (tableLines.length < 2 || !isSeparator) {
|
|
80
|
+
// Not a valid table — render accumulated lines as paragraphs
|
|
81
|
+
for (const tl of tableLines) {
|
|
82
|
+
outputBlocks.push(`<p>${inlineToHtml(tl)}</p>`);
|
|
83
|
+
}
|
|
84
|
+
inTable = false;
|
|
85
|
+
tableLines = [];
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const alignments = parseAlignments(tableLines[1]);
|
|
90
|
+
const headerCells = parseTableCells(tableLines[0]);
|
|
91
|
+
|
|
92
|
+
// Build header row
|
|
93
|
+
const thHtml = headerCells
|
|
94
|
+
.map((cell, i) => {
|
|
95
|
+
const align = alignments[i];
|
|
96
|
+
const style = align ? ` style="text-align: ${align}"` : '';
|
|
97
|
+
return `<th${style}>${inlineToHtml(cell)}</th>`;
|
|
98
|
+
})
|
|
99
|
+
.join('');
|
|
100
|
+
|
|
101
|
+
// Build body rows
|
|
102
|
+
const bodyHtml = tableLines
|
|
103
|
+
.slice(2)
|
|
104
|
+
.map((rowLine) => {
|
|
105
|
+
const cells = parseTableCells(rowLine);
|
|
106
|
+
const tdHtml = cells
|
|
107
|
+
.map((cell, i) => {
|
|
108
|
+
const align = alignments[i];
|
|
109
|
+
const style = align ? ` style="text-align: ${align}"` : '';
|
|
110
|
+
return `<td${style}>${inlineToHtml(cell)}</td>`;
|
|
111
|
+
})
|
|
112
|
+
.join('');
|
|
113
|
+
return `<tr>${tdHtml}</tr>`;
|
|
114
|
+
})
|
|
115
|
+
.join('');
|
|
116
|
+
|
|
117
|
+
outputBlocks.push(`<table><thead><tr>${thHtml}</tr></thead><tbody>${bodyHtml}</tbody></table>`);
|
|
118
|
+
|
|
119
|
+
inTable = false;
|
|
120
|
+
tableLines = [];
|
|
121
|
+
};
|
|
122
|
+
|
|
66
123
|
for (let i = 0; i < lines.length; i++) {
|
|
67
124
|
const line = lines[i];
|
|
68
125
|
|
|
@@ -91,6 +148,11 @@ export function markdownToTiptap(markdown: string): string {
|
|
|
91
148
|
continue;
|
|
92
149
|
}
|
|
93
150
|
|
|
151
|
+
// If in table and current line is not a table row, flush
|
|
152
|
+
if (inTable && !/^\|.*\|$/.test(line.trim())) {
|
|
153
|
+
flushTable();
|
|
154
|
+
}
|
|
155
|
+
|
|
94
156
|
// Blank line flushes list
|
|
95
157
|
if (line.trim() === '') {
|
|
96
158
|
flushList();
|
|
@@ -174,6 +236,17 @@ export function markdownToTiptap(markdown: string): string {
|
|
|
174
236
|
continue;
|
|
175
237
|
}
|
|
176
238
|
|
|
239
|
+
// Table row
|
|
240
|
+
if (/^\|.*\|$/.test(line.trim())) {
|
|
241
|
+
if (!inTable) {
|
|
242
|
+
flushList();
|
|
243
|
+
inTable = true;
|
|
244
|
+
tableLines = [];
|
|
245
|
+
}
|
|
246
|
+
tableLines.push(line);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
177
250
|
// Regular paragraph
|
|
178
251
|
flushList();
|
|
179
252
|
outputBlocks.push(`<p>${inlineToHtml(line)}</p>`);
|
|
@@ -187,6 +260,7 @@ export function markdownToTiptap(markdown: string): string {
|
|
|
187
260
|
);
|
|
188
261
|
}
|
|
189
262
|
flushList();
|
|
263
|
+
flushTable();
|
|
190
264
|
|
|
191
265
|
return outputBlocks.join('') || '<p></p>';
|
|
192
266
|
}
|
|
@@ -267,6 +341,69 @@ export function tiptapToMarkdown(html: string): string {
|
|
|
267
341
|
continue;
|
|
268
342
|
}
|
|
269
343
|
|
|
344
|
+
// Table (with optional Tiptap tableWrapper div; table tag may have style attrs)
|
|
345
|
+
const tableMatch =
|
|
346
|
+
remaining.match(
|
|
347
|
+
/^<div[^>]*class="[^"]*tableWrapper[^"]*"[^>]*><table[^>]*>(.*?)<\/table>\s*<\/div>/s,
|
|
348
|
+
) || remaining.match(/^<table[^>]*>(.*?)<\/table>/s);
|
|
349
|
+
if (tableMatch) {
|
|
350
|
+
const tableContent = tableMatch[1];
|
|
351
|
+
|
|
352
|
+
// Extract all rows with their cells
|
|
353
|
+
const rows: { content: string; align: string | null; isHeader: boolean }[][] = [];
|
|
354
|
+
const rowRegex = /<tr[^>]*>(.*?)<\/tr>/gs;
|
|
355
|
+
let rowExec;
|
|
356
|
+
while ((rowExec = rowRegex.exec(tableContent)) !== null) {
|
|
357
|
+
const rowHtml = rowExec[1];
|
|
358
|
+
const cells: { content: string; align: string | null; isHeader: boolean }[] = [];
|
|
359
|
+
const cellRegex = /<(th|td)([^>]*)>(.*?)<\/\1>/gs;
|
|
360
|
+
let cellExec;
|
|
361
|
+
while ((cellExec = cellRegex.exec(rowHtml)) !== null) {
|
|
362
|
+
const tag = cellExec[1];
|
|
363
|
+
const attrs = cellExec[2];
|
|
364
|
+
const content = htmlToInline(cellExec[3].replace(/<\/?p>/g, ''));
|
|
365
|
+
const alignExec = attrs.match(/text-align:\s*(left|center|right)/);
|
|
366
|
+
cells.push({
|
|
367
|
+
content,
|
|
368
|
+
align: alignExec ? alignExec[1] : null,
|
|
369
|
+
isHeader: tag === 'th',
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (cells.length > 0) {
|
|
373
|
+
rows.push(cells);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (rows.length > 0) {
|
|
378
|
+
// Header row = first row with th cells, or just the first row
|
|
379
|
+
const headerIdx = rows.findIndex((r) => r.some((c) => c.isHeader));
|
|
380
|
+
const hIdx = headerIdx >= 0 ? headerIdx : 0;
|
|
381
|
+
const headerRow = rows[hIdx];
|
|
382
|
+
const dataRows = rows.filter((_, i) => i !== hIdx);
|
|
383
|
+
|
|
384
|
+
const aligns = headerRow.map((c) => c.align);
|
|
385
|
+
lines.push('| ' + headerRow.map((c) => c.content || ' ').join(' | ') + ' |');
|
|
386
|
+
lines.push(
|
|
387
|
+
'| ' +
|
|
388
|
+
aligns
|
|
389
|
+
.map((a) => {
|
|
390
|
+
if (a === 'center') return ':---:';
|
|
391
|
+
if (a === 'right') return '---:';
|
|
392
|
+
return '---';
|
|
393
|
+
})
|
|
394
|
+
.join(' | ') +
|
|
395
|
+
' |',
|
|
396
|
+
);
|
|
397
|
+
for (const row of dataRows) {
|
|
398
|
+
lines.push('| ' + row.map((c) => c.content || ' ').join(' | ') + ' |');
|
|
399
|
+
}
|
|
400
|
+
lines.push('');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
remaining = remaining.slice(tableMatch[0].length);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
|
|
270
407
|
// Task list
|
|
271
408
|
const taskListMatch = remaining.match(/^<ul[^>]*data-type="taskList"[^>]*>(.*?)<\/ul>/s);
|
|
272
409
|
if (taskListMatch) {
|
|
@@ -348,6 +485,27 @@ export function tiptapToMarkdown(html: string): string {
|
|
|
348
485
|
);
|
|
349
486
|
}
|
|
350
487
|
|
|
488
|
+
// ─── Table helpers ───────────────────────────────────────
|
|
489
|
+
|
|
490
|
+
/** Split a GFM table row into trimmed cell strings (strips outer pipes). */
|
|
491
|
+
function parseTableCells(line: string): string[] {
|
|
492
|
+
let inner = line.trim();
|
|
493
|
+
if (inner.startsWith('|')) inner = inner.slice(1);
|
|
494
|
+
if (inner.endsWith('|')) inner = inner.slice(0, -1);
|
|
495
|
+
return inner.split('|').map((cell) => cell.trim());
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Parse a GFM separator line into column alignments. */
|
|
499
|
+
function parseAlignments(separatorLine: string): (string | null)[] {
|
|
500
|
+
return parseTableCells(separatorLine).map((cell) => {
|
|
501
|
+
const s = cell.replace(/\s/g, '');
|
|
502
|
+
if (s.startsWith(':') && s.endsWith(':')) return 'center';
|
|
503
|
+
if (s.endsWith(':')) return 'right';
|
|
504
|
+
if (s.startsWith(':')) return 'left';
|
|
505
|
+
return null;
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
351
509
|
// ─── Helpers ─────────────────────────────────────────────
|
|
352
510
|
|
|
353
511
|
function escapeHtml(text: string): string {
|
|
@@ -384,12 +542,12 @@ function inlineToHtml(text: string): string {
|
|
|
384
542
|
// Inline code: `text`
|
|
385
543
|
result = result.replace(RE_INLINE_CODE, '<code>$1</code>');
|
|
386
544
|
|
|
545
|
+
// Images first:  — must be before links so the `!` prefix is consumed
|
|
546
|
+
result = result.replace(RE_IMAGE, '<img alt="$1" src="$2">');
|
|
547
|
+
|
|
387
548
|
// Links: [text](url)
|
|
388
549
|
result = result.replace(RE_LINK, '<a href="$2">$1</a>');
|
|
389
550
|
|
|
390
|
-
// Images: 
|
|
391
|
-
result = result.replace(RE_IMAGE, '<img alt="$1" src="$2">');
|
|
392
|
-
|
|
393
551
|
return result;
|
|
394
552
|
}
|
|
395
553
|
|