tosijs-styled-editor 0.4.3 → 0.4.5

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/CHANGELOG.md ADDED
@@ -0,0 +1,311 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.4.5] - 2026-09-17
11
+
12
+ ### Added
13
+
14
+ - **`SECURITY.md`**, with the one thing it needs to say: a sanitizer bypass
15
+ belongs to [`tosijs-kilpi`](https://github.com/tonioloewald/kilpi/issues),
16
+ because that is where the code lives. The README's security section now points
17
+ at kilpi's policy as authoritative rather than restating it — a copy of a
18
+ policy drifts from the policy, which is the same failure the extraction
19
+ removed from the code.
20
+ - **`NOTICE`**, for the three Apache-2.0 works the drop-in `dist/index.js`
21
+ bundles.
22
+
23
+ ### Changed
24
+
25
+ - **Sanitization moved to [`tosijs-kilpi`](https://github.com/tonioloewald/kilpi)**,
26
+ the same code extracted as a standalone library so it is not maintained in two
27
+ places. No API change: `sanitizeInPlace` and `isSafeNavigationUrl` are still
28
+ exported from this package, `editor.sanitize` still works the same way, and
29
+ behaviour is identical.
30
+
31
+ The reason it matters is not tidiness. When the sanitizer briefly existed
32
+ twice, a URL-normalization fix reached one copy and not the other — recorded
33
+ as M1 in the 0.4.4 review. Across two repositories that drift would not even
34
+ appear in a diff. kilpi carries DOMPurify's published 223-fixture corpus as a
35
+ hard publish gate, which this package could not run on its own.
36
+
37
+ `tosijs-kilpi` is a real dependency (this package's first — tosijs and
38
+ tosijs-ui remain peers), at `^1.0.0`. kilpi went 1.0.0 for that reason alone:
39
+ `^0.1.0` resolves to `>=0.1.0 <0.2.0`, so a 0.2.0 security fix would have
40
+ reached no installed consumer, and for a dependency that *is* the XSS defence
41
+ a range that blocks propagation is a defect in itself. It is external in `dist/module.js`, so a consumer who
42
+ also depends on it directly gets one copy, and bundled into `dist/index.js`,
43
+ which assumes no installs.
44
+
45
+
46
+ ## [0.4.4] - 2026-09-16
47
+
48
+ First release since 0.4.3 to reach npm. 0.4.4 and 0.4.5 were versioned in the
49
+ repo during development and **never published**, so this is numbered 0.4.4:
50
+ semver describes what consumers observe between releases, and consumers
51
+ observed none of it.
52
+
53
+ Everything below through the 0.2.0 heading shipped across 0.4.2–0.4.4. The
54
+ earlier 0.4.x releases went out without changelog sections of their own, so
55
+ they are collected here rather than reconstructed inaccurately.
56
+
57
+ ### Security
58
+
59
+ - **Pasted and dropped HTML is now sanitized** before it enters the document.
60
+ The editor replaced `contentEditable` but not the sanitization the browser
61
+ was doing on its behalf: clipboard and drop HTML went in through `innerHTML`
62
+ verbatim, so `<img onerror>`, `<svg onload>`, `javascript:` URLs and
63
+ `<script>` reached the live document — and from there `value`,
64
+ `internals.setFormValue` and every undo snapshot, meaning a host storing
65
+ `value` stored the payload. Handlers, executing elements and unsafe URL
66
+ schemes are now stripped at the single shared paste/drop choke point.
67
+ Ordinary formatting and unregistered custom elements are preserved.
68
+ `editor.sanitize` is a swappable hook if you would rather supply your own
69
+ (DOMPurify drops in; see the README).
70
+ *This path was unreachable in 0.4.2–0.4.3 only because `insertionPoint()` was
71
+ broken; fixing that is what made it live again.*
72
+ - **Ctrl/Cmd-clicking a link checks the URL scheme** and always opens a new
73
+ context. `javascript:` executes in the embedding page's origin and `noopener`
74
+ does not prevent it; `_self`/`_top` are resolved before `noopener` is
75
+ consulted, so a document-supplied `target` could run it same-origin.
76
+ `setLink` validates the scheme too.
77
+ - **Commands built from runtime values no longer go through the string form.**
78
+ `executeCommand` splits on `;`, and a data URI contains `;` by spec — so
79
+ every dropped image produced `<img src="data:image/png">` plus a bogus second
80
+ command, and a crafted filename could inject one. `doCommandWith(name, ...args)`
81
+ passes arguments without parsing.
82
+
83
+ ### Fixed
84
+
85
+ - **Mobile browsers no longer zoom when you select text.** Two independent
86
+ causes: the caret is a real `<input>` (that is what raises the mobile
87
+ keyboard) and inherited the UA default form-control font size of 11px, and
88
+ iOS Safari zooms the page on focus below 16px; and double-tap selects a word
89
+ here, which a touch browser reads as zoom. Fixed by sizing the caret at 16px
90
+ and setting `touch-action: manipulation` on the document, which keeps panning
91
+ and pinch-zoom. Deliberately NOT fixed with `user-scalable=no`, which would
92
+ fail WCAG 1.4.4.
93
+ - **The caret no longer strands itself when the document scrolls.** The overlay
94
+ is positioned in viewport coordinates but only repainted when the selection
95
+ bounds changed, so scrolling left it where the text used to be. Scroll, window
96
+ resize and a `ResizeObserver` on the document now repaint it; measured drift is
97
+ 0 at every scroll offset, and it hides correctly once its line scrolls out of
98
+ view.
99
+ - **Commands that insert at the caret worked again.** `insertionPoint()` selected
100
+ `input.caret`, which has matched nothing since the bound markers stopped being
101
+ `<input>` elements — so `insertFootnote`, `insertTable`, `insertImage` and
102
+ `setLink` all bailed out silently. Its unit test passed throughout because it
103
+ built the `<input>` itself and asserted the selector found it; the test now
104
+ uses the real `createBounds()`.
105
+
106
+ ### Changed
107
+
108
+ - Both `setTimeout` calls replaced with the signals they were approximating: a
109
+ `ResizeObserver` tracks the touch-affordance padding transition continuously
110
+ instead of waiting a guessed 160ms (and no longer leaves the affordances
111
+ invisible if the transition never runs), and the touch menu's dismiss handler
112
+ ignores the event that opened it by identity instead of deferring its own
113
+ subscription.
114
+ - The drop-in `dist/index.js` build is minified: 83.5kB → 70.8kB gzipped.
115
+
116
+
117
+ ### Added
118
+
119
+ - **Drag and drop editing.** Selected text is a real draggable object, offering
120
+ `text/html` and `text/plain` so the receiver picks — which means dragging works
121
+ between windows, between browsers, and to and from the desktop. Move within
122
+ the editor, Alt to copy, and leaving the editor is always a copy. Dropped image
123
+ files come in as data URIs; dropped HTML runs through the same `pastemode`
124
+ path as a paste.
125
+
126
+ - **Links**: `setLink <url> [target]` and `removeLink`. Defaults to
127
+ `target="_blank"` with `rel="noopener"`. Clicking a link in the editor places
128
+ the caret; Ctrl/Cmd-click follows it.
129
+ - **Images**: `insertImage <url> [alt…]`. An `<img>` is already a leaf node, so
130
+ selection and deletion treat it as one thing with no special casing.
131
+ - **Footnotes**: `insertFootnote [text…]` and `renumberFootnotes`. Numbers are
132
+ derived from document order rather than stored, so inserting in the middle
133
+ renumbers the rest and reorders the list. Note that renumbering happens at
134
+ INSERTION time only — deleting a reference with Backspace currently leaves its
135
+ entry orphaned in the list and does not renumber the survivors. Tracked in
136
+ `TODO.md`; `EXTENSIBILITY.md` covers why the fix is a lifecycle change rather
137
+ than another call to `renumberFootnotes`.
138
+ - An **Insert** menu carrying all three.
139
+
140
+ - **Localization**, following tosijs-ui's conventions rather than a private
141
+ scheme: `localized` on the element translates the built-in widgets and adds a
142
+ flag-only language picker. Toolbar buttons carry `data-tosi-localized` (a JSON
143
+ attribute-to-key map, re-applied on locale change), menus set `localized`, and
144
+ menu labels are `<tosi-localized>`, so custom widgets get the same treatment.
145
+ - `localized-strings.tsv` — a sample table in English and Suomi covering all 51
146
+ UI strings. Adding a language is adding a column. Column 0 is both the lookup
147
+ key and the English text, so missing cells fall back to English and a
148
+ half-translated column is safe to ship.
149
+ - README is pinned to the top of the doc-site nav.
150
+ - A Right-to-Left doc page with live examples: RTL blocks in Arabic and Hebrew,
151
+ LTR-with-embedded-RTL, and RTL blocks with embedded LTR runs (inline code,
152
+ URLs, version numbers) — the cases where visual and logical order disagree and
153
+ a DOM-only selection has to earn its keep.
154
+ - Menu dropdowns are compacted (30px rows, 16px horizontal padding) and scoped
155
+ to this editor's own menus.
156
+
157
+ - `setList ul | ol | none` — bulleted and numbered list formatting, exposed as
158
+ toolbar buttons and Style menu entries. Adjacent selected blocks become one
159
+ list, a converted block merges into an adjacent list of the same type (so
160
+ `<ol>` numbering continues), and re-applying the current type toggles it off.
161
+ - Live behaviour tests that run in a real browser, covering the click
162
+ positioning that happy-dom cannot see.
163
+ - `llms.txt`, a sitemap, and prerendered doc pages, generated by the doc system.
164
+ - `.haltija.json` pinning agent browser commands to this project's dev origin.
165
+
166
+ ### Fixed
167
+
168
+ - **The caret no longer reshapes the text it sits in.** It was an `<input>`
169
+ between characters, and a replaced element breaks the shaping run: measured on
170
+ Arabic, a neighbouring glyph's advance moved 6.2 → 6.7 even though the caret's
171
+ box was already width-neutral, and absolute positioning did not rescue it. The
172
+ in-text anchors are now plain spans — bit-identical to no markup at all — and
173
+ the focusable caret is positioned over the text instead of inside it. Placing
174
+ the caret in an Arabic paragraph now leaves its width unchanged at 565.7px.
175
+ A collapsed selection shows an ordinary caret; an expanded one keeps its two
176
+ edges distinguishable.
177
+
178
+ - **Arabic text jittered when you moused over it.** Not a shaping problem, as it
179
+ appeared: spanification preserves shaping, ligatures and per-glyph positions
180
+ exactly (measured). The cause was wrapping each SPACE in its own span, which
181
+ changes which spaces CSS collapses — the same number collapse, but not the
182
+ same ones, so words merge and gaps open mid-word. Whitespace now stays a text
183
+ node, which reproduces the original layout space for space.
184
+
185
+ - **Live examples now fill their preview instead of taking a fixed height.**
186
+ `tosi-example` is `height: var(--tosi-example-height)` (320px) and becomes
187
+ `100vh` when maximized, so the editor's hardcoded 340px both overflowed the
188
+ normal case — the EXAMPLE scrolled rather than the document — and ignored the
189
+ space when maximized. The sizing and the preview's padding reset now live in
190
+ the site config's `headExtra`, so every doc page gets them, including the RTL
191
+ page which had no CSS block of its own.
192
+
193
+ - **The selection was unreadable in dark mode.** `.selected` was a hardcoded
194
+ `rgba(0,0,255,0.3)` and `.selected-block` a hardcoded `#ddf` — a pale blue
195
+ that light text disappears into — and the caret/bounds were `background: black`,
196
+ invisible on a dark page. All three now derive from `--editor-ink` mixed into
197
+ `--editor-surface` (and `currentColor` for the bounds), so they tint whichever
198
+ way the page is themed. The band is lifted toward white BEFORE the ink is
199
+ mixed in — nearly a no-op on an already-white page, but it raises a dark one
200
+ clear of the background, which a plain ink-into-surface mix cannot do because
201
+ the surface dominates. Dark is deliberately given MORE measured separation
202
+ than light, because light-on-dark halates and reads as less contrast at the
203
+ same numbers. Measured light 0.75 against a 0.99 page (separation 0.24), dark
204
+ 0.44 against 0.11 (separation 0.33).
205
+
206
+ - **Emoji were torn in half.** `spanify` split text with `split('')`, which
207
+ splits by UTF-16 code UNIT, so an emoji's surrogate pair became two lone
208
+ surrogates rendering as `?`. Splitting is now by grapheme cluster via
209
+ `Intl.Segmenter`, which also keeps flags (two regional indicators), skin-tone
210
+ modifiers, ZWJ sequences and combining marks whole — every one of those is one
211
+ thing a user clicks on or deletes.
212
+ - **Dark mode: the document was black text on a near-black page.** The surface
213
+ followed the page theme but `color` was the system `CanvasText`, which does
214
+ not. Text and surface are now a paired `--editor-text` / `--editor-surface`,
215
+ and a consumer that themes one must theme both.
216
+ - Added **Cut** to the touch selection menu, which had Copy and Paste but no Cut.
217
+
218
+ - **The caret sat on the wrong side of the line when typing LTR into an RTL
219
+ block** (and vice versa). The caret is an element, and bidi treats an empty
220
+ inline as a NEUTRAL, so it resolved against the block's base direction instead
221
+ of the run being typed. Typing across a direction boundary now wraps the run
222
+ and the caret in a `<span dir>` isolate, extending one isolate rather than
223
+ creating one per keystroke. Neutral characters take whichever run they land in.
224
+
225
+ - **Typing over a selection deleted it and inserted nothing.** A regression from
226
+ the double-click fix: `resetBounds()` derives bounds from `.selected`, which
227
+ lands the caret INSIDE the last selected character, and `deleteSelection()`
228
+ then removed the caret along with the selection — leaving no insertion point,
229
+ so every keystroke was silently swallowed. The caret is now moved out of the
230
+ way before the selected chains are deleted.
231
+
232
+ - Left-to-right runs inside right-to-left paragraphs — `<code>`, `<kbd>`,
233
+ `<samp>` — inherited the paragraph's base direction, so a URL's slashes or a
234
+ trailing period resolved to the wrong end. They now get their own
235
+ `direction: ltr; unicode-bidi: isolate`, which the new RTL page surfaced.
236
+
237
+ - Double-clicking left the caret blinking where the click landed instead of at
238
+ the end of the selected word: word and block gestures expanded the marked
239
+ range without moving the `.sel-start`/`.sel-end` elements.
240
+ - Clicking in the dead space to the right of a line did nothing, and
241
+ double-clicking there selected a word around stale bounds. Click position now
242
+ resolves to the nearest character on the clicked line.
243
+ - `editable.commands` was never consulted — `executeCommand` resolved names
244
+ against the module-level registry, so the documented way to add a custom
245
+ command had no effect. The registry now travels on `EditableContext`.
246
+ - README and the component doc comment advertised `<tosi-editable>`, which was
247
+ never the registered tag. It is `<tosijs-styled-editor>`.
248
+
249
+ ### Changed
250
+
251
+ - The Highlight button now uses Lucide's `highlighter` icon, registered through
252
+ `defineIcons`. The previous `penTool` read as a fountain pen — a different
253
+ tool. Stored following tosijs-ui's own convention: no `xmlns`, `width`,
254
+ `height`, `fill`, `stroke` or `stroke-*`, since the host supplies all of that
255
+ from `--tosi-icon-*` and a hardwired stroke would ignore the current colour.
256
+
257
+ - **Renamed to one name everywhere: `tosijs-styled-editor`.** The element is now
258
+ `<tosijs-styled-editor>` (was `<tosi-styled-editor>`), the class is
259
+ `TosijsStyledEditor`, the creator is `tosijsStyledEditor()`, and the source is
260
+ `src/tosijs-styled-editor.ts`. Only the repo directory stays `tosijs-editor`.
261
+ Breaking, and free to take now because the package is unpublished.
262
+ - Doc pages have distinct titles — "A Rich Text Editor Component" (README) and
263
+ "Editor Component" — so the site nav no longer shows two near-identical entries.
264
+ - Site icon, header mark and social image now use `static/tosijs-editor.svg`.
265
+ - Chrome restyled around a pen-ink-blue accent (`#27488c`), shared by the
266
+ component and the doc site. The menubar, toolbar and document now read as
267
+ three distinct surfaces, mixed from a single `--editor-ink` custom property so
268
+ a consumer can re-theme the whole thing by setting one value. Toolbar buttons
269
+ are compact 26px squares, styled by the component rather than left to each
270
+ consumer to re-invent. Those styles ship as `lightStyleSpec`, not
271
+ `::slotted()`: slotted content is light DOM, so the host page's own `button`
272
+ rules win the cascade — which had left the buttons as white chips on the
273
+ tinted bars.
274
+
275
+ - **Build**: replaced the bespoke `dev.ts` with `bin/site.ts`, a thin wrapper
276
+ over tosijs-ui's doc system (`buildSite`/`devServer`). The full build is
277
+ `bun run make` — there is deliberately no `build` script, because `bun build`
278
+ is a Bun builtin and the two would differ.
279
+ - Bundling moved out of the long-lived watch process into child processes;
280
+ Bun's bundler never returns its native arena (oven-sh/bun#34053).
281
+ - `docs/` is now the generated Pages web root and is committed.
282
+ - Peer floors raised to `tosijs ^1.10.1` / `tosijs-ui ^1.13.0`.
283
+ - Migrated off `elementCreator({ tag })` and `static styleSpec`, both deprecated
284
+ in the upgrade, to `static preferredTagName` and `static shadowStyleSpec`.
285
+
286
+ ### Security
287
+
288
+ - `happy-dom` → 20.14.0 (GHSA-w4gp-fjgq-3q4g, GHSA-6q6h-j7hj-3r64) and a minimal
289
+ `ws` override → `^8.21.0` (GHSA-96hv-2xvq-fx4p), both surfaced by the build's
290
+ dependency audit gate.
291
+ - `tls/` is gitignored as an allowlist so a dev TLS private key cannot be staged.
292
+
293
+ ### Removed
294
+
295
+ - The legacy jQuery implementation (`edx-*.js`, `lib/jquery-2.1.4.js`, and the
296
+ orphaned HTML pages) — 336K that nothing in the build referenced. Recover from
297
+ git history if ever needed: `git show 298bf16 -- edx-editable.js`.
298
+
299
+ ## [0.2.0]
300
+
301
+ ### Added
302
+
303
+ - Touch selection affordances and context menu; component renamed to
304
+ `<tosijs-styled-editor>`.
305
+
306
+ ## [0.1.0]
307
+
308
+ ### Added
309
+
310
+ - Initial rewrite as a tosijs web component: custom selection via spanification,
311
+ command-based editing, grid tables, and toolbar/menubar factories.
package/NOTICE ADDED
@@ -0,0 +1,16 @@
1
+ tosijs-styled-editor
2
+ Copyright 2026 Tonio Loewald
3
+
4
+ The drop-in build (dist/index.js) bundles the following Apache-2.0 works:
5
+
6
+ tosijs — Copyright 2026 Tonio Loewald
7
+ tosijs-ui — Copyright 2026 Tonio Loewald
8
+ tosijs-kilpi — Copyright 2026 Tonio Loewald
9
+ https://github.com/tonioloewald/kilpi
10
+
11
+ dist/module.js leaves all three external and bundles none of them.
12
+
13
+ tosijs-kilpi's verification corpus is vendored from DOMPurify
14
+ (Copyright (c) 2015 Mario Heiderich, MPL-2.0 OR Apache-2.0). That corpus is a
15
+ test fixture in kilpi's repository and is not distributed in kilpi's package or
16
+ in this one, so no DOMPurify material is redistributed here.
package/README.md CHANGED
@@ -27,7 +27,11 @@ A pure web-component. What it does **not** use:
27
27
 
28
28
  - No `document.execCommand`
29
29
  - No `contentEditable`
30
- - No horrible browser selection and Range APIs
30
+ - No `getSelection`, no browser selection model, no `execCommand`-era APIs
31
+
32
+ Range is used, but only as a **measuring tape** — `getBoundingClientRect()` to
33
+ ask the layout engine where a character is. It is never a selection model, and
34
+ nothing is handed back to the browser to edit.
31
35
 
32
36
  What you get instead:
33
37
 
@@ -59,7 +63,13 @@ Live site and docs: <https://editor.tosijs.net>
59
63
  npm install tosijs-styled-editor
60
64
  ```
61
65
 
62
- Peer dependencies: `tosijs`, `tosijs-ui`
66
+ **Peer dependencies** (you install these): `tosijs`, `tosijs-ui`
67
+
68
+ **Runtime dependency** (installed automatically):
69
+ [`tosijs-kilpi`](https://github.com/tonioloewald/kilpi) — the sanitizer applied
70
+ to pasted and dropped content. It has no dependencies of its own and is about
71
+ 0.8 kB gzipped. The drop-in `dist/index.js` build bundles it; the ESM build
72
+ leaves it external so you get one copy if you also depend on it directly.
63
73
 
64
74
  ## Usage
65
75
 
@@ -110,11 +120,68 @@ for (const widget of defaultToolbar()) {
110
120
  The editor uses three layers:
111
121
 
112
122
  1. **DOM utilities** (`dom-utils.ts`) — leaf-node traversal; nearly all operations work with leaf nodes
113
- 2. **Selection** (`selection.ts`) — custom selection via "spanification" (wrapping characters in `<span>` elements to determine exact positions without browser APIs)
123
+ 2. **Selection** (`selection.ts`) — a custom selection model. Character positions are found by MEASURING with a Range, which does not touch the document; wrapping characters in spans to measure them changes the thing being measured (it breaks shaping, so cursive scripts come apart and lines re-wrap)
114
124
  3. **Commands** (`commands.ts`) — extensible command system for formatting and editing
115
125
 
116
126
  The caret is an `<input>` element, so mobile browsers show their keyboard automatically.
117
127
 
128
+ ## Security: what is sanitized, and what is not
129
+
130
+ Replacing `contentEditable` also means replacing the sanitization the browser was
131
+ doing on your behalf.
132
+
133
+ **Sanitized** — content arriving from outside the document, which is the path an
134
+ attacker controls: **paste** and **drop**, both through one shared choke point.
135
+
136
+ The filtering itself is [`tosijs-kilpi`](https://github.com/tonioloewald/kilpi),
137
+ and **[its SECURITY.md is the authoritative policy](https://github.com/tonioloewald/kilpi/blob/main/SECURITY.md)** —
138
+ read it before relying on this. It is deliberately not restated here, because a
139
+ copy of a policy drifts from the policy. The one thing worth repeating, because
140
+ it is a trade rather than a detail:
141
+
142
+ > kilpi is a **denylist** for elements and attributes and an **allowlist** for URL
143
+ > schemes. That is why unknown elements survive — your plugin markup round-trips
144
+ > intact — and it is also why an element that becomes dangerous in a future
145
+ > browser, and that kilpi has never heard of, would pass through. If protection
146
+ > from the not-yet-known matters more to you than preserving unknown markup, use
147
+ > DOMPurify instead (see the hook below).
148
+
149
+ **NOT sanitized** — content you supply, which is inside your own trust boundary:
150
+
151
+ - `editor.value = html`
152
+ - initial light-DOM content
153
+
154
+ **If you are upgrading from 0.4.3 or earlier:** documents your users created
155
+ before 0.4.4 may already contain a payload that was pasted in, and this component
156
+ cannot fix that for you — setting `value` does not filter. Sanitize your stored
157
+ corpus as part of the upgrade.
158
+
159
+ **Using a different sanitizer.** `editor.sanitize` is the hook — it receives a
160
+ detached element and mutates it:
161
+
162
+ ```javascript
163
+ editor.sanitize = (root) => {
164
+ DOMPurify.sanitize(root, {
165
+ IN_PLACE: true,
166
+ FORBID_TAGS: ['style'],
167
+ CUSTOM_ELEMENT_HANDLING: {
168
+ tagNameCheck: /^[a-z][a-z0-9]*-[a-z0-9-]*$/,
169
+ attributeNameCheck: /^data-|^slot$|^dir$/,
170
+ },
171
+ })
172
+ }
173
+ ```
174
+
175
+ It takes an element rather than an HTML string on purpose: a string signature
176
+ would force a serialize-and-reparse round trip, and that round trip is where
177
+ mutation XSS lives. Note the `CUSTOM_ELEMENT_HANDLING` block — DOMPurify unwraps
178
+ unknown custom elements by default, which would discard plugin markup kilpi
179
+ preserves.
180
+
181
+ **Reporting a vulnerability.** If it is in the sanitizer, file it against
182
+ [kilpi](https://github.com/tonioloewald/kilpi/issues) — that is where the code
183
+ lives. Anything else, [this repository](https://github.com/tonioloewald/tosijs-editor/issues).
184
+
118
185
  ## Keyboard Behavior
119
186
 
120
187
  ### General editing
@@ -216,10 +283,10 @@ setBlocks line-height 2.5
216
283
 
217
284
  ### Drag and drop
218
285
 
219
- Selected text becomes a real draggable object — spanification means the
220
- selection is made of elements, which is exactly what HTML5 drag and drop wants,
221
- so dragging works between windows, between browsers, and to and from the
222
- desktop with no extra machinery.
286
+ Selected text becomes a real draggable object — the selection is marked on
287
+ elements, which is exactly what HTML5 drag and drop wants, so dragging works
288
+ between windows, between browsers, and to and from the desktop with no extra
289
+ machinery.
223
290
 
224
291
  Every drag offers **both representations**, and the receiver picks:
225
292
 
package/SECURITY.md ADDED
@@ -0,0 +1,32 @@
1
+ # Security
2
+
3
+ ## Reporting
4
+
5
+ - **A sanitizer bypass** — pasted or dropped content that reaches the document
6
+ still able to execute — belongs to
7
+ [`tosijs-kilpi`](https://github.com/tonioloewald/kilpi/issues). That is where
8
+ the filtering code lives; this package only calls it.
9
+ - **Anything else** — [this repository's
10
+ issues](https://github.com/tonioloewald/tosijs-editor/issues).
11
+
12
+ If you would rather not disclose publicly first, open an issue with no details
13
+ and we will find a private channel.
14
+
15
+ ## What this package is responsible for
16
+
17
+ - applying sanitization at the **paste and drop choke point**, before any node
18
+ enters the document (`insertTransfer`)
19
+ - the `editor.sanitize` hook, so a host can substitute its own sanitizer
20
+ - the two URL guards this package owns: `setLink`, and following a link on
21
+ Ctrl/Cmd-click
22
+
23
+ ## What it is NOT responsible for
24
+
25
+ - **the sanitization policy itself** — that is kilpi's, and
26
+ [kilpi's SECURITY.md](https://github.com/tonioloewald/kilpi/blob/main/SECURITY.md)
27
+ is authoritative. It is deliberately not restated here, because a copy of a
28
+ policy drifts from the policy.
29
+ - **content the host supplies**: `editor.value = html` and initial light-DOM
30
+ content are inside your trust boundary and are not filtered.
31
+ - **documents stored before 0.4.4**, which may already contain a pasted payload.
32
+ Setting `value` does not filter, so sanitize your corpus as part of upgrading.
@@ -21,7 +21,7 @@ export interface EditableContext {
21
21
  findAll(selector: string): Element[];
22
22
  selectedLeafNodes(): Node[];
23
23
  selectedBlocks(): Element[];
24
- insertionPoint(): HTMLInputElement | null;
24
+ insertionPoint(): HTMLElement | null;
25
25
  block(node: Node): Element | null;
26
26
  normalize(): void;
27
27
  focus(): void;
@@ -44,4 +44,14 @@ export declare const commands: Record<string, Command>;
44
44
  * Supports chained commands separated by semicolons.
45
45
  * Example: "setText font-weight bold; setBlockType h2"
46
46
  */
47
+ /**
48
+ * Run a single command with already-separated arguments.
49
+ *
50
+ * The string form splits on `;` and whitespace, so any runtime value
51
+ * interpolated into it can inject a second command — and a data URI contains
52
+ * `;` BY SPEC (`data:image/png;base64,…`), which silently broke every dropped
53
+ * image. Call sites that build a command from a URL, a filename or anything
54
+ * else the user did not type must use this form instead.
55
+ */
56
+ export declare function runCommand(ctx: EditableContext, name: string, ...args: string[]): void;
47
57
  export declare function executeCommand(ctx: EditableContext, commandString: string): void;
@@ -83,3 +83,16 @@ export declare function caretGeometryAt(marker: Element, root: Element): {
83
83
  top: number;
84
84
  height: number;
85
85
  } | null;
86
+ /**
87
+ * Sanitization lives in `tosijs-kilpi` — the same code, extracted so it is not
88
+ * maintained in two places.
89
+ *
90
+ * It was duplicated briefly, and that is exactly the shape that produced review
91
+ * finding M1 of the 0.4.4 cycle: one URL-normalization defect fixed in one of
92
+ * two copies, silently leaving the other. Across two repositories that drift
93
+ * would not even be visible in a diff.
94
+ *
95
+ * Re-exported here so the editor's public API is unchanged and every call site
96
+ * keeps importing from `./dom-utils`.
97
+ */
98
+ export { sanitizeInPlace, isSafeNavigationUrl } from 'tosijs-kilpi';