@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.
- package/CHANGELOG.md +108 -70
- package/LICENSE +21 -21
- package/README.md +286 -247
- package/dist/canvas/box-clipboard.js +238 -238
- package/dist/canvas/box-selection.js +80 -80
- package/dist/canvas/canvas-editor.js +502 -502
- package/dist/canvas/controller.js +717 -717
- package/dist/canvas/geometry.js +131 -131
- package/dist/canvas/gesture.d.ts +0 -1
- package/dist/canvas/gesture.js +289 -289
- package/dist/canvas/handles.js +212 -212
- package/dist/canvas/menu.js +82 -82
- package/dist/canvas/mode-controller.js +81 -81
- package/dist/canvas/nudge.js +108 -108
- package/dist/canvas/overlay.js +149 -149
- package/dist/canvas/snap.js +132 -132
- package/dist/canvas/viewport.js +112 -112
- package/dist/canvas/z-order.js +171 -171
- package/dist/commands/alignment.js +150 -150
- package/dist/commands/box.js +272 -272
- package/dist/commands/clipboard.js +93 -93
- package/dist/commands/graphemes.js +112 -112
- package/dist/commands/marks.js +153 -153
- package/dist/commands/paragraph-props.js +176 -176
- package/dist/commands/size.js +368 -368
- package/dist/commands/text.js +291 -291
- package/dist/docx.d.ts +1 -0
- package/dist/docx.js +15 -13
- package/dist/editor/editing-host.js +434 -434
- package/dist/element.js +41 -41
- package/dist/fonts/OFL-Carlito.txt +94 -0
- package/dist/fonts/calibri-metrics.d.ts +27 -0
- package/dist/fonts/calibri-metrics.js +88 -0
- package/dist/history/history.js +433 -433
- package/dist/index.js +63 -63
- package/dist/input/beforeinput.js +224 -224
- package/dist/input/clipboard.js +107 -107
- package/dist/input/composition.js +341 -341
- package/dist/input/keymap.js +54 -54
- package/dist/io/ooxml/custgeom.js +72 -72
- package/dist/io/ooxml/drawingml.js +232 -232
- package/dist/io/ooxml/edits.js +67 -67
- package/dist/io/ooxml/export-splice.js +880 -880
- package/dist/io/ooxml/export.js +365 -365
- package/dist/io/ooxml/fill.d.ts +93 -8
- package/dist/io/ooxml/fill.js +618 -562
- package/dist/io/ooxml/fit.d.ts +252 -0
- package/dist/io/ooxml/fit.js +598 -0
- package/dist/io/ooxml/import-gate.js +200 -200
- package/dist/io/ooxml/import.js +691 -691
- package/dist/io/ooxml/inflate.js +126 -126
- package/dist/io/ooxml/media.js +87 -87
- package/dist/io/ooxml/ns.d.ts +2 -0
- package/dist/io/ooxml/ns.js +26 -21
- package/dist/io/ooxml/numbering.js +290 -290
- package/dist/io/ooxml/opc-write.js +171 -171
- package/dist/io/ooxml/provenance.js +93 -93
- package/dist/io/ooxml/recipe.js +708 -708
- package/dist/io/ooxml/relationships.js +59 -59
- package/dist/io/ooxml/run-fonts.d.ts +89 -0
- package/dist/io/ooxml/run-fonts.js +378 -0
- package/dist/io/ooxml/slots.d.ts +4 -0
- package/dist/io/ooxml/slots.js +582 -581
- package/dist/io/ooxml/styles-resolve.js +257 -257
- package/dist/io/ooxml/units.js +43 -43
- package/dist/io/ooxml/value.js +120 -120
- package/dist/io/ooxml/xml-scan.js +616 -616
- package/dist/io/ooxml/zip-container.js +228 -228
- package/dist/io/ooxml/zip-reject.js +106 -106
- package/dist/measure/placement.js +83 -83
- package/dist/measure/report.js +75 -75
- package/dist/model/ids.js +70 -70
- package/dist/model/normalize.js +146 -146
- package/dist/model/position.js +283 -283
- package/dist/model/transaction.js +209 -209
- package/dist/platform/capabilities.js +25 -25
- package/dist/react/index.js +21 -21
- package/dist/selection/controller.js +315 -315
- package/dist/selection/dom-map.js +334 -334
- package/dist/state/editor-state.js +109 -109
- package/dist/styles.css +66 -66
- package/package.json +94 -93
package/dist/model/position.js
CHANGED
|
@@ -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
|
+
}
|