svelte-rtf-editor 1.5.0 → 1.5.2
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/README.md +2 -2
- package/dist/RtfViewer.svelte +1 -1
- package/dist/components/InkEditor.svelte +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/rtf-parser.d.ts +13 -2
- package/dist/rtf-parser.js +21 -58
- package/dist/rtf-writer.js +23 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,7 +169,7 @@ import {
|
|
|
169
169
|
|
|
170
170
|
| Function | Description |
|
|
171
171
|
|---------------------------------------|----------------------------------------------------|
|
|
172
|
-
| `rtfToHtml(rtf: string): string`
|
|
172
|
+
| `rtfToHtml(rtf: string, options?: { fontSize? }): string` | Parse an RTF string and return an HTML string. `fontSize` is the size of the screen it will be shown on (default 12) |
|
|
173
173
|
| `htmlToRtf(el: HTMLElement, options?: { fontFamily?, fontSize? }): string` | Convert a DOM element's content to RTF, in Arial 12 pt unless told otherwise |
|
|
174
174
|
| `estimateRtfBytes(el: HTMLElement): number` | Approximate RTF size of a DOM element without converting it |
|
|
175
175
|
| `readRtfFile(file: File): Promise<string>` | Read a `.rtf` File object and return HTML |
|
|
@@ -195,7 +195,7 @@ htmlToRtf(el, { fontFamily: 'Calibri', fontSize: 11 });
|
|
|
195
195
|
- If you call `htmlToRtf` yourself on the editor's content, pass the same options you gave the editor, or the two produce different RTF.
|
|
196
196
|
- `fontFamily` names one font. Given a CSS list such as `'Arial, sans-serif'`, only the first family is used.
|
|
197
197
|
- `fontSize` is rounded to the nearest half-point, which is what RTF measures in, and kept between 1 and 1638 pt.
|
|
198
|
-
- `
|
|
198
|
+
- If you show RTF in an editor or viewer set to another size, pass the same `fontSize` to `rtfToHtml`. Text at that size then carries no size of its own, text at any other size keeps it, and headings are recognized by the same ratios the writer uses. `InkEditor`'s RTF import and `RtfViewer` do this themselves. At another size than 12 pt, a heading must also be bold, as every heading the writer emits is.
|
|
199
199
|
|
|
200
200
|
The font has to be installed on the machine that displays the document. The RTF names it, and a reader without it substitutes one of its own.
|
|
201
201
|
|
package/dist/RtfViewer.svelte
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
const screenFontFamily = $derived(cssFontFamily(fontFamily));
|
|
20
20
|
const screenFontSize = $derived(normalizeFontSize(fontSize));
|
|
21
21
|
|
|
22
|
-
const htmlContent = $derived(rtfToHtml(content));
|
|
22
|
+
const htmlContent = $derived(rtfToHtml(content, { fontSize }));
|
|
23
23
|
|
|
24
24
|
let copied = $state(false);
|
|
25
25
|
|
package/dist/index.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ export { default as InkEditor } from './components/InkEditor.svelte';
|
|
|
3
3
|
export { rtfToHtml, readRtfFile } from './rtf-parser.js';
|
|
4
4
|
export { htmlToRtf, estimateRtfBytes, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from './rtf-writer.js';
|
|
5
5
|
export type { RtfWriteOptions } from './rtf-writer.js';
|
|
6
|
+
export type { RtfReadOptions } from './rtf-parser.js';
|
|
6
7
|
export { htmlToMarkdown, downloadFile } from './utils.js';
|
package/dist/rtf-parser.d.ts
CHANGED
|
@@ -10,5 +10,16 @@
|
|
|
10
10
|
*
|
|
11
11
|
* \cb (character background) is silently ignored on import.
|
|
12
12
|
*/
|
|
13
|
-
|
|
14
|
-
export
|
|
13
|
+
/** Options for rtfToHtml. */
|
|
14
|
+
export interface RtfReadOptions {
|
|
15
|
+
/**
|
|
16
|
+
* The body text size of the screen the HTML will be shown on, in points:
|
|
17
|
+
* the editor's or viewer's fontSize. Text at this size carries no size of
|
|
18
|
+
* its own, text at any other size keeps its font-size, and headings are
|
|
19
|
+
* recognized by their ratio to it (HEADING_SCALE). Default: 12, which reads
|
|
20
|
+
* a document exactly as 1.4.0 did.
|
|
21
|
+
*/
|
|
22
|
+
fontSize?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function rtfToHtml(rtfString: string, options?: RtfReadOptions): string;
|
|
25
|
+
export declare function readRtfFile(file: File, options?: RtfReadOptions): Promise<string>;
|
package/dist/rtf-parser.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* \cb (character background) is silently ignored on import.
|
|
12
12
|
*/
|
|
13
|
-
import { HEADING_SCALE } from './rtf-writer.js';
|
|
13
|
+
import { HEADING_SCALE, normalizeFontSize } from './rtf-writer.js';
|
|
14
14
|
// ── Highlight palette (RTF \highlight 1-16) ──
|
|
15
15
|
const HIGHLIGHT_COLORS = {
|
|
16
16
|
1: '#000000', // Black
|
|
@@ -357,52 +357,7 @@ const destinationWords = new Set([
|
|
|
357
357
|
'latentstyles', 'datastore', 'mmathPr', 'author', 'operator',
|
|
358
358
|
'title', 'subject', 'doccomm', 'company', 'category', 'keywords'
|
|
359
359
|
]);
|
|
360
|
-
|
|
361
|
-
const DEFAULT_HALF_POINTS = 24;
|
|
362
|
-
/**
|
|
363
|
-
* The document's body text size, in half-points: the size most of its text is
|
|
364
|
-
* set in. Headings are recognized relative to it, so a document written at
|
|
365
|
-
* 10 or 14 pt keeps its paragraphs and headings apart instead of being judged
|
|
366
|
-
* against a fixed 12 pt.
|
|
367
|
-
*/
|
|
368
|
-
function bodyHalfPoints(root) {
|
|
369
|
-
const chars = new Map();
|
|
370
|
-
function visit(nodes, size) {
|
|
371
|
-
for (const node of nodes) {
|
|
372
|
-
if (node.type === 'group') {
|
|
373
|
-
const first = node.children.find((n) => n.type === 'control');
|
|
374
|
-
const starred = node.children[0]?.type === 'control' && node.children[0].word === '*';
|
|
375
|
-
if (starred || (first && (first.word === 'pict' || destinationWords.has(first.word))))
|
|
376
|
-
continue;
|
|
377
|
-
visit(node.children, size);
|
|
378
|
-
}
|
|
379
|
-
else if (node.type === 'control') {
|
|
380
|
-
if (node.word === 'fs' && node.param != null && node.param > 0)
|
|
381
|
-
size = node.param;
|
|
382
|
-
else if (node.word === 'plain')
|
|
383
|
-
size = DEFAULT_HALF_POINTS;
|
|
384
|
-
}
|
|
385
|
-
else {
|
|
386
|
-
const n = node.value.trim().length;
|
|
387
|
-
if (n)
|
|
388
|
-
chars.set(size, (chars.get(size) ?? 0) + n);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
return size;
|
|
392
|
-
}
|
|
393
|
-
visit(root.children, DEFAULT_HALF_POINTS);
|
|
394
|
-
let best = DEFAULT_HALF_POINTS;
|
|
395
|
-
let most = 0;
|
|
396
|
-
for (const [size, n] of chars) {
|
|
397
|
-
if (n > most) {
|
|
398
|
-
best = size;
|
|
399
|
-
most = n;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
return best;
|
|
403
|
-
}
|
|
404
|
-
// ── Renderer ──
|
|
405
|
-
export function rtfToHtml(rtfString) {
|
|
360
|
+
export function rtfToHtml(rtfString, options = {}) {
|
|
406
361
|
const tokens = tokenize(rtfString);
|
|
407
362
|
const tree = buildTree(tokens);
|
|
408
363
|
let colorTable = [''];
|
|
@@ -444,9 +399,9 @@ export function rtfToHtml(rtfString) {
|
|
|
444
399
|
}
|
|
445
400
|
}
|
|
446
401
|
}
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
const bodySize =
|
|
402
|
+
// The screen's body size, in half-points. It is given, not guessed from the
|
|
403
|
+
// document: guessing misread short documents and ones set below 12 pt.
|
|
404
|
+
const bodySize = normalizeFontSize(options.fontSize) * 2;
|
|
450
405
|
// State stack for nested groups
|
|
451
406
|
function defaultState() {
|
|
452
407
|
return {
|
|
@@ -471,7 +426,7 @@ export function rtfToHtml(rtfString) {
|
|
|
471
426
|
function cloneState() {
|
|
472
427
|
return { ...currentState() };
|
|
473
428
|
}
|
|
474
|
-
function buildSpanOpen(state) {
|
|
429
|
+
function buildSpanOpen(state, withSize = true) {
|
|
475
430
|
const styles = [];
|
|
476
431
|
if (state.colorIndex > 0 && state.colorIndex < colorTable.length) {
|
|
477
432
|
const color = colorTable[state.colorIndex];
|
|
@@ -484,7 +439,7 @@ export function rtfToHtml(rtfString) {
|
|
|
484
439
|
if (bg)
|
|
485
440
|
styles.push(`background-color:${bg}`);
|
|
486
441
|
}
|
|
487
|
-
if (state.fontSize && state.fontSize !== bodySize) {
|
|
442
|
+
if (withSize && state.fontSize && state.fontSize !== bodySize) {
|
|
488
443
|
styles.push(`font-size:${state.fontSize / 2}pt`);
|
|
489
444
|
}
|
|
490
445
|
if (styles.length === 0)
|
|
@@ -494,12 +449,16 @@ export function rtfToHtml(rtfString) {
|
|
|
494
449
|
function wrapText(text, state) {
|
|
495
450
|
if (!text)
|
|
496
451
|
return '';
|
|
452
|
+
// A size on spaces alone is not shown and would be read as the
|
|
453
|
+
// paragraph's size, so it is dropped. Underline, strikethrough and
|
|
454
|
+
// highlight are kept: an underlined blank is a signature line.
|
|
455
|
+
const onlySpaces = /^[ \t\r\n\f]*$/.test(text);
|
|
497
456
|
let result = text
|
|
498
457
|
.replace(/&/g, '&')
|
|
499
458
|
.replace(/</g, '<')
|
|
500
459
|
.replace(/>/g, '>');
|
|
501
460
|
// Apply span styles
|
|
502
|
-
const spanOpen = buildSpanOpen(state);
|
|
461
|
+
const spanOpen = buildSpanOpen(state, !onlySpaces);
|
|
503
462
|
if (spanOpen) {
|
|
504
463
|
result = `${spanOpen}${result}</span>`;
|
|
505
464
|
}
|
|
@@ -888,15 +847,19 @@ export function rtfToHtml(rtfString) {
|
|
|
888
847
|
const halfPoints = Math.round(parseFloat(sizeMatch[1]) * 2);
|
|
889
848
|
const alignAttr = align !== 'left' ? ` style="text-align:${align}"` : '';
|
|
890
849
|
const atLeast = (scale) => halfPoints >= Math.round(bodySize * scale);
|
|
891
|
-
|
|
850
|
+
// At the default 12 pt, size alone makes a heading, as it always has. At
|
|
851
|
+
// another screen size, ordinary 12 pt text can be a step above the body,
|
|
852
|
+
// so a heading must also be bold, as every heading the writer emits is.
|
|
853
|
+
const looksLikeHeading = bodySize === 24 || /^<strong>/.test(trimmed);
|
|
854
|
+
if (looksLikeHeading && atLeast(HEADING_SCALE.h1)) {
|
|
892
855
|
rendered.push(`<h1${alignAttr}>${stripSizeSpan(trimmed)}</h1>`);
|
|
893
856
|
continue;
|
|
894
857
|
}
|
|
895
|
-
if (atLeast(HEADING_SCALE.h2)) {
|
|
858
|
+
if (looksLikeHeading && atLeast(HEADING_SCALE.h2)) {
|
|
896
859
|
rendered.push(`<h2${alignAttr}>${stripSizeSpan(trimmed)}</h2>`);
|
|
897
860
|
continue;
|
|
898
861
|
}
|
|
899
|
-
if (atLeast(HEADING_SCALE.h3)) {
|
|
862
|
+
if (looksLikeHeading && atLeast(HEADING_SCALE.h3)) {
|
|
900
863
|
rendered.push(`<h3${alignAttr}>${stripSizeSpan(trimmed)}</h3>`);
|
|
901
864
|
continue;
|
|
902
865
|
}
|
|
@@ -916,13 +879,13 @@ function stripSizeSpan(html) {
|
|
|
916
879
|
function stripWrappingEm(html) {
|
|
917
880
|
return html.replace(/^<em>([\s\S]*)<\/em>$/, '$1');
|
|
918
881
|
}
|
|
919
|
-
export function readRtfFile(file) {
|
|
882
|
+
export function readRtfFile(file, options = {}) {
|
|
920
883
|
return new Promise((resolve, reject) => {
|
|
921
884
|
const reader = new FileReader();
|
|
922
885
|
reader.onload = () => {
|
|
923
886
|
try {
|
|
924
887
|
const text = reader.result;
|
|
925
|
-
const html = rtfToHtml(text);
|
|
888
|
+
const html = rtfToHtml(text, options);
|
|
926
889
|
resolve(html);
|
|
927
890
|
}
|
|
928
891
|
catch (err) {
|
package/dist/rtf-writer.js
CHANGED
|
@@ -448,6 +448,11 @@ function walkChildren(parent, ctx) {
|
|
|
448
448
|
const node = children[i];
|
|
449
449
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
450
450
|
const text = node.textContent || '';
|
|
451
|
+
// Whitespace between two blocks (the newline in "</p>\n<p>") shows as
|
|
452
|
+
// nothing in a browser. Written out, it became a space at the start of
|
|
453
|
+
// the next paragraph.
|
|
454
|
+
if (!ctx.inPre && COLLAPSIBLE_WHITESPACE.test(text) && besideBlock(node))
|
|
455
|
+
continue;
|
|
451
456
|
if (ctx.inPre) {
|
|
452
457
|
// Split before escaping — escapeRtf collapses newlines, so the
|
|
453
458
|
// line structure a <pre> block depends on has to become \line
|
|
@@ -655,6 +660,24 @@ function walkChildren(parent, ctx) {
|
|
|
655
660
|
function scaled(ctx, ratio) {
|
|
656
661
|
return Math.round(ctx.bodySize * ratio);
|
|
657
662
|
}
|
|
663
|
+
/**
|
|
664
|
+
* Whitespace a browser collapses: spaces, tabs and line breaks, but not a
|
|
665
|
+
* non-breaking space, which is there on purpose.
|
|
666
|
+
*/
|
|
667
|
+
const COLLAPSIBLE_WHITESPACE = /^[ \t\r\n\f]*$/;
|
|
668
|
+
const BLOCK_TAGS = new Set([
|
|
669
|
+
'p', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'table', 'thead',
|
|
670
|
+
'tbody', 'tfoot', 'tr', 'td', 'th', 'blockquote', 'pre', 'hr', 'figure', 'figcaption'
|
|
671
|
+
]);
|
|
672
|
+
function isBlock(node) {
|
|
673
|
+
return node?.nodeType === Node.ELEMENT_NODE && BLOCK_TAGS.has(node.tagName.toLowerCase());
|
|
674
|
+
}
|
|
675
|
+
/** A text node with a block element on either side, or at the edge of one. */
|
|
676
|
+
function besideBlock(node) {
|
|
677
|
+
const prev = node.previousSibling;
|
|
678
|
+
const next = node.nextSibling;
|
|
679
|
+
return isBlock(prev) || isBlock(next) || (!prev && !next && isBlock(node.parentNode));
|
|
680
|
+
}
|
|
658
681
|
function getColorPrefix(el, ctx) {
|
|
659
682
|
// Support both inline style.color (spans) and the color attribute (<font>)
|
|
660
683
|
const color = el.style?.color ||
|
package/package.json
CHANGED