@ansonlai/docx-redline-js 0.8.4 → 0.9.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 +221 -12
- package/CHANGELOG.md +176 -0
- package/README.md +125 -12
- 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/dist/docx-redline-js.esm.js +4189 -767
- 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 +4292 -870
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +4292 -870
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_FAST_START.md +2 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +19 -3
- package/docs/SKILL_AUTHORING.md +165 -150
- package/docs/TESTING.md +1142 -1059
- package/docs/schemas/document-operations.schema.json +54 -1
- package/document/docx-document.js +1022 -1010
- package/engine/oxml-engine.js +736 -749
- 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 +47 -1
- package/index.js +11 -2
- package/node/cli-help.js +1 -0
- package/node/cli.js +992 -975
- 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/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/document-operation-applier.js +213 -0
- package/services/document-operation-contract.js +75 -0
- package/services/document-operation-mutations.js +3346 -2721
- package/services/error-recovery.js +17 -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 +1 -0
- package/services/receipt-collector.js +288 -288
- package/services/revision-comment-management.js +38 -5
- package/services/revision-fidelity-verifier.js +498 -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
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
|
|