svelte-rtf-editor 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/rtf-parser.js +75 -1
- package/dist/rtf-writer.js +43 -1
- package/package.json +2 -1
package/dist/rtf-parser.js
CHANGED
|
@@ -230,6 +230,12 @@ export function rtfToHtml(rtfString) {
|
|
|
230
230
|
let paragraphs = [];
|
|
231
231
|
let paragraphHasText = false;
|
|
232
232
|
let paragraphHasListMarker = false;
|
|
233
|
+
// Table state
|
|
234
|
+
let tableRows = [];
|
|
235
|
+
let currentRowCells = [];
|
|
236
|
+
let currentRowIsHeader = false;
|
|
237
|
+
let currentRowCellRights = [];
|
|
238
|
+
let inTableCell = false;
|
|
233
239
|
// First pass: extract color table and font table from the root group
|
|
234
240
|
const rootGroup = tree.children[0]?.type === 'group' ? tree.children[0] : tree;
|
|
235
241
|
for (let i = 0; i < rootGroup.children.length; i++) {
|
|
@@ -324,7 +330,32 @@ export function rtfToHtml(rtfString) {
|
|
|
324
330
|
'latentstyles', 'datastore', 'mmathPr', 'author', 'operator',
|
|
325
331
|
'title', 'subject', 'doccomm', 'company', 'category', 'keywords'
|
|
326
332
|
]);
|
|
333
|
+
function flushTable() {
|
|
334
|
+
if (tableRows.length === 0)
|
|
335
|
+
return;
|
|
336
|
+
let html = '<table style="border-collapse:collapse;width:100%;margin:4px 0">';
|
|
337
|
+
for (const row of tableRows) {
|
|
338
|
+
html += '<tr>';
|
|
339
|
+
const tag = row.isHeader ? 'th' : 'td';
|
|
340
|
+
const total = row.cellRights[row.cellRights.length - 1] || 1;
|
|
341
|
+
let prevRight = 0;
|
|
342
|
+
for (let ci = 0; ci < row.cells.length; ci++) {
|
|
343
|
+
const right = row.cellRights[ci] ?? total;
|
|
344
|
+
const pct = ((right - prevRight) / total * 100).toFixed(1);
|
|
345
|
+
prevRight = right;
|
|
346
|
+
const style = row.isHeader
|
|
347
|
+
? `border-bottom:1px solid;padding:2px 6px;text-align:left;width:${pct}%`
|
|
348
|
+
: `padding:2px 6px;width:${pct}%`;
|
|
349
|
+
html += `<${tag} style="${style}">${row.cells[ci] || ''}</${tag}>`;
|
|
350
|
+
}
|
|
351
|
+
html += '</tr>';
|
|
352
|
+
}
|
|
353
|
+
html += '</table>';
|
|
354
|
+
paragraphs.push(html);
|
|
355
|
+
tableRows = [];
|
|
356
|
+
}
|
|
327
357
|
function flushParagraph() {
|
|
358
|
+
flushTable();
|
|
328
359
|
const trimmed = currentParagraph.trim();
|
|
329
360
|
if (!trimmed) {
|
|
330
361
|
if (!paragraphHasListMarker) {
|
|
@@ -414,7 +445,45 @@ export function rtfToHtml(rtfString) {
|
|
|
414
445
|
state.fontIndex = p;
|
|
415
446
|
break;
|
|
416
447
|
case 'par':
|
|
417
|
-
|
|
448
|
+
if (!inTableCell)
|
|
449
|
+
flushParagraph();
|
|
450
|
+
break;
|
|
451
|
+
case 'trowd':
|
|
452
|
+
currentRowCells = [];
|
|
453
|
+
currentRowIsHeader = false;
|
|
454
|
+
currentRowCellRights = [];
|
|
455
|
+
break;
|
|
456
|
+
case 'trgaph':
|
|
457
|
+
case 'trkeep':
|
|
458
|
+
case 'trleft':
|
|
459
|
+
case 'trhdr':
|
|
460
|
+
case 'brdrs':
|
|
461
|
+
case 'brdrnil':
|
|
462
|
+
case 'clbrdrr':
|
|
463
|
+
case 'clbrdrl':
|
|
464
|
+
case 'clbrdrt':
|
|
465
|
+
break; // table formatting, ignored
|
|
466
|
+
case 'clbrdrb':
|
|
467
|
+
currentRowIsHeader = true;
|
|
468
|
+
break;
|
|
469
|
+
case 'cellx':
|
|
470
|
+
if (p != null)
|
|
471
|
+
currentRowCellRights.push(p);
|
|
472
|
+
break;
|
|
473
|
+
case 'intbl':
|
|
474
|
+
inTableCell = true;
|
|
475
|
+
break;
|
|
476
|
+
case 'cell':
|
|
477
|
+
currentRowCells.push(currentParagraph.trim());
|
|
478
|
+
currentParagraph = '';
|
|
479
|
+
paragraphHasText = false;
|
|
480
|
+
break;
|
|
481
|
+
case 'row':
|
|
482
|
+
tableRows.push({ cells: currentRowCells, isHeader: currentRowIsHeader, cellRights: currentRowCellRights });
|
|
483
|
+
currentRowCells = [];
|
|
484
|
+
currentRowIsHeader = false;
|
|
485
|
+
currentRowCellRights = [];
|
|
486
|
+
inTableCell = false;
|
|
418
487
|
break;
|
|
419
488
|
case 'pard':
|
|
420
489
|
state.bold = false;
|
|
@@ -526,6 +595,7 @@ export function rtfToHtml(rtfString) {
|
|
|
526
595
|
}
|
|
527
596
|
}
|
|
528
597
|
walk(rootGroup.children);
|
|
598
|
+
flushTable(); // flush any trailing table rows
|
|
529
599
|
// Only flush if there's actually pending content — the final \par in most RTF
|
|
530
600
|
// files already flushed the last paragraph, so an unconditional call here
|
|
531
601
|
// would push a spurious empty paragraph that becomes a trailing <p><br></p>.
|
|
@@ -555,6 +625,10 @@ export function rtfToHtml(rtfString) {
|
|
|
555
625
|
rendered.push('<hr>');
|
|
556
626
|
continue;
|
|
557
627
|
}
|
|
628
|
+
if (trimmed.startsWith('<table')) {
|
|
629
|
+
rendered.push(trimmed);
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
558
632
|
const sizeMatch = trimmed.match(/font-size:\s*([\d.]+)pt/);
|
|
559
633
|
if (sizeMatch) {
|
|
560
634
|
const pt = parseFloat(sizeMatch[1]);
|
package/dist/rtf-writer.js
CHANGED
|
@@ -93,7 +93,7 @@ export function htmlToRtf(editorEl) {
|
|
|
93
93
|
'{\\f1\\froman\\fcharset0 Georgia;}' +
|
|
94
94
|
'{\\f2\\fmodern\\fcharset0 Courier New;}' +
|
|
95
95
|
'}';
|
|
96
|
-
const body = walkChildren(editorEl, { colorIndex, inPre: false, listCounter: 0 });
|
|
96
|
+
const body = walkChildren(editorEl, { colorIndex, inPre: false, listCounter: 0, inTableCell: false });
|
|
97
97
|
const rtf = '{\\rtf1\\ansi\\ansicpg1252\\deff0' +
|
|
98
98
|
fontTable +
|
|
99
99
|
colorTable +
|
|
@@ -149,6 +149,11 @@ function walkChildren(parent, ctx) {
|
|
|
149
149
|
break;
|
|
150
150
|
case 'p':
|
|
151
151
|
case 'div': {
|
|
152
|
+
// Inside a table cell \pard is already declared; just emit inline content.
|
|
153
|
+
if (ctx.inTableCell) {
|
|
154
|
+
rtf += walkChildren(el, ctx);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
152
157
|
// A <p> with no children or only a single <br> is a blank line —
|
|
153
158
|
// emit a bare \par rather than \pard \line\par, which adds an extra line.
|
|
154
159
|
const isBlankLine = el.childNodes.length === 0 ||
|
|
@@ -163,6 +168,43 @@ function walkChildren(parent, ctx) {
|
|
|
163
168
|
rtf += `\\pard ${colorPrefix}${inlineRtf}${colorSuffix}\\par\n`;
|
|
164
169
|
break;
|
|
165
170
|
}
|
|
171
|
+
case 'table': {
|
|
172
|
+
const PAGE_WIDTH = 8640; // twips ≈ 6 in (standard letter body width)
|
|
173
|
+
const rows = Array.from(el.querySelectorAll(':scope > tr, :scope > thead > tr, :scope > tbody > tr, :scope > tfoot > tr'));
|
|
174
|
+
for (const row of rows) {
|
|
175
|
+
const cells = Array.from(row.querySelectorAll(':scope > td, :scope > th'));
|
|
176
|
+
if (cells.length === 0)
|
|
177
|
+
continue;
|
|
178
|
+
const isHeader = cells.some(c => c.tagName.toLowerCase() === 'th');
|
|
179
|
+
// Derive right-edge twip positions from the width:X% style set by the parser.
|
|
180
|
+
// Fall back to equal column widths if the style is absent.
|
|
181
|
+
const cellRights = [];
|
|
182
|
+
let cumTwips = 0;
|
|
183
|
+
for (const cell of cells) {
|
|
184
|
+
const pct = parseFloat(cell.style.width) || (100 / cells.length);
|
|
185
|
+
cumTwips += Math.round((pct / 100) * PAGE_WIDTH);
|
|
186
|
+
cellRights.push(cumTwips);
|
|
187
|
+
}
|
|
188
|
+
cellRights[cellRights.length - 1] = PAGE_WIDTH; // snap last edge to avoid rounding drift
|
|
189
|
+
// Row definition
|
|
190
|
+
let rowRtf = '{\n\\trowd \\trgaph120\n';
|
|
191
|
+
for (let ci = 0; ci < cells.length; ci++) {
|
|
192
|
+
if (isHeader)
|
|
193
|
+
rowRtf += `\\clbrdrb\\brdrs`;
|
|
194
|
+
rowRtf += `\\cellx${cellRights[ci]}`;
|
|
195
|
+
}
|
|
196
|
+
rowRtf += '\n\\trkeep\\intbl\n{';
|
|
197
|
+
// Cell bodies
|
|
198
|
+
const cellCtx = { ...ctx, inTableCell: true };
|
|
199
|
+
for (const cell of cells) {
|
|
200
|
+
const content = walkChildren(cell, cellCtx);
|
|
201
|
+
rowRtf += `{\\pard\\intbl \\f0 \\sa0 \\li0 \\fi0 ${content}\\par}\n\\cell`;
|
|
202
|
+
}
|
|
203
|
+
rowRtf += '}\n\\intbl\\row}\n';
|
|
204
|
+
rtf += rowRtf;
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
166
208
|
case 'blockquote':
|
|
167
209
|
rtf += `\\pard\\li720\\i ${walkChildren(el, ctx)}\\i0\\par\n`;
|
|
168
210
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-rtf-editor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "RTF viewer and rich-text editor components for Svelte 5",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/jcelano/svelte-rtf-editor#readme",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"@sveltejs/kit": "^2.53.4",
|
|
50
50
|
"@sveltejs/package": "^2.5.7",
|
|
51
51
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
52
|
+
"happy-dom": "^20.8.3",
|
|
52
53
|
"publint": "^0.3.18",
|
|
53
54
|
"svelte": "^5.53.7",
|
|
54
55
|
"svelte-check": "^4.4.4",
|