@ansonlai/docx-redline-js 0.8.3 → 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 +803 -586
- package/README.md +1018 -901
- 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 +4551 -967
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +112 -106
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +4690 -1106
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +4690 -1106
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_FAST_START.md +2 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +23 -4
- package/docs/SKILL_AUTHORING.md +165 -150
- package/docs/TESTING.md +1142 -1059
- package/docs/schemas/document-operations.schema.json +221 -168
- package/document/docx-document.js +1022 -1010
- package/engine/oxml-engine.js +567 -560
- package/engine/paragraph-mark-revisions.js +292 -0
- package/engine/reconstruction-mapper.js +312 -311
- package/engine/reconstruction-mode.js +183 -183
- 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 +51 -1
- package/index.js +47 -3
- package/node/cli-help.js +1 -0
- package/node/cli.js +992 -974
- 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/pipeline/serialization.js +111 -94
- 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 +26 -0
- package/services/capture-engine.js +228 -226
- package/services/document-operation-applier.js +215 -0
- package/services/document-operation-contract.js +628 -537
- package/services/document-operation-mutations.js +789 -46
- 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 +612 -436
- package/services/operation-heuristics.js +197 -173
- package/services/operation-preflight.js +1 -0
- package/services/receipt-collector.js +288 -288
- package/services/revision-comment-management.js +634 -594
- package/services/revision-fidelity-verifier.js +498 -0
- package/services/standalone-docx-plumbing.js +627 -631
- package/services/standalone-operation-runner.d.ts +606 -427
- package/services/table-reconciliation.js +103 -49
|
@@ -1,243 +1,227 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Word OOXML ingestion helpers for plain-text/markdown export.
|
|
3
|
-
*
|
|
4
|
-
* These helpers are intentionally conservative and only map obvious signals:
|
|
5
|
-
* - Paragraph structure
|
|
6
|
-
* - Heading styles
|
|
7
|
-
* - List paragraph properties
|
|
8
|
-
* - Run-level bold/italic formatting
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { parseOoxmlSafe } from '../adapters/xml-adapter.js';
|
|
12
|
-
import { NS_W } from '../core/types.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (doc.documentElement
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return parsed;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
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
|
-
if (
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
if (!
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
*/
|
|
229
|
-
export function ingestWordOoxmlToMarkdownResult(ooxml) {
|
|
230
|
-
const parsed = parseWordOoxml(ooxml);
|
|
231
|
-
if (!parsed.doc) {
|
|
232
|
-
return { text: '', status: 'error', error: parsed.error, warnings: parsed.warnings };
|
|
233
|
-
}
|
|
234
|
-
const doc = parsed.doc;
|
|
235
|
-
|
|
236
|
-
const paragraphs = getWordParagraphs(doc);
|
|
237
|
-
if (paragraphs.length === 0) {
|
|
238
|
-
return { text: '', status: 'ok', warnings: parsed.warnings };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const lines = paragraphs.map(paragraphToMarkdown);
|
|
242
|
-
return { text: lines.join('\n\n').trim(), status: 'ok', warnings: parsed.warnings };
|
|
243
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Word OOXML ingestion helpers for plain-text/markdown export.
|
|
3
|
+
*
|
|
4
|
+
* These helpers are intentionally conservative and only map obvious signals:
|
|
5
|
+
* - Paragraph structure
|
|
6
|
+
* - Heading styles
|
|
7
|
+
* - List paragraph properties
|
|
8
|
+
* - Run-level bold/italic formatting
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { parseOoxmlSafe } from '../adapters/xml-adapter.js';
|
|
12
|
+
import { NS_W } from '../core/types.js';
|
|
13
|
+
import { getCurrentChild, getCurrentParagraphProperties, getCurrentRunProperties } from '../core/current-properties.js';
|
|
14
|
+
import { getXmlParseError } from '../core/xml-query.js';
|
|
15
|
+
import { isNodeVisibleInRevisionView, readCanonicalRunText } from '../core/paragraph-text.js';
|
|
16
|
+
|
|
17
|
+
function hasParserError(doc) {
|
|
18
|
+
if (!doc || !doc.documentElement) return true;
|
|
19
|
+
if (doc.documentElement.localName === 'parsererror') return true;
|
|
20
|
+
return !!getXmlParseError(doc);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseWordOoxml(ooxml) {
|
|
24
|
+
const parsed = parseOoxmlSafe(ooxml, 'application/xml');
|
|
25
|
+
if (parsed.error || hasParserError(parsed.doc)) return { ...parsed, doc: null };
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getWordParagraphs(doc) {
|
|
30
|
+
if (!doc) return [];
|
|
31
|
+
const namespaced = Array.from(doc.getElementsByTagNameNS(NS_W, 'p'));
|
|
32
|
+
if (namespaced.length > 0) return namespaced;
|
|
33
|
+
return Array.from(doc.getElementsByTagNameNS('*', 'p')).filter(node => node?.localName === 'p');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
function getWordAttribute(element, names) {
|
|
38
|
+
if (!element) return '';
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
const value = element.getAttribute(name);
|
|
41
|
+
if (value != null && value !== '') return value;
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getRunFormatting(run) {
|
|
47
|
+
// Current run properties only: a w:rPrChange snapshot must not make historical b/i/rStyle look current.
|
|
48
|
+
const rPr = getCurrentRunProperties(run);
|
|
49
|
+
if (!rPr) return { bold: false, italic: false };
|
|
50
|
+
const rStyle = getCurrentChild(rPr, 'rStyle');
|
|
51
|
+
const rStyleValue = getWordAttribute(rStyle, ['w:val', 'val']).toLowerCase();
|
|
52
|
+
const styleImpliesBold = rStyleValue.includes('strong') || rStyleValue.includes('bold');
|
|
53
|
+
const styleImpliesItalic = rStyleValue.includes('italic') || rStyleValue.includes('emphasis');
|
|
54
|
+
return {
|
|
55
|
+
bold: !!getCurrentChild(rPr, 'b') || styleImpliesBold,
|
|
56
|
+
italic: !!getCurrentChild(rPr, 'i') || styleImpliesItalic
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function collectParagraphSegments(paragraph) {
|
|
61
|
+
const segments = [];
|
|
62
|
+
const runs = Array.from(paragraph?.getElementsByTagNameNS?.(NS_W, 'r') || []);
|
|
63
|
+
for (const run of runs) {
|
|
64
|
+
if (!isNodeVisibleInRevisionView(run, paragraph, 'accepted')) continue;
|
|
65
|
+
const text = readCanonicalRunText(run, { boundary: paragraph, revisionView: 'accepted' });
|
|
66
|
+
if (!text) continue;
|
|
67
|
+
segments.push({
|
|
68
|
+
text,
|
|
69
|
+
...getRunFormatting(run)
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return segments;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeInlineWhitespace(text) {
|
|
76
|
+
const normalizedLines = String(text || '')
|
|
77
|
+
.replace(/\r/g, '')
|
|
78
|
+
.split('\n')
|
|
79
|
+
.map(line => line.replace(/[ \t]+/g, ' ').trim());
|
|
80
|
+
return normalizedLines.join('\n').trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function wrapRunMarkdown(text, format) {
|
|
84
|
+
const raw = String(text || '');
|
|
85
|
+
if (!raw) return '';
|
|
86
|
+
if (!format?.bold && !format?.italic) return raw;
|
|
87
|
+
|
|
88
|
+
const match = raw.match(/^(\s*)([\s\S]*?)(\s*)$/);
|
|
89
|
+
if (!match) return raw;
|
|
90
|
+
const leading = match[1] || '';
|
|
91
|
+
const core = match[2] || '';
|
|
92
|
+
const trailing = match[3] || '';
|
|
93
|
+
if (!core.trim()) return raw;
|
|
94
|
+
|
|
95
|
+
let wrapped = core;
|
|
96
|
+
if (format.bold && format.italic) wrapped = `***${core}***`;
|
|
97
|
+
else if (format.bold) wrapped = `**${core}**`;
|
|
98
|
+
else if (format.italic) wrapped = `*${core}*`;
|
|
99
|
+
return `${leading}${wrapped}${trailing}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function parseHeadingLevel(paragraph) {
|
|
103
|
+
const { pPr, pStyleElement, outlineLvlElement } = getCurrentParagraphProperties(paragraph);
|
|
104
|
+
if (!pPr) return null;
|
|
105
|
+
|
|
106
|
+
const styleVal = getWordAttribute(pStyleElement, ['w:val', 'val']);
|
|
107
|
+
if (styleVal) {
|
|
108
|
+
const match = styleVal.match(/^heading\s*([1-9])$/i);
|
|
109
|
+
if (match) {
|
|
110
|
+
const level = Number.parseInt(match[1], 10);
|
|
111
|
+
if (Number.isInteger(level)) return Math.min(Math.max(level, 1), 6);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const outlineRaw = getWordAttribute(outlineLvlElement, ['w:val', 'val']);
|
|
116
|
+
const outline = Number.parseInt(outlineRaw, 10);
|
|
117
|
+
if (Number.isInteger(outline) && outline >= 0) {
|
|
118
|
+
return Math.min(outline + 1, 6);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function parseListInfo(paragraph) {
|
|
125
|
+
const { numPr } = getCurrentParagraphProperties(paragraph);
|
|
126
|
+
if (!numPr) return null;
|
|
127
|
+
|
|
128
|
+
const ilvlEl = numPr.ilvlElement;
|
|
129
|
+
const numIdEl = numPr.numIdElement;
|
|
130
|
+
const ilvl = Number.parseInt(getWordAttribute(ilvlEl, ['w:val', 'val']), 10);
|
|
131
|
+
const numId = getWordAttribute(numIdEl, ['w:val', 'val']);
|
|
132
|
+
|
|
133
|
+
const safeLevel = Number.isInteger(ilvl) && ilvl >= 0 ? ilvl : 0;
|
|
134
|
+
const marker = numId === '1' ? '-' : '1.';
|
|
135
|
+
return { level: safeLevel, marker };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function paragraphToPlainText(paragraph) {
|
|
139
|
+
const text = collectParagraphSegments(paragraph).map(segment => segment.text).join('');
|
|
140
|
+
return normalizeInlineWhitespace(text);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function paragraphToMarkdown(paragraph) {
|
|
144
|
+
const segments = collectParagraphSegments(paragraph);
|
|
145
|
+
const inline = segments.map(segment => wrapRunMarkdown(segment.text, segment)).join('');
|
|
146
|
+
const normalizedInline = normalizeInlineWhitespace(inline);
|
|
147
|
+
if (!normalizedInline) return '';
|
|
148
|
+
|
|
149
|
+
const headingLevel = parseHeadingLevel(paragraph);
|
|
150
|
+
if (headingLevel != null) {
|
|
151
|
+
return `${'#'.repeat(headingLevel)} ${normalizedInline}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const list = parseListInfo(paragraph);
|
|
155
|
+
if (list) {
|
|
156
|
+
const indent = ' '.repeat(list.level);
|
|
157
|
+
return `${indent}${list.marker} ${normalizedInline}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return normalizedInline;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Ingests Word OOXML and returns readable plain text.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} ooxml
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
169
|
+
export function ingestWordOoxmlToPlainText(ooxml) {
|
|
170
|
+
return ingestWordOoxmlToPlainTextResult(ooxml).text;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Result-returning plain-text ingestion for callers that need parse failures
|
|
175
|
+
* distinguished from legitimately empty documents.
|
|
176
|
+
*
|
|
177
|
+
* @param {unknown} ooxml
|
|
178
|
+
* @returns {{ text: string, status: 'ok'|'error', error?: {code:string,message:string}, warnings?: string[] }}
|
|
179
|
+
*/
|
|
180
|
+
export function ingestWordOoxmlToPlainTextResult(ooxml) {
|
|
181
|
+
const parsed = parseWordOoxml(ooxml);
|
|
182
|
+
if (!parsed.doc) {
|
|
183
|
+
return { text: '', status: 'error', error: parsed.error, warnings: parsed.warnings };
|
|
184
|
+
}
|
|
185
|
+
const doc = parsed.doc;
|
|
186
|
+
|
|
187
|
+
const paragraphs = getWordParagraphs(doc);
|
|
188
|
+
if (paragraphs.length === 0) {
|
|
189
|
+
const fallback = normalizeInlineWhitespace(doc.documentElement?.textContent || '');
|
|
190
|
+
return { text: fallback, status: 'ok', warnings: parsed.warnings };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const lines = paragraphs.map(paragraphToPlainText);
|
|
194
|
+
return { text: lines.join('\n\n').trim(), status: 'ok', warnings: parsed.warnings };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Ingests Word OOXML and returns basic markdown.
|
|
199
|
+
*
|
|
200
|
+
* @param {string} ooxml
|
|
201
|
+
* @returns {string}
|
|
202
|
+
*/
|
|
203
|
+
export function ingestWordOoxmlToMarkdown(ooxml) {
|
|
204
|
+
return ingestWordOoxmlToMarkdownResult(ooxml).text;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Result-returning markdown ingestion counterpart.
|
|
209
|
+
*
|
|
210
|
+
* @param {unknown} ooxml
|
|
211
|
+
* @returns {{ text: string, status: 'ok'|'error', error?: {code:string,message:string}, warnings?: string[] }}
|
|
212
|
+
*/
|
|
213
|
+
export function ingestWordOoxmlToMarkdownResult(ooxml) {
|
|
214
|
+
const parsed = parseWordOoxml(ooxml);
|
|
215
|
+
if (!parsed.doc) {
|
|
216
|
+
return { text: '', status: 'error', error: parsed.error, warnings: parsed.warnings };
|
|
217
|
+
}
|
|
218
|
+
const doc = parsed.doc;
|
|
219
|
+
|
|
220
|
+
const paragraphs = getWordParagraphs(doc);
|
|
221
|
+
if (paragraphs.length === 0) {
|
|
222
|
+
return { text: '', status: 'ok', warnings: parsed.warnings };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const lines = paragraphs.map(paragraphToMarkdown);
|
|
226
|
+
return { text: lines.join('\n\n').trim(), status: 'ok', warnings: parsed.warnings };
|
|
227
|
+
}
|