@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.
Files changed (50) hide show
  1. package/dist/EditorContext.d.ts +6 -2
  2. package/dist/EditorContext.d.ts.map +1 -1
  3. package/dist/EditorContext.js +3 -1
  4. package/dist/EditorContext.js.map +1 -1
  5. package/dist/EditorShell.d.ts +11 -1
  6. package/dist/EditorShell.d.ts.map +1 -1
  7. package/dist/EditorShell.js +9 -7
  8. package/dist/EditorShell.js.map +1 -1
  9. package/dist/ImageNodeView.d.ts +15 -0
  10. package/dist/ImageNodeView.d.ts.map +1 -0
  11. package/dist/ImageNodeView.js +52 -0
  12. package/dist/ImageNodeView.js.map +1 -0
  13. package/dist/PreviewControls.d.ts +41 -0
  14. package/dist/PreviewControls.d.ts.map +1 -0
  15. package/dist/PreviewControls.js +201 -0
  16. package/dist/PreviewControls.js.map +1 -0
  17. package/dist/PreviewPanel.d.ts +7 -7
  18. package/dist/PreviewPanel.d.ts.map +1 -1
  19. package/dist/PreviewPanel.js +183 -199
  20. package/dist/PreviewPanel.js.map +1 -1
  21. package/dist/Toolbar.d.ts +8 -1
  22. package/dist/Toolbar.d.ts.map +1 -1
  23. package/dist/Toolbar.js +145 -20
  24. package/dist/Toolbar.js.map +1 -1
  25. package/dist/WysiwygEditor.d.ts.map +1 -1
  26. package/dist/WysiwygEditor.js +3 -1
  27. package/dist/WysiwygEditor.js.map +1 -1
  28. package/dist/__tests__/tiptapBridge.test.d.ts +2 -0
  29. package/dist/__tests__/tiptapBridge.test.d.ts.map +1 -0
  30. package/dist/__tests__/tiptapBridge.test.js +241 -0
  31. package/dist/__tests__/tiptapBridge.test.js.map +1 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +1 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/tiptapBridge.d.ts.map +1 -1
  37. package/dist/tiptapBridge.js +146 -5
  38. package/dist/tiptapBridge.js.map +1 -1
  39. package/package.json +5 -4
  40. package/src/EditorContext.tsx +8 -1
  41. package/src/EditorShell.tsx +71 -32
  42. package/src/ImageNodeView.tsx +70 -0
  43. package/src/PreviewControls.tsx +340 -0
  44. package/src/PreviewPanel.tsx +216 -287
  45. package/src/Toolbar.tsx +449 -17
  46. package/src/WysiwygEditor.tsx +3 -1
  47. package/src/__tests__/tiptapBridge.test.ts +290 -0
  48. package/src/index.ts +6 -0
  49. package/src/styles/editor.css +257 -16
  50. package/src/tiptapBridge.ts +164 -6
@@ -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
- // Simple conversion of markdown constructs to HTML that Tiptap understands.
43
- // This is intentionally straightforward — Tiptap's parser handles the HTML.
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: ![alt](src) — 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: ![alt](src)
391
- result = result.replace(RE_IMAGE, '<img alt="$1" src="$2">');
392
-
393
551
  return result;
394
552
  }
395
553