autumnnote 2.1.0 → 2.3.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.
Files changed (65) hide show
  1. package/README.md +74 -6
  2. package/dist/autumnnote.cjs +27 -22
  3. package/dist/autumnnote.es.js +474 -591
  4. package/dist/autumnnote.es.js.map +1 -1
  5. package/dist/autumnnote.min.js +27 -22
  6. package/dist/autumnnote.umd.js +27 -22
  7. package/dist/autumnnote.umd.js.map +1 -1
  8. package/dist/icon-data-V0Xqv-wX.js +255 -0
  9. package/dist/icon-data-V0Xqv-wX.js.map +1 -0
  10. package/package.json +12 -3
  11. package/types/index.d.ts +8 -0
  12. package/src/js/Context.js +0 -854
  13. package/src/js/core/detectLang.js +0 -98
  14. package/src/js/core/dom.js +0 -372
  15. package/src/js/core/env.js +0 -38
  16. package/src/js/core/key.js +0 -66
  17. package/src/js/core/lists.js +0 -121
  18. package/src/js/core/markdown.js +0 -695
  19. package/src/js/core/range.js +0 -194
  20. package/src/js/core/sanitise.js +0 -304
  21. package/src/js/editing/History.js +0 -266
  22. package/src/js/editing/Style.js +0 -812
  23. package/src/js/editing/Table.js +0 -105
  24. package/src/js/editing/Typing.js +0 -397
  25. package/src/js/index.js +0 -193
  26. package/src/js/index.umd.js +0 -17
  27. package/src/js/module/AutoSaveRestore.js +0 -125
  28. package/src/js/module/BaseDialog.js +0 -133
  29. package/src/js/module/BaseMediaTooltip.js +0 -142
  30. package/src/js/module/BaseResizer.js +0 -322
  31. package/src/js/module/BubbleToolbar.js +0 -483
  32. package/src/js/module/Buttons.js +0 -399
  33. package/src/js/module/Clipboard.js +0 -579
  34. package/src/js/module/CodeTooltip.js +0 -493
  35. package/src/js/module/Codeview.js +0 -125
  36. package/src/js/module/ContextMenu.js +0 -621
  37. package/src/js/module/Editor.js +0 -747
  38. package/src/js/module/EmojiDialog.js +0 -254
  39. package/src/js/module/FindReplace.js +0 -512
  40. package/src/js/module/Fullscreen.js +0 -80
  41. package/src/js/module/IconDialog.js +0 -618
  42. package/src/js/module/ImageCropOverlay.js +0 -586
  43. package/src/js/module/ImageDialog.js +0 -193
  44. package/src/js/module/ImageResizer.js +0 -42
  45. package/src/js/module/ImageTooltip.js +0 -285
  46. package/src/js/module/LinkDialog.js +0 -145
  47. package/src/js/module/LinkTooltip.js +0 -250
  48. package/src/js/module/MarkdownShortcuts.js +0 -250
  49. package/src/js/module/Mention.js +0 -365
  50. package/src/js/module/Placeholder.js +0 -51
  51. package/src/js/module/ShortcutsDialog.js +0 -111
  52. package/src/js/module/SlashMenu.js +0 -376
  53. package/src/js/module/Statusbar.js +0 -246
  54. package/src/js/module/TableTooltip.js +0 -1392
  55. package/src/js/module/Toolbar.js +0 -855
  56. package/src/js/module/VideoDialog.js +0 -193
  57. package/src/js/module/VideoResizer.js +0 -66
  58. package/src/js/module/VideoTooltip.js +0 -248
  59. package/src/js/module/emoji-data.js +0 -496
  60. package/src/js/module/table-grid.js +0 -102
  61. package/src/js/module/table-icons.js +0 -33
  62. package/src/js/renderer.js +0 -120
  63. package/src/js/settings.js +0 -214
  64. package/src/styles/_variables.scss +0 -48
  65. package/src/styles/autumnnote.scss +0 -2877
@@ -1,194 +0,0 @@
1
- /**
2
- * range.js - Selection and Range utilities
3
- * Inspired by Summernote's range.js — rewritten as vanilla JS
4
- */
5
-
6
- import { isElement, closest } from './dom.js';
7
-
8
- // ---------------------------------------------------------------------------
9
- // WrappedRange — a convenience wrapper over the native Range API
10
- // ---------------------------------------------------------------------------
11
-
12
- export class WrappedRange {
13
- /**
14
- * @param {Node} sc - start container
15
- * @param {number} so - start offset
16
- * @param {Node} ec - end container
17
- * @param {number} eo - end offset
18
- */
19
- constructor(sc, so, ec, eo) {
20
- this.sc = sc;
21
- this.so = so;
22
- this.ec = ec;
23
- this.eo = eo;
24
- }
25
-
26
- /** @returns {boolean} */
27
- isCollapsed() {
28
- return this.sc === this.ec && this.so === this.eo;
29
- }
30
-
31
- /** @returns {Range} */
32
- toNativeRange() {
33
- const range = document.createRange();
34
- try {
35
- range.setStart(this.sc, this.so);
36
- range.setEnd(this.ec, this.eo);
37
- } catch (_e) {
38
- void _e; // guard against detached nodes
39
- }
40
- return range;
41
- }
42
-
43
- /**
44
- * Select this wrapped range in the globalThis.
45
- */
46
- select() {
47
- const sel = globalThis.getSelection();
48
- if (!sel) return;
49
- sel.removeAllRanges();
50
- sel.addRange(this.toNativeRange());
51
- }
52
-
53
- /**
54
- * Returns the common ancestor element of this range.
55
- * @returns {Element|null}
56
- */
57
- commonAncestor() {
58
- const native = this.toNativeRange();
59
- const ancestor = native.commonAncestorContainer;
60
- return /** @type {Element|null} */ (isElement(ancestor) ? ancestor : ancestor.parentElement);
61
- }
62
-
63
- /**
64
- * Returns the nearest paragraph/block ancestor within the editable area.
65
- * @param {HTMLElement} editable
66
- * @returns {Element|null}
67
- */
68
- blockNode(editable) {
69
- return /** @type {Element|null} */ (closest(this.sc, (n) => isElement(n) && n !== editable, editable));
70
- }
71
-
72
- /**
73
- * Returns either the selected text string or empty string.
74
- * @returns {string}
75
- */
76
- toString() {
77
- return this.toNativeRange().toString();
78
- }
79
-
80
- /**
81
- * Returns the bounding DOMRect of the range (or null).
82
- * @returns {DOMRect|null}
83
- */
84
- getClientRects() {
85
- const rects = this.toNativeRange().getClientRects();
86
- return rects.length > 0 ? rects[rects.length - 1] : null;
87
- }
88
-
89
- /**
90
- * Inserts a node at the start of this range.
91
- * @param {Node} node
92
- */
93
- insertNode(node) {
94
- const native = this.toNativeRange();
95
- native.insertNode(node);
96
- }
97
-
98
- }
99
-
100
- // ---------------------------------------------------------------------------
101
- // Factory helpers
102
- // ---------------------------------------------------------------------------
103
-
104
- /**
105
- * Creates a WrappedRange from a native Range object.
106
- * @param {Range} range
107
- * @returns {WrappedRange}
108
- */
109
- export function fromNativeRange(range) {
110
- return new WrappedRange(
111
- range.startContainer,
112
- range.startOffset,
113
- range.endContainer,
114
- range.endOffset,
115
- );
116
- }
117
-
118
- /**
119
- * Returns a WrappedRange for the current globalThis selection,
120
- * optionally restricted to a given editable element.
121
- * @param {HTMLElement} [editable]
122
- * @returns {WrappedRange|null}
123
- */
124
- export function currentRange(editable) {
125
- const sel = globalThis.getSelection();
126
- if (!sel || sel.rangeCount === 0) return null;
127
- const native = sel.getRangeAt(0);
128
- // Optionally check that the selection is inside the editable element
129
- if (editable && !editable.contains(native.commonAncestorContainer)) {
130
- return null;
131
- }
132
- return fromNativeRange(native);
133
- }
134
-
135
- /**
136
- * Creates a WrappedRange that covers the entire content of an element.
137
- * @param {HTMLElement} el
138
- * @returns {WrappedRange}
139
- */
140
- export function rangeFromElement(el) {
141
- return new WrappedRange(el, 0, el, el.childNodes.length);
142
- }
143
-
144
- /**
145
- * Creates a collapsed range (cursor) at the given node / offset.
146
- * @param {Node} node
147
- * @param {number} offset
148
- * @returns {WrappedRange}
149
- */
150
- export function collapsedRange(node, offset = 0) {
151
- return new WrappedRange(node, offset, node, offset);
152
- }
153
-
154
- // ---------------------------------------------------------------------------
155
- // Utility helpers
156
- // ---------------------------------------------------------------------------
157
-
158
- /**
159
- * Returns true if the current selection is inside the given element.
160
- * @param {HTMLElement} el
161
- * @returns {boolean}
162
- */
163
- export function isSelectionInside(el) {
164
- const sel = globalThis.getSelection();
165
- if (!sel || sel.rangeCount === 0) return false;
166
- return el.contains(sel.getRangeAt(0).commonAncestorContainer);
167
- }
168
-
169
- /**
170
- * Saves the current selection, executes fn, then restores the selection.
171
- * @param {Function} fn
172
- */
173
- export function withSavedRange(fn) {
174
- const sel = globalThis.getSelection();
175
- if (!sel || sel.rangeCount === 0) {
176
- fn(null);
177
- return;
178
- }
179
- const saved = sel.getRangeAt(0).cloneRange();
180
- fn(fromNativeRange(saved));
181
- sel.removeAllRanges();
182
- sel.addRange(saved);
183
- }
184
-
185
- /**
186
- * Splits the text node at the given offset and returns the two halves.
187
- * @param {Text} textNode
188
- * @param {number} offset
189
- * @returns {[Text, Text]}
190
- */
191
- export function splitText(textNode, offset) {
192
- const after = textNode.splitText(offset);
193
- return [textNode, after];
194
- }
@@ -1,304 +0,0 @@
1
- /**
2
- * sanitise.js - Shared HTML and URL sanitisation utilities
3
- *
4
- * Single source of truth used by Editor, Clipboard, Codeview, and renderer.
5
- * DOM-parser based — no regex-based stripping of HTML (avoids bypass tricks).
6
- */
7
-
8
- /**
9
- * Tags that are unconditionally removed from editor content.
10
- *
11
- * Beyond the obvious script hosts this covers two SVG/MathML-specific classes:
12
- *
13
- * - SMIL animation (`animate`, `set`, `animateTransform`, `animateMotion`)
14
- * can rewrite an attribute *after* sanitisation finishes, so
15
- * `<svg><a><animate attributeName="href" values="javascript:…">` survives an
16
- * attribute-level filter untouched and still navigates on click. The editor
17
- * only ever emits static `<svg>` icons, so animation is pure attack surface.
18
- *
19
- * - `mglyph` / `malignmark` / `annotation-xml` are HTML integration points
20
- * inside the MathML namespace. They make the parser switch namespaces
21
- * mid-tree, which is what lets a crafted fragment re-parse into different
22
- * markup than it serialised from (mXSS). The rest of MathML is left alone so
23
- * pasted formulae survive.
24
- */
25
- const PROHIBITED_TAGS = [
26
- 'script', 'style', 'iframe', 'object', 'embed', 'form', 'base', 'template',
27
- 'link', 'meta', 'noscript', 'portal', 'frame', 'frameset', 'applet',
28
- 'animate', 'set', 'animatetransform', 'animatemotion',
29
- 'mglyph', 'malignmark', 'annotation-xml',
30
- ];
31
-
32
- /** Tags whose element wrapper is stripped but content (child nodes) is preserved. */
33
- const UNWRAP_TAGS = new Set(['button']);
34
-
35
- /** Attributes whose values must be sanitised as URLs. */
36
- const URL_ATTRS = ['href', 'src', 'action', 'formaction', 'xlink:href', 'poster', 'background', 'srcset'];
37
-
38
- /**
39
- * URL attributes that address media rather than navigation, so they are
40
- * validated against SAFE_MEDIA_PROTOCOLS regardless of which element carries
41
- * them (unlike `src`, whose meaning depends on the owning tag).
42
- */
43
- const MEDIA_URL_ATTRS = new Set(['poster', 'background', 'srcset']);
44
-
45
- /**
46
- * Attributes removed outright: the editor never emits them and their only
47
- * effect is an outbound request the author did not ask for. `ping` fires a
48
- * POST beacon to arbitrary hosts when a link is clicked.
49
- */
50
- const BEACON_ATTRS = new Set(['ping']);
51
-
52
- /** Inline style properties the editor's own toolbar/table features persist on saved content. */
53
- const ALLOWED_STYLE_PROPS = new Set([
54
- 'color', 'background-color', 'font-size', 'line-height',
55
- 'text-align', 'vertical-align',
56
- 'width', 'min-width', 'height', 'min-height',
57
- 'border-width', 'border-style', 'border-color', 'padding',
58
- ]);
59
-
60
- /**
61
- * Value patterns that are never safe regardless of property.
62
- * `image-set()` and `src()` are covered alongside `url()` — all three fetch an
63
- * external resource, so allowing them would let pasted content phone home.
64
- */
65
- const DANGEROUS_STYLE_VALUE_RE = /url\s*\(|image-set\s*\(|src\s*\(|expression\s*\(|@import|javascript:|vbscript:|behavior\s*:|-moz-binding/i;
66
-
67
- /** Trusted hosts for iframe embeds when allowIframes is enabled. */
68
- const TRUSTED_IFRAME_HOSTS = new Set([
69
- 'www.youtube.com',
70
- 'youtube.com',
71
- 'm.youtube.com',
72
- 'www.youtube-nocookie.com',
73
- 'youtube-nocookie.com',
74
- 'player.vimeo.com',
75
- ]);
76
-
77
- const SAFE_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
78
- const SAFE_MEDIA_PROTOCOLS = new Set(['http:', 'https:', 'blob:']);
79
- const SAFE_RASTER_DATA_RE = /^data:image\/(?:png|jpe?g|gif|webp|avif|bmp);base64,[a-z0-9+/=\s]+$/i;
80
- const URL_BASE = 'https://autumnnote.invalid/';
81
-
82
- /**
83
- * Produce a sanitized HTML string with dangerous elements and attributes removed.
84
- *
85
- * Removes disallowed tags and wrappers, strips event-handler attributes, rejects
86
- * `javascript:`/`vbscript:` URLs and most `data:` URIs, restricts iframe `src`
87
- * to trusted hosts when enabled, and permits only checklist checkboxes as inputs.
88
- *
89
- * @param {string} html - HTML fragment to sanitize.
90
- * @param {Object} [options]
91
- * @param {boolean} [options.allowIframes=false] - If true, `iframe` elements are not removed but their `src` is restricted to trusted hosts and `srcdoc` is removed.
92
- * @returns {string} The sanitized HTML fragment.
93
- */
94
- export function sanitiseHTML(html, { allowIframes = false } = {}) {
95
- const doc = new DOMParser().parseFromString(`<body>${html || ''}</body>`, 'text/html');
96
-
97
- // Single querySelectorAll pass — collect all elements once to avoid
98
- // repeated full-tree traversals for each category of check.
99
- const allElements = Array.from(doc.querySelectorAll('*'));
100
-
101
- // Build the prohibited tag set for fast O(1) lookup
102
- const prohibited = new Set(
103
- allowIframes ? PROHIBITED_TAGS.filter((t) => t !== 'iframe') : PROHIBITED_TAGS,
104
- );
105
-
106
- for (const el of allElements) {
107
- const tag = el.tagName.toLowerCase();
108
-
109
- // Unwrap elements whose wrapper is unsafe but whose content should be kept
110
- if (UNWRAP_TAGS.has(tag)) {
111
- el.replaceWith(...el.childNodes);
112
- continue;
113
- }
114
-
115
- // Remove outright dangerous elements
116
- if (prohibited.has(tag)) {
117
- el.remove();
118
- continue;
119
- }
120
-
121
- // Invalid embeds are removed rather than retained as empty iframes.
122
- if (tag === 'iframe') {
123
- const src = el.getAttribute('src');
124
- if (!src || !isTrustedIframeSrc(src)) {
125
- el.remove();
126
- continue;
127
- }
128
- }
129
-
130
- // Strip dangerous attributes
131
- for (const attr of Array.from(el.attributes)) {
132
- // Remove all event handlers (onclick, onload, onerror, …)
133
- if (attr.name.startsWith('on')) {
134
- el.removeAttribute(attr.name);
135
- continue;
136
- }
137
- // Filter the style attribute down to an allowlisted set of safe
138
- // properties (see ALLOWED_STYLE_PROPS) — not a blanket strip, since
139
- // the editor's own toolbar/table features persist inline styles
140
- // (text color/highlight, font size, line height, table alignment/
141
- // sizing/borders) that must survive sanitisation.
142
- if (attr.name === 'style') {
143
- const cleaned = sanitiseStyleValue(attr.value);
144
- if (cleaned) el.setAttribute('style', cleaned);
145
- else el.removeAttribute('style');
146
- continue;
147
- }
148
- // Drop tracking-beacon attributes outright
149
- if (BEACON_ATTRS.has(attr.name)) {
150
- el.removeAttribute(attr.name);
151
- continue;
152
- }
153
- // Sanitise URL attributes
154
- if (URL_ATTRS.includes(attr.name)) {
155
- const val = attr.value.trim();
156
- const isMediaSource = MEDIA_URL_ATTRS.has(attr.name) ||
157
- (attr.name === 'src' && ['IMG', 'VIDEO', 'AUDIO', 'SOURCE'].includes(el.tagName));
158
- const allowData = el.tagName === 'IMG';
159
- const safe = attr.name === 'srcset'
160
- ? isSafeSrcset(val, { allowData })
161
- : isSafeUrl(val, { media: isMediaSource, allowData });
162
- if (!safe) {
163
- el.removeAttribute(attr.name);
164
- continue;
165
- }
166
- }
167
- // Strip iframe HTML-injection vectors; limit src to trusted hosts
168
- if (el.tagName === 'IFRAME') {
169
- if (attr.name === 'srcdoc') {
170
- el.removeAttribute(attr.name);
171
- continue;
172
- }
173
- if (attr.name === 'src' && !isTrustedIframeSrc(attr.value)) {
174
- el.removeAttribute(attr.name);
175
- }
176
- }
177
- }
178
-
179
- if (tag === 'a' && el.getAttribute('target') === '_blank') {
180
- el.setAttribute('rel', 'noopener noreferrer');
181
- }
182
-
183
- // Allow only input[type="checkbox"] inside ul.an-checklist li
184
- if (tag === 'input') {
185
- const inChecklist = el.closest('ul.an-checklist') !== null &&
186
- el.closest('li') !== null;
187
- if (!inChecklist || el.getAttribute('type') !== 'checkbox') {
188
- el.remove();
189
- } else {
190
- for (const attr of Array.from(el.attributes)) {
191
- if (!['type', 'checked', 'contenteditable'].includes(attr.name)) {
192
- el.removeAttribute(attr.name);
193
- }
194
- }
195
- }
196
- }
197
- }
198
-
199
- return doc.body.innerHTML;
200
- }
201
-
202
- /**
203
- * Filters a style attribute value down to an allowlisted set of CSS
204
- * properties (ALLOWED_STYLE_PROPS), dropping any declaration whose value
205
- * contains a dangerous construct — url(), expression(), an "import" rule,
206
- * javascript:/vbscript:, IE behavior/-moz-binding — regardless of property.
207
- * @param {string} value
208
- * @returns {string} The filtered declaration list, or '' if nothing survives.
209
- */
210
- function sanitiseStyleValue(value) {
211
- const kept = [];
212
- for (const decl of (value || '').split(';')) {
213
- const idx = decl.indexOf(':');
214
- if (idx === -1) continue;
215
- const prop = decl.slice(0, idx).trim().toLowerCase();
216
- const val = decl.slice(idx + 1).trim();
217
- if (!prop || !val) continue;
218
- if (!ALLOWED_STYLE_PROPS.has(prop)) continue;
219
- if (DANGEROUS_STYLE_VALUE_RE.test(val)) continue;
220
- kept.push(`${prop}: ${val}`);
221
- }
222
- return kept.join('; ');
223
- }
224
-
225
- /**
226
- * Validates every candidate URL in a `srcset` attribute.
227
- *
228
- * Per the HTML srcset grammar a candidate URL is a run of non-whitespace
229
- * characters — commas may appear *inside* it, which is why `data:` URLs work
230
- * there — optionally followed by a width (`300w`) or density (`2x`) descriptor.
231
- * Splitting on whitespace and skipping descriptor tokens therefore yields the
232
- * URL set. Anything unparseable makes the whole attribute fail, since a
233
- * partially-trusted candidate list is not something we can express.
234
- *
235
- * @param {string} value
236
- * @param {{ allowData?: boolean }} [options]
237
- * @returns {boolean}
238
- */
239
- function isSafeSrcset(value, { allowData = false } = {}) {
240
- const tokens = (value || '').trim().split(/\s+/).filter(Boolean);
241
- for (const token of tokens) {
242
- if (/^[\d.]+[xw],?$/i.test(token)) continue; // width/density descriptor
243
- const url = token.replace(/,+$/, ''); // trailing comma = candidate separator
244
- if (!url) continue;
245
- if (!isSafeUrl(url, { media: true, allowData })) return false;
246
- }
247
- return true;
248
- }
249
-
250
- /**
251
- * Returns true if iframe src points to an approved video host.
252
- * Relative, protocol-relative and invalid URLs are rejected.
253
- * @param {string} src
254
- * @returns {boolean}
255
- */
256
- function isTrustedIframeSrc(src) {
257
- const trimmed = (src || '').trim();
258
- if (!trimmed) return false;
259
- if (trimmed.startsWith('//') || trimmed.startsWith('/')) return false;
260
- try {
261
- const url = new URL(trimmed);
262
- if (url.protocol !== 'https:') return false;
263
- return TRUSTED_IFRAME_HOSTS.has(url.hostname.toLowerCase());
264
- } catch {
265
- return false;
266
- }
267
- }
268
-
269
- /**
270
- * Sanitises a URL string, rejecting dangerous protocols.
271
- *
272
- * Blocked protocols: javascript:, vbscript:
273
- * Optionally blocked: data: (safe to allow for img/src base64 embeds)
274
- *
275
- * @param {string} url
276
- * @param {{ allowData?: boolean, media?: boolean }} [opts]
277
- * @returns {string|null} The original URL if safe, null if rejected.
278
- */
279
- export function sanitiseUrl(url, { allowData = false, media = allowData } = {}) {
280
- const trimmed = (url || '').trim();
281
- if (!trimmed && url == null) return null;
282
- return isSafeUrl(trimmed, { media, allowData }) ? url : null;
283
- }
284
-
285
- /**
286
- * Validate a URL using the browser URL parser so ASCII whitespace/control
287
- * characters cannot disguise a dangerous protocol (for example java\nscript:).
288
- * @param {string} value
289
- * @param {{media?: boolean, allowData?: boolean}} [options]
290
- * @returns {boolean}
291
- */
292
- function isSafeUrl(value, { media = false, allowData = false } = {}) {
293
- const trimmed = (value || '').trim();
294
- if (!trimmed) return true;
295
- if (allowData && SAFE_RASTER_DATA_RE.test(trimmed)) return true;
296
-
297
- try {
298
- const parsed = new URL(trimmed, URL_BASE);
299
- const protocols = media ? SAFE_MEDIA_PROTOCOLS : SAFE_LINK_PROTOCOLS;
300
- return protocols.has(parsed.protocol);
301
- } catch {
302
- return false;
303
- }
304
- }