@bendyline/squisq-editor-react 1.1.1 → 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/PreviewControls.js +1 -1
- package/dist/PreviewControls.js.map +1 -1
- package/dist/Toolbar.d.ts.map +1 -1
- package/dist/Toolbar.js +143 -10
- package/dist/Toolbar.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/tiptapBridge.d.ts.map +1 -1
- package/dist/tiptapBridge.js +142 -0
- package/dist/tiptapBridge.js.map +1 -1
- package/package.json +4 -4
- package/src/PreviewControls.tsx +1 -1
- package/src/Toolbar.tsx +427 -12
- package/src/__tests__/tiptapBridge.test.ts +290 -0
- package/src/styles/editor.css +229 -11
- package/src/tiptapBridge.ts +159 -0
package/src/tiptapBridge.ts
CHANGED
|
@@ -51,6 +51,8 @@ export function markdownToTiptap(markdown: string): string {
|
|
|
51
51
|
let inList = false;
|
|
52
52
|
let listItems: string[] = [];
|
|
53
53
|
let listType: 'ul' | 'ol' | 'task' = 'ul';
|
|
54
|
+
let inTable = false;
|
|
55
|
+
let tableLines: string[] = [];
|
|
54
56
|
|
|
55
57
|
const flushList = () => {
|
|
56
58
|
if (inList && listItems.length > 0) {
|
|
@@ -62,6 +64,62 @@ export function markdownToTiptap(markdown: string): string {
|
|
|
62
64
|
}
|
|
63
65
|
};
|
|
64
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
|
+
|
|
65
123
|
for (let i = 0; i < lines.length; i++) {
|
|
66
124
|
const line = lines[i];
|
|
67
125
|
|
|
@@ -90,6 +148,11 @@ export function markdownToTiptap(markdown: string): string {
|
|
|
90
148
|
continue;
|
|
91
149
|
}
|
|
92
150
|
|
|
151
|
+
// If in table and current line is not a table row, flush
|
|
152
|
+
if (inTable && !/^\|.*\|$/.test(line.trim())) {
|
|
153
|
+
flushTable();
|
|
154
|
+
}
|
|
155
|
+
|
|
93
156
|
// Blank line flushes list
|
|
94
157
|
if (line.trim() === '') {
|
|
95
158
|
flushList();
|
|
@@ -173,6 +236,17 @@ export function markdownToTiptap(markdown: string): string {
|
|
|
173
236
|
continue;
|
|
174
237
|
}
|
|
175
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
|
+
|
|
176
250
|
// Regular paragraph
|
|
177
251
|
flushList();
|
|
178
252
|
outputBlocks.push(`<p>${inlineToHtml(line)}</p>`);
|
|
@@ -186,6 +260,7 @@ export function markdownToTiptap(markdown: string): string {
|
|
|
186
260
|
);
|
|
187
261
|
}
|
|
188
262
|
flushList();
|
|
263
|
+
flushTable();
|
|
189
264
|
|
|
190
265
|
return outputBlocks.join('') || '<p></p>';
|
|
191
266
|
}
|
|
@@ -266,6 +341,69 @@ export function tiptapToMarkdown(html: string): string {
|
|
|
266
341
|
continue;
|
|
267
342
|
}
|
|
268
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
|
+
|
|
269
407
|
// Task list
|
|
270
408
|
const taskListMatch = remaining.match(/^<ul[^>]*data-type="taskList"[^>]*>(.*?)<\/ul>/s);
|
|
271
409
|
if (taskListMatch) {
|
|
@@ -347,6 +485,27 @@ export function tiptapToMarkdown(html: string): string {
|
|
|
347
485
|
);
|
|
348
486
|
}
|
|
349
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
|
+
|
|
350
509
|
// ─── Helpers ─────────────────────────────────────────────
|
|
351
510
|
|
|
352
511
|
function escapeHtml(text: string): string {
|