@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/ids.js
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @typedef {object} IdFactory
|
|
3
|
-
* @property {() => number} next
|
|
4
|
-
* @property {() => number} peek
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Monotonic id source, scoped to one document. Per-document rather than global
|
|
9
|
-
* so tests are deterministic; ARCHITECTURE.md §4 only requires uniqueness within
|
|
10
|
-
* a document.
|
|
11
|
-
*
|
|
12
|
-
* When loading a document from storage, seed `start` past the largest existing
|
|
13
|
-
* id so new nodes never collide with restored ones.
|
|
14
|
-
*
|
|
15
|
-
* @param {number} [start]
|
|
16
|
-
* @returns {IdFactory}
|
|
17
|
-
*/
|
|
18
|
-
export function createIdFactory(start = 1) {
|
|
19
|
-
let next = start;
|
|
20
|
-
return {
|
|
21
|
-
next() {
|
|
22
|
-
return next++;
|
|
23
|
-
},
|
|
24
|
-
peek() {
|
|
25
|
-
return next;
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Scans an already-constructed Doc for its highest NodeId — `doc.id`, every
|
|
32
|
-
* container/paragraph id under `doc.content`, every decoration id, every
|
|
33
|
-
* image id. Runs carry no NodeId (node.js's Run typedef has no `id` field) so
|
|
34
|
-
* they are not visited.
|
|
35
|
-
*
|
|
36
|
-
* Mirrors the maxId-tracking pattern `document-json.js`'s JSON parser and
|
|
37
|
-
* `import.js`'s DOCX importer each use to seed a factory past every id
|
|
38
|
-
* already in use — but against a FINISHED Doc rather than while building one,
|
|
39
|
-
* since neither existing tracker (one interleaved with raw JSON field
|
|
40
|
-
* parsing, one a live mint-time counter) is callable against a Doc that
|
|
41
|
-
* already exists. Used by `loadDocument` to reseed the shared IdFactory when
|
|
42
|
-
* swapping documents (ADR-0027 Ek 49) — without this, a factory minted for
|
|
43
|
-
* one document keeps handing out ids that may already be in use in the next.
|
|
44
|
-
* @param {import("./node.js").Doc} doc
|
|
45
|
-
* @returns {number}
|
|
46
|
-
*/
|
|
47
|
-
export function maxNodeId(doc) {
|
|
48
|
-
let max = doc.id;
|
|
49
|
-
for (const container of doc.content) {
|
|
50
|
-
if (container.id > max) {
|
|
51
|
-
max = container.id;
|
|
52
|
-
}
|
|
53
|
-
for (const paragraph of container.content) {
|
|
54
|
-
if (paragraph.id > max) {
|
|
55
|
-
max = paragraph.id;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
for (const decoration of doc.decorations) {
|
|
60
|
-
if (decoration.id > max) {
|
|
61
|
-
max = decoration.id;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
for (const image of doc.images) {
|
|
65
|
-
if (image.id > max) {
|
|
66
|
-
max = image.id;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return max;
|
|
70
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} IdFactory
|
|
3
|
+
* @property {() => number} next
|
|
4
|
+
* @property {() => number} peek
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Monotonic id source, scoped to one document. Per-document rather than global
|
|
9
|
+
* so tests are deterministic; ARCHITECTURE.md §4 only requires uniqueness within
|
|
10
|
+
* a document.
|
|
11
|
+
*
|
|
12
|
+
* When loading a document from storage, seed `start` past the largest existing
|
|
13
|
+
* id so new nodes never collide with restored ones.
|
|
14
|
+
*
|
|
15
|
+
* @param {number} [start]
|
|
16
|
+
* @returns {IdFactory}
|
|
17
|
+
*/
|
|
18
|
+
export function createIdFactory(start = 1) {
|
|
19
|
+
let next = start;
|
|
20
|
+
return {
|
|
21
|
+
next() {
|
|
22
|
+
return next++;
|
|
23
|
+
},
|
|
24
|
+
peek() {
|
|
25
|
+
return next;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Scans an already-constructed Doc for its highest NodeId — `doc.id`, every
|
|
32
|
+
* container/paragraph id under `doc.content`, every decoration id, every
|
|
33
|
+
* image id. Runs carry no NodeId (node.js's Run typedef has no `id` field) so
|
|
34
|
+
* they are not visited.
|
|
35
|
+
*
|
|
36
|
+
* Mirrors the maxId-tracking pattern `document-json.js`'s JSON parser and
|
|
37
|
+
* `import.js`'s DOCX importer each use to seed a factory past every id
|
|
38
|
+
* already in use — but against a FINISHED Doc rather than while building one,
|
|
39
|
+
* since neither existing tracker (one interleaved with raw JSON field
|
|
40
|
+
* parsing, one a live mint-time counter) is callable against a Doc that
|
|
41
|
+
* already exists. Used by `loadDocument` to reseed the shared IdFactory when
|
|
42
|
+
* swapping documents (ADR-0027 Ek 49) — without this, a factory minted for
|
|
43
|
+
* one document keeps handing out ids that may already be in use in the next.
|
|
44
|
+
* @param {import("./node.js").Doc} doc
|
|
45
|
+
* @returns {number}
|
|
46
|
+
*/
|
|
47
|
+
export function maxNodeId(doc) {
|
|
48
|
+
let max = doc.id;
|
|
49
|
+
for (const container of doc.content) {
|
|
50
|
+
if (container.id > max) {
|
|
51
|
+
max = container.id;
|
|
52
|
+
}
|
|
53
|
+
for (const paragraph of container.content) {
|
|
54
|
+
if (paragraph.id > max) {
|
|
55
|
+
max = paragraph.id;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const decoration of doc.decorations) {
|
|
60
|
+
if (decoration.id > max) {
|
|
61
|
+
max = decoration.id;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const image of doc.images) {
|
|
65
|
+
if (image.id > max) {
|
|
66
|
+
max = image.id;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return max;
|
|
70
|
+
}
|
package/dist/model/normalize.js
CHANGED
|
@@ -1,146 +1,146 @@
|
|
|
1
|
-
import { devAssert } from "../dev.js";
|
|
2
|
-
import { canvasOf, createContainer, createDoc, createRun, marksEqual, rebuildParagraph } from "./node.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Canonicalises a run list:
|
|
6
|
-
* 1. drop empty runs
|
|
7
|
-
* 2. merge adjacent runs that share a mark set
|
|
8
|
-
* 3. a paragraph with no text keeps exactly one empty run, so the renderer has
|
|
9
|
-
* somewhere to put the caret
|
|
10
|
-
*
|
|
11
|
-
* None of these rules move a character, so **no offset changes** — that is what
|
|
12
|
-
* lets positions survive normalisation without being mapped (ADR-0002).
|
|
13
|
-
*
|
|
14
|
-
* @param {import("./node.js").Run[]} runs
|
|
15
|
-
* @returns {import("./node.js").Run[]}
|
|
16
|
-
*/
|
|
17
|
-
function normalizeRuns(runs) {
|
|
18
|
-
/** @type {import("./node.js").Run[]} */
|
|
19
|
-
const out = [];
|
|
20
|
-
|
|
21
|
-
for (const run of runs) {
|
|
22
|
-
if (run.text === "") {
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
const last = out[out.length - 1];
|
|
26
|
-
if (last !== undefined && marksEqual(last.marks, run.marks)) {
|
|
27
|
-
out[out.length - 1] = createRun(last.text + run.text, last.marks);
|
|
28
|
-
} else {
|
|
29
|
-
out.push(run);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (out.length === 0) {
|
|
34
|
-
// Keep the first original run rather than a fresh one so an empty paragraph
|
|
35
|
-
// retains its marks — a paragraph split after bold text stays bold.
|
|
36
|
-
out.push(runs.length > 0 ? runs[0] : createRun(""));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return out;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Returns the same paragraph object when it is already normalised. Preserving
|
|
44
|
-
* identity matters: the renderer skips paragraphs that compare `===`.
|
|
45
|
-
* @param {import("./node.js").Paragraph} para
|
|
46
|
-
* @returns {import("./node.js").Paragraph}
|
|
47
|
-
*/
|
|
48
|
-
export function normalizeParagraph(para) {
|
|
49
|
-
const runs = normalizeRuns(para.content);
|
|
50
|
-
|
|
51
|
-
if (runs.length === para.content.length) {
|
|
52
|
-
let identical = true;
|
|
53
|
-
for (let i = 0; i < runs.length; i += 1) {
|
|
54
|
-
if (runs[i] !== para.content[i]) {
|
|
55
|
-
identical = false;
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
if (identical) {
|
|
60
|
-
return para;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return rebuildParagraph(para, runs);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Returns the same container object when every paragraph in it is already
|
|
69
|
-
* normalised — the identity preservation that lets the renderer skip whole
|
|
70
|
-
* boxes with `===`.
|
|
71
|
-
* @param {import("./node.js").Container} container
|
|
72
|
-
* @param {Set<import("./node.js").NodeId>} [dirtyIds]
|
|
73
|
-
* @returns {import("./node.js").Container}
|
|
74
|
-
*/
|
|
75
|
-
export function normalizeContainer(container, dirtyIds) {
|
|
76
|
-
devAssert(
|
|
77
|
-
container.content.length > 0,
|
|
78
|
-
"container must always hold at least one paragraph",
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
/** @type {import("./node.js").Paragraph[] | null} */
|
|
82
|
-
let paragraphs = null;
|
|
83
|
-
|
|
84
|
-
for (let i = 0; i < container.content.length; i += 1) {
|
|
85
|
-
const para = container.content[i];
|
|
86
|
-
if (dirtyIds !== undefined && !dirtyIds.has(para.id)) {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
const next = normalizeParagraph(para);
|
|
90
|
-
if (next !== para) {
|
|
91
|
-
if (paragraphs === null) {
|
|
92
|
-
paragraphs = container.content.slice();
|
|
93
|
-
}
|
|
94
|
-
paragraphs[i] = next;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return paragraphs === null ? container : createContainer(container.id, paragraphs);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Normalises the paragraphs a transaction touched, walking containers then
|
|
103
|
-
* paragraphs. Pass `dirtyIds` to skip the rest — untouched nodes keep their
|
|
104
|
-
* identity either way, but skipping saves the scan on large documents.
|
|
105
|
-
*
|
|
106
|
-
* Two emptiness rules, both **bugs rather than states to repair**: a document
|
|
107
|
-
* with zero containers, and a container with zero paragraphs. No step in the set
|
|
108
|
-
* can produce either (`DeleteStep` cannot remove a paragraph,
|
|
109
|
-
* `MergeParagraphsStep` requires a following sibling *inside the same
|
|
110
|
-
* container*, and `RemoveContainerStep` is the only way to drop a container at
|
|
111
|
-
* all). Repairing them here would mean minting a node id inside normalisation,
|
|
112
|
-
* which would break `invert(apply(step)) === identity`. So they are asserted.
|
|
113
|
-
*
|
|
114
|
-
* Note this permits a document to reach zero containers via `RemoveContainer` —
|
|
115
|
-
* the assertion catches it, and it is the command layer's job not to remove the
|
|
116
|
-
* last one.
|
|
117
|
-
*
|
|
118
|
-
* @param {import("./node.js").Doc} doc
|
|
119
|
-
* @param {Set<import("./node.js").NodeId>} [dirtyIds]
|
|
120
|
-
* @returns {import("./node.js").Doc}
|
|
121
|
-
*/
|
|
122
|
-
export function normalizeDoc(doc, dirtyIds) {
|
|
123
|
-
devAssert(doc.content.length > 0, "document must always hold at least one container");
|
|
124
|
-
|
|
125
|
-
/** @type {import("./node.js").Container[] | null} */
|
|
126
|
-
let content = null;
|
|
127
|
-
|
|
128
|
-
for (let i = 0; i < doc.content.length; i += 1) {
|
|
129
|
-
const container = doc.content[i];
|
|
130
|
-
const next = normalizeContainer(container, dirtyIds);
|
|
131
|
-
if (next !== container) {
|
|
132
|
-
if (content === null) {
|
|
133
|
-
content = doc.content.slice();
|
|
134
|
-
}
|
|
135
|
-
content[i] = next;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (content === null) {
|
|
140
|
-
return doc;
|
|
141
|
-
}
|
|
142
|
-
// canvasOf, never a hand-listed set: a branch missing here would be silently
|
|
143
|
-
// reset to its empty default by createDoc (that is exactly how Doc.images was
|
|
144
|
-
// lost on every edit — see canvasOf's own note).
|
|
145
|
-
return createDoc(doc.id, content, doc.layout, canvasOf(doc));
|
|
146
|
-
}
|
|
1
|
+
import { devAssert } from "../dev.js";
|
|
2
|
+
import { canvasOf, createContainer, createDoc, createRun, marksEqual, rebuildParagraph } from "./node.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Canonicalises a run list:
|
|
6
|
+
* 1. drop empty runs
|
|
7
|
+
* 2. merge adjacent runs that share a mark set
|
|
8
|
+
* 3. a paragraph with no text keeps exactly one empty run, so the renderer has
|
|
9
|
+
* somewhere to put the caret
|
|
10
|
+
*
|
|
11
|
+
* None of these rules move a character, so **no offset changes** — that is what
|
|
12
|
+
* lets positions survive normalisation without being mapped (ADR-0002).
|
|
13
|
+
*
|
|
14
|
+
* @param {import("./node.js").Run[]} runs
|
|
15
|
+
* @returns {import("./node.js").Run[]}
|
|
16
|
+
*/
|
|
17
|
+
function normalizeRuns(runs) {
|
|
18
|
+
/** @type {import("./node.js").Run[]} */
|
|
19
|
+
const out = [];
|
|
20
|
+
|
|
21
|
+
for (const run of runs) {
|
|
22
|
+
if (run.text === "") {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const last = out[out.length - 1];
|
|
26
|
+
if (last !== undefined && marksEqual(last.marks, run.marks)) {
|
|
27
|
+
out[out.length - 1] = createRun(last.text + run.text, last.marks);
|
|
28
|
+
} else {
|
|
29
|
+
out.push(run);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (out.length === 0) {
|
|
34
|
+
// Keep the first original run rather than a fresh one so an empty paragraph
|
|
35
|
+
// retains its marks — a paragraph split after bold text stays bold.
|
|
36
|
+
out.push(runs.length > 0 ? runs[0] : createRun(""));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns the same paragraph object when it is already normalised. Preserving
|
|
44
|
+
* identity matters: the renderer skips paragraphs that compare `===`.
|
|
45
|
+
* @param {import("./node.js").Paragraph} para
|
|
46
|
+
* @returns {import("./node.js").Paragraph}
|
|
47
|
+
*/
|
|
48
|
+
export function normalizeParagraph(para) {
|
|
49
|
+
const runs = normalizeRuns(para.content);
|
|
50
|
+
|
|
51
|
+
if (runs.length === para.content.length) {
|
|
52
|
+
let identical = true;
|
|
53
|
+
for (let i = 0; i < runs.length; i += 1) {
|
|
54
|
+
if (runs[i] !== para.content[i]) {
|
|
55
|
+
identical = false;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (identical) {
|
|
60
|
+
return para;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return rebuildParagraph(para, runs);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the same container object when every paragraph in it is already
|
|
69
|
+
* normalised — the identity preservation that lets the renderer skip whole
|
|
70
|
+
* boxes with `===`.
|
|
71
|
+
* @param {import("./node.js").Container} container
|
|
72
|
+
* @param {Set<import("./node.js").NodeId>} [dirtyIds]
|
|
73
|
+
* @returns {import("./node.js").Container}
|
|
74
|
+
*/
|
|
75
|
+
export function normalizeContainer(container, dirtyIds) {
|
|
76
|
+
devAssert(
|
|
77
|
+
container.content.length > 0,
|
|
78
|
+
"container must always hold at least one paragraph",
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
/** @type {import("./node.js").Paragraph[] | null} */
|
|
82
|
+
let paragraphs = null;
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < container.content.length; i += 1) {
|
|
85
|
+
const para = container.content[i];
|
|
86
|
+
if (dirtyIds !== undefined && !dirtyIds.has(para.id)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const next = normalizeParagraph(para);
|
|
90
|
+
if (next !== para) {
|
|
91
|
+
if (paragraphs === null) {
|
|
92
|
+
paragraphs = container.content.slice();
|
|
93
|
+
}
|
|
94
|
+
paragraphs[i] = next;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return paragraphs === null ? container : createContainer(container.id, paragraphs);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Normalises the paragraphs a transaction touched, walking containers then
|
|
103
|
+
* paragraphs. Pass `dirtyIds` to skip the rest — untouched nodes keep their
|
|
104
|
+
* identity either way, but skipping saves the scan on large documents.
|
|
105
|
+
*
|
|
106
|
+
* Two emptiness rules, both **bugs rather than states to repair**: a document
|
|
107
|
+
* with zero containers, and a container with zero paragraphs. No step in the set
|
|
108
|
+
* can produce either (`DeleteStep` cannot remove a paragraph,
|
|
109
|
+
* `MergeParagraphsStep` requires a following sibling *inside the same
|
|
110
|
+
* container*, and `RemoveContainerStep` is the only way to drop a container at
|
|
111
|
+
* all). Repairing them here would mean minting a node id inside normalisation,
|
|
112
|
+
* which would break `invert(apply(step)) === identity`. So they are asserted.
|
|
113
|
+
*
|
|
114
|
+
* Note this permits a document to reach zero containers via `RemoveContainer` —
|
|
115
|
+
* the assertion catches it, and it is the command layer's job not to remove the
|
|
116
|
+
* last one.
|
|
117
|
+
*
|
|
118
|
+
* @param {import("./node.js").Doc} doc
|
|
119
|
+
* @param {Set<import("./node.js").NodeId>} [dirtyIds]
|
|
120
|
+
* @returns {import("./node.js").Doc}
|
|
121
|
+
*/
|
|
122
|
+
export function normalizeDoc(doc, dirtyIds) {
|
|
123
|
+
devAssert(doc.content.length > 0, "document must always hold at least one container");
|
|
124
|
+
|
|
125
|
+
/** @type {import("./node.js").Container[] | null} */
|
|
126
|
+
let content = null;
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < doc.content.length; i += 1) {
|
|
129
|
+
const container = doc.content[i];
|
|
130
|
+
const next = normalizeContainer(container, dirtyIds);
|
|
131
|
+
if (next !== container) {
|
|
132
|
+
if (content === null) {
|
|
133
|
+
content = doc.content.slice();
|
|
134
|
+
}
|
|
135
|
+
content[i] = next;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (content === null) {
|
|
140
|
+
return doc;
|
|
141
|
+
}
|
|
142
|
+
// canvasOf, never a hand-listed set: a branch missing here would be silently
|
|
143
|
+
// reset to its empty default by createDoc (that is exactly how Doc.images was
|
|
144
|
+
// lost on every edit — see canvasOf's own note).
|
|
145
|
+
return createDoc(doc.id, content, doc.layout, canvasOf(doc));
|
|
146
|
+
}
|