@bendyline/squisq-editor-react 1.1.1 → 1.2.1

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.
@@ -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 {