@sametalmdr/word-editor 0.2.0-alpha → 0.3.0-alpha

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 (82) hide show
  1. package/CHANGELOG.md +108 -70
  2. package/LICENSE +21 -21
  3. package/README.md +286 -247
  4. package/dist/canvas/box-clipboard.js +238 -238
  5. package/dist/canvas/box-selection.js +80 -80
  6. package/dist/canvas/canvas-editor.js +502 -502
  7. package/dist/canvas/controller.js +717 -717
  8. package/dist/canvas/geometry.js +131 -131
  9. package/dist/canvas/gesture.d.ts +0 -1
  10. package/dist/canvas/gesture.js +289 -289
  11. package/dist/canvas/handles.js +212 -212
  12. package/dist/canvas/menu.js +82 -82
  13. package/dist/canvas/mode-controller.js +81 -81
  14. package/dist/canvas/nudge.js +108 -108
  15. package/dist/canvas/overlay.js +149 -149
  16. package/dist/canvas/snap.js +132 -132
  17. package/dist/canvas/viewport.js +112 -112
  18. package/dist/canvas/z-order.js +171 -171
  19. package/dist/commands/alignment.js +150 -150
  20. package/dist/commands/box.js +272 -272
  21. package/dist/commands/clipboard.js +93 -93
  22. package/dist/commands/graphemes.js +112 -112
  23. package/dist/commands/marks.js +153 -153
  24. package/dist/commands/paragraph-props.js +176 -176
  25. package/dist/commands/size.js +368 -368
  26. package/dist/commands/text.js +291 -291
  27. package/dist/docx.d.ts +1 -0
  28. package/dist/docx.js +15 -13
  29. package/dist/editor/editing-host.js +434 -434
  30. package/dist/element.js +41 -41
  31. package/dist/fonts/OFL-Carlito.txt +94 -0
  32. package/dist/fonts/calibri-metrics.d.ts +27 -0
  33. package/dist/fonts/calibri-metrics.js +88 -0
  34. package/dist/history/history.js +433 -433
  35. package/dist/index.js +63 -63
  36. package/dist/input/beforeinput.js +224 -224
  37. package/dist/input/clipboard.js +107 -107
  38. package/dist/input/composition.js +341 -341
  39. package/dist/input/keymap.js +54 -54
  40. package/dist/io/ooxml/custgeom.js +72 -72
  41. package/dist/io/ooxml/drawingml.js +232 -232
  42. package/dist/io/ooxml/edits.js +67 -67
  43. package/dist/io/ooxml/export-splice.js +880 -880
  44. package/dist/io/ooxml/export.js +365 -365
  45. package/dist/io/ooxml/fill.d.ts +93 -8
  46. package/dist/io/ooxml/fill.js +618 -562
  47. package/dist/io/ooxml/fit.d.ts +252 -0
  48. package/dist/io/ooxml/fit.js +598 -0
  49. package/dist/io/ooxml/import-gate.js +200 -200
  50. package/dist/io/ooxml/import.js +691 -691
  51. package/dist/io/ooxml/inflate.js +126 -126
  52. package/dist/io/ooxml/media.js +87 -87
  53. package/dist/io/ooxml/ns.d.ts +2 -0
  54. package/dist/io/ooxml/ns.js +26 -21
  55. package/dist/io/ooxml/numbering.js +290 -290
  56. package/dist/io/ooxml/opc-write.js +171 -171
  57. package/dist/io/ooxml/provenance.js +93 -93
  58. package/dist/io/ooxml/recipe.js +708 -708
  59. package/dist/io/ooxml/relationships.js +59 -59
  60. package/dist/io/ooxml/run-fonts.d.ts +89 -0
  61. package/dist/io/ooxml/run-fonts.js +378 -0
  62. package/dist/io/ooxml/slots.d.ts +4 -0
  63. package/dist/io/ooxml/slots.js +582 -581
  64. package/dist/io/ooxml/styles-resolve.js +257 -257
  65. package/dist/io/ooxml/units.js +43 -43
  66. package/dist/io/ooxml/value.js +120 -120
  67. package/dist/io/ooxml/xml-scan.js +616 -616
  68. package/dist/io/ooxml/zip-container.js +228 -228
  69. package/dist/io/ooxml/zip-reject.js +106 -106
  70. package/dist/measure/placement.js +83 -83
  71. package/dist/measure/report.js +75 -75
  72. package/dist/model/ids.js +70 -70
  73. package/dist/model/normalize.js +146 -146
  74. package/dist/model/position.js +283 -283
  75. package/dist/model/transaction.js +209 -209
  76. package/dist/platform/capabilities.js +25 -25
  77. package/dist/react/index.js +21 -21
  78. package/dist/selection/controller.js +315 -315
  79. package/dist/selection/dom-map.js +334 -334
  80. package/dist/state/editor-state.js +109 -109
  81. package/dist/styles.css +66 -66
  82. package/package.json +94 -93
@@ -1,283 +1,283 @@
1
- import { locateParagraph, paragraphLength } from "./node.js";
2
-
3
- /**
4
- * A place between two characters. `offset` indexes into the paragraph's
5
- * concatenated text — *not* a run index plus an intra-run offset. That choice is
6
- * what makes positions survive `normalize()` untouched: merging or dropping runs
7
- * never moves a character, so it never moves an offset. See ADR-0002.
8
- *
9
- * `path` is `[containerId, paragraphId]` since the container refactor
10
- * (ADR-0007). It was `[paragraphId]` in P1, and ADR-0002 kept it an array
11
- * precisely so this could grow — `paragraphIdOf` reads the *last* element and
12
- * did not have to change.
13
- *
14
- * @typedef {object} Position
15
- * @property {import("./node.js").NodeId[]} path
16
- * @property {number} offset
17
- */
18
-
19
- /**
20
- * `anchor` is where the selection started, `head` where it is now — direction
21
- * matters for shift+arrow, so this is not a from/to pair.
22
- * @typedef {object} Selection
23
- * @property {Position} anchor
24
- * @property {Position} head
25
- */
26
-
27
- /**
28
- * Where a position lands inside a paragraph's runs.
29
- *
30
- * Run boundaries are **left-biased**: at the seam between run `i` and run `i+1`
31
- * the position resolves to the end of run `i`, so `offsetInRun` may equal the
32
- * run's length. This matches the left-biased mark inheritance in
33
- * `commands/marks.js` — typing right after bold text continues bold.
34
- *
35
- * @typedef {object} ResolvedPosition
36
- * @property {number} containerIndex
37
- * @property {import("./node.js").Container} container
38
- * @property {number} paraIndex
39
- * @property {import("./node.js").Paragraph} para
40
- * @property {number} runIndex
41
- * @property {number} offsetInRun
42
- */
43
-
44
- /**
45
- * @param {import("./node.js").NodeId} containerId
46
- * @param {import("./node.js").NodeId} paragraphId
47
- * @param {number} offset
48
- * @returns {Position}
49
- */
50
- export function createPosition(containerId, paragraphId, offset) {
51
- return { path: [containerId, paragraphId], offset };
52
- }
53
-
54
- /**
55
- * Same place, different offset. Preferred over rebuilding with
56
- * `createPosition` when a step moves a position *within* its own paragraph:
57
- * it carries the whole path forward, so it cannot silently drop the container.
58
- * @param {Position} pos
59
- * @param {number} offset
60
- * @returns {Position}
61
- */
62
- export function withOffset(pos, offset) {
63
- return { path: pos.path.slice(), offset };
64
- }
65
-
66
- /**
67
- * The paragraph a position lives in. Reads the **last** path element, so it is
68
- * independent of how deep the path is — unchanged from P1 across the container
69
- * refactor (ADR-0007).
70
- * @param {Position} pos
71
- * @returns {import("./node.js").NodeId}
72
- */
73
- export function paragraphIdOf(pos) {
74
- return pos.path[pos.path.length - 1];
75
- }
76
-
77
- /**
78
- * The container a position lives in.
79
- * @param {Position} pos
80
- * @returns {import("./node.js").NodeId}
81
- */
82
- export function containerIdOf(pos) {
83
- return pos.path[0];
84
- }
85
-
86
- /**
87
- * @param {Position} a
88
- * @param {Position} b
89
- * @returns {boolean}
90
- */
91
- export function positionsEqual(a, b) {
92
- return (
93
- a.offset === b.offset &&
94
- a.path.length === b.path.length &&
95
- a.path.every((id, i) => id === b.path[i])
96
- );
97
- }
98
-
99
- /**
100
- * @param {Position} a
101
- * @param {Position} b
102
- * @returns {boolean}
103
- */
104
- export function samePara(a, b) {
105
- return paragraphIdOf(a) === paragraphIdOf(b);
106
- }
107
-
108
- /**
109
- * @param {Position} a
110
- * @param {Position} b
111
- * @returns {boolean}
112
- */
113
- export function sameContainer(a, b) {
114
- return containerIdOf(a) === containerIdOf(b);
115
- }
116
-
117
- /**
118
- * Drills a position down to a run. Returns null when the paragraph is gone —
119
- * callers must decide whether that is a bug or a stale position.
120
- * @param {import("./node.js").Doc} doc
121
- * @param {Position} pos
122
- * @returns {ResolvedPosition | null}
123
- */
124
- export function resolve(doc, pos) {
125
- const found = locateParagraph(doc, paragraphIdOf(pos));
126
- if (found === null) {
127
- return null;
128
- }
129
- const { containerIndex, container, paraIndex, para } = found;
130
-
131
- let acc = 0;
132
- for (let runIndex = 0; runIndex < para.content.length; runIndex += 1) {
133
- const end = acc + para.content[runIndex].text.length;
134
- // `<=` rather than `<` is the left bias: a position at a seam belongs to the
135
- // run that ends there.
136
- if (pos.offset <= end) {
137
- return { containerIndex, container, paraIndex, para, runIndex, offsetInRun: pos.offset - acc };
138
- }
139
- acc = end;
140
- }
141
-
142
- const lastIndex = para.content.length - 1;
143
- return {
144
- containerIndex,
145
- container,
146
- paraIndex,
147
- para,
148
- runIndex: lastIndex,
149
- offsetInRun: para.content[lastIndex].text.length,
150
- };
151
- }
152
-
153
- /**
154
- * Document order: container index first, then paragraph index within it, then
155
- * offset. Returns null when either paragraph is missing.
156
- *
157
- * Note this orders *across* containers even though a text selection may not span
158
- * them (see `isSingleContainer`) — comparison is also used for things like
159
- * ordering two independent anchors, where cross-container order is meaningful.
160
- *
161
- * @param {import("./node.js").Doc} doc
162
- * @param {Position} a
163
- * @param {Position} b
164
- * @returns {-1 | 0 | 1 | null}
165
- */
166
- export function comparePositions(doc, a, b) {
167
- const aId = paragraphIdOf(a);
168
- const bId = paragraphIdOf(b);
169
-
170
- if (aId === bId) {
171
- if (a.offset === b.offset) {
172
- return 0;
173
- }
174
- return a.offset < b.offset ? -1 : 1;
175
- }
176
-
177
- const aAt = locateParagraph(doc, aId);
178
- const bAt = locateParagraph(doc, bId);
179
- if (aAt === null || bAt === null) {
180
- return null;
181
- }
182
- if (aAt.containerIndex !== bAt.containerIndex) {
183
- return aAt.containerIndex < bAt.containerIndex ? -1 : 1;
184
- }
185
- return aAt.paraIndex < bAt.paraIndex ? -1 : 1;
186
- }
187
-
188
- /**
189
- * Clamps `offset` into the paragraph. Returns null when the paragraph is gone.
190
- * @param {import("./node.js").Doc} doc
191
- * @param {Position} pos
192
- * @returns {Position | null}
193
- */
194
- export function clampPosition(doc, pos) {
195
- const found = locateParagraph(doc, paragraphIdOf(pos));
196
- if (found === null) {
197
- return null;
198
- }
199
- const max = paragraphLength(found.para);
200
- if (pos.offset >= 0 && pos.offset <= max) {
201
- return pos;
202
- }
203
- return { path: pos.path.slice(), offset: Math.max(0, Math.min(pos.offset, max)) };
204
- }
205
-
206
- /**
207
- * @param {Position} anchor
208
- * @param {Position} head
209
- * @returns {Selection}
210
- */
211
- export function createSelection(anchor, head) {
212
- return { anchor, head };
213
- }
214
-
215
- /**
216
- * @param {Position} pos
217
- * @returns {Selection}
218
- */
219
- export function collapsedSelection(pos) {
220
- return { anchor: pos, head: pos };
221
- }
222
-
223
- /**
224
- * @param {Selection} sel
225
- * @returns {boolean}
226
- */
227
- export function isCollapsed(sel) {
228
- return positionsEqual(sel.anchor, sel.head);
229
- }
230
-
231
- /**
232
- * @param {Selection} a
233
- * @param {Selection} b
234
- * @returns {boolean}
235
- */
236
- export function selectionsEqual(a, b) {
237
- return positionsEqual(a.anchor, b.anchor) && positionsEqual(a.head, b.head);
238
- }
239
-
240
- /**
241
- * The selection as an ordered range. Returns null when either endpoint's
242
- * paragraph is missing.
243
- * @param {import("./node.js").Doc} doc
244
- * @param {Selection} sel
245
- * @returns {{ from: Position, to: Position } | null}
246
- */
247
- export function selectionRange(doc, sel) {
248
- const order = comparePositions(doc, sel.anchor, sel.head);
249
- if (order === null) {
250
- return null;
251
- }
252
- return order <= 0
253
- ? { from: sel.anchor, to: sel.head }
254
- : { from: sel.head, to: sel.anchor };
255
- }
256
-
257
- /**
258
- * True when both endpoints sit in the same paragraph. Drives the `DeleteStep`
259
- * single-paragraph rule.
260
- *
261
- * Also drives `input/composition.js`'s rejection of multi-paragraph
262
- * composing ranges (P1.9a, ADR-0004 §2).
263
- * @param {Selection} sel
264
- * @returns {boolean}
265
- */
266
- export function isSingleParagraph(sel) {
267
- return samePara(sel.anchor, sel.head);
268
- }
269
-
270
- /**
271
- * True when both endpoints sit in the same container.
272
- *
273
- * ADR-0007 makes this an **invariant** for text selections, not merely a
274
- * question: a text selection may not span two boxes. On a canvas the boxes have
275
- * no meaningful document order between them, so "select from box A into box B"
276
- * has no sensible meaning; selecting whole boxes is a `BoxSelection` instead.
277
- * Commands assert this rather than trying to handle the cross-container case.
278
- * @param {Selection} sel
279
- * @returns {boolean}
280
- */
281
- export function isSingleContainer(sel) {
282
- return sameContainer(sel.anchor, sel.head);
283
- }
1
+ import { locateParagraph, paragraphLength } from "./node.js";
2
+
3
+ /**
4
+ * A place between two characters. `offset` indexes into the paragraph's
5
+ * concatenated text — *not* a run index plus an intra-run offset. That choice is
6
+ * what makes positions survive `normalize()` untouched: merging or dropping runs
7
+ * never moves a character, so it never moves an offset. See ADR-0002.
8
+ *
9
+ * `path` is `[containerId, paragraphId]` since the container refactor
10
+ * (ADR-0007). It was `[paragraphId]` in P1, and ADR-0002 kept it an array
11
+ * precisely so this could grow — `paragraphIdOf` reads the *last* element and
12
+ * did not have to change.
13
+ *
14
+ * @typedef {object} Position
15
+ * @property {import("./node.js").NodeId[]} path
16
+ * @property {number} offset
17
+ */
18
+
19
+ /**
20
+ * `anchor` is where the selection started, `head` where it is now — direction
21
+ * matters for shift+arrow, so this is not a from/to pair.
22
+ * @typedef {object} Selection
23
+ * @property {Position} anchor
24
+ * @property {Position} head
25
+ */
26
+
27
+ /**
28
+ * Where a position lands inside a paragraph's runs.
29
+ *
30
+ * Run boundaries are **left-biased**: at the seam between run `i` and run `i+1`
31
+ * the position resolves to the end of run `i`, so `offsetInRun` may equal the
32
+ * run's length. This matches the left-biased mark inheritance in
33
+ * `commands/marks.js` — typing right after bold text continues bold.
34
+ *
35
+ * @typedef {object} ResolvedPosition
36
+ * @property {number} containerIndex
37
+ * @property {import("./node.js").Container} container
38
+ * @property {number} paraIndex
39
+ * @property {import("./node.js").Paragraph} para
40
+ * @property {number} runIndex
41
+ * @property {number} offsetInRun
42
+ */
43
+
44
+ /**
45
+ * @param {import("./node.js").NodeId} containerId
46
+ * @param {import("./node.js").NodeId} paragraphId
47
+ * @param {number} offset
48
+ * @returns {Position}
49
+ */
50
+ export function createPosition(containerId, paragraphId, offset) {
51
+ return { path: [containerId, paragraphId], offset };
52
+ }
53
+
54
+ /**
55
+ * Same place, different offset. Preferred over rebuilding with
56
+ * `createPosition` when a step moves a position *within* its own paragraph:
57
+ * it carries the whole path forward, so it cannot silently drop the container.
58
+ * @param {Position} pos
59
+ * @param {number} offset
60
+ * @returns {Position}
61
+ */
62
+ export function withOffset(pos, offset) {
63
+ return { path: pos.path.slice(), offset };
64
+ }
65
+
66
+ /**
67
+ * The paragraph a position lives in. Reads the **last** path element, so it is
68
+ * independent of how deep the path is — unchanged from P1 across the container
69
+ * refactor (ADR-0007).
70
+ * @param {Position} pos
71
+ * @returns {import("./node.js").NodeId}
72
+ */
73
+ export function paragraphIdOf(pos) {
74
+ return pos.path[pos.path.length - 1];
75
+ }
76
+
77
+ /**
78
+ * The container a position lives in.
79
+ * @param {Position} pos
80
+ * @returns {import("./node.js").NodeId}
81
+ */
82
+ export function containerIdOf(pos) {
83
+ return pos.path[0];
84
+ }
85
+
86
+ /**
87
+ * @param {Position} a
88
+ * @param {Position} b
89
+ * @returns {boolean}
90
+ */
91
+ export function positionsEqual(a, b) {
92
+ return (
93
+ a.offset === b.offset &&
94
+ a.path.length === b.path.length &&
95
+ a.path.every((id, i) => id === b.path[i])
96
+ );
97
+ }
98
+
99
+ /**
100
+ * @param {Position} a
101
+ * @param {Position} b
102
+ * @returns {boolean}
103
+ */
104
+ export function samePara(a, b) {
105
+ return paragraphIdOf(a) === paragraphIdOf(b);
106
+ }
107
+
108
+ /**
109
+ * @param {Position} a
110
+ * @param {Position} b
111
+ * @returns {boolean}
112
+ */
113
+ export function sameContainer(a, b) {
114
+ return containerIdOf(a) === containerIdOf(b);
115
+ }
116
+
117
+ /**
118
+ * Drills a position down to a run. Returns null when the paragraph is gone —
119
+ * callers must decide whether that is a bug or a stale position.
120
+ * @param {import("./node.js").Doc} doc
121
+ * @param {Position} pos
122
+ * @returns {ResolvedPosition | null}
123
+ */
124
+ export function resolve(doc, pos) {
125
+ const found = locateParagraph(doc, paragraphIdOf(pos));
126
+ if (found === null) {
127
+ return null;
128
+ }
129
+ const { containerIndex, container, paraIndex, para } = found;
130
+
131
+ let acc = 0;
132
+ for (let runIndex = 0; runIndex < para.content.length; runIndex += 1) {
133
+ const end = acc + para.content[runIndex].text.length;
134
+ // `<=` rather than `<` is the left bias: a position at a seam belongs to the
135
+ // run that ends there.
136
+ if (pos.offset <= end) {
137
+ return { containerIndex, container, paraIndex, para, runIndex, offsetInRun: pos.offset - acc };
138
+ }
139
+ acc = end;
140
+ }
141
+
142
+ const lastIndex = para.content.length - 1;
143
+ return {
144
+ containerIndex,
145
+ container,
146
+ paraIndex,
147
+ para,
148
+ runIndex: lastIndex,
149
+ offsetInRun: para.content[lastIndex].text.length,
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Document order: container index first, then paragraph index within it, then
155
+ * offset. Returns null when either paragraph is missing.
156
+ *
157
+ * Note this orders *across* containers even though a text selection may not span
158
+ * them (see `isSingleContainer`) — comparison is also used for things like
159
+ * ordering two independent anchors, where cross-container order is meaningful.
160
+ *
161
+ * @param {import("./node.js").Doc} doc
162
+ * @param {Position} a
163
+ * @param {Position} b
164
+ * @returns {-1 | 0 | 1 | null}
165
+ */
166
+ export function comparePositions(doc, a, b) {
167
+ const aId = paragraphIdOf(a);
168
+ const bId = paragraphIdOf(b);
169
+
170
+ if (aId === bId) {
171
+ if (a.offset === b.offset) {
172
+ return 0;
173
+ }
174
+ return a.offset < b.offset ? -1 : 1;
175
+ }
176
+
177
+ const aAt = locateParagraph(doc, aId);
178
+ const bAt = locateParagraph(doc, bId);
179
+ if (aAt === null || bAt === null) {
180
+ return null;
181
+ }
182
+ if (aAt.containerIndex !== bAt.containerIndex) {
183
+ return aAt.containerIndex < bAt.containerIndex ? -1 : 1;
184
+ }
185
+ return aAt.paraIndex < bAt.paraIndex ? -1 : 1;
186
+ }
187
+
188
+ /**
189
+ * Clamps `offset` into the paragraph. Returns null when the paragraph is gone.
190
+ * @param {import("./node.js").Doc} doc
191
+ * @param {Position} pos
192
+ * @returns {Position | null}
193
+ */
194
+ export function clampPosition(doc, pos) {
195
+ const found = locateParagraph(doc, paragraphIdOf(pos));
196
+ if (found === null) {
197
+ return null;
198
+ }
199
+ const max = paragraphLength(found.para);
200
+ if (pos.offset >= 0 && pos.offset <= max) {
201
+ return pos;
202
+ }
203
+ return { path: pos.path.slice(), offset: Math.max(0, Math.min(pos.offset, max)) };
204
+ }
205
+
206
+ /**
207
+ * @param {Position} anchor
208
+ * @param {Position} head
209
+ * @returns {Selection}
210
+ */
211
+ export function createSelection(anchor, head) {
212
+ return { anchor, head };
213
+ }
214
+
215
+ /**
216
+ * @param {Position} pos
217
+ * @returns {Selection}
218
+ */
219
+ export function collapsedSelection(pos) {
220
+ return { anchor: pos, head: pos };
221
+ }
222
+
223
+ /**
224
+ * @param {Selection} sel
225
+ * @returns {boolean}
226
+ */
227
+ export function isCollapsed(sel) {
228
+ return positionsEqual(sel.anchor, sel.head);
229
+ }
230
+
231
+ /**
232
+ * @param {Selection} a
233
+ * @param {Selection} b
234
+ * @returns {boolean}
235
+ */
236
+ export function selectionsEqual(a, b) {
237
+ return positionsEqual(a.anchor, b.anchor) && positionsEqual(a.head, b.head);
238
+ }
239
+
240
+ /**
241
+ * The selection as an ordered range. Returns null when either endpoint's
242
+ * paragraph is missing.
243
+ * @param {import("./node.js").Doc} doc
244
+ * @param {Selection} sel
245
+ * @returns {{ from: Position, to: Position } | null}
246
+ */
247
+ export function selectionRange(doc, sel) {
248
+ const order = comparePositions(doc, sel.anchor, sel.head);
249
+ if (order === null) {
250
+ return null;
251
+ }
252
+ return order <= 0
253
+ ? { from: sel.anchor, to: sel.head }
254
+ : { from: sel.head, to: sel.anchor };
255
+ }
256
+
257
+ /**
258
+ * True when both endpoints sit in the same paragraph. Drives the `DeleteStep`
259
+ * single-paragraph rule.
260
+ *
261
+ * Also drives `input/composition.js`'s rejection of multi-paragraph
262
+ * composing ranges (P1.9a, ADR-0004 §2).
263
+ * @param {Selection} sel
264
+ * @returns {boolean}
265
+ */
266
+ export function isSingleParagraph(sel) {
267
+ return samePara(sel.anchor, sel.head);
268
+ }
269
+
270
+ /**
271
+ * True when both endpoints sit in the same container.
272
+ *
273
+ * ADR-0007 makes this an **invariant** for text selections, not merely a
274
+ * question: a text selection may not span two boxes. On a canvas the boxes have
275
+ * no meaningful document order between them, so "select from box A into box B"
276
+ * has no sensible meaning; selecting whole boxes is a `BoxSelection` instead.
277
+ * Commands assert this rather than trying to handle the cross-container case.
278
+ * @param {Selection} sel
279
+ * @returns {boolean}
280
+ */
281
+ export function isSingleContainer(sel) {
282
+ return sameContainer(sel.anchor, sel.head);
283
+ }