u2ui 1.5.10 → 1.5.12

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,22 @@ 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 inside it, and importing that as html collapsed every
75
+ line break into a single line. `ExternalInput` still decides by flavor list —
76
+ reading the payload before preventing native insertion would give a failed read
77
+ somewhere to fall back to — but the payload now decides what the flavor *is*:
78
+ without a single tag it is text, escaped and joined by breaks, which is all the
79
+ structure such a paste has. A `text/plain` paste with no html flavor beside it
80
+ stays native as before.
81
+
82
+ Keeping the content of an unlisted element is right for almost all of them, and
83
+ wrong for the few a browser never renders the children of: unwrapping a `<style>`
84
+ from a word processor left its stylesheet standing in the text. `SanitizePolicy`
85
+ now carries a `drop` list — `base head link meta noscript script style template
86
+ title` — that `narrow()` removes with their content, while everything else keeps
87
+ dissolving into what the author wrote.
88
+
73
89
  A native paste now meets the attribute policy. Nothing is parsed there — the
74
90
  browser inserts its own payload — so it was the one import path without an
75
91
  allowlist, and layout ids, tracking attributes and inline styles came straight
@@ -103,7 +103,12 @@ 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. It
106
+ would be replaced. The adapter takes over only when `text/html` is present, and
107
+ the payload then decides what that flavor is: markup is imported as it stands,
108
+ while a flavor carrying none is the text it is — a pdf viewer labels its plain
109
+ selection `text/html` — and its line breaks, the only structure such a text has,
110
+ become breaks instead of collapsing. Reading the payload happens after native
111
+ insertion is prevented, so a failing read cannot fall back to it. It
107
112
  prevents native insertion first, sanitizes into a detached `DocumentFragment`,
108
113
  optionally cleans presentation cumulatively through the selected level, and
109
114
  passes the result and exact target range to the registered command. Drop thus
@@ -2,6 +2,7 @@ import {inputRange} from './input-pipeline.js';
2
2
  import {editingHost, isPlainTextHost} from '../selection/ownership/ownership.js';
3
3
 
4
4
  const INPUTS = new Set(['insertFromPaste', 'insertFromDrop']);
5
+ const ENTITIES = {'&': '&amp;', '<': '&lt;', '>': '&gt;'};
5
6
 
6
7
  // Owns rich external HTML before the browser can insert it. The selected
7
8
  // sanitizer, optional presentation cleanup, and mapped insertion command stay
@@ -100,9 +101,8 @@ export class ExternalInput {
100
101
  if (!hasHtml(event.dataTransfer)) return;
101
102
  event.preventDefault();
102
103
  try {
103
- const html = event.dataTransfer.getData('text/html');
104
104
  const range = inputRange(event, this.#surface);
105
- this.insert(html, {range, inputType: event.inputType});
105
+ this.insert(importHtml(event.dataTransfer), {range, inputType: event.inputType});
106
106
  } catch (error) {
107
107
  this.#surface.emit('u2-rte-error', {
108
108
  transaction: null,
@@ -123,8 +123,18 @@ export class ExternalInput {
123
123
  }
124
124
  }
125
125
 
126
+ // Reading the payload belongs after preventDefault, where a failure stays fail-closed, so the flavor
127
+ // list is all this may look at. Plain text without an html flavor is the browser's to insert.
126
128
  function hasHtml(transfer) {
127
- if (!transfer || typeof transfer.getData !== 'function') return false;
128
- const types = [...(transfer.types || [])].map(type => String(type).toLowerCase());
129
- return types.includes('text/html');
129
+ if (typeof transfer?.getData !== 'function') return false;
130
+ return [...(transfer.types || [])].some(type => String(type).toLowerCase() === 'text/html');
131
+ }
132
+
133
+ // Markup as it is, an html flavor without any as the text it is: a pdf viewer labels its plain
134
+ // selection that way, and html would collapse the line breaks that are all the structure it has.
135
+ function importHtml(transfer) {
136
+ const html = String(transfer.getData('text/html') || '');
137
+ if (/<[a-z!/]/i.test(html)) return html;
138
+ const text = String(transfer.getData('text/plain') || '') || html;
139
+ return text.replace(/[&<>]/g, character => ENTITIES[character]).replace(/\r?\n/g, '<br>');
130
140
  }
@@ -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. Parsed input gets this from the safe sink; markup the browser
73
- // inserted itself has to be narrowed afterwards.
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) || allowed.includes(element.localName)) continue;
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.10",
3
+ "version": "1.5.12",
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",