svelte-rtf-editor 0.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/README.md +224 -0
- package/dist/RtfViewer.svelte +95 -0
- package/dist/RtfViewer.svelte.d.ts +6 -0
- package/dist/components/ColorPicker.svelte +72 -0
- package/dist/components/ColorPicker.svelte.d.ts +8 -0
- package/dist/components/InkEditor.svelte +703 -0
- package/dist/components/InkEditor.svelte.d.ts +36 -0
- package/dist/components/Modal.svelte +104 -0
- package/dist/components/Modal.svelte.d.ts +11 -0
- package/dist/components/Toolbar.svelte +182 -0
- package/dist/components/Toolbar.svelte.d.ts +11 -0
- package/dist/components/ToolbarButton.svelte +71 -0
- package/dist/components/ToolbarButton.svelte.d.ts +10 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +7 -0
- package/dist/rtf-parser.d.ts +10 -0
- package/dist/rtf-parser.js +553 -0
- package/dist/rtf-writer.d.ts +9 -0
- package/dist/rtf-writer.js +281 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +96 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# svelte-rtf-editor
|
|
2
|
+
|
|
3
|
+
RTF viewer and rich-text editor components for **Svelte 5**.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/svelte-rtf-editor)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **`RtfViewer`** — Renders an RTF string as formatted HTML with a one-click "Copy RTF" button
|
|
13
|
+
- **`InkEditor`** — Full rich-text editor that reads and writes RTF, built on `contenteditable`
|
|
14
|
+
- Formatting toolbar: bold, italic, underline, strikethrough, font colour
|
|
15
|
+
- Block-level controls: headings (H1–H3), paragraph, blockquote, code block
|
|
16
|
+
- Import an RTF file from disk; export as RTF, HTML, or Markdown
|
|
17
|
+
- Optional auto-save to `localStorage`
|
|
18
|
+
- Word count and character count status bar
|
|
19
|
+
- Keyboard shortcuts (`Ctrl+S` to save, `Ctrl+B/I/U` for formatting)
|
|
20
|
+
- **Utility functions** — `rtfToHtml`, `htmlToRtf`, `readRtfFile`, `htmlToMarkdown`, `downloadFile`
|
|
21
|
+
- Zero runtime dependencies — just Svelte 5 as a peer dependency
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install svelte-rtf-editor
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Read-only RTF viewer
|
|
36
|
+
|
|
37
|
+
```svelte
|
|
38
|
+
<script>
|
|
39
|
+
import { RtfViewer } from 'svelte-rtf-editor';
|
|
40
|
+
|
|
41
|
+
const rtf = String.raw`{\rtf1\ansi {\b Hello}, {\i world}!}`;
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<RtfViewer content={rtf} />
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The viewer renders the RTF as HTML and places a **Copy RTF** button in the top-right corner.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### Rich-text editor
|
|
52
|
+
|
|
53
|
+
```svelte
|
|
54
|
+
<script>
|
|
55
|
+
import { InkEditor } from 'svelte-rtf-editor';
|
|
56
|
+
|
|
57
|
+
let html = '';
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<InkEditor
|
|
61
|
+
placeholder="Start writing…"
|
|
62
|
+
onchange={({ html: h }) => (html = h)}
|
|
63
|
+
/>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### Getting RTF out of the editor
|
|
67
|
+
|
|
68
|
+
Bind a reference to the component with `bind:this` and call `getRTF()`:
|
|
69
|
+
|
|
70
|
+
```svelte
|
|
71
|
+
<script>
|
|
72
|
+
import { InkEditor } from 'svelte-rtf-editor';
|
|
73
|
+
|
|
74
|
+
let editor;
|
|
75
|
+
|
|
76
|
+
function save() {
|
|
77
|
+
const rtf = editor.getRTF();
|
|
78
|
+
console.log(rtf); // {\rtf1\ansi ...}
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<InkEditor bind:this={editor} />
|
|
83
|
+
<button onclick={save}>Save</button>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Pre-loading content
|
|
87
|
+
|
|
88
|
+
Pass an HTML string as `content`. If the content is already RTF, convert it first:
|
|
89
|
+
|
|
90
|
+
```svelte
|
|
91
|
+
<script>
|
|
92
|
+
import { InkEditor, rtfToHtml } from 'svelte-rtf-editor';
|
|
93
|
+
|
|
94
|
+
const rtfFromServer = String.raw`{\rtf1\ansi {\b Hello}}`;
|
|
95
|
+
const html = rtfToHtml(rtfFromServer);
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<InkEditor content={html} autosave={false} />
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## API
|
|
104
|
+
|
|
105
|
+
### `<RtfViewer>`
|
|
106
|
+
|
|
107
|
+
| Prop | Type | Default | Description |
|
|
108
|
+
|-----------|----------|---------|------------------------------------------|
|
|
109
|
+
| `content` | `string` | — | Raw RTF string to parse and display |
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### `<InkEditor>`
|
|
114
|
+
|
|
115
|
+
#### Props
|
|
116
|
+
|
|
117
|
+
| Prop | Type | Default | Description |
|
|
118
|
+
|-----------------|------------|------------------------------|-----------------------------------------------------------|
|
|
119
|
+
| `content` | `string` | `'<p></p>'` | Initial HTML content |
|
|
120
|
+
| `placeholder` | `string` | `'Start writing something beautiful...'` | Placeholder text when editor is empty |
|
|
121
|
+
| `autosave` | `boolean` | `true` | Save content to `localStorage` on change |
|
|
122
|
+
| `storageKey` | `string` | `'ink-editor-content'` | `localStorage` key used for auto-save |
|
|
123
|
+
| `showToolbar` | `boolean` | `true` | Show the formatting toolbar |
|
|
124
|
+
| `showStatusBar` | `boolean` | `true` | Show the word/character count bar |
|
|
125
|
+
| `minHeight` | `string` | `'40vh'` | CSS `min-height` of the editing area |
|
|
126
|
+
| `readonly` | `boolean` | `false` | Disable editing |
|
|
127
|
+
|
|
128
|
+
#### Callback props
|
|
129
|
+
|
|
130
|
+
| Prop | Payload | Description |
|
|
131
|
+
|------------|----------------------------------------------|---------------------------------------------|
|
|
132
|
+
| `onchange` | `{ html, text, wordCount, charCount }` | Fired on every content change |
|
|
133
|
+
| `onsave` | `{ html }` | Fired on auto-save or `Ctrl+S` |
|
|
134
|
+
| `onimport` | `{ html }` | Fired after a successful RTF file import |
|
|
135
|
+
|
|
136
|
+
#### Methods (via `bind:this`)
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
editor.getHTML() // → string — current editor HTML
|
|
140
|
+
editor.getText() // → string — plain text (no tags)
|
|
141
|
+
editor.setHTML(html) // → void — replace content programmatically
|
|
142
|
+
editor.getMarkdown() // → string — Markdown conversion of content
|
|
143
|
+
editor.getRTF() // → string — RTF conversion of content
|
|
144
|
+
editor.clear() // → void — clear the editor
|
|
145
|
+
editor.focus() // → void — focus the editor
|
|
146
|
+
editor.exportFile(format) // → void — download as 'html' | 'md' | 'rtf'
|
|
147
|
+
editor.importRtf() // → void — open the file picker to import .rtf
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Utility functions
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { rtfToHtml, htmlToRtf, readRtfFile, htmlToMarkdown, downloadFile } from 'svelte-rtf-editor';
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
| Function | Description |
|
|
159
|
+
|---------------------------------------|----------------------------------------------------|
|
|
160
|
+
| `rtfToHtml(rtf: string): string` | Parse an RTF string and return an HTML string |
|
|
161
|
+
| `htmlToRtf(html: string): string` | Convert an HTML string to RTF |
|
|
162
|
+
| `readRtfFile(file: File): Promise<string>` | Read a `.rtf` File object and return HTML |
|
|
163
|
+
| `htmlToMarkdown(el: HTMLElement): string` | Convert a DOM element's content to Markdown |
|
|
164
|
+
| `downloadFile(name, content, mime): void` | Trigger a file download in the browser |
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Theming
|
|
169
|
+
|
|
170
|
+
The components use CSS custom properties with sensible fallbacks, so they work out of the box but are easy to customise. Override these in your own CSS:
|
|
171
|
+
|
|
172
|
+
```css
|
|
173
|
+
:root {
|
|
174
|
+
--text: #2c2520;
|
|
175
|
+
--text-muted: #8a7e72;
|
|
176
|
+
--surface: #f2f0ec;
|
|
177
|
+
--border: #e5e2dc;
|
|
178
|
+
--ink: #1a1714;
|
|
179
|
+
--accent: #6e56cf;
|
|
180
|
+
--accent-soft: #ede9fe;
|
|
181
|
+
--radius-sm: 5px;
|
|
182
|
+
--transition: 150ms ease;
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Keyboard shortcuts
|
|
189
|
+
|
|
190
|
+
| Shortcut | Action |
|
|
191
|
+
|------------|-------------------|
|
|
192
|
+
| `Ctrl+B` | Bold |
|
|
193
|
+
| `Ctrl+I` | Italic |
|
|
194
|
+
| `Ctrl+U` | Underline |
|
|
195
|
+
| `Ctrl+S` | Save (fires `onsave`) |
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Local development
|
|
200
|
+
|
|
201
|
+
Clone this repo and link it to your project for instant iteration without publishing:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
# In svelte-rtf-editor/
|
|
205
|
+
npm run prepack # builds dist/
|
|
206
|
+
npm link
|
|
207
|
+
|
|
208
|
+
# In your project/
|
|
209
|
+
npm link svelte-rtf-editor
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Re-run `npm run prepack` in this repo after any change. When done:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
# In your project/
|
|
216
|
+
npm unlink svelte-rtf-editor
|
|
217
|
+
npm install svelte-rtf-editor
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
MIT
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { rtfToHtml } from "./rtf-parser.js";
|
|
3
|
+
|
|
4
|
+
let { content }: { content: string } = $props();
|
|
5
|
+
|
|
6
|
+
const htmlContent = $derived(rtfToHtml(content));
|
|
7
|
+
|
|
8
|
+
let copied = $state(false);
|
|
9
|
+
|
|
10
|
+
async function copyRtf() {
|
|
11
|
+
await navigator.clipboard.writeText(content);
|
|
12
|
+
copied = true;
|
|
13
|
+
setTimeout(() => (copied = false), 2000);
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div class="rtf-viewer-wrap">
|
|
18
|
+
<button class="copy-btn" onclick={copyRtf} title="Copy raw RTF">
|
|
19
|
+
{#if copied}
|
|
20
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
21
|
+
Copied
|
|
22
|
+
{:else}
|
|
23
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
24
|
+
Copy RTF
|
|
25
|
+
{/if}
|
|
26
|
+
</button>
|
|
27
|
+
<div class="rtf-viewer">
|
|
28
|
+
{@html htmlContent}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<style>
|
|
33
|
+
.rtf-viewer-wrap {
|
|
34
|
+
position: relative;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.copy-btn {
|
|
38
|
+
position: absolute;
|
|
39
|
+
top: 0;
|
|
40
|
+
right: 0;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 5px;
|
|
44
|
+
padding: 4px 10px;
|
|
45
|
+
font-size: 12px;
|
|
46
|
+
font-weight: 500;
|
|
47
|
+
border: 1px solid var(--border, #e5e2dc);
|
|
48
|
+
border-radius: var(--radius-sm, 5px);
|
|
49
|
+
background: var(--surface, #f2f0ec);
|
|
50
|
+
color: var(--text-muted, #8a7e72);
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
transition: all 150ms ease;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.copy-btn:hover {
|
|
56
|
+
background: var(--border, #e5e2dc);
|
|
57
|
+
color: var(--text, #2c2520);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.copy-btn svg {
|
|
61
|
+
width: 13px;
|
|
62
|
+
height: 13px;
|
|
63
|
+
flex-shrink: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.rtf-viewer {
|
|
67
|
+
line-height: 1.4;
|
|
68
|
+
white-space: normal;
|
|
69
|
+
word-wrap: break-word;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.rtf-viewer :global(br) {
|
|
73
|
+
display: inline;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.rtf-viewer :global(strong) {
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.rtf-viewer :global(em) {
|
|
81
|
+
font-style: italic;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.rtf-viewer :global(u) {
|
|
85
|
+
text-decoration: underline;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.rtf-viewer :global(p) {
|
|
89
|
+
margin: 0 0 0.2em;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.rtf-viewer :global(p:last-child) {
|
|
93
|
+
margin-bottom: 0;
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
value?: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
onchange?: (color: string) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let { value = '#2C2520', title = 'Color', onchange }: Props = $props();
|
|
9
|
+
|
|
10
|
+
let currentColor = $state(value);
|
|
11
|
+
|
|
12
|
+
function handleChange(e: Event) {
|
|
13
|
+
currentColor = (e.target as HTMLInputElement).value;
|
|
14
|
+
onchange?.(currentColor);
|
|
15
|
+
}
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<div class="color-picker-wrap">
|
|
19
|
+
<button class="color-swatch" {title} type="button">
|
|
20
|
+
<div class="color-swatch-inner" style="background: {currentColor};"></div>
|
|
21
|
+
</button>
|
|
22
|
+
<input
|
|
23
|
+
type="color"
|
|
24
|
+
class="color-input"
|
|
25
|
+
value={currentColor}
|
|
26
|
+
onchange={handleChange}
|
|
27
|
+
{title}
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<style>
|
|
32
|
+
.color-picker-wrap {
|
|
33
|
+
position: relative;
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.color-swatch {
|
|
39
|
+
width: 34px;
|
|
40
|
+
height: 34px;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
border: none;
|
|
45
|
+
background: transparent;
|
|
46
|
+
border-radius: var(--radius-sm);
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
transition: all var(--transition);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.color-swatch:hover {
|
|
52
|
+
background: var(--surface);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.color-swatch-inner {
|
|
56
|
+
width: 18px;
|
|
57
|
+
height: 18px;
|
|
58
|
+
border-radius: 4px;
|
|
59
|
+
border: 2px solid var(--border);
|
|
60
|
+
transition: all var(--transition);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.color-input {
|
|
64
|
+
position: absolute;
|
|
65
|
+
width: 34px;
|
|
66
|
+
height: 34px;
|
|
67
|
+
opacity: 0;
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
top: 0;
|
|
70
|
+
left: 0;
|
|
71
|
+
}
|
|
72
|
+
</style>
|