u2ui 1.5.10 → 1.5.11
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/js/rte/PLAN.md
CHANGED
|
@@ -70,6 +70,19 @@ Nothing did that afterwards — the bold mark recognizes `<b>`, but only a mark
|
|
|
70
70
|
command ever makes an element canonical — so a strict list would otherwise have
|
|
71
71
|
turned every pasted emphasis into plain text.
|
|
72
72
|
|
|
73
|
+
Not every `text/html` flavor is html. A pdf viewer offers its selection as one
|
|
74
|
+
while putting plain text with line breaks inside it, and importing that as html
|
|
75
|
+
collapsed them all into a single line. `ExternalInput` now takes a paste over
|
|
76
|
+
only when the flavor carries markup, so text stays with the browser, which
|
|
77
|
+
inserts it with its breaks intact.
|
|
78
|
+
|
|
79
|
+
Keeping the content of an unlisted element is right for almost all of them, and
|
|
80
|
+
wrong for the few a browser never renders the children of: unwrapping a `<style>`
|
|
81
|
+
from a word processor left its stylesheet standing in the text. `SanitizePolicy`
|
|
82
|
+
now carries a `drop` list — `base head link meta noscript script style template
|
|
83
|
+
title` — that `narrow()` removes with their content, while everything else keeps
|
|
84
|
+
dissolving into what the author wrote.
|
|
85
|
+
|
|
73
86
|
A native paste now meets the attribute policy. Nothing is parsed there — the
|
|
74
87
|
browser inserts its own payload — so it was the one import path without an
|
|
75
88
|
allowlist, and layout ids, tracking attributes and inline styles came straight
|
|
@@ -103,7 +103,10 @@ const external = new ExternalInput(surface, {
|
|
|
103
103
|
|
|
104
104
|
For `insertFromPaste` and `insertFromDrop`, the contenteditable Input Events
|
|
105
105
|
contract supplies rich and plain data in `dataTransfer` plus the DOM range that
|
|
106
|
-
would be replaced. The adapter takes over only when `text/html` is present
|
|
106
|
+
would be replaced. The adapter takes over only when `text/html` is present and
|
|
107
|
+
carries markup: a pdf viewer labels its plain selection `text/html`, and
|
|
108
|
+
importing that as html would collapse the line breaks the text flavor keeps, so
|
|
109
|
+
a flavor without a single tag is left to the browser. It
|
|
107
110
|
prevents native insertion first, sanitizes into a detached `DocumentFragment`,
|
|
108
111
|
optionally cleans presentation cumulatively through the selected level, and
|
|
109
112
|
passes the result and exact target range to the registered command. Drop thus
|
|
@@ -97,7 +97,7 @@ export class ExternalInput {
|
|
|
97
97
|
#beforeInput = event => {
|
|
98
98
|
if (!this.#owns(event) || !INPUTS.has(event.inputType) || !event.cancelable
|
|
99
99
|
|| event.defaultPrevented || event.isComposing || isPlainTextHost(this.#root)) return;
|
|
100
|
-
if (!
|
|
100
|
+
if (!hasMarkup(event.dataTransfer)) return;
|
|
101
101
|
event.preventDefault();
|
|
102
102
|
try {
|
|
103
103
|
const html = event.dataTransfer.getData('text/html');
|
|
@@ -123,8 +123,13 @@ export class ExternalInput {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
// An html flavor carrying no markup is text that was labelled html — a pdf viewer offers its
|
|
127
|
+
// selection that way. Importing it as html would collapse the line breaks it carries, while the
|
|
128
|
+
// browser inserts the text flavor with them intact, so that one stays with the browser.
|
|
129
|
+
function hasMarkup(transfer) {
|
|
127
130
|
if (!transfer || typeof transfer.getData !== 'function') return false;
|
|
128
131
|
const types = [...(transfer.types || [])].map(type => String(type).toLowerCase());
|
|
129
|
-
|
|
132
|
+
if (!types.includes('text/html')) return false;
|
|
133
|
+
if (!types.includes('text/plain')) return true;
|
|
134
|
+
return /<[a-z!/]/i.test(transfer.getData('text/html'));
|
|
130
135
|
}
|
|
@@ -13,8 +13,15 @@ repairs editable document shape.
|
|
|
13
13
|
- `elements`: element names the security policy can accept;
|
|
14
14
|
- `attributes`: global (`*`) and per-element attribute names;
|
|
15
15
|
- `protocols`: per-element rules for URL attributes;
|
|
16
|
+
- `drop`: element names that `narrow()` removes with their content;
|
|
16
17
|
- `comments` and `dataAttributes`: explicit booleans, both false by default.
|
|
17
18
|
|
|
19
|
+
An element outside `elements` keeps its content — dropping it would lose text the
|
|
20
|
+
author wrote. `drop` names the exception: elements a browser never renders the
|
|
21
|
+
children of, where unwrapping would turn a stylesheet, a script, or document
|
|
22
|
+
metadata into visible text. It defaults to `base head link meta noscript script
|
|
23
|
+
style template title` and, like every default, an application may replace it.
|
|
24
|
+
|
|
18
25
|
The supplied `sanitizePolicy` accepts the `document` element preset, ordinary
|
|
19
26
|
editor metadata, and conservative web URLs. An image may carry a `data:` URL,
|
|
20
27
|
because an image executes nothing — not even an SVG one, which browsers draw in
|
|
@@ -27,10 +27,18 @@ const DEFAULT_PROTOCOLS = Object.freeze({
|
|
|
27
27
|
q: Object.freeze({cite: Object.freeze(['http', 'https', 'relative'])}),
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
// Elements a browser never renders the children of. Unwrapping one turns source — a stylesheet, a
|
|
31
|
+
// script, document metadata — into visible text, so these go with their content. Every other unknown
|
|
32
|
+
// element keeps its content: dropping it would lose text the author wrote.
|
|
33
|
+
const DEFAULT_DROP = Object.freeze([
|
|
34
|
+
'base', 'head', 'link', 'meta', 'noscript', 'script', 'style', 'template', 'title',
|
|
35
|
+
]);
|
|
36
|
+
|
|
30
37
|
export const sanitizeDefaults = Object.freeze({
|
|
31
38
|
elements: elementPresets.document,
|
|
32
39
|
attributes: DEFAULT_ATTRIBUTES,
|
|
33
40
|
protocols: DEFAULT_PROTOCOLS,
|
|
41
|
+
drop: DEFAULT_DROP,
|
|
34
42
|
comments: false,
|
|
35
43
|
dataAttributes: false,
|
|
36
44
|
});
|
|
@@ -41,6 +49,7 @@ export class SanitizePolicy {
|
|
|
41
49
|
|
|
42
50
|
constructor(options = {}) {
|
|
43
51
|
this.elements = names(options.elements ?? sanitizeDefaults.elements, NAME, 'element');
|
|
52
|
+
this.drop = names(options.drop ?? sanitizeDefaults.drop, NAME, 'element');
|
|
44
53
|
this.#attributes = groups(options.attributes ?? sanitizeDefaults.attributes, ATTRIBUTE, 'attribute');
|
|
45
54
|
this.#protocols = protocolGroups(options.protocols ?? sanitizeDefaults.protocols);
|
|
46
55
|
this.attributeNames = Object.freeze(unique(Object.values(this.#attributes).flat()));
|
|
@@ -69,8 +78,9 @@ export class SanitizePolicy {
|
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
// Reduces a subtree to the elements this policy allows, keeping the content
|
|
72
|
-
// of the rest
|
|
73
|
-
// inserted itself has to be
|
|
81
|
+
// of the rest — except the `drop` elements, which go with theirs. Parsed input
|
|
82
|
+
// gets this from the safe sink; markup the browser inserted itself has to be
|
|
83
|
+
// narrowed afterwards.
|
|
74
84
|
//
|
|
75
85
|
// `skip` leaves an element to a later stage. Structural repair already
|
|
76
86
|
// removes what the content model rejects, and it does so knowing that
|
|
@@ -86,8 +96,15 @@ export class SanitizePolicy {
|
|
|
86
96
|
: this.elements.filter(name => elements.includes(name));
|
|
87
97
|
const changed = [];
|
|
88
98
|
for (const element of descendants(root).reverse()) {
|
|
89
|
-
if (preserve?.has(element)
|
|
99
|
+
if (preserve?.has(element)) continue;
|
|
90
100
|
if (!element.parentNode || skip?.(element)) continue;
|
|
101
|
+
if (this.drop.includes(element.localName)) {
|
|
102
|
+
if (map) map.remove(element);
|
|
103
|
+
else element.remove();
|
|
104
|
+
changed.push(element);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (allowed.includes(element.localName)) continue;
|
|
91
108
|
const equivalent = alias?.[element.localName];
|
|
92
109
|
if (equivalent && allowed.includes(equivalent)) {
|
|
93
110
|
const replacement = element.ownerDocument.createElement(equivalent);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u2ui",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.11",
|
|
4
4
|
"description": "HTML Advanced: More Capabilities, Same Feel, Same Simplicity",
|
|
5
5
|
"keywords": ["web-components","pattern-library","component-library","ui-toolkit","design-system"],
|
|
6
6
|
"author": "Tobias Buschor",
|