minisiwyg-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/LICENSE +21 -0
- package/README.md +271 -0
- package/dist/defaults.d.ts +2 -0
- package/dist/editor.d.ts +11 -0
- package/dist/index.cjs +6 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +7 -0
- package/dist/policy.cjs +2 -0
- package/dist/policy.cjs.map +7 -0
- package/dist/policy.d.ts +16 -0
- package/dist/policy.js +2 -0
- package/dist/policy.js.map +7 -0
- package/dist/sanitize.cjs +2 -0
- package/dist/sanitize.cjs.map +7 -0
- package/dist/sanitize.d.ts +16 -0
- package/dist/sanitize.js +2 -0
- package/dist/sanitize.js.map +7 -0
- package/dist/shared.d.ts +16 -0
- package/dist/toolbar.cjs +2 -0
- package/dist/toolbar.cjs.map +7 -0
- package/dist/toolbar.d.ts +10 -0
- package/dist/toolbar.js +2 -0
- package/dist/toolbar.js.map +7 -0
- package/dist/types.d.ts +37 -0
- package/package.json +75 -0
- package/src/defaults.ts +32 -0
- package/src/editor.ts +376 -0
- package/src/index.ts +13 -0
- package/src/policy.ts +226 -0
- package/src/sanitize.ts +169 -0
- package/src/shared.ts +44 -0
- package/src/toolbar.css +43 -0
- package/src/toolbar.ts +159 -0
- package/src/types.ts +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Erik Leon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# minisiwyg-editor
|
|
2
|
+
|
|
3
|
+
A sub-5kb gzipped, zero-dependency WYSIWYG editor with built-in XSS protection.
|
|
4
|
+
|
|
5
|
+
Spiritual successor to [Pell](https://github.com/jaredreich/pell) (~1.2kb, 12k stars, abandoned with known XSS vulnerabilities). minisiwyg-editor treats security as architecture, not an afterthought. The sanitizer is built into the editor via a declarative policy engine, not bolted on as a dependency.
|
|
6
|
+
|
|
7
|
+
## Live Demo
|
|
8
|
+
|
|
9
|
+
Try it in your browser: **[erikleon.github.io/minisiwyg-editor](https://erikleon.github.io/minisiwyg-editor/)**
|
|
10
|
+
|
|
11
|
+
The demo runs the full editor + toolbar in ~3.5kb gzipped. Paste an XSS payload (`<img src=x onerror=alert(1)>`) and watch the sanitizer strip it in real time.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Tiny.** ~3.5kb gzipped total. 5kb hard limit enforced in CI.
|
|
16
|
+
- **Zero runtime dependencies.** Nothing to audit, nothing to break.
|
|
17
|
+
- **XSS protection at every entry point.** Whitelist-based HTML sanitizer blocks `javascript:`, `data:`, event handlers, and encoded bypass attempts. Tested against OWASP XSS cheat sheet vectors.
|
|
18
|
+
- **Declarative policy.** JSON-serializable rules define allowed tags, attributes, protocols, depth, and length. Store policies in a database, transmit them over the wire, validate them with a schema.
|
|
19
|
+
- **Tree-shakeable exports.** Import only what you need. The sanitizer works standalone without the editor.
|
|
20
|
+
- **TypeScript-first.** Full type definitions shipped with the package.
|
|
21
|
+
- **CSP-safe.** No inline styles, no `eval`, no `Function` constructor.
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install minisiwyg-editor
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { createEditor } from 'minisiwyg-editor';
|
|
31
|
+
import { createToolbar } from 'minisiwyg-editor/toolbar';
|
|
32
|
+
|
|
33
|
+
const editor = createEditor(document.querySelector('#editor')!, {
|
|
34
|
+
onChange: (html) => console.log(html),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const toolbar = createToolbar(editor);
|
|
38
|
+
document.querySelector('#toolbar')!.appendChild(toolbar.element);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or use the sanitizer standalone, with no editor:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { sanitize, DEFAULT_POLICY } from 'minisiwyg-editor/sanitize';
|
|
45
|
+
|
|
46
|
+
const dirty = '<p onclick="alert(1)">Hello <script>steal(cookies)</script><strong>world</strong></p>';
|
|
47
|
+
const clean = sanitize(dirty, DEFAULT_POLICY);
|
|
48
|
+
// → '<p>Hello <strong>world</strong></p>'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Subpath Exports
|
|
52
|
+
|
|
53
|
+
minisiwyg-editor ships four independent modules. Each can be imported separately, and unused modules are tree-shaken out of your bundle.
|
|
54
|
+
|
|
55
|
+
| Export | Size (gzip) | Description |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `minisiwyg-editor/sanitize` | ~1.6kb | Standalone HTML sanitizer. No DOM dependencies beyond `<template>`. |
|
|
58
|
+
| `minisiwyg-editor/policy` | ~0.3kb | MutationObserver-based runtime enforcement. Defense-in-depth. |
|
|
59
|
+
| `minisiwyg-editor` | ~1.6kb | contentEditable core with paste handler and formatting commands. |
|
|
60
|
+
| `minisiwyg-editor/toolbar` | ~0.1kb | Optional toolbar UI with ARIA roles and keyboard navigation. |
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// Use just the sanitizer
|
|
64
|
+
import { sanitize, DEFAULT_POLICY } from 'minisiwyg-editor/sanitize';
|
|
65
|
+
|
|
66
|
+
// Use the full editor
|
|
67
|
+
import { createEditor } from 'minisiwyg-editor';
|
|
68
|
+
|
|
69
|
+
// Add the optional toolbar
|
|
70
|
+
import { createToolbar } from 'minisiwyg-editor/toolbar';
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Sanitizer
|
|
74
|
+
|
|
75
|
+
The sanitizer is the security core. It parses HTML via a `<template>` element (no script execution), walks the DOM tree depth-first, and removes anything not in the whitelist.
|
|
76
|
+
|
|
77
|
+
### How It Works
|
|
78
|
+
|
|
79
|
+
1. HTML is parsed into a DOM tree using `<template>` (safe, no scripts execute)
|
|
80
|
+
2. The tree is walked depth-first, checking each node against the policy
|
|
81
|
+
3. Disallowed tags are removed (strip mode) or unwrapped to plain text (unwrap mode)
|
|
82
|
+
4. Disallowed attributes are stripped. Event handlers (`on*`) are always stripped.
|
|
83
|
+
5. URL attributes (`href`, `src`, `action`) are validated against allowed protocols
|
|
84
|
+
6. `javascript:` and `data:` URLs are hardcoded denials, they cannot be allowed via policy
|
|
85
|
+
7. Tags are normalized: `<b>` becomes `<strong>`, `<i>` becomes `<em>`
|
|
86
|
+
8. Depth and length limits are enforced
|
|
87
|
+
|
|
88
|
+
### Protocol Bypass Protection
|
|
89
|
+
|
|
90
|
+
The sanitizer decodes HTML entities (`j` to `j`), URL encoding (`%6A` to `j`), strips whitespace and control characters, and normalizes case before checking protocols. This blocks common XSS bypass techniques:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// All of these are blocked:
|
|
94
|
+
sanitize('<a href="javascript:alert(1)">', DEFAULT_POLICY); // direct
|
|
95
|
+
sanitize('<a href="JaVaScRiPt:alert(1)">', DEFAULT_POLICY); // mixed case
|
|
96
|
+
sanitize('<a href="javascript:alert(1)">', DEFAULT_POLICY); // HTML entities
|
|
97
|
+
sanitize('<a href="%6Aavascript:alert(1)">', DEFAULT_POLICY); // URL encoding
|
|
98
|
+
sanitize('<a href=" java\tscript:alert(1)">', DEFAULT_POLICY); // whitespace
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Policy
|
|
102
|
+
|
|
103
|
+
A `SanitizePolicy` is a plain object that controls what HTML is allowed. It is JSON-serializable.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
interface SanitizePolicy {
|
|
107
|
+
tags: Record<string, string[]>; // Allowed tags → allowed attributes
|
|
108
|
+
strip: boolean; // true: remove disallowed nodes. false: unwrap (keep text)
|
|
109
|
+
maxDepth: number; // Maximum nesting depth
|
|
110
|
+
maxLength: number; // Maximum text content length
|
|
111
|
+
protocols: string[]; // Allowed URL protocols (javascript/data always denied)
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### DEFAULT_POLICY
|
|
116
|
+
|
|
117
|
+
The built-in default policy allows common formatting tags with sensible limits:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
{
|
|
121
|
+
tags: {
|
|
122
|
+
p: [], br: [], strong: [], em: [],
|
|
123
|
+
a: ['href', 'title', 'target'],
|
|
124
|
+
h1: [], h2: [], h3: [],
|
|
125
|
+
ul: [], ol: [], li: [],
|
|
126
|
+
blockquote: [], pre: [], code: [],
|
|
127
|
+
},
|
|
128
|
+
strip: true,
|
|
129
|
+
maxDepth: 10,
|
|
130
|
+
maxLength: 100_000,
|
|
131
|
+
protocols: ['https', 'http', 'mailto'],
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`DEFAULT_POLICY` is deeply frozen at runtime. It cannot be mutated.
|
|
136
|
+
|
|
137
|
+
### Custom Policies
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { sanitize } from 'minisiwyg-editor/sanitize';
|
|
141
|
+
import type { SanitizePolicy } from 'minisiwyg-editor/sanitize';
|
|
142
|
+
|
|
143
|
+
// Minimal: only bold and italic, no links
|
|
144
|
+
const strict: SanitizePolicy = {
|
|
145
|
+
tags: { strong: [], em: [] },
|
|
146
|
+
strip: false, // unwrap disallowed tags (keep text content)
|
|
147
|
+
maxDepth: 5,
|
|
148
|
+
maxLength: 10_000,
|
|
149
|
+
protocols: [], // no URL attributes allowed anyway
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
sanitize('<div><a href="https://example.com">Click <b>here</b></a></div>', strict);
|
|
153
|
+
// → 'Click <strong>here</strong>'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
// Permissive: allow images
|
|
158
|
+
const permissive: SanitizePolicy = {
|
|
159
|
+
tags: {
|
|
160
|
+
...DEFAULT_POLICY.tags,
|
|
161
|
+
img: ['src', 'alt', 'width', 'height'],
|
|
162
|
+
},
|
|
163
|
+
strip: true,
|
|
164
|
+
maxDepth: 15,
|
|
165
|
+
maxLength: 500_000,
|
|
166
|
+
protocols: ['https', 'http', 'mailto'],
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Editor API
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
import { createEditor } from 'minisiwyg-editor';
|
|
174
|
+
|
|
175
|
+
const editor = createEditor(element, {
|
|
176
|
+
policy: DEFAULT_POLICY, // optional, defaults to DEFAULT_POLICY
|
|
177
|
+
onChange: (html) => save(html), // optional change callback
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The returned `Editor` exposes:
|
|
182
|
+
|
|
183
|
+
| Method | Description |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `exec(command, value?)` | Run a formatting command. See commands below. |
|
|
186
|
+
| `queryState(command)` | Returns `true` if the format is active at the cursor. |
|
|
187
|
+
| `getHTML()` | Returns the current sanitized HTML. |
|
|
188
|
+
| `getText()` | Returns the current text content. |
|
|
189
|
+
| `on(event, handler)` | Subscribe to `change`, `paste`, `overflow`, or `error` events. |
|
|
190
|
+
| `destroy()` | Disconnect the observer and remove all listeners. |
|
|
191
|
+
|
|
192
|
+
Supported commands: `bold`, `italic`, `heading` (with value `'1'`, `'2'`, or `'3'`), `blockquote`, `unorderedList`, `orderedList`, `link` (with URL value), `unlink`, `codeBlock`.
|
|
193
|
+
|
|
194
|
+
## Toolbar
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { createToolbar } from 'minisiwyg-editor/toolbar';
|
|
198
|
+
|
|
199
|
+
const toolbar = createToolbar(editor, {
|
|
200
|
+
// Optional. Defaults to all built-in actions in this order:
|
|
201
|
+
actions: ['bold', 'italic', 'heading', 'unorderedList', 'orderedList', 'link', 'codeBlock'],
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
document.body.appendChild(toolbar.element);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The toolbar renders a `<div role="toolbar">` containing `<button>` elements with `aria-label` and `aria-pressed` attributes. Arrow keys move focus between buttons; Tab exits the toolbar. The link button uses `window.prompt()` to collect a URL and validates it against the active policy's protocols. Call `toolbar.destroy()` to remove it.
|
|
208
|
+
|
|
209
|
+
## Security Model
|
|
210
|
+
|
|
211
|
+
The editor has two layers of XSS protection:
|
|
212
|
+
|
|
213
|
+
1. **Paste handler (primary boundary).** When the user pastes content, it is intercepted, parsed via `<template>`, sanitized through the policy, and inserted using the Selection/Range API. Dangerous content never enters the DOM.
|
|
214
|
+
|
|
215
|
+
2. **MutationObserver (defense-in-depth).** A MutationObserver watches the contentEditable element for DOM mutations and removes anything that violates the policy. This catches edge cases where content enters through browser behavior (drag-and-drop, spell-check replacements, browser extensions).
|
|
216
|
+
|
|
217
|
+
The sanitizer itself is tested against OWASP XSS cheat sheet vectors in both happy-dom (fast unit tests) and Playwright (real browser tests).
|
|
218
|
+
|
|
219
|
+
### What Is Always Blocked
|
|
220
|
+
|
|
221
|
+
- `<script>`, `<iframe>`, `<object>`, `<embed>`, `<style>`, `<form>` tags
|
|
222
|
+
- All event handler attributes (`onclick`, `onerror`, `onload`, etc.)
|
|
223
|
+
- `javascript:` and `data:` URLs, regardless of policy configuration
|
|
224
|
+
- HTML entity, URL encoding, and mixed-case bypass attempts
|
|
225
|
+
- HTML comments and processing instructions
|
|
226
|
+
- Content beyond the configured depth and length limits
|
|
227
|
+
|
|
228
|
+
## Browser Support
|
|
229
|
+
|
|
230
|
+
minisiwyg-editor requires browsers that support `<template>`, `MutationObserver`, and `contentEditable`. This covers all modern browsers:
|
|
231
|
+
|
|
232
|
+
- Chrome 26+
|
|
233
|
+
- Firefox 22+
|
|
234
|
+
- Safari 8+
|
|
235
|
+
- Edge 13+
|
|
236
|
+
|
|
237
|
+
## Development
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
npm install # install dev dependencies
|
|
241
|
+
npm run build # esbuild: ESM + CJS output + type declarations
|
|
242
|
+
npm test # vitest with happy-dom
|
|
243
|
+
npx playwright test # OWASP XSS vectors in real browsers
|
|
244
|
+
npm run size-check # fails if total gzipped > 5kb
|
|
245
|
+
npm run typecheck # TypeScript type checking
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Run a single test file:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
npx vitest run test/sanitize.test.ts
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Architecture
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
src/
|
|
258
|
+
types.ts Shared TypeScript interfaces (SanitizePolicy, Editor, Toolbar)
|
|
259
|
+
defaults.ts DEFAULT_POLICY (deep-frozen)
|
|
260
|
+
sanitize.ts DOM tree walker, whitelist engine, protocol validation
|
|
261
|
+
policy.ts MutationObserver wrapper, re-entrancy guard
|
|
262
|
+
editor.ts contentEditable core, paste handler, execCommand
|
|
263
|
+
toolbar.ts Optional ARIA toolbar UI
|
|
264
|
+
index.ts Re-exports for main entry point
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Bottom-up dependency chain: sanitize < policy < editor < toolbar. Each layer is tested independently before the next one builds on it.
|
|
268
|
+
|
|
269
|
+
## License
|
|
270
|
+
|
|
271
|
+
MIT
|
package/dist/editor.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EditorOptions, Editor } from './types';
|
|
2
|
+
export type { Editor, EditorOptions } from './types';
|
|
3
|
+
export { DEFAULT_POLICY } from './defaults';
|
|
4
|
+
/**
|
|
5
|
+
* Create a contentEditable-based editor with built-in sanitization.
|
|
6
|
+
*
|
|
7
|
+
* The paste handler is the primary security boundary — it sanitizes HTML
|
|
8
|
+
* before insertion via Selection/Range API. The MutationObserver-based
|
|
9
|
+
* policy enforcer provides defense-in-depth.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createEditor(element: HTMLElement, options?: EditorOptions): Editor;
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";var O=Object.defineProperty;var q=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var Y=Object.prototype.hasOwnProperty;var $=(e,r)=>{for(var n in r)O(e,n,{get:r[n],enumerable:!0})},K=(e,r,n,s)=>{if(r&&typeof r=="object"||typeof r=="function")for(let a of j(r))!Y.call(e,a)&&a!==n&&O(e,a,{get:()=>r[a],enumerable:!(s=q(r,a))||s.enumerable});return e};var G=e=>K(O({},"__esModule",{value:!0}),e);var X={};$(X,{DEFAULT_POLICY:()=>A,createEditor:()=>B,createPolicyEnforcer:()=>R,createToolbar:()=>_,sanitize:()=>H,sanitizeToFragment:()=>P});module.exports=G(X);var N={tags:{p:[],br:[],strong:[],em:[],a:["href","title","target"],h1:[],h2:[],h3:[],ul:[],ol:[],li:[],blockquote:[],pre:[],code:[]},strip:!0,maxDepth:10,maxLength:1e5,protocols:["https","http","mailto"]};Object.freeze(N);Object.freeze(N.protocols);for(let e of Object.values(N.tags))Object.freeze(e);Object.freeze(N.tags);var A=N;var k={b:"strong",i:"em"},S=new Set(["href","src","action","formaction"]),W=new Set(["javascript","data"]);function Z(e){let r=e.trim();r=r.replace(/&#x([0-9a-f]+);?/gi,(s,a)=>String.fromCharCode(parseInt(a,16))),r=r.replace(/&#(\d+);?/g,(s,a)=>String.fromCharCode(parseInt(a,10)));try{r=decodeURIComponent(r)}catch{}r=r.replace(/[\s\x00-\x1f\u00A0\u1680\u2000-\u200B\u2028\u2029\u202F\u205F\u3000\uFEFF]+/g,"");let n=r.match(/^([a-z][a-z0-9+\-.]*)\s*:/i);return n?n[1].toLowerCase():null}function T(e,r){let n=Z(e);return n===null?!0:W.has(n)?!1:r.includes(n)}function D(e,r,n){let s=Array.from(e.childNodes);for(let a of s){if(a.nodeType===3)continue;if(a.nodeType!==1){e.removeChild(a);continue}let t=a,m=t.tagName.toLowerCase(),f=k[m];if(f&&(m=f),n>=r.maxDepth){e.removeChild(t);continue}let d=r.tags[m];if(d===void 0){if(r.strip)e.removeChild(t);else{for(D(t,r,n);t.firstChild;)e.insertBefore(t.firstChild,t);e.removeChild(t)}continue}let u=t;if(f&&t.tagName.toLowerCase()!==f){let i=t.ownerDocument.createElement(f);for(;t.firstChild;)i.appendChild(t.firstChild);e.replaceChild(i,t),u=i}let y=Array.from(u.attributes);for(let L of y){let i=L.name.toLowerCase();if(i.startsWith("on")){u.removeAttribute(L.name);continue}if(!d.includes(i)){u.removeAttribute(L.name);continue}S.has(i)&&(T(L.value,r.protocols)||u.removeAttribute(L.name))}D(u,r,n+1)}}function P(e,r){let n=document.createElement("template");if(!e)return n.content;n.innerHTML=e;let s=n.content;return D(s,r,0),r.maxLength>0&&(s.textContent?.length??0)>r.maxLength&&M(s,r.maxLength),s}function H(e,r){if(!e)return"";let n=P(e,r),s=document.createElement("div");return s.appendChild(n),s.innerHTML}function M(e,r){let n=r,s=Array.from(e.childNodes);for(let a of s){if(n<=0){e.removeChild(a);continue}if(a.nodeType===3){let t=a.textContent??"";t.length>n?(a.textContent=t.slice(0,n),n=0):n-=t.length}else a.nodeType===1?n=M(a,n):e.removeChild(a)}return n}function Q(e,r){let n=0,s=e.parentNode;for(;s&&s!==r;)s.nodeType===1&&n++,s=s.parentNode;return n}function I(e,r,n){let s=e.tagName.toLowerCase(),a=k[s];if(a&&(s=a),Q(e,n)>=r.maxDepth)return e.parentNode?.removeChild(e),!0;let m=r.tags[s];if(m===void 0){if(r.strip)e.parentNode?.removeChild(e);else{let d=e.parentNode;if(d){for(;e.firstChild;)d.insertBefore(e.firstChild,e);d.removeChild(e)}}return!0}let f=e;if(a&&e.tagName.toLowerCase()!==a){let d=e.ownerDocument.createElement(a);for(;e.firstChild;)d.appendChild(e.firstChild);for(let u of Array.from(e.attributes))d.setAttribute(u.name,u.value);e.parentNode?.replaceChild(d,e),f=d}for(let d of Array.from(f.attributes)){let u=d.name.toLowerCase();if(u.startsWith("on")){f.removeAttribute(d.name);continue}if(!m.includes(u)){f.removeAttribute(d.name);continue}S.has(u)&&(T(d.value,r.protocols)||f.removeAttribute(d.name))}return!1}function U(e,r,n){let s=Array.from(e.childNodes);for(let a of s){if(a.nodeType!==1){a.nodeType!==3&&e.removeChild(a);continue}I(a,r,n)||U(a,r,n)}}function R(e,r){if(!r||!r.tags)throw new TypeError('Policy must have a "tags" property');let n=!1,s=[];function a(m){for(let f of s)f(m)}let t=new MutationObserver(m=>{if(!n){n=!0;try{for(let f of m)if(f.type==="childList")for(let d of Array.from(f.addedNodes)){if(d.nodeType===3)continue;if(d.nodeType!==1){d.parentNode?.removeChild(d);continue}I(d,r,e)||U(d,r,e)}else if(f.type==="attributes"){let d=f.target;if(d.nodeType!==1)continue;let u=f.attributeName;if(!u)continue;let y=d.tagName.toLowerCase(),L=k[y]||y,i=r.tags[L];if(!i)continue;let p=u.toLowerCase();if(p.startsWith("on")){d.removeAttribute(u);continue}if(!i.includes(p)){d.removeAttribute(u);continue}if(S.has(p)){let c=d.getAttribute(u);c&&!T(c,r.protocols)&&d.removeAttribute(u)}}}catch(f){a(f instanceof Error?f:new Error(String(f)))}finally{n=!1}}});return t.observe(e,{childList:!0,attributes:!0,subtree:!0}),{destroy(){t.disconnect()},on(m,f){m==="error"&&s.push(f)}}}var F=new Set(["bold","italic","heading","blockquote","unorderedList","orderedList","link","unlink","codeBlock"]);function B(e,r){if(!e)throw new TypeError("createEditor requires an HTMLElement");if(!e.ownerDocument||!e.parentNode)throw new TypeError("createEditor requires an element attached to the DOM");let n=r?.policy??A,s={tags:Object.fromEntries(Object.entries(n.tags).map(([c,l])=>[c,[...l]])),strip:n.strip,maxDepth:n.maxDepth,maxLength:n.maxLength,protocols:[...n.protocols]},a={},t=e.ownerDocument;function m(c,...l){for(let o of a[c]??[])o(...l)}e.contentEditable="true";let f=R(e,s);f.on("error",c=>m("error",c));function d(c){c.preventDefault();let l=c.clipboardData;if(!l)return;let o=t.getSelection();if(o&&o.rangeCount>0&&o.anchorNode&&L(o.anchorNode,"PRE")){let w=l.getData("text/plain");if(!w)return;s.maxLength>0&&(e.textContent?.length??0)+w.length>s.maxLength&&m("overflow",s.maxLength);let x=o.getRangeAt(0);x.deleteContents();let z=t.createTextNode(w);x.insertNode(z),x.setStartAfter(z),x.collapse(!0),o.removeAllRanges(),o.addRange(x),m("paste",e.innerHTML),m("change",e.innerHTML);return}let v=l.getData("text/html");if(!v){let h=l.getData("text/plain");if(!h)return;v=h.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\n/g,"<br>")}let g=P(v,s),b=t.getSelection();if(!b||b.rangeCount===0)return;let E=b.getRangeAt(0);if(E.deleteContents(),s.maxLength>0){let h=g.textContent?.length??0;(e.textContent?.length??0)+h>s.maxLength&&m("overflow",s.maxLength)}let C=g.lastChild;if(E.insertNode(g),C){let h=t.createRange();h.setStartAfter(C),h.collapse(!0),b.removeAllRanges(),b.addRange(h)}m("paste",e.innerHTML),m("change",e.innerHTML)}function u(){m("change",e.innerHTML),r?.onChange?.(e.innerHTML)}function y(c){let l=t.getSelection();if(!l||l.rangeCount===0)return;let o=l.anchorNode;if(!o)return;let v=L(o,"PRE");if(c.key==="Enter"&&v){c.preventDefault();let g=l.getRangeAt(0);g.deleteContents();let b=t.createTextNode(`
|
|
2
|
+
`);g.insertNode(b),g.setStartAfter(b),g.collapse(!0),l.removeAllRanges(),l.addRange(g),m("change",e.innerHTML)}if(c.key==="Backspace"&&v){let g=v.textContent||"";if(l.anchorOffset===0&&(g===""||g===`
|
|
3
|
+
`)){c.preventDefault();let C=t.createElement("p");C.appendChild(t.createElement("br")),v.parentNode?.replaceChild(C,v);let h=t.createRange();h.selectNodeContents(C),h.collapse(!0),l.removeAllRanges(),l.addRange(h),m("change",e.innerHTML)}}}e.addEventListener("keydown",y),e.addEventListener("paste",d),e.addEventListener("input",u);function L(c,l){let o=c;for(;o&&o!==e;){if(o.nodeType===1&&o.tagName===l)return o;o=o.parentNode}return null}function i(c,l){let o=c;for(;o&&o!==e;){if(o.nodeType===1&&o.tagName===l)return!0;o=o.parentNode}return!1}return{exec(c,l){if(!F.has(c))throw new Error(`Unknown editor command: "${c}"`);switch(e.focus(),c){case"bold":t.execCommand("bold",!1);break;case"italic":t.execCommand("italic",!1);break;case"heading":{let o=l??"1";if(!["1","2","3"].includes(o))throw new Error(`Invalid heading level: "${o}". Use 1, 2, or 3`);t.execCommand("formatBlock",!1,`<h${o}>`);break}case"blockquote":t.execCommand("formatBlock",!1,"<blockquote>");break;case"unorderedList":t.execCommand("insertUnorderedList",!1);break;case"orderedList":t.execCommand("insertOrderedList",!1);break;case"link":{if(!l)throw new Error("Link command requires a URL value");let o=l.trim();if(!T(o,s.protocols)){m("error",new Error(`Protocol not allowed: ${o}`));return}t.execCommand("createLink",!1,o);break}case"unlink":t.execCommand("unlink",!1);break;case"codeBlock":{let o=t.getSelection();if(!o||o.rangeCount===0)break;let v=o.anchorNode,g=v?L(v,"PRE"):null;if(g){let b=t.createElement("p");b.textContent=g.textContent||"",g.parentNode?.replaceChild(b,g);let E=t.createRange();E.selectNodeContents(b),E.collapse(!1),o.removeAllRanges(),o.addRange(E)}else{let E=o.getRangeAt(0).startContainer;for(;E.parentNode&&E.parentNode!==e;)E=E.parentNode;let C=t.createElement("pre"),h=t.createElement("code"),w=E.textContent||"";h.textContent=w.endsWith(`
|
|
4
|
+
`)?w:w+`
|
|
5
|
+
`,C.appendChild(h),E.parentNode===e?e.replaceChild(C,E):e.appendChild(C);let x=t.createRange();x.selectNodeContents(h),x.collapse(!1),o.removeAllRanges(),o.addRange(x)}m("change",e.innerHTML);break}}},queryState(c){if(!F.has(c))throw new Error(`Unknown editor command: "${c}"`);let l=t.getSelection();if(!l||l.rangeCount===0)return!1;let o=l.anchorNode;if(!o||!e.contains(o))return!1;switch(c){case"bold":return i(o,"STRONG")||i(o,"B");case"italic":return i(o,"EM")||i(o,"I");case"heading":return i(o,"H1")||i(o,"H2")||i(o,"H3");case"blockquote":return i(o,"BLOCKQUOTE");case"unorderedList":return i(o,"UL");case"orderedList":return i(o,"OL");case"link":return i(o,"A");case"unlink":return!1;case"codeBlock":return i(o,"PRE");default:return!1}},getHTML(){return e.innerHTML},getText(){return e.textContent??""},destroy(){e.removeEventListener("keydown",y),e.removeEventListener("paste",d),e.removeEventListener("input",u),f.destroy(),e.contentEditable="false"},on(c,l){a[c]||(a[c]=[]),a[c].push(l)}}}var J={bold:"Bold",italic:"Italic",heading:"Heading",blockquote:"Blockquote",unorderedList:"Bulleted list",orderedList:"Numbered list",link:"Link",unlink:"Remove link",codeBlock:"Code block"},V=["bold","italic","heading","unorderedList","orderedList","link","codeBlock"];function _(e,r){let n=r?.actions??V,s=document,a=r?.element??s.createElement("div");a.setAttribute("role","toolbar"),a.setAttribute("aria-label","Text formatting"),a.classList.add("minisiwyg-toolbar");let t=[];for(let i of n){let p=s.createElement("button");p.type="button",p.className=`minisiwyg-btn minisiwyg-btn-${i}`;let c=J[i]??i;p.setAttribute("aria-label",c),p.setAttribute("aria-pressed","false"),p.textContent=c,p.tabIndex=t.length===0?0:-1,p.addEventListener("click",()=>m(i)),a.appendChild(p),t.push(p)}function m(i){try{if(i==="link"){let p=window.prompt("Enter URL")?.trim();if(!p||!T(p,A.protocols))return;e.exec("link",p)}else e.exec(i)}catch{}f()}function f(){for(let i=0;i<t.length;i++){let p=n[i];try{let c=e.queryState(p);t[i].setAttribute("aria-pressed",String(c)),t[i].classList.toggle("minisiwyg-btn-active",c)}catch{}}}function d(i){let p=i.target,c=t.indexOf(p);if(c===-1)return;let l=-1;i.key==="ArrowRight"||i.key==="ArrowDown"?(i.preventDefault(),l=(c+1)%t.length):i.key==="ArrowLeft"||i.key==="ArrowUp"?(i.preventDefault(),l=(c-1+t.length)%t.length):i.key==="Home"?(i.preventDefault(),l=0):i.key==="End"&&(i.preventDefault(),l=t.length-1),l>=0&&(t[c].tabIndex=-1,t[l].tabIndex=0,t[l].focus())}a.addEventListener("keydown",d);let u=0;function y(){cancelAnimationFrame(u),u=requestAnimationFrame(f)}return s.addEventListener("selectionchange",y),{element:a,destroy(){cancelAnimationFrame(u),a.removeEventListener("keydown",d),s.removeEventListener("selectionchange",y);for(let i of t)i.remove();r?.element||a.remove(),t.length=0}}}
|
|
6
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts", "../src/defaults.ts", "../src/shared.ts", "../src/sanitize.ts", "../src/policy.ts", "../src/editor.ts", "../src/toolbar.ts"],
|
|
4
|
+
"sourcesContent": ["export type {\n SanitizePolicy,\n EditorOptions,\n Editor,\n ToolbarOptions,\n Toolbar,\n} from './types';\nexport { DEFAULT_POLICY } from './defaults';\nexport { sanitize, sanitizeToFragment } from './sanitize';\nexport { createPolicyEnforcer } from './policy';\nexport type { PolicyEnforcer } from './policy';\nexport { createEditor } from './editor';\nexport { createToolbar } from './toolbar';\n", "import type { SanitizePolicy } from './types';\n\nconst policy: SanitizePolicy = {\n tags: {\n p: [],\n br: [],\n strong: [],\n em: [],\n a: ['href', 'title', 'target'],\n h1: [],\n h2: [],\n h3: [],\n ul: [],\n ol: [],\n li: [],\n blockquote: [],\n pre: [],\n code: [],\n },\n strip: true,\n maxDepth: 10,\n maxLength: 100_000,\n protocols: ['https', 'http', 'mailto'],\n};\n\n// Deep freeze to prevent mutation of security-critical defaults\nObject.freeze(policy);\nObject.freeze(policy.protocols);\nfor (const attrs of Object.values(policy.tags)) Object.freeze(attrs);\nObject.freeze(policy.tags);\n\nexport const DEFAULT_POLICY: Readonly<SanitizePolicy> = policy;\n", "/** Tag normalization map: browser-variant tags \u2192 semantic equivalents. */\nexport const TAG_NORMALIZE: Record<string, string> = {\n b: 'strong',\n i: 'em',\n};\n\n/** Attributes that contain URLs and need protocol validation. */\nexport const URL_ATTRS = new Set(['href', 'src', 'action', 'formaction']);\n\n/** Protocols that are always denied regardless of policy. */\nexport const DENIED_PROTOCOLS = new Set(['javascript', 'data']);\n\n/**\n * Parse a URL-like string and extract the protocol.\n * Returns the lowercase protocol name (without colon), or null if none found.\n */\nexport function extractProtocol(value: string): string | null {\n let decoded = value.trim();\n decoded = decoded.replace(/&#x([0-9a-f]+);?/gi, (_, hex) =>\n String.fromCharCode(parseInt(hex, 16)),\n );\n decoded = decoded.replace(/&#(\\d+);?/g, (_, dec) =>\n String.fromCharCode(parseInt(dec, 10)),\n );\n try {\n decoded = decodeURIComponent(decoded);\n } catch {\n // keep entity-decoded result\n }\n decoded = decoded.replace(/[\\s\\x00-\\x1f\\u00A0\\u1680\\u2000-\\u200B\\u2028\\u2029\\u202F\\u205F\\u3000\\uFEFF]+/g, '');\n const match = decoded.match(/^([a-z][a-z0-9+\\-.]*)\\s*:/i);\n return match ? match[1].toLowerCase() : null;\n}\n\n/**\n * Check if a URL value is allowed by the given protocol list.\n * javascript: and data: are always denied.\n */\nexport function isProtocolAllowed(value: string, allowedProtocols: string[]): boolean {\n const protocol = extractProtocol(value);\n if (protocol === null) return true;\n if (DENIED_PROTOCOLS.has(protocol)) return false;\n return allowedProtocols.includes(protocol);\n}\n", "import type { SanitizePolicy } from './types';\nimport { TAG_NORMALIZE, URL_ATTRS, isProtocolAllowed } from './shared';\nexport { DEFAULT_POLICY } from './defaults';\nexport type { SanitizePolicy } from './types';\n\n/**\n * Walk a DOM tree depth-first and sanitize according to policy.\n * Mutates the tree in place.\n */\nfunction walkAndSanitize(\n parent: Node,\n policy: SanitizePolicy,\n depth: number,\n): void {\n const children = Array.from(parent.childNodes);\n\n for (const node of children) {\n // Text nodes: always allowed (length enforcement happens after walk)\n if (node.nodeType === 3) continue;\n\n // Non-element, non-text nodes (comments, processing instructions): remove\n if (node.nodeType !== 1) {\n parent.removeChild(node);\n continue;\n }\n\n const el = node as Element;\n let tagName = el.tagName.toLowerCase();\n\n // Normalize tags (b\u2192strong, i\u2192em)\n const normalized = TAG_NORMALIZE[tagName];\n if (normalized) {\n tagName = normalized;\n }\n\n // Check depth limit\n if (depth >= policy.maxDepth) {\n parent.removeChild(el);\n continue;\n }\n\n // Check tag whitelist\n const allowedAttrs = policy.tags[tagName];\n if (allowedAttrs === undefined) {\n // Tag not allowed\n if (policy.strip) {\n // Remove the node and all its children\n parent.removeChild(el);\n } else {\n // Unwrap: sanitize children first, then move them up\n walkAndSanitize(el, policy, depth);\n while (el.firstChild) {\n parent.insertBefore(el.firstChild, el);\n }\n parent.removeChild(el);\n }\n continue;\n }\n\n // Tag is allowed. If it was normalized, replace with correct element.\n let current: Element = el;\n if (normalized && el.tagName.toLowerCase() !== normalized) {\n const doc = el.ownerDocument!;\n const replacement = doc.createElement(normalized);\n while (el.firstChild) {\n replacement.appendChild(el.firstChild);\n }\n parent.replaceChild(replacement, el);\n current = replacement;\n }\n\n // Strip disallowed attributes\n const attrs = Array.from(current.attributes);\n for (const attr of attrs) {\n const attrName = attr.name.toLowerCase();\n\n // Always strip event handlers (on*)\n if (attrName.startsWith('on')) {\n current.removeAttribute(attr.name);\n continue;\n }\n\n // Check attribute whitelist\n if (!allowedAttrs.includes(attrName)) {\n current.removeAttribute(attr.name);\n continue;\n }\n\n // Validate URL protocols on URL-bearing attributes\n if (URL_ATTRS.has(attrName)) {\n if (!isProtocolAllowed(attr.value, policy.protocols)) {\n current.removeAttribute(attr.name);\n }\n }\n }\n\n // Recurse into children\n walkAndSanitize(current, policy, depth + 1);\n }\n}\n\n/**\n * Sanitize an HTML string and return a DocumentFragment.\n * Avoids the serialize\u2192reparse round-trip that can cause mXSS.\n */\nexport function sanitizeToFragment(html: string, policy: SanitizePolicy): DocumentFragment {\n const template = document.createElement('template');\n if (!html) return template.content;\n\n template.innerHTML = html;\n const fragment = template.content;\n\n walkAndSanitize(fragment, policy, 0);\n\n if (policy.maxLength > 0 && (fragment.textContent?.length ?? 0) > policy.maxLength) {\n truncateToLength(fragment, policy.maxLength);\n }\n\n return fragment;\n}\n\n/**\n * Sanitize an HTML string according to the given policy.\n *\n * Uses a <template> element to parse HTML without executing scripts.\n * Walks the resulting DOM tree depth-first, removing disallowed elements\n * and attributes. Returns the sanitized HTML string.\n */\nexport function sanitize(html: string, policy: SanitizePolicy): string {\n if (!html) return '';\n\n const fragment = sanitizeToFragment(html, policy);\n const container = document.createElement('div');\n container.appendChild(fragment);\n return container.innerHTML;\n}\n\n/**\n * Truncate a DOM tree's text content to a maximum length.\n * Removes nodes beyond the limit while preserving structure.\n */\nfunction truncateToLength(node: Node, maxLength: number): number {\n let remaining = maxLength;\n\n const children = Array.from(node.childNodes);\n for (const child of children) {\n if (remaining <= 0) {\n node.removeChild(child);\n continue;\n }\n\n if (child.nodeType === 3) {\n // Text node\n const text = child.textContent ?? '';\n if (text.length > remaining) {\n child.textContent = text.slice(0, remaining);\n remaining = 0;\n } else {\n remaining -= text.length;\n }\n } else if (child.nodeType === 1) {\n remaining = truncateToLength(child, remaining);\n } else {\n node.removeChild(child);\n }\n }\n\n return remaining;\n}\n", "import type { SanitizePolicy } from './types';\nimport { TAG_NORMALIZE, URL_ATTRS, isProtocolAllowed } from './shared';\n\nexport { DEFAULT_POLICY } from './defaults';\nexport type { SanitizePolicy } from './types';\n\nexport interface PolicyEnforcer {\n destroy(): void;\n on(event: 'error', handler: (error: Error) => void): void;\n}\n\n/**\n * Get the nesting depth of a node within a root element.\n */\nfunction getDepth(node: Node, root: Node): number {\n let depth = 0;\n let current = node.parentNode;\n while (current && current !== root) {\n if (current.nodeType === 1) depth++;\n current = current.parentNode;\n }\n return depth;\n}\n\n/**\n * Check if an element is allowed by the policy and fix it if not.\n * Returns true if the node was removed/replaced.\n */\nfunction enforceElement(\n el: Element,\n policy: SanitizePolicy,\n root: HTMLElement,\n): boolean {\n let tagName = el.tagName.toLowerCase();\n const normalized = TAG_NORMALIZE[tagName];\n if (normalized) tagName = normalized;\n\n // Check depth\n const depth = getDepth(el, root);\n if (depth >= policy.maxDepth) {\n el.parentNode?.removeChild(el);\n return true;\n }\n\n // Check tag whitelist\n const allowedAttrs = policy.tags[tagName];\n if (allowedAttrs === undefined) {\n if (policy.strip) {\n el.parentNode?.removeChild(el);\n } else {\n // Unwrap: move children up, then remove the element\n const parent = el.parentNode;\n if (parent) {\n while (el.firstChild) {\n parent.insertBefore(el.firstChild, el);\n }\n parent.removeChild(el);\n }\n }\n return true;\n }\n\n // Normalize tag if needed (e.g. <b> \u2192 <strong>)\n let current: Element = el;\n if (normalized && el.tagName.toLowerCase() !== normalized) {\n const replacement = el.ownerDocument.createElement(normalized);\n while (el.firstChild) {\n replacement.appendChild(el.firstChild);\n }\n // Copy allowed attributes\n for (const attr of Array.from(el.attributes)) {\n replacement.setAttribute(attr.name, attr.value);\n }\n el.parentNode?.replaceChild(replacement, el);\n current = replacement;\n }\n\n // Strip disallowed attributes\n for (const attr of Array.from(current.attributes)) {\n const attrName = attr.name.toLowerCase();\n\n if (attrName.startsWith('on')) {\n current.removeAttribute(attr.name);\n continue;\n }\n\n if (!allowedAttrs.includes(attrName)) {\n current.removeAttribute(attr.name);\n continue;\n }\n\n if (URL_ATTRS.has(attrName)) {\n if (!isProtocolAllowed(attr.value, policy.protocols)) {\n current.removeAttribute(attr.name);\n }\n }\n }\n\n return false;\n}\n\n/**\n * Recursively enforce policy on all descendants of a node.\n */\nfunction enforceSubtree(node: Node, policy: SanitizePolicy, root: HTMLElement): void {\n const children = Array.from(node.childNodes);\n for (const child of children) {\n if (child.nodeType !== 1) {\n // Remove non-text, non-element nodes (comments, etc.)\n if (child.nodeType !== 3) {\n node.removeChild(child);\n }\n continue;\n }\n const removed = enforceElement(child as Element, policy, root);\n if (!removed) {\n enforceSubtree(child, policy, root);\n }\n }\n}\n\n/**\n * Create a policy enforcer that uses MutationObserver to enforce\n * the sanitization policy on a live DOM element.\n *\n * This is defense-in-depth \u2014 the paste handler is the primary security boundary.\n * The observer catches mutations from execCommand, programmatic DOM manipulation,\n * and other sources.\n */\nexport function createPolicyEnforcer(\n element: HTMLElement,\n policy: SanitizePolicy,\n): PolicyEnforcer {\n if (!policy || !policy.tags) {\n throw new TypeError('Policy must have a \"tags\" property');\n }\n\n let isApplyingFix = false;\n const errorHandlers: Array<(error: Error) => void> = [];\n\n function emitError(error: Error): void {\n for (const handler of errorHandlers) {\n handler(error);\n }\n }\n\n const observer = new MutationObserver((mutations) => {\n if (isApplyingFix) return;\n isApplyingFix = true;\n\n try {\n for (const mutation of mutations) {\n if (mutation.type === 'childList') {\n for (const node of Array.from(mutation.addedNodes)) {\n // Skip text nodes\n if (node.nodeType === 3) continue;\n\n // Remove non-element nodes\n if (node.nodeType !== 1) {\n node.parentNode?.removeChild(node);\n continue;\n }\n\n const removed = enforceElement(node as Element, policy, element);\n if (!removed) {\n // Also enforce on all descendants of the added node\n enforceSubtree(node, policy, element);\n }\n }\n } else if (mutation.type === 'attributes') {\n const target = mutation.target as Element;\n if (target.nodeType !== 1) continue;\n\n const attrName = mutation.attributeName;\n if (!attrName) continue;\n\n const tagName = target.tagName.toLowerCase();\n const normalizedTag = TAG_NORMALIZE[tagName] || tagName;\n const allowedAttrs = policy.tags[normalizedTag];\n\n if (!allowedAttrs) continue;\n\n const lowerAttr = attrName.toLowerCase();\n\n if (lowerAttr.startsWith('on')) {\n target.removeAttribute(attrName);\n continue;\n }\n\n if (!allowedAttrs.includes(lowerAttr)) {\n target.removeAttribute(attrName);\n continue;\n }\n\n if (URL_ATTRS.has(lowerAttr)) {\n const value = target.getAttribute(attrName);\n if (value && !isProtocolAllowed(value, policy.protocols)) {\n target.removeAttribute(attrName);\n }\n }\n }\n }\n } catch (err) {\n emitError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n isApplyingFix = false;\n }\n });\n\n observer.observe(element, {\n childList: true,\n attributes: true,\n subtree: true,\n });\n\n return {\n destroy() {\n observer.disconnect();\n },\n on(event: 'error', handler: (error: Error) => void) {\n if (event === 'error') {\n errorHandlers.push(handler);\n }\n },\n };\n}\n", "import type { SanitizePolicy, EditorOptions, Editor } from './types';\nimport { DEFAULT_POLICY } from './defaults';\nimport { sanitizeToFragment } from './sanitize';\nimport { createPolicyEnforcer, type PolicyEnforcer } from './policy';\nimport { isProtocolAllowed } from './shared';\n\nexport type { Editor, EditorOptions } from './types';\nexport { DEFAULT_POLICY } from './defaults';\n\ntype EditorEvent = 'change' | 'paste' | 'overflow' | 'error';\ntype EventHandler = (...args: unknown[]) => void;\n\nconst SUPPORTED_COMMANDS = new Set([\n 'bold',\n 'italic',\n 'heading',\n 'blockquote',\n 'unorderedList',\n 'orderedList',\n 'link',\n 'unlink',\n 'codeBlock',\n]);\n\n/**\n * Create a contentEditable-based editor with built-in sanitization.\n *\n * The paste handler is the primary security boundary \u2014 it sanitizes HTML\n * before insertion via Selection/Range API. The MutationObserver-based\n * policy enforcer provides defense-in-depth.\n */\nexport function createEditor(\n element: HTMLElement,\n options?: EditorOptions,\n): Editor {\n if (!element) {\n throw new TypeError('createEditor requires an HTMLElement');\n }\n if (!element.ownerDocument || !element.parentNode) {\n throw new TypeError('createEditor requires an element attached to the DOM');\n }\n\n const src = options?.policy ?? DEFAULT_POLICY;\n const policy: SanitizePolicy = {\n tags: Object.fromEntries(\n Object.entries(src.tags).map(([k, v]) => [k, [...v]]),\n ),\n strip: src.strip,\n maxDepth: src.maxDepth,\n maxLength: src.maxLength,\n protocols: [...src.protocols],\n };\n\n const handlers: Record<string, EventHandler[]> = {};\n const doc = element.ownerDocument;\n\n function emit(event: EditorEvent, ...args: unknown[]): void {\n for (const handler of handlers[event] ?? []) {\n handler(...args);\n }\n }\n\n // Set up contentEditable\n element.contentEditable = 'true';\n\n // Attach policy enforcer (MutationObserver defense-in-depth)\n const enforcer: PolicyEnforcer = createPolicyEnforcer(element, policy);\n enforcer.on('error', (err) => emit('error', err));\n\n // Paste handler \u2014 the primary security boundary\n function onPaste(e: ClipboardEvent): void {\n e.preventDefault();\n\n const clipboard = e.clipboardData;\n if (!clipboard) return;\n\n // Inside code block: paste as plain text only\n const sel = doc.getSelection();\n if (sel && sel.rangeCount > 0 && sel.anchorNode) {\n const pre = findAncestor(sel.anchorNode, 'PRE');\n if (pre) {\n const text = clipboard.getData('text/plain');\n if (!text) return;\n if (policy.maxLength > 0) {\n const currentLen = element.textContent?.length ?? 0;\n if (currentLen + text.length > policy.maxLength) {\n emit('overflow', policy.maxLength);\n }\n }\n const range = sel.getRangeAt(0);\n range.deleteContents();\n const textNode = doc.createTextNode(text);\n range.insertNode(textNode);\n range.setStartAfter(textNode);\n range.collapse(true);\n sel.removeAllRanges();\n sel.addRange(range);\n emit('paste', element.innerHTML);\n emit('change', element.innerHTML);\n return;\n }\n }\n\n // Prefer HTML, fall back to plain text\n let html = clipboard.getData('text/html');\n if (!html) {\n const text = clipboard.getData('text/plain');\n if (!text) return;\n // Escape plain text and convert newlines to <br>\n html = text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .replace(/\\n/g, '<br>');\n }\n\n // Sanitize through policy \u2014 returns DocumentFragment directly\n // to avoid the serialize\u2192reparse mXSS vector\n const fragment = sanitizeToFragment(html, policy);\n\n // Insert via Selection/Range API (NOT execCommand('insertHTML'))\n const selection = doc.getSelection();\n if (!selection || selection.rangeCount === 0) return;\n\n const range = selection.getRangeAt(0);\n range.deleteContents();\n\n // Check overflow using text content length\n if (policy.maxLength > 0) {\n const pasteTextLen = fragment.textContent?.length ?? 0;\n const currentLen = element.textContent?.length ?? 0;\n if (currentLen + pasteTextLen > policy.maxLength) {\n emit('overflow', policy.maxLength);\n }\n }\n\n // Remember last inserted node for cursor positioning\n let lastNode: Node | null = fragment.lastChild;\n range.insertNode(fragment);\n\n // Move cursor after inserted content\n if (lastNode) {\n const newRange = doc.createRange();\n newRange.setStartAfter(lastNode);\n newRange.collapse(true);\n selection.removeAllRanges();\n selection.addRange(newRange);\n }\n\n emit('paste', element.innerHTML);\n emit('change', element.innerHTML);\n }\n\n // Input handler for change events\n function onInput(): void {\n emit('change', element.innerHTML);\n options?.onChange?.(element.innerHTML);\n }\n\n // Keydown handler for code block behavior\n function onKeydown(e: KeyboardEvent): void {\n const sel = doc.getSelection();\n if (!sel || sel.rangeCount === 0) return;\n const anchor = sel.anchorNode;\n if (!anchor) return;\n\n const pre = findAncestor(anchor, 'PRE');\n\n if (e.key === 'Enter' && pre) {\n // Insert newline instead of new paragraph\n e.preventDefault();\n const range = sel.getRangeAt(0);\n range.deleteContents();\n const textNode = doc.createTextNode('\\n');\n range.insertNode(textNode);\n range.setStartAfter(textNode);\n range.collapse(true);\n sel.removeAllRanges();\n sel.addRange(range);\n emit('change', element.innerHTML);\n }\n\n if (e.key === 'Backspace' && pre) {\n // At start of empty pre, convert to <p>\n const text = pre.textContent || '';\n const isAtStart = sel.anchorOffset === 0;\n const isEmpty = text === '' || text === '\\n';\n if (isAtStart && isEmpty) {\n e.preventDefault();\n const p = doc.createElement('p');\n p.appendChild(doc.createElement('br'));\n pre.parentNode?.replaceChild(p, pre);\n const range = doc.createRange();\n range.selectNodeContents(p);\n range.collapse(true);\n sel.removeAllRanges();\n sel.addRange(range);\n emit('change', element.innerHTML);\n }\n }\n }\n\n element.addEventListener('keydown', onKeydown);\n element.addEventListener('paste', onPaste);\n element.addEventListener('input', onInput);\n\n function findAncestor(node: Node, tagName: string): Element | null {\n let current: Node | null = node;\n while (current && current !== element) {\n if (current.nodeType === 1 && (current as Element).tagName === tagName) return current as Element;\n current = current.parentNode;\n }\n return null;\n }\n\n function hasAncestor(node: Node, tagName: string): boolean {\n let current: Node | null = node;\n while (current && current !== element) {\n if (current.nodeType === 1 && (current as Element).tagName === tagName) return true;\n current = current.parentNode;\n }\n return false;\n }\n\n const editor: Editor = {\n exec(command: string, value?: string): void {\n if (!SUPPORTED_COMMANDS.has(command)) {\n throw new Error(`Unknown editor command: \"${command}\"`);\n }\n\n element.focus();\n\n switch (command) {\n case 'bold':\n doc.execCommand('bold', false);\n break;\n case 'italic':\n doc.execCommand('italic', false);\n break;\n case 'heading': {\n const level = value ?? '1';\n if (!['1', '2', '3'].includes(level)) {\n throw new Error(`Invalid heading level: \"${level}\". Use 1, 2, or 3`);\n }\n doc.execCommand('formatBlock', false, `<h${level}>`);\n break;\n }\n case 'blockquote':\n doc.execCommand('formatBlock', false, '<blockquote>');\n break;\n case 'unorderedList':\n doc.execCommand('insertUnorderedList', false);\n break;\n case 'orderedList':\n doc.execCommand('insertOrderedList', false);\n break;\n case 'link': {\n if (!value) {\n throw new Error('Link command requires a URL value');\n }\n const trimmed = value.trim();\n if (!isProtocolAllowed(trimmed, policy.protocols)) {\n emit('error', new Error(`Protocol not allowed: ${trimmed}`));\n return;\n }\n doc.execCommand('createLink', false, trimmed);\n break;\n }\n case 'unlink':\n doc.execCommand('unlink', false);\n break;\n case 'codeBlock': {\n const sel = doc.getSelection();\n if (!sel || sel.rangeCount === 0) break;\n const anchor = sel.anchorNode;\n const pre = anchor ? findAncestor(anchor, 'PRE') : null;\n if (pre) {\n // Toggle off: unwrap <pre><code> to <p>\n const p = doc.createElement('p');\n p.textContent = pre.textContent || '';\n pre.parentNode?.replaceChild(p, pre);\n const r = doc.createRange();\n r.selectNodeContents(p);\n r.collapse(false);\n sel.removeAllRanges();\n sel.addRange(r);\n } else {\n // Wrap current block in <pre><code>\n const range = sel.getRangeAt(0);\n let block = range.startContainer;\n while (block.parentNode && block.parentNode !== element) {\n block = block.parentNode;\n }\n const pre2 = doc.createElement('pre');\n const code = doc.createElement('code');\n const blockText = block.textContent || '';\n code.textContent = blockText.endsWith('\\n') ? blockText : blockText + '\\n';\n pre2.appendChild(code);\n if (block.parentNode === element) {\n element.replaceChild(pre2, block);\n } else {\n element.appendChild(pre2);\n }\n const r = doc.createRange();\n r.selectNodeContents(code);\n r.collapse(false);\n sel.removeAllRanges();\n sel.addRange(r);\n }\n emit('change', element.innerHTML);\n break;\n }\n }\n },\n\n queryState(command: string): boolean {\n if (!SUPPORTED_COMMANDS.has(command)) {\n throw new Error(`Unknown editor command: \"${command}\"`);\n }\n\n const sel = doc.getSelection();\n if (!sel || sel.rangeCount === 0) return false;\n\n const node = sel.anchorNode;\n if (!node || !element.contains(node)) return false;\n\n switch (command) {\n case 'bold':\n return hasAncestor(node, 'STRONG') || hasAncestor(node, 'B');\n case 'italic':\n return hasAncestor(node, 'EM') || hasAncestor(node, 'I');\n case 'heading':\n return hasAncestor(node, 'H1') || hasAncestor(node, 'H2') || hasAncestor(node, 'H3');\n case 'blockquote':\n return hasAncestor(node, 'BLOCKQUOTE');\n case 'unorderedList':\n return hasAncestor(node, 'UL');\n case 'orderedList':\n return hasAncestor(node, 'OL');\n case 'link':\n return hasAncestor(node, 'A');\n case 'unlink':\n return false;\n case 'codeBlock':\n return hasAncestor(node, 'PRE');\n default:\n return false;\n }\n },\n\n getHTML(): string {\n return element.innerHTML;\n },\n\n getText(): string {\n return element.textContent ?? '';\n },\n\n destroy(): void {\n element.removeEventListener('keydown', onKeydown);\n element.removeEventListener('paste', onPaste);\n element.removeEventListener('input', onInput);\n enforcer.destroy();\n element.contentEditable = 'false';\n },\n\n on(event: string, handler: EventHandler): void {\n if (!handlers[event]) handlers[event] = [];\n handlers[event].push(handler);\n },\n };\n\n return editor;\n}\n", "import type { Editor, ToolbarOptions, Toolbar } from './types';\nimport { isProtocolAllowed } from './shared';\nimport { DEFAULT_POLICY } from './defaults';\n\nexport type { ToolbarOptions, Toolbar } from './types';\n\nconst ACTION_LABELS: Record<string, string> = {\n bold: 'Bold',\n italic: 'Italic',\n heading: 'Heading',\n blockquote: 'Blockquote',\n unorderedList: 'Bulleted list',\n orderedList: 'Numbered list',\n link: 'Link',\n unlink: 'Remove link',\n codeBlock: 'Code block',\n};\n\nconst DEFAULT_ACTIONS = [\n 'bold',\n 'italic',\n 'heading',\n 'unorderedList',\n 'orderedList',\n 'link',\n 'codeBlock',\n];\n\n/**\n * Create a toolbar that drives an Editor instance.\n *\n * Renders a `<div role=\"toolbar\">` with buttons for each action.\n * Supports ARIA roles, keyboard navigation (arrow keys between\n * buttons, Tab exits), and active-state tracking via selectionchange.\n */\nexport function createToolbar(\n editor: Editor,\n options?: ToolbarOptions,\n): Toolbar {\n const actions = options?.actions ?? DEFAULT_ACTIONS;\n const doc = document;\n\n // Container\n const container = options?.element ?? doc.createElement('div');\n container.setAttribute('role', 'toolbar');\n container.setAttribute('aria-label', 'Text formatting');\n container.classList.add('minisiwyg-toolbar');\n\n const buttons: HTMLButtonElement[] = [];\n\n for (const action of actions) {\n const btn = doc.createElement('button');\n btn.type = 'button';\n btn.className = `minisiwyg-btn minisiwyg-btn-${action}`;\n const label = ACTION_LABELS[action] ?? action;\n btn.setAttribute('aria-label', label);\n btn.setAttribute('aria-pressed', 'false');\n btn.textContent = label;\n\n // Only first button is in tab order; rest use arrow keys\n btn.tabIndex = buttons.length === 0 ? 0 : -1;\n\n btn.addEventListener('click', () => onButtonClick(action));\n\n container.appendChild(btn);\n buttons.push(btn);\n }\n\n // Caller is responsible for placing toolbar.element in the DOM\n\n function onButtonClick(action: string): void {\n try {\n if (action === 'link') {\n const url = window.prompt('Enter URL')?.trim();\n if (!url) return;\n if (!isProtocolAllowed(url, DEFAULT_POLICY.protocols)) return;\n editor.exec('link', url);\n } else {\n editor.exec(action);\n }\n } catch {\n // Unknown or invalid commands \u2014 don't crash the toolbar\n }\n updateActiveStates();\n }\n\n function updateActiveStates(): void {\n for (let i = 0; i < buttons.length; i++) {\n const action = actions[i];\n try {\n const active = editor.queryState(action);\n buttons[i].setAttribute('aria-pressed', String(active));\n buttons[i].classList.toggle('minisiwyg-btn-active', active);\n } catch {\n // queryState may throw for unknown commands; ignore\n }\n }\n }\n\n // Keyboard navigation within toolbar\n function onKeydown(e: KeyboardEvent): void {\n const target = e.target as HTMLElement;\n const idx = buttons.indexOf(target as HTMLButtonElement);\n if (idx === -1) return;\n\n let next = -1;\n if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {\n e.preventDefault();\n next = (idx + 1) % buttons.length;\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {\n e.preventDefault();\n next = (idx - 1 + buttons.length) % buttons.length;\n } else if (e.key === 'Home') {\n e.preventDefault();\n next = 0;\n } else if (e.key === 'End') {\n e.preventDefault();\n next = buttons.length - 1;\n }\n\n if (next >= 0) {\n buttons[idx].tabIndex = -1;\n buttons[next].tabIndex = 0;\n buttons[next].focus();\n }\n }\n\n container.addEventListener('keydown', onKeydown);\n\n // Track selection changes to update active states (debounced to one per frame)\n let rafId = 0;\n function onSelectionChange(): void {\n cancelAnimationFrame(rafId);\n rafId = requestAnimationFrame(updateActiveStates);\n }\n\n doc.addEventListener('selectionchange', onSelectionChange);\n\n // Return the container element for the caller to place in the DOM\n const toolbar: Toolbar = {\n element: container,\n destroy(): void {\n cancelAnimationFrame(rafId);\n container.removeEventListener('keydown', onKeydown);\n doc.removeEventListener('selectionchange', onSelectionChange);\n // Remove buttons\n for (const btn of buttons) {\n btn.remove();\n }\n // Remove container if we created it (not user-provided)\n if (!options?.element) {\n container.remove();\n }\n buttons.length = 0;\n },\n };\n\n return toolbar;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["src_exports", "__export", "DEFAULT_POLICY", "createEditor", "createPolicyEnforcer", "createToolbar", "sanitize", "sanitizeToFragment", "__toCommonJS", "policy", "attrs", "DEFAULT_POLICY", "TAG_NORMALIZE", "URL_ATTRS", "DENIED_PROTOCOLS", "extractProtocol", "value", "decoded", "_", "hex", "dec", "match", "isProtocolAllowed", "allowedProtocols", "protocol", "walkAndSanitize", "parent", "policy", "depth", "children", "node", "el", "tagName", "normalized", "TAG_NORMALIZE", "allowedAttrs", "current", "replacement", "attrs", "attr", "attrName", "URL_ATTRS", "isProtocolAllowed", "sanitizeToFragment", "html", "template", "fragment", "truncateToLength", "sanitize", "container", "maxLength", "remaining", "child", "text", "getDepth", "node", "root", "depth", "current", "enforceElement", "el", "policy", "tagName", "normalized", "TAG_NORMALIZE", "allowedAttrs", "parent", "replacement", "attr", "attrName", "URL_ATTRS", "isProtocolAllowed", "enforceSubtree", "children", "child", "createPolicyEnforcer", "element", "isApplyingFix", "errorHandlers", "emitError", "error", "handler", "observer", "mutations", "mutation", "target", "normalizedTag", "lowerAttr", "value", "err", "event", "SUPPORTED_COMMANDS", "createEditor", "element", "options", "src", "DEFAULT_POLICY", "policy", "k", "v", "handlers", "doc", "emit", "event", "args", "handler", "enforcer", "createPolicyEnforcer", "err", "onPaste", "e", "clipboard", "sel", "findAncestor", "text", "range", "textNode", "html", "fragment", "sanitizeToFragment", "selection", "pasteTextLen", "lastNode", "newRange", "onInput", "onKeydown", "anchor", "pre", "p", "node", "tagName", "current", "hasAncestor", "command", "value", "level", "trimmed", "isProtocolAllowed", "r", "block", "pre2", "code", "blockText", "ACTION_LABELS", "DEFAULT_ACTIONS", "createToolbar", "editor", "options", "actions", "doc", "container", "buttons", "action", "btn", "label", "onButtonClick", "url", "isProtocolAllowed", "DEFAULT_POLICY", "updateActiveStates", "active", "onKeydown", "e", "target", "idx", "next", "rafId", "onSelectionChange"]
|
|
7
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { SanitizePolicy, EditorOptions, Editor, ToolbarOptions, Toolbar, } from './types';
|
|
2
|
+
export { DEFAULT_POLICY } from './defaults';
|
|
3
|
+
export { sanitize, sanitizeToFragment } from './sanitize';
|
|
4
|
+
export { createPolicyEnforcer } from './policy';
|
|
5
|
+
export type { PolicyEnforcer } from './policy';
|
|
6
|
+
export { createEditor } from './editor';
|
|
7
|
+
export { createToolbar } from './toolbar';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
var N={tags:{p:[],br:[],strong:[],em:[],a:["href","title","target"],h1:[],h2:[],h3:[],ul:[],ol:[],li:[],blockquote:[],pre:[],code:[]},strip:!0,maxDepth:10,maxLength:1e5,protocols:["https","http","mailto"]};Object.freeze(N);Object.freeze(N.protocols);for(let e of Object.values(N.tags))Object.freeze(e);Object.freeze(N.tags);var w=N;var k={b:"strong",i:"em"},S=new Set(["href","src","action","formaction"]),U=new Set(["javascript","data"]);function F(e){let o=e.trim();o=o.replace(/&#x([0-9a-f]+);?/gi,(s,a)=>String.fromCharCode(parseInt(a,16))),o=o.replace(/&#(\d+);?/g,(s,a)=>String.fromCharCode(parseInt(a,10)));try{o=decodeURIComponent(o)}catch{}o=o.replace(/[\s\x00-\x1f\u00A0\u1680\u2000-\u200B\u2028\u2029\u202F\u205F\u3000\uFEFF]+/g,"");let i=o.match(/^([a-z][a-z0-9+\-.]*)\s*:/i);return i?i[1].toLowerCase():null}function A(e,o){let i=F(e);return i===null?!0:U.has(i)?!1:o.includes(i)}function R(e,o,i){let s=Array.from(e.childNodes);for(let a of s){if(a.nodeType===3)continue;if(a.nodeType!==1){e.removeChild(a);continue}let t=a,m=t.tagName.toLowerCase(),f=k[m];if(f&&(m=f),i>=o.maxDepth){e.removeChild(t);continue}let d=o.tags[m];if(d===void 0){if(o.strip)e.removeChild(t);else{for(R(t,o,i);t.firstChild;)e.insertBefore(t.firstChild,t);e.removeChild(t)}continue}let u=t;if(f&&t.tagName.toLowerCase()!==f){let n=t.ownerDocument.createElement(f);for(;t.firstChild;)n.appendChild(t.firstChild);e.replaceChild(n,t),u=n}let y=Array.from(u.attributes);for(let L of y){let n=L.name.toLowerCase();if(n.startsWith("on")){u.removeAttribute(L.name);continue}if(!d.includes(n)){u.removeAttribute(L.name);continue}S.has(n)&&(A(L.value,o.protocols)||u.removeAttribute(L.name))}R(u,o,i+1)}}function P(e,o){let i=document.createElement("template");if(!e)return i.content;i.innerHTML=e;let s=i.content;return R(s,o,0),o.maxLength>0&&(s.textContent?.length??0)>o.maxLength&&z(s,o.maxLength),s}function B(e,o){if(!e)return"";let i=P(e,o),s=document.createElement("div");return s.appendChild(i),s.innerHTML}function z(e,o){let i=o,s=Array.from(e.childNodes);for(let a of s){if(i<=0){e.removeChild(a);continue}if(a.nodeType===3){let t=a.textContent??"";t.length>i?(a.textContent=t.slice(0,i),i=0):i-=t.length}else a.nodeType===1?i=z(a,i):e.removeChild(a)}return i}function _(e,o){let i=0,s=e.parentNode;for(;s&&s!==o;)s.nodeType===1&&i++,s=s.parentNode;return i}function H(e,o,i){let s=e.tagName.toLowerCase(),a=k[s];if(a&&(s=a),_(e,i)>=o.maxDepth)return e.parentNode?.removeChild(e),!0;let m=o.tags[s];if(m===void 0){if(o.strip)e.parentNode?.removeChild(e);else{let d=e.parentNode;if(d){for(;e.firstChild;)d.insertBefore(e.firstChild,e);d.removeChild(e)}}return!0}let f=e;if(a&&e.tagName.toLowerCase()!==a){let d=e.ownerDocument.createElement(a);for(;e.firstChild;)d.appendChild(e.firstChild);for(let u of Array.from(e.attributes))d.setAttribute(u.name,u.value);e.parentNode?.replaceChild(d,e),f=d}for(let d of Array.from(f.attributes)){let u=d.name.toLowerCase();if(u.startsWith("on")){f.removeAttribute(d.name);continue}if(!m.includes(u)){f.removeAttribute(d.name);continue}S.has(u)&&(A(d.value,o.protocols)||f.removeAttribute(d.name))}return!1}function M(e,o,i){let s=Array.from(e.childNodes);for(let a of s){if(a.nodeType!==1){a.nodeType!==3&&e.removeChild(a);continue}H(a,o,i)||M(a,o,i)}}function O(e,o){if(!o||!o.tags)throw new TypeError('Policy must have a "tags" property');let i=!1,s=[];function a(m){for(let f of s)f(m)}let t=new MutationObserver(m=>{if(!i){i=!0;try{for(let f of m)if(f.type==="childList")for(let d of Array.from(f.addedNodes)){if(d.nodeType===3)continue;if(d.nodeType!==1){d.parentNode?.removeChild(d);continue}H(d,o,e)||M(d,o,e)}else if(f.type==="attributes"){let d=f.target;if(d.nodeType!==1)continue;let u=f.attributeName;if(!u)continue;let y=d.tagName.toLowerCase(),L=k[y]||y,n=o.tags[L];if(!n)continue;let p=u.toLowerCase();if(p.startsWith("on")){d.removeAttribute(u);continue}if(!n.includes(p)){d.removeAttribute(u);continue}if(S.has(p)){let c=d.getAttribute(u);c&&!A(c,o.protocols)&&d.removeAttribute(u)}}}catch(f){a(f instanceof Error?f:new Error(String(f)))}finally{i=!1}}});return t.observe(e,{childList:!0,attributes:!0,subtree:!0}),{destroy(){t.disconnect()},on(m,f){m==="error"&&s.push(f)}}}var I=new Set(["bold","italic","heading","blockquote","unorderedList","orderedList","link","unlink","codeBlock"]);function q(e,o){if(!e)throw new TypeError("createEditor requires an HTMLElement");if(!e.ownerDocument||!e.parentNode)throw new TypeError("createEditor requires an element attached to the DOM");let i=o?.policy??w,s={tags:Object.fromEntries(Object.entries(i.tags).map(([c,l])=>[c,[...l]])),strip:i.strip,maxDepth:i.maxDepth,maxLength:i.maxLength,protocols:[...i.protocols]},a={},t=e.ownerDocument;function m(c,...l){for(let r of a[c]??[])r(...l)}e.contentEditable="true";let f=O(e,s);f.on("error",c=>m("error",c));function d(c){c.preventDefault();let l=c.clipboardData;if(!l)return;let r=t.getSelection();if(r&&r.rangeCount>0&&r.anchorNode&&L(r.anchorNode,"PRE")){let T=l.getData("text/plain");if(!T)return;s.maxLength>0&&(e.textContent?.length??0)+T.length>s.maxLength&&m("overflow",s.maxLength);let x=r.getRangeAt(0);x.deleteContents();let D=t.createTextNode(T);x.insertNode(D),x.setStartAfter(D),x.collapse(!0),r.removeAllRanges(),r.addRange(x),m("paste",e.innerHTML),m("change",e.innerHTML);return}let v=l.getData("text/html");if(!v){let h=l.getData("text/plain");if(!h)return;v=h.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\n/g,"<br>")}let g=P(v,s),b=t.getSelection();if(!b||b.rangeCount===0)return;let E=b.getRangeAt(0);if(E.deleteContents(),s.maxLength>0){let h=g.textContent?.length??0;(e.textContent?.length??0)+h>s.maxLength&&m("overflow",s.maxLength)}let C=g.lastChild;if(E.insertNode(g),C){let h=t.createRange();h.setStartAfter(C),h.collapse(!0),b.removeAllRanges(),b.addRange(h)}m("paste",e.innerHTML),m("change",e.innerHTML)}function u(){m("change",e.innerHTML),o?.onChange?.(e.innerHTML)}function y(c){let l=t.getSelection();if(!l||l.rangeCount===0)return;let r=l.anchorNode;if(!r)return;let v=L(r,"PRE");if(c.key==="Enter"&&v){c.preventDefault();let g=l.getRangeAt(0);g.deleteContents();let b=t.createTextNode(`
|
|
2
|
+
`);g.insertNode(b),g.setStartAfter(b),g.collapse(!0),l.removeAllRanges(),l.addRange(g),m("change",e.innerHTML)}if(c.key==="Backspace"&&v){let g=v.textContent||"";if(l.anchorOffset===0&&(g===""||g===`
|
|
3
|
+
`)){c.preventDefault();let C=t.createElement("p");C.appendChild(t.createElement("br")),v.parentNode?.replaceChild(C,v);let h=t.createRange();h.selectNodeContents(C),h.collapse(!0),l.removeAllRanges(),l.addRange(h),m("change",e.innerHTML)}}}e.addEventListener("keydown",y),e.addEventListener("paste",d),e.addEventListener("input",u);function L(c,l){let r=c;for(;r&&r!==e;){if(r.nodeType===1&&r.tagName===l)return r;r=r.parentNode}return null}function n(c,l){let r=c;for(;r&&r!==e;){if(r.nodeType===1&&r.tagName===l)return!0;r=r.parentNode}return!1}return{exec(c,l){if(!I.has(c))throw new Error(`Unknown editor command: "${c}"`);switch(e.focus(),c){case"bold":t.execCommand("bold",!1);break;case"italic":t.execCommand("italic",!1);break;case"heading":{let r=l??"1";if(!["1","2","3"].includes(r))throw new Error(`Invalid heading level: "${r}". Use 1, 2, or 3`);t.execCommand("formatBlock",!1,`<h${r}>`);break}case"blockquote":t.execCommand("formatBlock",!1,"<blockquote>");break;case"unorderedList":t.execCommand("insertUnorderedList",!1);break;case"orderedList":t.execCommand("insertOrderedList",!1);break;case"link":{if(!l)throw new Error("Link command requires a URL value");let r=l.trim();if(!A(r,s.protocols)){m("error",new Error(`Protocol not allowed: ${r}`));return}t.execCommand("createLink",!1,r);break}case"unlink":t.execCommand("unlink",!1);break;case"codeBlock":{let r=t.getSelection();if(!r||r.rangeCount===0)break;let v=r.anchorNode,g=v?L(v,"PRE"):null;if(g){let b=t.createElement("p");b.textContent=g.textContent||"",g.parentNode?.replaceChild(b,g);let E=t.createRange();E.selectNodeContents(b),E.collapse(!1),r.removeAllRanges(),r.addRange(E)}else{let E=r.getRangeAt(0).startContainer;for(;E.parentNode&&E.parentNode!==e;)E=E.parentNode;let C=t.createElement("pre"),h=t.createElement("code"),T=E.textContent||"";h.textContent=T.endsWith(`
|
|
4
|
+
`)?T:T+`
|
|
5
|
+
`,C.appendChild(h),E.parentNode===e?e.replaceChild(C,E):e.appendChild(C);let x=t.createRange();x.selectNodeContents(h),x.collapse(!1),r.removeAllRanges(),r.addRange(x)}m("change",e.innerHTML);break}}},queryState(c){if(!I.has(c))throw new Error(`Unknown editor command: "${c}"`);let l=t.getSelection();if(!l||l.rangeCount===0)return!1;let r=l.anchorNode;if(!r||!e.contains(r))return!1;switch(c){case"bold":return n(r,"STRONG")||n(r,"B");case"italic":return n(r,"EM")||n(r,"I");case"heading":return n(r,"H1")||n(r,"H2")||n(r,"H3");case"blockquote":return n(r,"BLOCKQUOTE");case"unorderedList":return n(r,"UL");case"orderedList":return n(r,"OL");case"link":return n(r,"A");case"unlink":return!1;case"codeBlock":return n(r,"PRE");default:return!1}},getHTML(){return e.innerHTML},getText(){return e.textContent??""},destroy(){e.removeEventListener("keydown",y),e.removeEventListener("paste",d),e.removeEventListener("input",u),f.destroy(),e.contentEditable="false"},on(c,l){a[c]||(a[c]=[]),a[c].push(l)}}}var j={bold:"Bold",italic:"Italic",heading:"Heading",blockquote:"Blockquote",unorderedList:"Bulleted list",orderedList:"Numbered list",link:"Link",unlink:"Remove link",codeBlock:"Code block"},Y=["bold","italic","heading","unorderedList","orderedList","link","codeBlock"];function $(e,o){let i=o?.actions??Y,s=document,a=o?.element??s.createElement("div");a.setAttribute("role","toolbar"),a.setAttribute("aria-label","Text formatting"),a.classList.add("minisiwyg-toolbar");let t=[];for(let n of i){let p=s.createElement("button");p.type="button",p.className=`minisiwyg-btn minisiwyg-btn-${n}`;let c=j[n]??n;p.setAttribute("aria-label",c),p.setAttribute("aria-pressed","false"),p.textContent=c,p.tabIndex=t.length===0?0:-1,p.addEventListener("click",()=>m(n)),a.appendChild(p),t.push(p)}function m(n){try{if(n==="link"){let p=window.prompt("Enter URL")?.trim();if(!p||!A(p,w.protocols))return;e.exec("link",p)}else e.exec(n)}catch{}f()}function f(){for(let n=0;n<t.length;n++){let p=i[n];try{let c=e.queryState(p);t[n].setAttribute("aria-pressed",String(c)),t[n].classList.toggle("minisiwyg-btn-active",c)}catch{}}}function d(n){let p=n.target,c=t.indexOf(p);if(c===-1)return;let l=-1;n.key==="ArrowRight"||n.key==="ArrowDown"?(n.preventDefault(),l=(c+1)%t.length):n.key==="ArrowLeft"||n.key==="ArrowUp"?(n.preventDefault(),l=(c-1+t.length)%t.length):n.key==="Home"?(n.preventDefault(),l=0):n.key==="End"&&(n.preventDefault(),l=t.length-1),l>=0&&(t[c].tabIndex=-1,t[l].tabIndex=0,t[l].focus())}a.addEventListener("keydown",d);let u=0;function y(){cancelAnimationFrame(u),u=requestAnimationFrame(f)}return s.addEventListener("selectionchange",y),{element:a,destroy(){cancelAnimationFrame(u),a.removeEventListener("keydown",d),s.removeEventListener("selectionchange",y);for(let n of t)n.remove();o?.element||a.remove(),t.length=0}}}export{w as DEFAULT_POLICY,q as createEditor,O as createPolicyEnforcer,$ as createToolbar,B as sanitize,P as sanitizeToFragment};
|
|
6
|
+
//# sourceMappingURL=index.js.map
|