svelte-rtf-editor 1.0.3 → 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 +59 -10
- 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,25 +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
187
|
// Each ';' terminates one color entry
|
|
152
188
|
const semiCount = (node.value.match(/;/g) || []).length;
|
|
153
189
|
for (let i = 0; i < semiCount; i++) {
|
|
154
|
-
|
|
190
|
+
finalizeEntry();
|
|
155
191
|
r = 0;
|
|
156
192
|
g = 0;
|
|
157
193
|
b = 0;
|
|
194
|
+
hasComponents = false;
|
|
158
195
|
}
|
|
159
196
|
}
|
|
160
197
|
}
|
|
@@ -218,10 +255,10 @@ export function rtfToHtml(rtfString) {
|
|
|
218
255
|
sub: false,
|
|
219
256
|
fontSize: 24, // 12pt default
|
|
220
257
|
colorIndex: 0,
|
|
258
|
+
highlightIndex: 0,
|
|
221
259
|
fontIndex: 0,
|
|
222
260
|
ucSkip: 1,
|
|
223
261
|
inList: false,
|
|
224
|
-
listType: null
|
|
225
262
|
};
|
|
226
263
|
}
|
|
227
264
|
const stateStack = [defaultState()];
|
|
@@ -239,6 +276,11 @@ export function rtfToHtml(rtfString) {
|
|
|
239
276
|
styles.push(`color:${color}`);
|
|
240
277
|
}
|
|
241
278
|
}
|
|
279
|
+
if (state.highlightIndex > 0) {
|
|
280
|
+
const bg = HIGHLIGHT_COLORS[state.highlightIndex];
|
|
281
|
+
if (bg)
|
|
282
|
+
styles.push(`background-color:${bg}`);
|
|
283
|
+
}
|
|
242
284
|
const ptSize = state.fontSize / 2;
|
|
243
285
|
if (ptSize && ptSize !== 12) {
|
|
244
286
|
styles.push(`font-size:${ptSize}pt`);
|
|
@@ -361,6 +403,12 @@ export function rtfToHtml(rtfString) {
|
|
|
361
403
|
if (p != null)
|
|
362
404
|
state.colorIndex = p;
|
|
363
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;
|
|
364
412
|
case 'f':
|
|
365
413
|
if (p != null)
|
|
366
414
|
state.fontIndex = p;
|
|
@@ -412,6 +460,8 @@ export function rtfToHtml(rtfString) {
|
|
|
412
460
|
state.sub = false;
|
|
413
461
|
state.fontSize = 24;
|
|
414
462
|
state.colorIndex = 0;
|
|
463
|
+
state.highlightIndex = 0;
|
|
464
|
+
state.fontIndex = 0;
|
|
415
465
|
break;
|
|
416
466
|
case 'page':
|
|
417
467
|
flushParagraph();
|
|
@@ -524,9 +574,8 @@ export function rtfToHtml(rtfString) {
|
|
|
524
574
|
rendered.push(`<p>${trimmed}</p>`);
|
|
525
575
|
}
|
|
526
576
|
const output = rendered.join('\n');
|
|
527
|
-
//
|
|
577
|
+
// fontTable is parsed (for future font rendering) but not yet used in output
|
|
528
578
|
void fontTable;
|
|
529
|
-
void paragraphHasText;
|
|
530
579
|
return output || '<p></p>';
|
|
531
580
|
}
|
|
532
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"
|