u2ui 1.5.3 → 1.5.7

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 (146) hide show
  1. package/el/ico/ico.js +5 -5
  2. package/el/rte/README.md +53 -13
  3. package/el/rte/manifest.json +19 -4
  4. package/el/rte/rte.css +31 -6
  5. package/el/rte/rte.js +174 -44
  6. package/el/rte0/README.md +37 -0
  7. package/el/rte0/manifest.json +33 -0
  8. package/el/rte0/rte0.css +9 -0
  9. package/el/rte0/rte0.js +56 -0
  10. package/el/system/styler.js +2 -0
  11. package/el/tooltip/tooltip.css +2 -2
  12. package/js/Placer/Placer.js +2 -1
  13. package/js/rte/AGENTS.md +142 -0
  14. package/js/rte/PLAN.md +487 -0
  15. package/js/rte/README.md +319 -25
  16. package/js/rte/blocks.js +7 -0
  17. package/js/rte/breaks.js +6 -0
  18. package/js/rte/classes.js +7 -0
  19. package/js/rte/docs/usage.md +845 -0
  20. package/js/rte/images.js +7 -0
  21. package/js/rte/link.js +8 -0
  22. package/js/rte/playground/README.md +95 -0
  23. package/js/rte/playground/hosts.html +553 -0
  24. package/js/rte/playground/index.html +835 -0
  25. package/js/rte/rte.js +56 -5
  26. package/js/rte/run.out +17 -0
  27. package/js/rte/source.js +12 -0
  28. package/js/rte/src/browser/README.md +40 -0
  29. package/js/rte/src/browser/interactive.js +17 -0
  30. package/js/rte/src/browser/range-rect.js +82 -0
  31. package/js/rte/src/client/README.md +385 -0
  32. package/js/rte/src/client/blocks.js +38 -0
  33. package/js/rte/src/client/breaks.js +212 -0
  34. package/js/rte/src/client/classes.js +48 -0
  35. package/js/rte/src/client/editor.js +472 -0
  36. package/js/rte/src/client/external-input.js +47 -0
  37. package/js/rte/src/client/history.js +13 -0
  38. package/js/rte/src/client/images.js +285 -0
  39. package/js/rte/src/client/link.js +567 -0
  40. package/js/rte/src/client/marks.js +25 -0
  41. package/js/rte/src/client/source.js +182 -0
  42. package/js/rte/src/client/structure.js +27 -0
  43. package/js/rte/src/client/tables.js +165 -0
  44. package/js/rte/src/client/unstyle.js +32 -0
  45. package/js/rte/src/command/README.md +520 -0
  46. package/js/rte/src/command/block-boundary.js +43 -0
  47. package/js/rte/src/command/block-style.js +120 -0
  48. package/js/rte/src/command/commands.js +159 -0
  49. package/js/rte/src/command/delete.js +119 -0
  50. package/js/rte/src/command/edit.js +78 -0
  51. package/js/rte/src/command/element.js +45 -0
  52. package/js/rte/src/command/enter.js +175 -0
  53. package/js/rte/src/command/fragment.js +76 -0
  54. package/js/rte/src/command/insert.js +52 -0
  55. package/js/rte/src/command/list.js +284 -0
  56. package/js/rte/src/command/mark.js +583 -0
  57. package/js/rte/src/command/pending-marks.js +216 -0
  58. package/js/rte/src/command/table.js +207 -0
  59. package/js/rte/src/command/unstyle.js +248 -0
  60. package/js/rte/src/config/README.md +96 -0
  61. package/js/rte/src/config/config.js +134 -0
  62. package/js/rte/src/core/README.md +63 -0
  63. package/js/rte/src/core/core.js +247 -0
  64. package/js/rte/src/history/README.md +83 -0
  65. package/js/rte/src/history/history.js +283 -0
  66. package/js/rte/src/input/README.md +173 -0
  67. package/js/rte/src/input/external-input.js +130 -0
  68. package/js/rte/src/input/input-pipeline.js +459 -0
  69. package/js/rte/src/mark/README.md +219 -0
  70. package/js/rte/src/mark/dom-adapter.js +100 -0
  71. package/js/rte/src/mark/mark.js +133 -0
  72. package/js/rte/src/mark/standard.js +76 -0
  73. package/js/rte/src/model/README.md +71 -0
  74. package/js/rte/src/model/content-model.js +213 -0
  75. package/js/rte/src/model/html/README.md +51 -0
  76. package/js/rte/src/model/html/html-model.js +90 -0
  77. package/js/rte/src/normalize/execute/README.md +33 -0
  78. package/js/rte/src/normalize/execute/repair-executor.js +129 -0
  79. package/js/rte/src/normalize/normalizer/README.md +48 -0
  80. package/js/rte/src/normalize/normalizer/normalizer.js +141 -0
  81. package/js/rte/src/normalize/repair/README.md +70 -0
  82. package/js/rte/src/normalize/repair/repair-planner.js +208 -0
  83. package/js/rte/src/sanitize/README.md +108 -0
  84. package/js/rte/src/sanitize/native.js +43 -0
  85. package/js/rte/src/sanitize/policy.js +206 -0
  86. package/js/rte/src/selection/README.md +39 -0
  87. package/js/rte/src/selection/map/README.md +53 -0
  88. package/js/rte/src/selection/map/point-map.js +307 -0
  89. package/js/rte/src/selection/ownership/README.md +32 -0
  90. package/js/rte/src/selection/ownership/ownership.js +42 -0
  91. package/js/rte/src/selection/point/README.md +34 -0
  92. package/js/rte/src/selection/point/point.js +83 -0
  93. package/js/rte/src/selection/range/README.md +38 -0
  94. package/js/rte/src/selection/range/edit-range.js +223 -0
  95. package/js/rte/src/selection/snapshot.js +64 -0
  96. package/js/rte/src/source/README.md +64 -0
  97. package/js/rte/src/source/source.js +193 -0
  98. package/js/rte/src/surface/README.md +36 -0
  99. package/js/rte/src/surface/content.js +16 -0
  100. package/js/rte/src/surface/surface.js +87 -0
  101. package/js/rte/src/transaction/README.md +35 -0
  102. package/js/rte/src/transaction/transaction.js +56 -0
  103. package/js/rte/src/ui/README.md +271 -0
  104. package/js/rte/src/ui/caret.js +17 -0
  105. package/js/rte/src/ui/chrome.js +214 -0
  106. package/js/rte/src/ui/handles.js +179 -0
  107. package/js/rte/src/ui/highlight.js +86 -0
  108. package/js/rte/src/ui/place.js +25 -0
  109. package/js/rte/src/ui/toolbar.js +258 -0
  110. package/js/rte/src/unstyle/README.md +103 -0
  111. package/js/rte/src/unstyle/unstyle.js +140 -0
  112. package/js/rte/tables.js +7 -0
  113. package/js/rte/unstyle.js +8 -0
  114. package/js/rte0/README.md +37 -0
  115. package/js/rte0/rte.js +7 -0
  116. package/js/{rte → rte0}/src/behavior.js +0 -1
  117. package/js/{rte → rte0}/src/events.js +0 -1
  118. package/js/{rte → rte0}/src/range.js +0 -2
  119. package/js/{rte → rte0}/src/ui.items.js +0 -2
  120. package/js/{rte → rte0}/src/ui.js +0 -1
  121. package/package.json +1 -1
  122. package/u2/Element.js +2 -2
  123. package/u2/plan-scoped-refactoring.md +133 -303
  124. package/u2/projects.json +18 -2
  125. package/u2/tools/update.repos.json.js +1 -1
  126. package/el/qrcode/package.json +0 -22
  127. /package/js/{rte → rte0}/old/NodeCleaner.js +0 -0
  128. /package/js/{rte → rte0}/old/Rte/Rte.mjs +0 -0
  129. /package/js/{rte → rte0}/old/Rte/Rte.ui.items.mjs +0 -0
  130. /package/js/{rte → rte0}/old/Rte/Rte.ui.mjs +0 -0
  131. /package/js/{rte → rte0}/old/Rte/crossbrowser.mjs +0 -0
  132. /package/js/{rte → rte0}/old/Rte/font/tinymce.svg +0 -0
  133. /package/js/{rte → rte0}/old/Rte/font/tinymce.woff +0 -0
  134. /package/js/{rte → rte0}/old/Rte/helpers.mjs +0 -0
  135. /package/js/{rte → rte0}/old/Rte/htmlparser.mjs +0 -0
  136. /package/js/{rte → rte0}/old/Rte/index.mjs +0 -0
  137. /package/js/{rte → rte0}/old/Rte/main.css +0 -0
  138. /package/js/{rte → rte0}/old/old.rte.js +0 -0
  139. /package/js/{rte → rte0}/old/range-selection.old.js +0 -0
  140. /package/js/{rte → rte0}/src/fixes.js +0 -0
  141. /package/js/{rte → rte0}/src/rte.js +0 -0
  142. /package/js/{rte → rte0}/src/ui/items/ai.js +0 -0
  143. /package/js/{rte → rte0}/src/ui/items/code.js +0 -0
  144. /package/js/{rte → rte0}/src/ui/items/undo.js +0 -0
  145. /package/js/{rte → rte0}/src/util/htmlparser.js +0 -0
  146. /package/js/{rte → rte0}/src/util/indentHtml.js +0 -0
@@ -0,0 +1,307 @@
1
+ import {Point, indexOf} from '../point/point.js';
2
+
3
+ export class PointMap {
4
+ #points = new Map();
5
+
6
+ constructor(points = []) {
7
+ for (const point of points) this.add(point);
8
+ }
9
+
10
+ add(point) {
11
+ assertPoint(point);
12
+ if (!this.#points.has(point)) {
13
+ this.#points.set(point, {
14
+ node: point.node,
15
+ offset: point.offset,
16
+ affinity: point.affinity,
17
+ });
18
+ }
19
+ return this;
20
+ }
21
+
22
+ has(point) {
23
+ return this.#points.has(point);
24
+ }
25
+
26
+ get(point) {
27
+ const mapped = this.#points.get(point);
28
+ if (!mapped) throw new RangeError('Point is not tracked by this map');
29
+ return new Point(mapped.node, mapped.offset, mapped.affinity);
30
+ }
31
+
32
+ insert(parent, offset, node) {
33
+ assertBoundary(parent, offset);
34
+ assertDetached(node, parent);
35
+ const reference = parent.childNodes[offset] || null;
36
+ parent.insertBefore(node, reference);
37
+ for (const point of this.#points.values()) {
38
+ if (point.node !== parent) continue;
39
+ if (point.offset > offset || point.offset === offset && point.affinity === 'forward') point.offset++;
40
+ }
41
+ return node;
42
+ }
43
+
44
+ insertFragment(parent, offset, fragment) {
45
+ assertBoundary(parent, offset);
46
+ if (fragment?.nodeType !== Node.DOCUMENT_FRAGMENT_NODE) {
47
+ throw new TypeError('Fragment insertion requires a DocumentFragment');
48
+ }
49
+ const nodes = [...fragment.childNodes];
50
+ const reference = parent.childNodes[offset] || null;
51
+ parent.insertBefore(fragment, reference);
52
+ for (const point of this.#points.values()) {
53
+ if (point.node === fragment) {
54
+ relocate(point, parent, offset + point.offset);
55
+ } else if (point.node === parent
56
+ && (point.offset > offset || point.offset === offset && point.affinity === 'forward')) {
57
+ point.offset += nodes.length;
58
+ }
59
+ }
60
+ return nodes;
61
+ }
62
+
63
+ insertText(node, offset, data) {
64
+ if (node?.nodeType !== Node.TEXT_NODE) throw new TypeError('Text can only be inserted into a text node');
65
+ if (!Number.isInteger(offset) || offset < 0 || offset > node.length) throw new RangeError('Invalid text insertion offset');
66
+ if (typeof data !== 'string') throw new TypeError('Inserted text must be a string');
67
+ node.insertData(offset, data);
68
+ for (const point of this.#points.values()) {
69
+ if (point.node !== node) continue;
70
+ if (point.offset > offset || point.offset === offset && point.affinity === 'forward') point.offset += data.length;
71
+ }
72
+ return node;
73
+ }
74
+
75
+ splitText(node, offset) {
76
+ if (node?.nodeType !== Node.TEXT_NODE) throw new TypeError('Only a text node can be split');
77
+ if (!Number.isInteger(offset) || offset < 0 || offset > node.length) throw new RangeError('Invalid text split offset');
78
+ if (!node.parentNode) throw new RangeError('A detached text node cannot be split by the map');
79
+ const parent = node.parentNode;
80
+ const index = indexOf(node);
81
+ const right = node.splitText(offset);
82
+ for (const point of this.#points.values()) {
83
+ if (point.node === node) {
84
+ if (point.offset > offset || point.offset === offset && point.affinity === 'forward') {
85
+ point.node = right;
86
+ point.offset -= offset;
87
+ }
88
+ } else if (point.node === parent && point.offset > index) {
89
+ point.offset++;
90
+ }
91
+ }
92
+ return right;
93
+ }
94
+
95
+ split(container, node, offset) {
96
+ if (container?.nodeType !== Node.ELEMENT_NODE) throw new TypeError('A split container must be an element');
97
+ if (node !== container && !container.contains(node)) throw new RangeError('A split boundary must belong to its container');
98
+ let parent = node;
99
+ let index = offset;
100
+ if (node.nodeType === Node.TEXT_NODE) {
101
+ if (!Number.isInteger(offset) || offset < 0 || offset > node.length) throw new RangeError('Invalid text split offset');
102
+ if (offset > 0 && offset < node.length) this.splitText(node, offset);
103
+ parent = node.parentNode;
104
+ index = indexOf(node) + (offset > 0 ? 1 : 0);
105
+ if (offset === node.length) this.#follow(node, offset, parent, index);
106
+ } else {
107
+ assertBoundary(node, offset);
108
+ }
109
+ while (parent !== container) {
110
+ const tail = parent.cloneNode(false);
111
+ tail.removeAttribute('id');
112
+ const grand = parent.parentNode;
113
+ const at = indexOf(parent) + 1;
114
+ this.insert(grand, at, tail);
115
+ // Both halves replace one element. Nothing belongs between them, and
116
+ // everything from the boundary onward belongs to the trailing half.
117
+ const trailing = [];
118
+ for (const point of this.#points.values()) {
119
+ if (point.node === grand && point.offset === at) point.offset++;
120
+ else if (point.node === parent && follows(point, index)) trailing.push([point, point.offset - index]);
121
+ }
122
+ while (parent.childNodes[index]) this.move(parent.childNodes[index], tail, tail.childNodes.length);
123
+ for (const [point, offset] of trailing) relocate(point, tail, offset);
124
+ parent = grand;
125
+ index = at;
126
+ }
127
+ return index;
128
+ }
129
+
130
+ wrap(nodes, wrapper) {
131
+ nodes = [...nodes];
132
+ if (!nodes.length) throw new RangeError('At least one node is required to wrap');
133
+ const parent = nodes[0].parentNode;
134
+ if (!parent || nodes.some(node => node.parentNode !== parent)) throw new RangeError('Wrapped nodes must share a parent');
135
+ const start = indexOf(nodes[0]);
136
+ if (nodes.some((node, index) => parent.childNodes[start + index] !== node)) {
137
+ throw new RangeError('Wrapped nodes must be contiguous and ordered');
138
+ }
139
+ assertDetached(wrapper, parent);
140
+ if (wrapper.childNodes.length) throw new RangeError('A wrapper must be empty');
141
+ const end = start + nodes.length;
142
+ parent.insertBefore(wrapper, nodes[0]);
143
+ for (const node of nodes) wrapper.append(node);
144
+ for (const point of this.#points.values()) {
145
+ if (point.node !== parent) continue;
146
+ if (point.offset < start) continue;
147
+ if (point.offset > end) {
148
+ point.offset += 1 - nodes.length;
149
+ } else if (point.offset === start) {
150
+ if (point.affinity === 'forward') relocate(point, wrapper, 0);
151
+ } else if (point.offset === end) {
152
+ if (point.affinity === 'backward') relocate(point, wrapper, nodes.length);
153
+ else point.offset = start + 1;
154
+ } else {
155
+ relocate(point, wrapper, point.offset - start);
156
+ }
157
+ }
158
+ return wrapper;
159
+ }
160
+
161
+ unwrap(wrapper) {
162
+ if (!wrapper?.parentNode) throw new RangeError('A detached node cannot be unwrapped');
163
+ const parent = wrapper.parentNode;
164
+ const index = indexOf(wrapper);
165
+ const count = wrapper.childNodes.length;
166
+ const fragment = wrapper.ownerDocument.createDocumentFragment();
167
+ while (wrapper.firstChild) fragment.append(wrapper.firstChild);
168
+ wrapper.replaceWith(fragment);
169
+ for (const point of this.#points.values()) {
170
+ if (point.node === wrapper) {
171
+ relocate(point, parent, index + point.offset);
172
+ } else if (point.node === parent && point.offset > index) {
173
+ point.offset += count - 1;
174
+ }
175
+ }
176
+ return wrapper;
177
+ }
178
+
179
+ replace(node, replacement) {
180
+ if (!node?.parentNode) throw new RangeError('A detached node cannot be replaced');
181
+ const parent = node.parentNode;
182
+ const index = indexOf(node);
183
+ assertDetached(replacement, parent);
184
+ node.replaceWith(replacement);
185
+ for (const point of this.#points.values()) {
186
+ if (!inside(point, node)) continue;
187
+ relocate(point, parent, index + (point.affinity === 'forward' ? 1 : 0));
188
+ }
189
+ return replacement;
190
+ }
191
+
192
+ replaceWrapper(node, replacement) {
193
+ if (node?.nodeType !== Node.ELEMENT_NODE || replacement?.nodeType !== Node.ELEMENT_NODE) {
194
+ throw new TypeError('Wrapper replacement requires two elements');
195
+ }
196
+ if (!node.parentNode) throw new RangeError('A detached element cannot be replaced');
197
+ assertDetached(replacement, node.parentNode);
198
+ if (replacement.childNodes.length) throw new RangeError('A replacement wrapper must be empty');
199
+ while (node.firstChild) replacement.append(node.firstChild);
200
+ node.replaceWith(replacement);
201
+ for (const point of this.#points.values()) {
202
+ if (point.node === node) point.node = replacement;
203
+ }
204
+ return replacement;
205
+ }
206
+
207
+ move(node, parent, offset) {
208
+ if (!node?.parentNode) throw new RangeError('A detached node cannot be moved');
209
+ assertBoundary(parent, offset);
210
+ if (node === parent || node.contains(parent)) throw new RangeError('A node cannot move into itself');
211
+ const oldParent = node.parentNode;
212
+ const oldIndex = indexOf(node);
213
+ const reference = parent.childNodes[offset] || null;
214
+ if (reference === node || oldParent === parent && node.nextSibling === reference) return node;
215
+ const followers = new Map();
216
+ for (const point of this.#points.values()) {
217
+ if (point.node !== oldParent) continue;
218
+ if (point.offset === oldIndex && point.affinity === 'forward') followers.set(point, 'before');
219
+ else if (point.offset === oldIndex + 1 && point.affinity === 'backward') followers.set(point, 'after');
220
+ }
221
+ parent.insertBefore(node, reference);
222
+ const newIndex = indexOf(node);
223
+ for (const point of this.#points.values()) {
224
+ if (followers.has(point) || inside(point, node)) continue;
225
+ if (point.node === oldParent && point.offset > oldIndex) point.offset--;
226
+ if (point.node === parent && (point.offset > newIndex || point.offset === newIndex && point.affinity === 'forward')) {
227
+ point.offset++;
228
+ }
229
+ }
230
+ for (const [point, edge] of followers) relocate(point, parent, newIndex + (edge === 'after' ? 1 : 0));
231
+ return node;
232
+ }
233
+
234
+ // A boundary expressed against a text node is the same boundary as the one
235
+ // after it in its parent; forward affinity has to see it that way.
236
+ #follow(node, offset, target, index) {
237
+ for (const point of this.#points.values()) {
238
+ if (point.node === node && point.offset === offset && point.affinity === 'forward') relocate(point, target, index);
239
+ }
240
+ }
241
+
242
+ mergeText(left, right) {
243
+ if (left?.nodeType !== Node.TEXT_NODE || right?.nodeType !== Node.TEXT_NODE) {
244
+ throw new TypeError('Only text nodes can be merged');
245
+ }
246
+ if (!left.parentNode || left.nextSibling !== right) throw new RangeError('Merged text nodes must be adjacent siblings');
247
+ const parent = left.parentNode;
248
+ const index = indexOf(right);
249
+ const offset = left.length;
250
+ left.appendData(right.data);
251
+ right.remove();
252
+ for (const point of this.#points.values()) {
253
+ if (point.node === right) {
254
+ relocate(point, left, offset + point.offset);
255
+ } else if (point.node === parent && point.offset === index) {
256
+ relocate(point, left, offset);
257
+ } else if (point.node === parent && point.offset > index) {
258
+ point.offset--;
259
+ }
260
+ }
261
+ return left;
262
+ }
263
+
264
+ remove(node) {
265
+ if (!node?.parentNode) throw new RangeError('A detached node cannot be removed');
266
+ const parent = node.parentNode;
267
+ const index = indexOf(node);
268
+ node.remove();
269
+ for (const point of this.#points.values()) {
270
+ if (inside(point, node)) relocate(point, parent, index);
271
+ else if (point.node === parent && point.offset > index) point.offset--;
272
+ }
273
+ return node;
274
+ }
275
+ }
276
+
277
+ function assertPoint(point) {
278
+ if (!(point instanceof Point)) throw new TypeError('A point map only tracks points');
279
+ }
280
+
281
+ function assertBoundary(node, offset) {
282
+ if (!node?.childNodes) throw new TypeError('A child boundary requires a DOM node');
283
+ if (!Number.isInteger(offset) || offset < 0 || offset > node.childNodes.length) {
284
+ throw new RangeError('Invalid child boundary offset');
285
+ }
286
+ }
287
+
288
+ function assertDetached(node, parent) {
289
+ if (!node?.nodeType) throw new TypeError('A DOM node is required');
290
+ if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) throw new TypeError('DocumentFragment insertion is not supported yet');
291
+ if (node.parentNode) throw new RangeError('Inserted nodes must be detached');
292
+ if (node === parent || node.contains(parent)) throw new RangeError('A node cannot contain itself');
293
+ }
294
+
295
+ function follows(point, index) {
296
+ return point.offset > index || point.offset === index && point.affinity === 'forward';
297
+ }
298
+
299
+ function inside(point, node) {
300
+ return point.node === node || node.contains(point.node);
301
+ }
302
+
303
+ function relocate(point, node, offset) {
304
+ point.node = node;
305
+ point.offset = offset;
306
+ }
307
+
@@ -0,0 +1,32 @@
1
+ # Selection ownership
2
+
3
+ When editable elements are nested, every node, selection, and input event must
4
+ belong to exactly one editor. The inner editor is independent; its work must not
5
+ leak into the outer editor.
6
+
7
+ `ownership.js` finds that owning editable element and recognizes the explicit
8
+ `contenteditable` boundaries between editors.
9
+
10
+ ## Contract
11
+
12
+ - `editingHost(node)` returns the nearest explicit `[contenteditable]` ancestor.
13
+ - `isEditableHost(element)` accepts the standard editable values: an empty
14
+ value, `true`, and `plaintext-only`.
15
+ - `isEditingBoundary(element)` additionally recognizes `false` as an explicit
16
+ isolation boundary.
17
+ - `isPlainTextHost(element)` reports `plaintext-only`, where structural editing
18
+ commands must not replace the browser's plain-text behavior.
19
+ - `belongsTo(node, host)` requires DOM containment and that exact nearest host.
20
+ - A nested editable therefore never leaks selection or mutations to its parent.
21
+ - `selectionOf(host)` uses the host's tree selection when available and falls
22
+ back to its owner document for current browser interoperability.
23
+
24
+ The functions use valid explicit attribute values rather than
25
+ `isContentEditable`; inherited or invalid values belong to the nearest real
26
+ boundary and do not create a new surface.
27
+
28
+ ## TODO
29
+
30
+ - Verify `ShadowRoot.getSelection()` behavior in every target browser.
31
+ - Define ownership for future non-`contenteditable` editing hosts, if needed.
32
+ - Add composed-range ownership once interoperable browser support exists.
@@ -0,0 +1,42 @@
1
+ const EDITABLE_VALUES = new Set(['', 'true', 'false', 'plaintext-only']);
2
+
3
+ export function isEditableHost(element) {
4
+ const value = editableValue(element);
5
+ return value === '' || value === 'true' || value === 'plaintext-only';
6
+ }
7
+
8
+ export function isPlainTextHost(element) {
9
+ return editableValue(element) === 'plaintext-only';
10
+ }
11
+
12
+ export function isEditingBoundary(element) {
13
+ return editableValue(element) !== null;
14
+ }
15
+
16
+ // The element context of a node: itself when it is one, otherwise its parent.
17
+ // Traversals start here so text and element boundaries behave identically.
18
+ export function elementOf(node) {
19
+ return node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement || null;
20
+ }
21
+
22
+ export function editingHost(node) {
23
+ for (let element = elementOf(node); element; element = element.parentElement) {
24
+ if (editableValue(element) !== null) return element;
25
+ }
26
+ return null;
27
+ }
28
+
29
+ export function belongsTo(node, host) {
30
+ return !!node && (node === host || host.contains(node)) && editingHost(node) === host;
31
+ }
32
+
33
+ export function selectionOf(host) {
34
+ const root = host.getRootNode();
35
+ return root.getSelection?.() || host.ownerDocument.getSelection();
36
+ }
37
+
38
+ function editableValue(element) {
39
+ if (!element?.hasAttribute?.('contenteditable')) return null;
40
+ const value = element.getAttribute('contenteditable').toLowerCase();
41
+ return EDITABLE_VALUES.has(value) ? value : null;
42
+ }
@@ -0,0 +1,34 @@
1
+ # Logical DOM points
2
+
3
+ A point names one exact place in the DOM: either a character offset inside text
4
+ or a gap between child nodes. It can represent a caret or one edge of a
5
+ selection. Its affinity says which side it belongs to when content is inserted
6
+ at that exact place.
7
+
8
+ `point.js` stores the place as a collapsed native `Range`, allowing the browser
9
+ to follow text splits, removals, and many other DOM mutations without marker
10
+ elements.
11
+
12
+ ## Contract
13
+
14
+ - A point is always a valid node/offset boundary when constructed.
15
+ - `start`, `end`, `before`, `after`, and `fromRange` make boundary intent clear.
16
+ - `node` and `offset` are live: they reflect native range relocation after DOM
17
+ mutations.
18
+ - `backward` affinity associates an ambiguous boundary with preceding content;
19
+ `forward` affinity associates it with following content. DOM operation
20
+ mappings will honor this when the native mutation rule is ambiguous.
21
+ - `compare()` orders points in the same DOM tree and rejects unrelated trees.
22
+ - `range()` and `clone()` never expose mutable internal state.
23
+ - `within(root)` checks the current relocated boundary against an editing root.
24
+
25
+ The class deliberately does not serialize a child-index path. Paths become
26
+ stale under unrelated sibling changes; live ranges plus explicit operation
27
+ mapping provide stronger local semantics.
28
+
29
+ ## TODO
30
+
31
+ - Extend affinity rules to content-model-defined atomic boundaries.
32
+ - Add stable logical offsets for fully replaced subtrees.
33
+ - Define atomic-node inside/before/after coercion through content policy.
34
+ - Test browser differences in live-range relocation with generated mutations.
@@ -0,0 +1,83 @@
1
+ export class Point {
2
+ #range;
3
+ #affinity;
4
+
5
+ constructor(node, offset, affinity = 'forward') {
6
+ if (!node?.nodeType) throw new TypeError('A point requires a DOM node');
7
+ if (affinity !== 'backward' && affinity !== 'forward') {
8
+ throw new TypeError('Point affinity must be backward or forward');
9
+ }
10
+ const length = boundaryLength(node);
11
+ if (!Number.isInteger(offset) || offset < 0 || offset > length) {
12
+ throw new RangeError(`Point offset must be between 0 and ${length}`);
13
+ }
14
+ const document = node.nodeType === 9 ? node : node.ownerDocument;
15
+ this.#range = document.createRange();
16
+ this.#range.setStart(node, offset);
17
+ this.#range.collapse(true);
18
+ this.#affinity = affinity;
19
+ }
20
+
21
+ static start(node, affinity = 'forward') {
22
+ return new this(node, 0, affinity);
23
+ }
24
+
25
+ static end(node, affinity = 'backward') {
26
+ return new this(node, boundaryLength(node), affinity);
27
+ }
28
+
29
+ static before(node, affinity = 'backward') {
30
+ if (!node.parentNode) throw new RangeError('A detached node has no position before it');
31
+ return new this(node.parentNode, indexOf(node), affinity);
32
+ }
33
+
34
+ static after(node, affinity = 'forward') {
35
+ if (!node.parentNode) throw new RangeError('A detached node has no position after it');
36
+ return new this(node.parentNode, indexOf(node) + 1, affinity);
37
+ }
38
+
39
+ static fromRange(range, edge, affinity = edge === 'end' ? 'backward' : 'forward') {
40
+ if (edge === 'start') return new this(range.startContainer, range.startOffset, affinity);
41
+ if (edge === 'end') return new this(range.endContainer, range.endOffset, affinity);
42
+ throw new TypeError('Range edge must be start or end');
43
+ }
44
+
45
+ get node() { return this.#range.startContainer; }
46
+ get offset() { return this.#range.startOffset; }
47
+ get affinity() { return this.#affinity; }
48
+
49
+ clone() {
50
+ return new this.constructor(this.node, this.offset, this.#affinity);
51
+ }
52
+
53
+ withAffinity(affinity) {
54
+ return new this.constructor(this.node, this.offset, affinity);
55
+ }
56
+
57
+ range() {
58
+ return this.#range.cloneRange();
59
+ }
60
+
61
+ compare(other) {
62
+ if (!(other instanceof Point)) throw new TypeError('Points can only be compared with points');
63
+ try {
64
+ return Math.sign(this.#range.compareBoundaryPoints(0, other.#range));
65
+ } catch {
66
+ throw new RangeError('Points belong to different DOM trees');
67
+ }
68
+ }
69
+
70
+ within(root) {
71
+ return this.node === root || root.contains(this.node);
72
+ }
73
+ }
74
+
75
+ function boundaryLength(node) {
76
+ return typeof node.length === 'number' ? node.length : node.childNodes.length;
77
+ }
78
+
79
+ // A node's child index in its parent. Child boundaries are expressed with it
80
+ // throughout the engine, so it lives beside the point that names them.
81
+ export function indexOf(node) {
82
+ return Array.prototype.indexOf.call(node.parentNode.childNodes, node);
83
+ }
@@ -0,0 +1,38 @@
1
+ # Edit ranges
2
+
3
+ An edit range is the part of one editor that a command will read or change. It
4
+ may be a caret, part of a text node, or a selection spanning several blocks.
5
+
6
+ `edit-range.js` wraps an ordered native `Range` with operations needed by editor
7
+ commands, such as finding selected text nodes, blocks, or fully covered roots.
8
+ Native range behavior remains visible instead of being hidden behind a generic
9
+ Proxy.
10
+
11
+ ## Contract
12
+
13
+ - Both boundaries must belong to one explicit editable root.
14
+ - Nested editable hosts are hard boundaries for construction and traversal.
15
+ - `fromSelection()` returns `null` for absent or foreign selections.
16
+ - `fromPoints()` rejects reversed points instead of silently collapsing.
17
+ - Returned native ranges and cloned edit ranges cannot mutate the original.
18
+ - `select()` restores forward or backward browser selections.
19
+ - `splitTextBoundaries()` exposes exactly selected text without changing its
20
+ content or expanding the selection.
21
+ - `textNodes()` returns intersecting non-empty text nodes in document order.
22
+ - `blocks(predicate)` returns the nearest policy-defined block for each
23
+ selected leaf, including empty blocks, in document order. A collapsed range
24
+ resolves only its nearest block.
25
+ - `roots()` returns maximal fully covered live nodes; partially selected
26
+ ancestors are traversed, never guessed as selected.
27
+ - `intersects()` and `contains()` respect editable ownership.
28
+
29
+ The class does not yet perform formatting. Commands will compose these
30
+ primitives inside transactions and explicitly mark resulting dirty scopes.
31
+
32
+ ## TODO
33
+
34
+ - Connect block predicates to the surface content model.
35
+ - Add atomic-node coercion from content policy.
36
+ - Add contextual fragment insertion without unsafe HTML sinks.
37
+ - Extend the isolated browser `rangeRect()` policy with writing-mode and Shadow
38
+ DOM verification before adding geometry to command-level ranges.