@ansonlai/docx-redline-js 0.8.4 → 0.10.0
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/ARCHITECTURE.md +679 -460
- package/CHANGELOG.md +208 -1
- package/README.md +152 -16
- package/core/current-properties.js +136 -0
- package/core/paragraph-text.js +211 -209
- package/core/types.js +9 -3
- package/core/word-revision-semantics.js +256 -0
- package/core/word-xml.js +160 -118
- package/dist/docx-redline-js.esm.js +4338 -796
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +111 -108
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +4463 -921
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +4463 -921
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_FAST_START.md +2 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +49 -7
- package/docs/SKILL_AUTHORING.md +169 -150
- package/docs/TESTING.md +1146 -1059
- package/docs/schemas/document-operations.schema.json +55 -2
- package/document/docx-document.js +1022 -1010
- package/engine/oxml-engine.js +21 -22
- package/engine/paragraph-mark-revisions.js +292 -0
- package/engine/reconstruction-mapper.js +312 -311
- package/engine/reconstruction-writer.js +67 -10
- package/engine/rpr-helpers.js +42 -0
- package/engine/run-builders.js +0 -73
- package/engine/surgical-diff-application.js +604 -605
- package/engine/table-mode.js +63 -63
- package/index.d.ts +53 -2
- package/index.js +13 -2
- package/node/cli-help.js +1 -0
- package/node/cli.js +21 -3
- package/orchestration/list-structural-fallback.js +67 -9
- package/package.json +1 -1
- package/pipeline/diff-engine.js +130 -1
- package/pipeline/ingestion-export.js +227 -243
- package/pipeline/ingestion-paragraph.js +93 -98
- package/pipeline/list-generation.js +415 -419
- package/pipeline/markdown-processor.js +5 -2
- package/pipeline/pipeline.js +206 -204
- package/scripts/export-validation-fixtures.mjs +19 -6
- package/scripts/generate-word-revision-semantics-matrix.mjs +134 -0
- package/scripts/lib/word-coverage-catalogue.mjs +7 -2
- package/scripts/render-multilevel-bullet-visual.ps1 +2 -2
- package/scripts/word-com-corpus-visual-suite.ps1 +2 -2
- package/scripts/word-com-differential.ps1 +443 -335
- package/scripts/word-com-visual-suite.ps1 +2 -2
- package/scripts/word-revision-semantics-matrix.ps1 +108 -0
- package/services/batch-operation-orchestrator.js +23 -3
- package/services/capture-engine.js +228 -226
- package/services/comment-replies.js +1 -1
- package/services/document-operation-applier.js +214 -0
- package/services/document-operation-contract.js +76 -0
- package/services/document-operation-mutations.js +688 -51
- package/services/error-recovery.js +17 -0
- package/services/inspection-comparison.js +82 -0
- package/services/list-conversion-diagnostics.js +296 -0
- package/services/list-format-change.js +586 -0
- package/services/list-format-mutation.js +416 -0
- package/services/list-format.js +305 -0
- package/services/numbering-allocation.js +642 -0
- package/services/numbering-helpers.js +354 -524
- package/services/operation-batch-compiler.js +183 -11
- package/services/operation-preflight.js +43 -33
- package/services/receipt-collector.js +288 -288
- package/services/revision-comment-management.js +38 -5
- package/services/revision-fidelity-verifier.js +484 -0
- package/services/standalone-docx-plumbing.js +627 -631
- package/services/standalone-operation-runner.d.ts +182 -5
- package/services/table-reconciliation.js +59 -7
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single accessor for CURRENT paragraph/run properties.
|
|
3
|
+
*
|
|
4
|
+
* Descendant lookups (`getElementsByTagNameNS`, `getWordDescendants`, ...) cross `w:pPrChange` and
|
|
5
|
+
* `w:rPrChange`, so a historical `numPr`/`pStyle`/`b` snapshot looks like a current value. Everything in
|
|
6
|
+
* this module reads direct children of `w:pPr` / `w:rPr` only. The `*PrChange` snapshot is reachable
|
|
7
|
+
* only through the explicit `getHistorical*` functions (reject simulation, restore).
|
|
8
|
+
*
|
|
9
|
+
* Depends only on `core/types.js`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { NS_W } from './types.js';
|
|
13
|
+
|
|
14
|
+
function isWordElement(node, localName) {
|
|
15
|
+
if (!node || node.nodeType !== 1) return false;
|
|
16
|
+
const name = String(node.localName || node.nodeName || '').replace(/^.*:/, '');
|
|
17
|
+
if (localName != null && name !== localName) return false;
|
|
18
|
+
return !node.namespaceURI || node.namespaceURI === NS_W;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** First direct child element with the given WordprocessingML local name. */
|
|
22
|
+
export function getDirectWordChild(node, localName) {
|
|
23
|
+
if (!node) return null;
|
|
24
|
+
for (const child of Array.from(node.childNodes || [])) {
|
|
25
|
+
if (isWordElement(child, localName)) return child;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Direct child elements with the given local name (all direct Word elements when omitted). */
|
|
31
|
+
export function getDirectWordChildren(node, localName) {
|
|
32
|
+
if (!node) return [];
|
|
33
|
+
return Array.from(node.childNodes || []).filter(child => isWordElement(child, localName));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Direct child of a properties element; never descends into `*PrChange` snapshots. */
|
|
37
|
+
export function getCurrentChild(propsEl, localName) {
|
|
38
|
+
return getDirectWordChild(propsEl, localName);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readVal(el) {
|
|
42
|
+
if (!el) return null;
|
|
43
|
+
const value = (typeof el.getAttributeNS === 'function' && el.getAttributeNS(NS_W, 'val'))
|
|
44
|
+
|| el.getAttribute?.('w:val')
|
|
45
|
+
|| el.getAttribute?.('val')
|
|
46
|
+
|| null;
|
|
47
|
+
return value === '' ? null : value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readNumPr(propsEl) {
|
|
51
|
+
const numPr = getDirectWordChild(propsEl, 'numPr');
|
|
52
|
+
if (!numPr) return null;
|
|
53
|
+
return {
|
|
54
|
+
element: numPr,
|
|
55
|
+
numId: readVal(getDirectWordChild(numPr, 'numId')),
|
|
56
|
+
ilvl: readVal(getDirectWordChild(numPr, 'ilvl')),
|
|
57
|
+
numIdElement: getDirectWordChild(numPr, 'numId'),
|
|
58
|
+
ilvlElement: getDirectWordChild(numPr, 'ilvl')
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readParagraphProps(pPr) {
|
|
63
|
+
const pStyleEl = getDirectWordChild(pPr, 'pStyle');
|
|
64
|
+
const outlineEl = getDirectWordChild(pPr, 'outlineLvl');
|
|
65
|
+
return {
|
|
66
|
+
pPr: pPr || null,
|
|
67
|
+
numPr: readNumPr(pPr),
|
|
68
|
+
pStyle: readVal(pStyleEl),
|
|
69
|
+
pStyleElement: pStyleEl,
|
|
70
|
+
outlineLvl: readVal(outlineEl),
|
|
71
|
+
outlineLvlElement: outlineEl
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Current paragraph properties: the paragraph's direct `w:pPr` only. */
|
|
76
|
+
export function getCurrentParagraphProperties(p) {
|
|
77
|
+
return readParagraphProps(getDirectWordChild(p, 'pPr'));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Current run properties: the run's direct `w:rPr` (null when absent). */
|
|
81
|
+
export function getCurrentRunProperties(r) {
|
|
82
|
+
return getDirectWordChild(r, 'rPr');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Paragraph-mark run properties (`w:pPr/w:rPr`), distinct from any run's `rPr`. */
|
|
86
|
+
export function getParagraphMarkProperties(p) {
|
|
87
|
+
return getDirectWordChild(getDirectWordChild(p, 'pPr'), 'rPr');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Historical paragraph properties: the `w:pPrChange/w:pPr` snapshot. Explicit opt-in; shape matches
|
|
92
|
+
* `getCurrentParagraphProperties`. Returns null when the paragraph has no tracked property change.
|
|
93
|
+
*/
|
|
94
|
+
export function getHistoricalParagraphProperties(p) {
|
|
95
|
+
const change = getDirectWordChild(getDirectWordChild(p, 'pPr'), 'pPrChange');
|
|
96
|
+
const snapshot = getDirectWordChild(change, 'pPr');
|
|
97
|
+
return snapshot ? readParagraphProps(snapshot) : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Historical run properties: the `w:rPrChange/w:rPr` snapshot, or null. */
|
|
101
|
+
export function getHistoricalRunProperties(r) {
|
|
102
|
+
const change = getDirectWordChild(getDirectWordChild(r, 'rPr'), 'rPrChange');
|
|
103
|
+
return getDirectWordChild(change, 'rPr');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** All `w:p` elements at or under `root` (the paragraph itself when `root` is one). */
|
|
107
|
+
function paragraphsUnder(root) {
|
|
108
|
+
if (!root || root.nodeType !== 1) return [];
|
|
109
|
+
const out = isWordElement(root, 'p') ? [root] : [];
|
|
110
|
+
if (typeof root.getElementsByTagNameNS === 'function') {
|
|
111
|
+
out.push(...Array.from(root.getElementsByTagNameNS(NS_W, 'p')));
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Current `w:numId` elements (direct `pPr/numPr/numId`) of every paragraph at or under `root`.
|
|
118
|
+
* Excludes `pPrChange` snapshots, so Reject still restores the snapshot numId.
|
|
119
|
+
*/
|
|
120
|
+
export function getCurrentNumIdElements(root) {
|
|
121
|
+
const out = [];
|
|
122
|
+
for (const p of paragraphsUnder(root)) {
|
|
123
|
+
const el = getCurrentParagraphProperties(p).numPr?.numIdElement;
|
|
124
|
+
if (el) out.push(el);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Every `w:numId` element referenced anywhere under `root`, INCLUDING `pPrChange` snapshots. Explicitly
|
|
131
|
+
* historical-inclusive: used when a numbering part must define every id the content can ever reference.
|
|
132
|
+
*/
|
|
133
|
+
export function getAllReferencedNumIdElements(root) {
|
|
134
|
+
if (!root || typeof root.getElementsByTagNameNS !== 'function') return [];
|
|
135
|
+
return Array.from(root.getElementsByTagNameNS(NS_W, 'numId'));
|
|
136
|
+
}
|
package/core/paragraph-text.js
CHANGED
|
@@ -1,209 +1,211 @@
|
|
|
1
|
-
import { NS_W } from './types.js';
|
|
2
|
-
|
|
3
|
-
function localName(node) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
else if (name === '
|
|
35
|
-
else if (name === '
|
|
36
|
-
else
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* @param {
|
|
55
|
-
* @
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
let
|
|
105
|
-
let
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
text = child.textContent || '';
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
} else if (name === '
|
|
117
|
-
text = '\
|
|
118
|
-
} else if (name === '
|
|
119
|
-
text = '\
|
|
120
|
-
} else if (name === '
|
|
121
|
-
text = '\
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
1
|
+
import { NS_W } from './types.js';
|
|
2
|
+
|
|
3
|
+
function localName(node) {
|
|
4
|
+
const local = node?.localName;
|
|
5
|
+
if (local) return local.indexOf(':') < 0 ? local : local.replace(/^.*:/, '');
|
|
6
|
+
return String(node?.nodeName || '').replace(/^.*:/, '');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Returns whether a node contributes to the selected revision view. */
|
|
10
|
+
export function isNodeVisibleInRevisionView(node, boundary = null, revisionView = 'accepted') {
|
|
11
|
+
const view = revisionView === 'current' ? 'accepted' : revisionView;
|
|
12
|
+
let cursor = node;
|
|
13
|
+
while (cursor && cursor !== boundary) {
|
|
14
|
+
const name = localName(cursor);
|
|
15
|
+
if (view === 'accepted' && (name === 'del' || name === 'moveFrom')) return false;
|
|
16
|
+
if (view === 'rejected' && (name === 'ins' || name === 'moveTo')) return false;
|
|
17
|
+
cursor = cursor.parentNode;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Reads exact visible text from a run, including tabs, breaks and non-breaking hyphens. */
|
|
23
|
+
export function readCanonicalRunText(run, options = {}) {
|
|
24
|
+
const revisionView = options.revisionView === 'current' ? 'accepted' : (options.revisionView || 'accepted');
|
|
25
|
+
const boundary = options.boundary || null;
|
|
26
|
+
if (!isNodeVisibleInRevisionView(run, boundary, revisionView)) return '';
|
|
27
|
+
let text = '';
|
|
28
|
+
const visit = node => {
|
|
29
|
+
for (let child = node?.firstChild; child; child = child.nextSibling) {
|
|
30
|
+
if (child?.nodeType !== 1 || (child.namespaceURI && child.namespaceURI !== NS_W)) continue;
|
|
31
|
+
if (!isNodeVisibleInRevisionView(child, boundary, revisionView)) continue;
|
|
32
|
+
const name = localName(child);
|
|
33
|
+
if (name === 't' || (name === 'delText' && revisionView === 'rejected')) text += child.textContent || '';
|
|
34
|
+
else if (name === 'tab') text += '\t';
|
|
35
|
+
else if (name === 'br' || name === 'cr') text += '\n';
|
|
36
|
+
else if (name === 'noBreakHyphen') text += '\u2011';
|
|
37
|
+
else if (name === 'softHyphen') text += '\u00ad';
|
|
38
|
+
else visit(child);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
visit(run);
|
|
42
|
+
return text;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const attr = (node, name) => node?.getAttribute?.(`w:${name}`) || node?.getAttribute?.(name) || '';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extracts revision-view text segments from a paragraph or container node.
|
|
49
|
+
*
|
|
50
|
+
* Emits structural text pieces tracking revision ancestry (ins, del, moveFrom, moveTo),
|
|
51
|
+
* computes independent accepted and rejected UTF-16 cursor offsets, and merges
|
|
52
|
+
* adjacent pieces with compatible carrier containers and revision metadata.
|
|
53
|
+
*
|
|
54
|
+
* @param {Element | null | undefined} paragraph - Paragraph or container element
|
|
55
|
+
* @param {Object} [options={}] - Extraction options
|
|
56
|
+
* @param {boolean} [options.mergeRuns=true] - Merge adjacent runs sharing compatible container and revision metadata
|
|
57
|
+
* @returns {Array<{ text: string, kind: 'baseline' | 'insertion' | 'deletion' | 'move_from' | 'move_to', author?: string, revisionId?: string, acceptedStart: number | null, rejectedStart: number | null }>}
|
|
58
|
+
*/
|
|
59
|
+
export function extractParagraphRevisionSegments(paragraph, options = {}) {
|
|
60
|
+
if (!paragraph) return [];
|
|
61
|
+
|
|
62
|
+
const rawPieces = [];
|
|
63
|
+
|
|
64
|
+
function walk(node, currentRevision, currentCarrier) {
|
|
65
|
+
for (const child of Array.from(node?.childNodes || [])) {
|
|
66
|
+
if (child?.nodeType !== 1) continue;
|
|
67
|
+
if (child.namespaceURI && child.namespaceURI !== NS_W) continue;
|
|
68
|
+
|
|
69
|
+
const name = localName(child);
|
|
70
|
+
if (name === 'pPr' || name === 'rPr') continue;
|
|
71
|
+
|
|
72
|
+
let nextRevision = currentRevision;
|
|
73
|
+
if (name === 'ins') {
|
|
74
|
+
nextRevision = {
|
|
75
|
+
kind: 'insertion',
|
|
76
|
+
author: attr(child, 'author') || undefined,
|
|
77
|
+
revisionId: attr(child, 'id') || undefined
|
|
78
|
+
};
|
|
79
|
+
} else if (name === 'del') {
|
|
80
|
+
nextRevision = {
|
|
81
|
+
kind: 'deletion',
|
|
82
|
+
author: attr(child, 'author') || undefined,
|
|
83
|
+
revisionId: attr(child, 'id') || undefined
|
|
84
|
+
};
|
|
85
|
+
} else if (name === 'moveFrom') {
|
|
86
|
+
nextRevision = {
|
|
87
|
+
kind: 'move_from',
|
|
88
|
+
author: attr(child, 'author') || undefined,
|
|
89
|
+
revisionId: attr(child, 'id') || undefined
|
|
90
|
+
};
|
|
91
|
+
} else if (name === 'moveTo') {
|
|
92
|
+
nextRevision = {
|
|
93
|
+
kind: 'move_to',
|
|
94
|
+
author: attr(child, 'author') || undefined,
|
|
95
|
+
revisionId: attr(child, 'id') || undefined
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let nextCarrier = currentCarrier;
|
|
100
|
+
if (name === 'r') {
|
|
101
|
+
nextCarrier = child;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let text = null;
|
|
105
|
+
let kind = nextRevision ? nextRevision.kind : 'baseline';
|
|
106
|
+
let author = nextRevision?.author;
|
|
107
|
+
let revisionId = nextRevision?.revisionId;
|
|
108
|
+
|
|
109
|
+
if (name === 't') {
|
|
110
|
+
text = child.textContent || '';
|
|
111
|
+
} else if (name === 'delText') {
|
|
112
|
+
text = child.textContent || '';
|
|
113
|
+
if (!nextRevision) {
|
|
114
|
+
kind = 'deletion';
|
|
115
|
+
}
|
|
116
|
+
} else if (name === 'tab') {
|
|
117
|
+
text = '\t';
|
|
118
|
+
} else if (name === 'br' || name === 'cr') {
|
|
119
|
+
text = '\n';
|
|
120
|
+
} else if (name === 'noBreakHyphen') {
|
|
121
|
+
text = '\u2011';
|
|
122
|
+
} else if (name === 'softHyphen') {
|
|
123
|
+
text = '\u00ad';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (text !== null) {
|
|
127
|
+
if (text.length > 0) {
|
|
128
|
+
rawPieces.push({
|
|
129
|
+
text,
|
|
130
|
+
kind,
|
|
131
|
+
author,
|
|
132
|
+
revisionId,
|
|
133
|
+
carrier: nextCarrier || child,
|
|
134
|
+
carrierContainer: (nextCarrier || child)?.parentNode || null
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
walk(child, nextRevision, nextCarrier);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const initialCarrier = localName(paragraph) === 'r' ? paragraph : null;
|
|
144
|
+
walk(paragraph, null, initialCarrier);
|
|
145
|
+
|
|
146
|
+
if (rawPieces.length === 0) return [];
|
|
147
|
+
|
|
148
|
+
const mergeRuns = options.mergeRuns !== false;
|
|
149
|
+
const merged = [];
|
|
150
|
+
let current = null;
|
|
151
|
+
|
|
152
|
+
for (const piece of rawPieces) {
|
|
153
|
+
if (!current) {
|
|
154
|
+
current = { ...piece };
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const sameKind = current.kind === piece.kind;
|
|
159
|
+
const sameAuthor = current.author === piece.author;
|
|
160
|
+
const sameRevisionId = current.revisionId === piece.revisionId;
|
|
161
|
+
const sameCarrier = current.carrier === piece.carrier;
|
|
162
|
+
const compatibleContainer = mergeRuns && current.carrierContainer === piece.carrierContainer;
|
|
163
|
+
|
|
164
|
+
if (sameKind && sameAuthor && sameRevisionId && (sameCarrier || compatibleContainer)) {
|
|
165
|
+
current.text += piece.text;
|
|
166
|
+
} else {
|
|
167
|
+
merged.push(current);
|
|
168
|
+
current = { ...piece };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (current) merged.push(current);
|
|
172
|
+
|
|
173
|
+
let acceptedCursor = 0;
|
|
174
|
+
let rejectedCursor = 0;
|
|
175
|
+
const segments = [];
|
|
176
|
+
|
|
177
|
+
for (const item of merged) {
|
|
178
|
+
const isAccepted = item.kind !== 'deletion' && item.kind !== 'move_from';
|
|
179
|
+
const isRejected = item.kind !== 'insertion' && item.kind !== 'move_to';
|
|
180
|
+
|
|
181
|
+
const acceptedStart = isAccepted ? acceptedCursor : null;
|
|
182
|
+
const rejectedStart = isRejected ? rejectedCursor : null;
|
|
183
|
+
|
|
184
|
+
if (isAccepted) acceptedCursor += item.text.length;
|
|
185
|
+
if (isRejected) rejectedCursor += item.text.length;
|
|
186
|
+
|
|
187
|
+
const segment = {
|
|
188
|
+
text: item.text,
|
|
189
|
+
kind: item.kind,
|
|
190
|
+
acceptedStart,
|
|
191
|
+
rejectedStart
|
|
192
|
+
};
|
|
193
|
+
if (item.author !== undefined) segment.author = item.author;
|
|
194
|
+
if (item.revisionId !== undefined) segment.revisionId = item.revisionId;
|
|
195
|
+
|
|
196
|
+
segments.push(segment);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return segments;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Canonical exact paragraph text used by inspection, targeting and ingestion. */
|
|
203
|
+
export function extractCanonicalParagraphText(paragraph, options = {}) {
|
|
204
|
+
if (!paragraph) return '';
|
|
205
|
+
const view = options.revisionView === 'current' ? 'accepted' : (options.revisionView || 'accepted');
|
|
206
|
+
const segments = extractParagraphRevisionSegments(paragraph, options);
|
|
207
|
+
if (view === 'rejected') {
|
|
208
|
+
return segments.filter(s => s.rejectedStart !== null).map(s => s.text).join('');
|
|
209
|
+
}
|
|
210
|
+
return segments.filter(s => s.acceptedStart !== null).map(s => s.text).join('');
|
|
211
|
+
}
|
package/core/types.js
CHANGED
|
@@ -329,9 +329,10 @@ export function createRevisionMetadata(author, allocatorOrNode = null, kind = nu
|
|
|
329
329
|
*
|
|
330
330
|
* @param {string} [author]
|
|
331
331
|
* @param {RevisionIdAllocator|Document|Element|null} [allocatorOrNode=null]
|
|
332
|
+
* @param {{ deferReceipt?: boolean }} [options] - Defer receipt recording to `event.recordUsed(kind)`.
|
|
332
333
|
* @returns {ReplacementRevisionEvent}
|
|
333
334
|
*/
|
|
334
|
-
export function createReplacementRevisionEvent(author, allocatorOrNode = null) {
|
|
335
|
+
export function createReplacementRevisionEvent(author, allocatorOrNode = null, { deferReceipt = false } = {}) {
|
|
335
336
|
const resolvedAuthor = typeof author === 'string' && author.trim()
|
|
336
337
|
? author.trim()
|
|
337
338
|
: getDefaultAuthor();
|
|
@@ -342,7 +343,7 @@ export function createReplacementRevisionEvent(author, allocatorOrNode = null) {
|
|
|
342
343
|
const deletionId = allocator.next();
|
|
343
344
|
const insertionId = allocator.next();
|
|
344
345
|
|
|
345
|
-
if (allocator._receiptCollector) {
|
|
346
|
+
if (allocator._receiptCollector && !deferReceipt) {
|
|
346
347
|
allocator._receiptCollector.recordRevision(deletionId, 'del');
|
|
347
348
|
allocator._receiptCollector.recordRevision(insertionId, 'ins');
|
|
348
349
|
}
|
|
@@ -351,7 +352,12 @@ export function createReplacementRevisionEvent(author, allocatorOrNode = null) {
|
|
|
351
352
|
deletionId,
|
|
352
353
|
insertionId,
|
|
353
354
|
author: resolvedAuthor,
|
|
354
|
-
date
|
|
355
|
+
date,
|
|
356
|
+
// With deferReceipt the caller records an id only once it is actually written to the document, so an id
|
|
357
|
+
// the diff never emits (a deletion of paragraph breaks only has no run to carry it) never reaches a receipt.
|
|
358
|
+
recordUsed: deferReceipt
|
|
359
|
+
? kind => allocator._receiptCollector?.recordRevision(kind === 'del' ? deletionId : insertionId, kind)
|
|
360
|
+
: null
|
|
355
361
|
};
|
|
356
362
|
}
|
|
357
363
|
|