u2ui 1.5.11 → 1.5.13

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
@@ -71,10 +71,13 @@ command ever makes an element canonical — so a strict list would otherwise hav
71
71
  turned every pasted emphasis into plain text.
72
72
 
73
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.
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.
78
81
 
79
82
  Keeping the content of an unlisted element is right for almost all of them, and
80
83
  wrong for the few a browser never renders the children of: unwrapping a `<style>`
@@ -103,10 +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 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
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
110
112
  prevents native insertion first, sanitizes into a detached `DocumentFragment`,
111
113
  optionally cleans presentation cumulatively through the selected level, and
112
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
@@ -97,12 +98,11 @@ export class ExternalInput {
97
98
  #beforeInput = event => {
98
99
  if (!this.#owns(event) || !INPUTS.has(event.inputType) || !event.cancelable
99
100
  || event.defaultPrevented || event.isComposing || isPlainTextHost(this.#root)) return;
100
- if (!hasMarkup(event.dataTransfer)) return;
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,13 +123,18 @@ export class ExternalInput {
123
123
  }
124
124
  }
125
125
 
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) {
130
- if (!transfer || typeof transfer.getData !== 'function') return false;
131
- const types = [...(transfer.types || [])].map(type => String(type).toLowerCase());
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'));
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.
128
+ function hasHtml(transfer) {
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>');
135
140
  }
@@ -13,7 +13,8 @@ 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
+ - `drop`: element names that `narrow()` removes with their content, before it
17
+ asks `skip` — no later stage may be left holding them;
17
18
  - `comments` and `dataAttributes`: explicit booleans, both false by default.
18
19
 
19
20
  An element outside `elements` keeps its content — dropping it would lose text the
@@ -85,6 +85,8 @@ export class SanitizePolicy {
85
85
  // `skip` leaves an element to a later stage. Structural repair already
86
86
  // removes what the content model rejects, and it does so knowing that
87
87
  // dissolving a block into inline content needs a line break to survive.
88
+ // A `drop` element is not that kind of question — no later stage may keep
89
+ // its content — so it goes before `skip` is asked.
88
90
  //
89
91
  // `alias` names an equivalent for an element that is not allowed. Dropping
90
92
  // `<b>` would lose the emphasis it carries, so a strict list can still keep
@@ -96,15 +98,14 @@ export class SanitizePolicy {
96
98
  : this.elements.filter(name => elements.includes(name));
97
99
  const changed = [];
98
100
  for (const element of descendants(root).reverse()) {
99
- if (preserve?.has(element)) continue;
100
- if (!element.parentNode || skip?.(element)) continue;
101
+ if (preserve?.has(element) || !element.parentNode) continue;
101
102
  if (this.drop.includes(element.localName)) {
102
103
  if (map) map.remove(element);
103
104
  else element.remove();
104
105
  changed.push(element);
105
106
  continue;
106
107
  }
107
- if (allowed.includes(element.localName)) continue;
108
+ if (skip?.(element) || allowed.includes(element.localName)) continue;
108
109
  const equivalent = alias?.[element.localName];
109
110
  if (equivalent && allowed.includes(equivalent)) {
110
111
  const replacement = element.ownerDocument.createElement(equivalent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u2ui",
3
- "version": "1.5.11",
3
+ "version": "1.5.13",
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",