svelte-rtf-editor 1.0.2 → 1.1.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/components/ColorPicker.svelte +5 -7
- package/dist/components/ColorPicker.svelte.d.ts +1 -1
- package/dist/components/InkEditor.svelte +7 -0
- package/dist/components/Toolbar.svelte +0 -1
- package/dist/rtf-parser.d.ts +5 -2
- package/dist/rtf-parser.js +65 -18
- package/dist/rtf-writer.d.ts +7 -3
- package/dist/rtf-writer.js +32 -17
- package/package.json +6 -6
|
@@ -5,24 +5,22 @@
|
|
|
5
5
|
onchange?: (color: string) => void;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
let { value = '#2C2520', title = 'Color', onchange }: Props = $props();
|
|
9
|
-
|
|
10
|
-
let currentColor = $state(value);
|
|
8
|
+
let { value = $bindable('#2C2520'), title = 'Color', onchange }: Props = $props();
|
|
11
9
|
|
|
12
10
|
function handleChange(e: Event) {
|
|
13
|
-
|
|
14
|
-
onchange?.(
|
|
11
|
+
value = (e.target as HTMLInputElement).value;
|
|
12
|
+
onchange?.(value);
|
|
15
13
|
}
|
|
16
14
|
</script>
|
|
17
15
|
|
|
18
16
|
<div class="color-picker-wrap">
|
|
19
17
|
<button class="color-swatch" {title} type="button">
|
|
20
|
-
<div class="color-swatch-inner" style="background: {
|
|
18
|
+
<div class="color-swatch-inner" style="background: {value};"></div>
|
|
21
19
|
</button>
|
|
22
20
|
<input
|
|
23
21
|
type="color"
|
|
24
22
|
class="color-input"
|
|
25
|
-
value={
|
|
23
|
+
value={value}
|
|
26
24
|
onchange={handleChange}
|
|
27
25
|
{title}
|
|
28
26
|
/>
|
|
@@ -3,6 +3,6 @@ interface Props {
|
|
|
3
3
|
title?: string;
|
|
4
4
|
onchange?: (color: string) => void;
|
|
5
5
|
}
|
|
6
|
-
declare const ColorPicker: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
declare const ColorPicker: import("svelte").Component<Props, {}, "value">;
|
|
7
7
|
type ColorPicker = ReturnType<typeof ColorPicker>;
|
|
8
8
|
export default ColorPicker;
|
|
@@ -187,6 +187,13 @@
|
|
|
187
187
|
function exec(cmd: string, value: string | null = null): void {
|
|
188
188
|
restoreSelection();
|
|
189
189
|
editorEl?.focus();
|
|
190
|
+
// Color commands require an actual text selection — a collapsed cursor
|
|
191
|
+
// would silently apply the color to nothing (or to subsequently typed
|
|
192
|
+
// text), which causes accidental color spans to appear in the RTF output.
|
|
193
|
+
if (cmd === 'foreColor' || cmd === 'hiliteColor') {
|
|
194
|
+
const sel = window.getSelection();
|
|
195
|
+
if (!sel || sel.isCollapsed) return;
|
|
196
|
+
}
|
|
190
197
|
document.execCommand(cmd, false, value ?? undefined);
|
|
191
198
|
updateToolbarState();
|
|
192
199
|
}
|
|
@@ -65,7 +65,6 @@
|
|
|
65
65
|
<!-- Colors -->
|
|
66
66
|
<div class="toolbar-group">
|
|
67
67
|
<ColorPicker value="#2C2520" title="Text color" onchange={(c) => exec('foreColor', c)} />
|
|
68
|
-
<ColorPicker value="#FFFBCC" title="Highlight" onchange={(c) => exec('hiliteColor', c)} />
|
|
69
68
|
</div>
|
|
70
69
|
|
|
71
70
|
<div class="toolbar-divider"></div>
|
package/dist/rtf-parser.d.ts
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Parses RTF control words and groups into an HTML string.
|
|
5
5
|
* Supports: bold, italic, underline, strikethrough, super/subscript,
|
|
6
|
-
* font size, foreground color,
|
|
7
|
-
*
|
|
6
|
+
* font size, foreground color (\cf), highlight background color (\highlight),
|
|
7
|
+
* color table, font table, paragraphs, bullet/numbered lists,
|
|
8
|
+
* Unicode characters, hex escapes, and nested groups.
|
|
9
|
+
*
|
|
10
|
+
* \cb (character background) is silently ignored on import.
|
|
8
11
|
*/
|
|
9
12
|
export declare function rtfToHtml(rtfString: string): string;
|
|
10
13
|
export declare function readRtfFile(file: File): Promise<string>;
|
package/dist/rtf-parser.js
CHANGED
|
@@ -3,9 +3,31 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Parses RTF control words and groups into an HTML string.
|
|
5
5
|
* Supports: bold, italic, underline, strikethrough, super/subscript,
|
|
6
|
-
* font size, foreground color,
|
|
7
|
-
*
|
|
6
|
+
* font size, foreground color (\cf), highlight background color (\highlight),
|
|
7
|
+
* color table, font table, paragraphs, bullet/numbered lists,
|
|
8
|
+
* Unicode characters, hex escapes, and nested groups.
|
|
9
|
+
*
|
|
10
|
+
* \cb (character background) is silently ignored on import.
|
|
8
11
|
*/
|
|
12
|
+
// ── Highlight palette (RTF \highlight 1-16) ──
|
|
13
|
+
const HIGHLIGHT_COLORS = {
|
|
14
|
+
1: '#000000', // Black
|
|
15
|
+
2: '#0000ff', // Blue
|
|
16
|
+
3: '#00ffff', // Cyan
|
|
17
|
+
4: '#00ff00', // Green
|
|
18
|
+
5: '#ff00ff', // Magenta
|
|
19
|
+
6: '#ff0000', // Red
|
|
20
|
+
7: '#ffff00', // Yellow
|
|
21
|
+
8: '#ffffff', // White
|
|
22
|
+
9: '#000080', // Dark Blue
|
|
23
|
+
10: '#008080', // Dark Cyan
|
|
24
|
+
11: '#008000', // Dark Green
|
|
25
|
+
12: '#800080', // Dark Magenta
|
|
26
|
+
13: '#800000', // Dark Red
|
|
27
|
+
14: '#808000', // Dark Yellow
|
|
28
|
+
15: '#808080', // Dark Gray
|
|
29
|
+
16: '#c0c0c0', // Light Gray
|
|
30
|
+
};
|
|
9
31
|
// ── Tokenizer ──
|
|
10
32
|
function tokenize(rtf) {
|
|
11
33
|
const tokens = [];
|
|
@@ -136,27 +158,40 @@ function buildTree(tokens) {
|
|
|
136
158
|
}
|
|
137
159
|
// ── Color table parser ──
|
|
138
160
|
function parseColorTable(group) {
|
|
139
|
-
const colors = ['']; // index 0 = auto/default
|
|
161
|
+
const colors = ['']; // index 0 = auto/default
|
|
140
162
|
let r = 0, g = 0, b = 0;
|
|
163
|
+
let hasComponents = false;
|
|
164
|
+
function finalizeEntry() {
|
|
165
|
+
// In standard RTF, a leading ";" represents the auto color (index 0).
|
|
166
|
+
// We already reserve colors[0] for auto, so do not append another entry.
|
|
167
|
+
if (colors.length === 1 && !hasComponents)
|
|
168
|
+
return;
|
|
169
|
+
colors.push(`rgb(${r},${g},${b})`);
|
|
170
|
+
}
|
|
141
171
|
for (const node of group.children) {
|
|
142
172
|
if (node.type === 'control') {
|
|
143
|
-
if (node.word === 'red')
|
|
173
|
+
if (node.word === 'red') {
|
|
144
174
|
r = node.param ?? 0;
|
|
145
|
-
|
|
175
|
+
hasComponents = true;
|
|
176
|
+
}
|
|
177
|
+
else if (node.word === 'green') {
|
|
146
178
|
g = node.param ?? 0;
|
|
147
|
-
|
|
179
|
+
hasComponents = true;
|
|
180
|
+
}
|
|
181
|
+
else if (node.word === 'blue') {
|
|
148
182
|
b = node.param ?? 0;
|
|
183
|
+
hasComponents = true;
|
|
184
|
+
}
|
|
149
185
|
}
|
|
150
186
|
else if (node.type === 'text' && node.value.includes(';')) {
|
|
151
|
-
//
|
|
152
|
-
const
|
|
153
|
-
for (let i = 0; i <
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
187
|
+
// Each ';' terminates one color entry
|
|
188
|
+
const semiCount = (node.value.match(/;/g) || []).length;
|
|
189
|
+
for (let i = 0; i < semiCount; i++) {
|
|
190
|
+
finalizeEntry();
|
|
191
|
+
r = 0;
|
|
192
|
+
g = 0;
|
|
193
|
+
b = 0;
|
|
194
|
+
hasComponents = false;
|
|
160
195
|
}
|
|
161
196
|
}
|
|
162
197
|
}
|
|
@@ -220,10 +255,10 @@ export function rtfToHtml(rtfString) {
|
|
|
220
255
|
sub: false,
|
|
221
256
|
fontSize: 24, // 12pt default
|
|
222
257
|
colorIndex: 0,
|
|
258
|
+
highlightIndex: 0,
|
|
223
259
|
fontIndex: 0,
|
|
224
260
|
ucSkip: 1,
|
|
225
261
|
inList: false,
|
|
226
|
-
listType: null
|
|
227
262
|
};
|
|
228
263
|
}
|
|
229
264
|
const stateStack = [defaultState()];
|
|
@@ -241,6 +276,11 @@ export function rtfToHtml(rtfString) {
|
|
|
241
276
|
styles.push(`color:${color}`);
|
|
242
277
|
}
|
|
243
278
|
}
|
|
279
|
+
if (state.highlightIndex > 0) {
|
|
280
|
+
const bg = HIGHLIGHT_COLORS[state.highlightIndex];
|
|
281
|
+
if (bg)
|
|
282
|
+
styles.push(`background-color:${bg}`);
|
|
283
|
+
}
|
|
244
284
|
const ptSize = state.fontSize / 2;
|
|
245
285
|
if (ptSize && ptSize !== 12) {
|
|
246
286
|
styles.push(`font-size:${ptSize}pt`);
|
|
@@ -363,6 +403,12 @@ export function rtfToHtml(rtfString) {
|
|
|
363
403
|
if (p != null)
|
|
364
404
|
state.colorIndex = p;
|
|
365
405
|
break;
|
|
406
|
+
case 'cb':
|
|
407
|
+
break; // \cb not rendered on import
|
|
408
|
+
case 'highlight':
|
|
409
|
+
if (p != null)
|
|
410
|
+
state.highlightIndex = p;
|
|
411
|
+
break;
|
|
366
412
|
case 'f':
|
|
367
413
|
if (p != null)
|
|
368
414
|
state.fontIndex = p;
|
|
@@ -414,6 +460,8 @@ export function rtfToHtml(rtfString) {
|
|
|
414
460
|
state.sub = false;
|
|
415
461
|
state.fontSize = 24;
|
|
416
462
|
state.colorIndex = 0;
|
|
463
|
+
state.highlightIndex = 0;
|
|
464
|
+
state.fontIndex = 0;
|
|
417
465
|
break;
|
|
418
466
|
case 'page':
|
|
419
467
|
flushParagraph();
|
|
@@ -526,9 +574,8 @@ export function rtfToHtml(rtfString) {
|
|
|
526
574
|
rendered.push(`<p>${trimmed}</p>`);
|
|
527
575
|
}
|
|
528
576
|
const output = rendered.join('\n');
|
|
529
|
-
//
|
|
577
|
+
// fontTable is parsed (for future font rendering) but not yet used in output
|
|
530
578
|
void fontTable;
|
|
531
|
-
void paragraphHasText;
|
|
532
579
|
return output || '<p></p>';
|
|
533
580
|
}
|
|
534
581
|
function stripSizeSpan(html) {
|
package/dist/rtf-writer.d.ts
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
* HTML to RTF converter
|
|
3
3
|
*
|
|
4
4
|
* Walks a contenteditable DOM tree and produces a valid RTF 1.x string.
|
|
5
|
-
* Supports: bold, italic, underline, strikethrough, font sizes (
|
|
6
|
-
* foreground colors, paragraphs, bullet
|
|
7
|
-
* code blocks,
|
|
5
|
+
* Supports: bold, italic, underline, strikethrough, font sizes (h1–h3),
|
|
6
|
+
* foreground colors (\cf), paragraphs, bullet/numbered lists, blockquotes,
|
|
7
|
+
* code blocks, hyperlinks, images (as alt-text placeholders), horizontal
|
|
8
|
+
* rules, line breaks, and Unicode characters.
|
|
9
|
+
*
|
|
10
|
+
* Background color is intentionally not written — RTF highlight support
|
|
11
|
+
* is inconsistent across viewers (TextEdit uses \AppleHighlight, not \highlight).
|
|
8
12
|
*/
|
|
9
13
|
export declare function htmlToRtf(editorEl: HTMLElement): string;
|
package/dist/rtf-writer.js
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
* HTML to RTF converter
|
|
3
3
|
*
|
|
4
4
|
* Walks a contenteditable DOM tree and produces a valid RTF 1.x string.
|
|
5
|
-
* Supports: bold, italic, underline, strikethrough, font sizes (
|
|
6
|
-
* foreground colors, paragraphs, bullet
|
|
7
|
-
* code blocks,
|
|
5
|
+
* Supports: bold, italic, underline, strikethrough, font sizes (h1–h3),
|
|
6
|
+
* foreground colors (\cf), paragraphs, bullet/numbered lists, blockquotes,
|
|
7
|
+
* code blocks, hyperlinks, images (as alt-text placeholders), horizontal
|
|
8
|
+
* rules, line breaks, and Unicode characters.
|
|
9
|
+
*
|
|
10
|
+
* Background color is intentionally not written — RTF highlight support
|
|
11
|
+
* is inconsistent across viewers (TextEdit uses \AppleHighlight, not \highlight).
|
|
8
12
|
*/
|
|
9
13
|
// ── Color helpers ──
|
|
10
14
|
function parseColor(colorStr) {
|
|
@@ -77,7 +81,9 @@ export function htmlToRtf(editorEl) {
|
|
|
77
81
|
colors.forEach((c, i) => {
|
|
78
82
|
colorIndex.set(colorKey(c), i + 1);
|
|
79
83
|
});
|
|
80
|
-
|
|
84
|
+
// Emit a leading auto/default color entry for broad RTF compatibility:
|
|
85
|
+
// \cf0 = auto, first explicit color starts at \cf1.
|
|
86
|
+
let colorTable = '{\\colortbl;';
|
|
81
87
|
for (const c of colors) {
|
|
82
88
|
colorTable += `\\red${c.r}\\green${c.g}\\blue${c.b};`;
|
|
83
89
|
}
|
|
@@ -97,20 +103,20 @@ export function htmlToRtf(editorEl) {
|
|
|
97
103
|
'}';
|
|
98
104
|
return rtf;
|
|
99
105
|
}
|
|
106
|
+
function addColor(colorStr, colorMap) {
|
|
107
|
+
if (!colorStr)
|
|
108
|
+
return;
|
|
109
|
+
const c = parseColor(colorStr);
|
|
110
|
+
if (c)
|
|
111
|
+
colorMap.set(colorKey(c), c);
|
|
112
|
+
}
|
|
100
113
|
function collectColors(el, colorMap) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (el.style?.backgroundColor) {
|
|
107
|
-
const c = parseColor(el.style.backgroundColor);
|
|
108
|
-
if (c)
|
|
109
|
-
colorMap.set(colorKey(c), c);
|
|
110
|
-
}
|
|
111
|
-
for (const child of el.children) {
|
|
114
|
+
addColor(el.style?.color, colorMap);
|
|
115
|
+
// <font color="..."> is produced by execCommand('foreColor')
|
|
116
|
+
if (el.tagName?.toLowerCase() === 'font')
|
|
117
|
+
addColor(el.getAttribute('color'), colorMap);
|
|
118
|
+
for (const child of el.children)
|
|
112
119
|
collectColors(child, colorMap);
|
|
113
|
-
}
|
|
114
120
|
}
|
|
115
121
|
function walkChildren(parent, ctx) {
|
|
116
122
|
let rtf = '';
|
|
@@ -244,6 +250,13 @@ function walkChildren(parent, ctx) {
|
|
|
244
250
|
rtf += `${colorPfx}${sizePfx}${walkChildren(el, ctx)}${sizesfx}${colorSfx}`;
|
|
245
251
|
break;
|
|
246
252
|
}
|
|
253
|
+
// <font color="..."> is produced by execCommand('foreColor') in contenteditable
|
|
254
|
+
case 'font': {
|
|
255
|
+
const colorPfx = getColorPrefix(el, ctx);
|
|
256
|
+
const colorSfx = colorPfx ? '\\cf0 ' : '';
|
|
257
|
+
rtf += `${colorPfx}${walkChildren(el, ctx)}${colorSfx}`;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
247
260
|
default:
|
|
248
261
|
rtf += walkChildren(el, ctx);
|
|
249
262
|
break;
|
|
@@ -252,7 +265,9 @@ function walkChildren(parent, ctx) {
|
|
|
252
265
|
return rtf;
|
|
253
266
|
}
|
|
254
267
|
function getColorPrefix(el, ctx) {
|
|
255
|
-
|
|
268
|
+
// Support both inline style.color (spans) and the color attribute (<font>)
|
|
269
|
+
const color = el.style?.color ||
|
|
270
|
+
(el.tagName.toLowerCase() === 'font' ? (el.getAttribute('color') ?? '') : '');
|
|
256
271
|
if (!color)
|
|
257
272
|
return '';
|
|
258
273
|
const c = parseColor(color);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-rtf-editor",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.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",
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@changesets/cli": "^2.30.0",
|
|
48
|
-
"@sveltejs/adapter-auto": "^7.0.
|
|
49
|
-
"@sveltejs/kit": "^2.
|
|
48
|
+
"@sveltejs/adapter-auto": "^7.0.1",
|
|
49
|
+
"@sveltejs/kit": "^2.53.4",
|
|
50
50
|
"@sveltejs/package": "^2.5.7",
|
|
51
51
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
52
|
-
"publint": "^0.3.
|
|
53
|
-
"svelte": "^5.
|
|
54
|
-
"svelte-check": "^4.4.
|
|
52
|
+
"publint": "^0.3.18",
|
|
53
|
+
"svelte": "^5.53.7",
|
|
54
|
+
"svelte-check": "^4.4.4",
|
|
55
55
|
"typescript": "^5.9.3",
|
|
56
56
|
"vite": "^7.3.1",
|
|
57
57
|
"vitest": "^4.0.18"
|