@ansonlai/docx-redline-js 0.4.0 → 0.5.1
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/AGENTS.md +646 -288
- package/ARCHITECTURE.md +215 -9
- package/CHANGELOG.md +319 -0
- package/README.md +604 -360
- package/adapters/config.js +45 -43
- package/bin/docx-redline.js +3 -0
- package/core/list-targeting.js +101 -110
- package/core/paragraph-targeting.js +501 -61
- package/core/paragraph-text.js +209 -0
- package/core/redline-validation.js +11 -5
- package/core/revision-cloning.js +38 -0
- package/core/types.js +64 -10
- package/core/word-xml.js +43 -15
- package/dist/docx-redline-js.esm.js +3145 -505
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +88 -76
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/TESTING.md +342 -23
- package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +1669 -0
- package/docs/plans/2026-09-08-cross-author-revision-slicing.md +505 -0
- package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +669 -0
- package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +427 -0
- package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +519 -0
- package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +69 -0
- package/docs/plans/completed/structural-revision-capability-matrix.md +115 -0
- package/docs/schemas/document-operations.schema.json +109 -0
- package/docs/test-comparison-dashboard.html +4250 -7
- package/engine/formatting-removal.js +11 -2
- package/engine/oxml-engine.js +508 -336
- package/engine/reconstruction-mode.js +15 -14
- package/engine/reconstruction-writer.js +247 -142
- package/engine/route-selection.js +35 -0
- package/engine/rpr-helpers.js +334 -35
- package/engine/run-builders.js +239 -196
- package/engine/surgical-diff-application.js +407 -50
- package/engine/surgical-mode.js +142 -6
- package/engine/surgical-run-splitting.js +103 -0
- package/engine/surgical-spans.js +52 -1
- package/engine/table-cell-context.js +3 -6
- package/engine/table-mode.js +1 -1
- package/index.d.ts +234 -6
- package/index.js +24 -1
- package/node/cli.js +322 -0
- package/node/docx-document.js +302 -0
- package/node/index.d.ts +31 -0
- package/node/index.js +2 -0
- package/node/zip-archive.js +52 -0
- package/orchestration/list-markdown.js +10 -16
- package/orchestration/list-parsing.js +7 -12
- package/orchestration/list-structural-fallback.js +21 -10
- package/package.json +123 -102
- package/pipeline/content-analysis.js +12 -17
- package/pipeline/ingestion-export.js +3 -31
- package/pipeline/ingestion-paragraph.js +10 -5
- package/pipeline/list-generation.js +150 -55
- package/pipeline/list-markers.js +70 -3
- package/pipeline/serialization.js +4 -2
- package/pipeline/structured-content.js +160 -0
- package/scripts/apply_changes.mjs +27 -0
- package/scripts/benchmark-operation-session.mjs +137 -0
- package/scripts/benchmark-targeting-browser.html +74 -0
- package/scripts/benchmark-targeting-hot-paths.mjs +67 -0
- package/scripts/benchmark-test-runner.mjs +59 -0
- package/scripts/build-test-dashboard.mjs +23 -0
- package/scripts/export-lane1-fixtures.mjs +380 -0
- package/scripts/export-reredline-stress-fixtures.mjs +317 -0
- package/scripts/export-validation-fixtures.mjs +1 -1
- package/scripts/extract_text.mjs +7 -0
- package/scripts/generate-cross-author-slicing-fixtures.ps1 +256 -0
- package/scripts/generate-paragraph-boundary-fixtures.ps1 +215 -0
- package/scripts/generate-test-dashboard.mjs +362 -11
- package/scripts/lib/word-coverage-catalogue.mjs +6 -2
- package/scripts/profile-route-selection.mjs +19 -0
- package/scripts/render-agenda-multilevel.mjs +0 -5
- package/scripts/render-multilevel-cases.mjs +0 -1
- package/scripts/run-tests.mjs +107 -35
- package/scripts/word-com-corpus-suite.ps1 +3 -0
- package/scripts/word-com-differential.ps1 +64 -4
- package/scripts/word-com-suite.ps1 +3 -0
- package/services/batch-operation-orchestrator.js +513 -0
- package/services/capture-engine.js +226 -0
- package/services/comment-builders.js +23 -6
- package/services/comment-engine.js +108 -47
- package/services/comment-locator.js +187 -82
- package/services/comment-replies.js +95 -0
- package/services/document-inspection.js +258 -0
- package/services/document-operation-applier.js +372 -0
- package/services/document-operation-contract.js +345 -0
- package/services/document-operation-mutations.js +1749 -0
- package/services/document-operation-session.js +258 -0
- package/services/numbering-service.js +14 -5
- package/services/operation-heuristics.js +173 -0
- package/services/operation-preflight.js +390 -0
- package/services/receipt-collector.js +288 -0
- package/services/revision-comment-management.js +77 -5
- package/services/revision-token.js +290 -0
- package/services/standalone-docx-plumbing.js +123 -8
- package/services/standalone-operation-runner.d.ts +296 -0
- package/services/standalone-operation-runner.js +10 -1455
- package/services/table-reconciliation.js +15 -6
- package/docs/VALIDATION.md +0 -183
- package/docs/WORD-MANUAL-REVIEW.md +0 -138
- package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +0 -210
- /package/docs/plans/{2026-08-30-reliability-testing-improvements.md → completed/2026-08-30-reliability-testing-improvements.md} +0 -0
package/node/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { deflateRawSync, inflateRawSync } from 'node:zlib';
|
|
2
|
+
|
|
3
|
+
const CRC_TABLE = (() => { const table = new Uint32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; table[n] = c >>> 0; } return table; })();
|
|
4
|
+
function crc32(buffer) { let crc = 0xffffffff; for (const byte of buffer) crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8); return (crc ^ 0xffffffff) >>> 0; }
|
|
5
|
+
|
|
6
|
+
export function unzipDocx(input) {
|
|
7
|
+
const source = Buffer.from(input);
|
|
8
|
+
let eocd = -1;
|
|
9
|
+
for (let offset = source.length - 22; offset >= Math.max(0, source.length - 0xffff - 22); offset--) if (source.readUInt32LE(offset) === 0x06054b50) { eocd = offset; break; }
|
|
10
|
+
if (eocd < 0) throw new Error('Invalid DOCX: ZIP end record not found.');
|
|
11
|
+
const count = source.readUInt16LE(eocd + 10); const centralOffset = source.readUInt32LE(eocd + 16);
|
|
12
|
+
if (count === 0xffff || centralOffset === 0xffffffff) throw new Error('Unsupported DOCX: ZIP64 archives are not supported.');
|
|
13
|
+
const entries = new Map(); let offset = centralOffset;
|
|
14
|
+
for (let index = 0; index < count; index++) {
|
|
15
|
+
if (source.readUInt32LE(offset) !== 0x02014b50) throw new Error('Invalid DOCX: central directory is corrupt.');
|
|
16
|
+
const flags = source.readUInt16LE(offset + 8); const method = source.readUInt16LE(offset + 10);
|
|
17
|
+
if (flags & 1) throw new Error('Unsupported DOCX: encrypted ZIP entries are not supported.');
|
|
18
|
+
const compressedSize = source.readUInt32LE(offset + 20); const nameLength = source.readUInt16LE(offset + 28);
|
|
19
|
+
const extraLength = source.readUInt16LE(offset + 30); const commentLength = source.readUInt16LE(offset + 32); const localOffset = source.readUInt32LE(offset + 42);
|
|
20
|
+
const name = source.toString('utf8', offset + 46, offset + 46 + nameLength);
|
|
21
|
+
if (source.readUInt32LE(localOffset) !== 0x04034b50) throw new Error('Invalid DOCX: local entry is corrupt.');
|
|
22
|
+
const localNameLength = source.readUInt16LE(localOffset + 26); const localExtraLength = source.readUInt16LE(localOffset + 28);
|
|
23
|
+
const start = localOffset + 30 + localNameLength + localExtraLength; const compressed = source.subarray(start, start + compressedSize);
|
|
24
|
+
if (method !== 0 && method !== 8) throw new Error(`Unsupported DOCX compression method: ${method}.`);
|
|
25
|
+
entries.set(name, method === 8 ? inflateRawSync(compressed) : Buffer.from(compressed));
|
|
26
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
27
|
+
}
|
|
28
|
+
return entries;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function zipDocx(entries) {
|
|
32
|
+
const local = []; const central = []; let offset = 0;
|
|
33
|
+
for (const [name, raw] of entries) {
|
|
34
|
+
const nameBytes = Buffer.from(name); const data = Buffer.from(raw); const compressed = deflateRawSync(data, { level: 9 });
|
|
35
|
+
const method = compressed.length < data.length ? 8 : 0; const payload = method === 8 ? compressed : data; const crc = crc32(data);
|
|
36
|
+
const lh = Buffer.alloc(30); lh.writeUInt32LE(0x04034b50); lh.writeUInt16LE(20,4); lh.writeUInt16LE(method,8); lh.writeUInt16LE(0x5c21,12); lh.writeUInt32LE(crc,14); lh.writeUInt32LE(payload.length,18); lh.writeUInt32LE(data.length,22); lh.writeUInt16LE(nameBytes.length,26);
|
|
37
|
+
local.push(lh, nameBytes, payload);
|
|
38
|
+
const ch = Buffer.alloc(46); ch.writeUInt32LE(0x02014b50); ch.writeUInt16LE(20,4); ch.writeUInt16LE(20,6); ch.writeUInt16LE(method,10); ch.writeUInt16LE(0x5c21,14); ch.writeUInt32LE(crc,16); ch.writeUInt32LE(payload.length,20); ch.writeUInt32LE(data.length,24); ch.writeUInt16LE(nameBytes.length,28); ch.writeUInt32LE(offset,42);
|
|
39
|
+
central.push(ch, nameBytes); offset += lh.length + nameBytes.length + payload.length;
|
|
40
|
+
}
|
|
41
|
+
const directory = Buffer.concat(central); const end = Buffer.alloc(22); end.writeUInt32LE(0x06054b50); end.writeUInt16LE(entries.size,8); end.writeUInt16LE(entries.size,10); end.writeUInt32LE(directory.length,12); end.writeUInt32LE(offset,16);
|
|
42
|
+
return Buffer.concat([...local, directory, end]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class MemoryZip {
|
|
46
|
+
constructor(entries) { this.entries = entries; }
|
|
47
|
+
file(path, value) {
|
|
48
|
+
if (value !== undefined) { this.entries.set(path, Buffer.isBuffer(value) ? Buffer.from(value) : Buffer.from(String(value))); return this; }
|
|
49
|
+
const data = this.entries.get(path); if (!data) return null;
|
|
50
|
+
return { async: async type => type === 'string' ? data.toString('utf8') : Buffer.from(data) };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -2,23 +2,19 @@
|
|
|
2
2
|
* Shared list markdown construction/parsing helpers for command adapters.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
matchListMarker,
|
|
7
|
+
stripListMarker
|
|
8
|
+
} from '../pipeline/list-markers.js';
|
|
9
|
+
|
|
10
|
+
export { inferNumberingStyleFromMarker } from '../pipeline/list-markers.js';
|
|
11
|
+
|
|
5
12
|
/**
|
|
6
13
|
* Infers numbering style from a list marker.
|
|
7
14
|
*
|
|
8
15
|
* @param {string} marker - Marker text
|
|
9
16
|
* @returns {'decimal'|'lowerAlpha'|'upperAlpha'|'lowerRoman'|'upperRoman'}
|
|
10
17
|
*/
|
|
11
|
-
export function inferNumberingStyleFromMarker(marker) {
|
|
12
|
-
const m = (marker || '').trim();
|
|
13
|
-
if (!m) return 'decimal';
|
|
14
|
-
if (/^\d+(?:\.\d+)*\.?$/.test(m) || /^\(\d+\)$/.test(m)) return 'decimal';
|
|
15
|
-
if (/^[ivxlcdm]+\.$/.test(m)) return 'lowerRoman';
|
|
16
|
-
if (/^[IVXLCDM]{2,}\.$/.test(m)) return 'upperRoman';
|
|
17
|
-
if (/^[a-z]\.$/.test(m)) return 'lowerAlpha';
|
|
18
|
-
if (/^[A-Z]\.$/.test(m)) return 'upperAlpha';
|
|
19
|
-
return 'decimal';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
18
|
/**
|
|
23
19
|
* Builds list markdown from normalized item+level input.
|
|
24
20
|
*
|
|
@@ -60,8 +56,6 @@ export function buildListMarkdown(itemsWithLevels, listType, numberingStyle) {
|
|
|
60
56
|
*/
|
|
61
57
|
export function normalizeListItemsWithLevels(rawItems, options = {}) {
|
|
62
58
|
const indentSpaces = Math.max(1, Number(options.indentSpaces) || 4);
|
|
63
|
-
const markersRegex = /^((?:\d+(?:\.\d+)*\.?|\((?:\d+|[a-zA-Z]|[ivxlcIVXLC]+)\)|[a-zA-Z]\.|\d+\.|[ivxlcIVXLC]+\.|[-*•])\s*)/;
|
|
64
|
-
|
|
65
59
|
return (rawItems || []).map((rawItem) => {
|
|
66
60
|
const item = String(rawItem ?? '');
|
|
67
61
|
const indentMatch = item.match(/^(\s*)/);
|
|
@@ -70,10 +64,10 @@ export function normalizeListItemsWithLevels(rawItems, options = {}) {
|
|
|
70
64
|
|
|
71
65
|
let stripped = item.trim();
|
|
72
66
|
let removedMarker = null;
|
|
73
|
-
const markerMatch = stripped
|
|
67
|
+
const markerMatch = matchListMarker(stripped, { allowZeroSpaceAfterMarker: true });
|
|
74
68
|
if (markerMatch) {
|
|
75
|
-
removedMarker = markerMatch[
|
|
76
|
-
stripped = stripped
|
|
69
|
+
removedMarker = markerMatch[2].trim() || null;
|
|
70
|
+
stripped = stripListMarker(stripped, { allowZeroSpaceAfterMarker: true });
|
|
77
71
|
}
|
|
78
72
|
|
|
79
73
|
return {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Keeps command-layer list parsing aligned with reconciliation marker logic.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { parseListItem } from '../pipeline/list-markers.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Parses markdown list-like content into structured items.
|
|
@@ -26,19 +26,14 @@ export function parseMarkdownListContent(content) {
|
|
|
26
26
|
for (const line of lines) {
|
|
27
27
|
if (!line.trim()) continue;
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
const indent = markerMatch[1] || '';
|
|
32
|
-
const marker = markerMatch[2].trim();
|
|
33
|
-
const text = stripListMarker(line, { allowZeroSpaceAfterMarker: false }).trim();
|
|
34
|
-
const level = Math.floor(indent.length / 2);
|
|
35
|
-
const isBullet = /^[-*+\u2022]$/.test(marker);
|
|
29
|
+
const parsed = parseListItem(line, { allowZeroSpaceAfterMarker: false, indentSpaces: 2 });
|
|
30
|
+
if (parsed) {
|
|
36
31
|
|
|
37
32
|
items.push({
|
|
38
|
-
type:
|
|
39
|
-
level,
|
|
40
|
-
text,
|
|
41
|
-
marker
|
|
33
|
+
type: parsed.markerType,
|
|
34
|
+
level: parsed.level,
|
|
35
|
+
text: parsed.text.trim(),
|
|
36
|
+
marker: parsed.marker
|
|
42
37
|
});
|
|
43
38
|
continue;
|
|
44
39
|
}
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
normalizeWhitespaceForTargeting
|
|
12
12
|
} from '../core/paragraph-targeting.js';
|
|
13
13
|
import { getParagraphListInfo } from '../core/list-targeting.js';
|
|
14
|
-
import {
|
|
14
|
+
import { executeListGeneration } from '../pipeline/list-generation.js';
|
|
15
|
+
import { NumberingService } from '../services/numbering-service.js';
|
|
15
16
|
import { preprocessMarkdown } from '../pipeline/markdown-processor.js';
|
|
16
17
|
import { parseMarkdownListContent, hasListItems } from './list-parsing.js';
|
|
17
18
|
import { inferNumberingStyleFromMarker } from './list-markdown.js';
|
|
@@ -433,7 +434,8 @@ export function buildSingleLineListStructuralFallbackPlan(options = {}) {
|
|
|
433
434
|
* @param {{
|
|
434
435
|
* author?: string,
|
|
435
436
|
* generateRedlines?: boolean,
|
|
436
|
-
*
|
|
437
|
+
* revisionIdAllocator?: import('../core/types.js').RevisionIdAllocator|null,
|
|
438
|
+
* pipeline?: import('../pipeline/pipeline.js').ReconciliationPipeline,
|
|
437
439
|
* setAbstractStartOverride?: boolean
|
|
438
440
|
* }} [options={}] - Execution options
|
|
439
441
|
* @returns {Promise<{
|
|
@@ -461,14 +463,23 @@ export async function executeSingleLineListStructuralFallback(plan, options = {}
|
|
|
461
463
|
|
|
462
464
|
const author = options.author || 'AI';
|
|
463
465
|
const generateRedlines = options.generateRedlines ?? true;
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
466
|
+
const result = options.pipeline
|
|
467
|
+
? await options.pipeline.executeListGeneration(
|
|
468
|
+
plan.listInput,
|
|
469
|
+
null,
|
|
470
|
+
null,
|
|
471
|
+
String(plan.originalText || '')
|
|
472
|
+
)
|
|
473
|
+
: await executeListGeneration({
|
|
474
|
+
cleanText: plan.listInput,
|
|
475
|
+
numberingContext: null,
|
|
476
|
+
originalRunModel: [],
|
|
477
|
+
originalText: String(plan.originalText || ''),
|
|
478
|
+
generateRedlines,
|
|
479
|
+
author,
|
|
480
|
+
revisionIdAllocator: options.revisionIdAllocator || null,
|
|
481
|
+
numberingService: new NumberingService()
|
|
482
|
+
});
|
|
472
483
|
|
|
473
484
|
const rawOxml = result?.oxml || result?.ooxml || '';
|
|
474
485
|
const oxml = trimTrailingBlankParagraph(rawOxml);
|
package/package.json
CHANGED
|
@@ -1,102 +1,123 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ansonlai/docx-redline-js",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Host-independent OOXML reconciliation engine for .docx manipulation with track changes",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"main": "./index.js",
|
|
8
|
-
"module": "./index.js",
|
|
9
|
-
"types": "./index.d.ts",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"./
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"test:
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"word",
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@ansonlai/docx-redline-js",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "Host-independent OOXML reconciliation engine for .docx manipulation with track changes",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./index.js",
|
|
8
|
+
"module": "./index.js",
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"bin": {
|
|
11
|
+
"docx-redline": "./bin/docx-redline.js"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./index.d.ts",
|
|
16
|
+
"import": "./index.js",
|
|
17
|
+
"default": "./index.js"
|
|
18
|
+
},
|
|
19
|
+
"./standalone-runner": {
|
|
20
|
+
"types": "./services/standalone-operation-runner.d.ts",
|
|
21
|
+
"import": "./services/standalone-operation-runner.js",
|
|
22
|
+
"default": "./services/standalone-operation-runner.js"
|
|
23
|
+
},
|
|
24
|
+
"./node": {
|
|
25
|
+
"types": "./node/index.d.ts",
|
|
26
|
+
"import": "./node/index.js",
|
|
27
|
+
"default": "./node/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./redline-operation-converter": "./orchestration/redline-operation-converter.js",
|
|
30
|
+
"./adapters/*": "./adapters/*",
|
|
31
|
+
"./core/*": "./core/*",
|
|
32
|
+
"./engine/*": "./engine/*",
|
|
33
|
+
"./pipeline/*": "./pipeline/*",
|
|
34
|
+
"./services/*": "./services/*",
|
|
35
|
+
"./orchestration/*": "./orchestration/*"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"adapters/",
|
|
39
|
+
"core/",
|
|
40
|
+
"engine/",
|
|
41
|
+
"pipeline/",
|
|
42
|
+
"services/",
|
|
43
|
+
"node/",
|
|
44
|
+
"bin/",
|
|
45
|
+
"orchestration/",
|
|
46
|
+
"scripts/",
|
|
47
|
+
"docs/",
|
|
48
|
+
"index.js",
|
|
49
|
+
"index.d.ts",
|
|
50
|
+
"dist/",
|
|
51
|
+
"ARCHITECTURE.md",
|
|
52
|
+
"AGENTS.md",
|
|
53
|
+
"CHANGELOG.md",
|
|
54
|
+
"README.md",
|
|
55
|
+
"LICENSE"
|
|
56
|
+
],
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"diff-match-patch": "^1.0.5"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@xmldom/xmldom": ">=0.8.0"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"@xmldom/xmldom": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@xmldom/xmldom": "^0.9.0",
|
|
70
|
+
"c8": "^12.0.0",
|
|
71
|
+
"docx-preview": "^0.4.0",
|
|
72
|
+
"esbuild": "^0.28.1",
|
|
73
|
+
"eslint": "^10.9.1",
|
|
74
|
+
"jszip": "^3.10.1",
|
|
75
|
+
"typescript": "^7.0.2"
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "node scripts/build.mjs",
|
|
79
|
+
"check:types": "tsc -p tsconfig.types.json && node scripts/check-types.mjs",
|
|
80
|
+
"lint": "eslint index.js adapters core engine pipeline services orchestration scripts tests",
|
|
81
|
+
"test:coverage": "c8 --reporter=text --reporter=json --reporter=json-summary --reports-dir=coverage node scripts/run-tests.mjs",
|
|
82
|
+
"coverage:gaps": "node scripts/report-coverage-gaps.mjs",
|
|
83
|
+
"benchmark:session": "node scripts/benchmark-operation-session.mjs",
|
|
84
|
+
"benchmark:targeting": "node scripts/benchmark-targeting-hot-paths.mjs",
|
|
85
|
+
"benchmark:tests": "node scripts/benchmark-test-runner.mjs",
|
|
86
|
+
"profile:routes": "node scripts/profile-route-selection.mjs",
|
|
87
|
+
"smoke:word": "powershell -File scripts/word-com-smoke.ps1",
|
|
88
|
+
"smoke:word:diff": "powershell -File scripts/word-com-differential.ps1",
|
|
89
|
+
"test:word": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/word-com-suite.ps1",
|
|
90
|
+
"test:word:visual": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/word-com-visual-suite.ps1",
|
|
91
|
+
"test:corpus:word": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/word-com-corpus-suite.ps1",
|
|
92
|
+
"test:corpus:word:visual": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/word-com-corpus-visual-suite.ps1",
|
|
93
|
+
"test:visual:inspect": "node scripts/inspect-visual-evidence.mjs",
|
|
94
|
+
"test:visual:sample": "node scripts/sample-multimodal-visual-check.mjs",
|
|
95
|
+
"fixtures:reredline": "node scripts/export-reredline-stress-fixtures.mjs",
|
|
96
|
+
"corpus:fetch:superdoc": "node scripts/fetch-superdoc-corpus.mjs",
|
|
97
|
+
"report:word:coverage": "node scripts/report-word-coverage.mjs",
|
|
98
|
+
"report:test:dashboard": "node scripts/build-test-dashboard.mjs",
|
|
99
|
+
"review:word:prepare": "node scripts/prepare-word-review.mjs",
|
|
100
|
+
"docx-redline": "node bin/docx-redline.js",
|
|
101
|
+
"test": "node scripts/run-tests.mjs",
|
|
102
|
+
"test:isolation": "node tests/no_word_api_index_check.mjs && node tests/core_dependency_graph_check.mjs",
|
|
103
|
+
"prepublishOnly": "npm run test:isolation && npm run build"
|
|
104
|
+
},
|
|
105
|
+
"keywords": [
|
|
106
|
+
"docx",
|
|
107
|
+
"ooxml",
|
|
108
|
+
"reconciliation",
|
|
109
|
+
"track-changes",
|
|
110
|
+
"redlines",
|
|
111
|
+
"word",
|
|
112
|
+
"office",
|
|
113
|
+
"document",
|
|
114
|
+
"xml"
|
|
115
|
+
],
|
|
116
|
+
"repository": {
|
|
117
|
+
"type": "git",
|
|
118
|
+
"url": "https://github.com/AnsonLai/docx-redline-js.git"
|
|
119
|
+
},
|
|
120
|
+
"engines": {
|
|
121
|
+
"node": ">=20.0.0"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { ContentType } from '../core/types.js';
|
|
6
|
-
import {
|
|
6
|
+
import { parseListItem } from './list-markers.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Parses table from markdown-style table text.
|
|
@@ -60,22 +60,17 @@ export function parseListItems(text) {
|
|
|
60
60
|
const items = [];
|
|
61
61
|
|
|
62
62
|
lines.forEach(line => {
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
text: stripListMarker(line, { allowZeroSpaceAfterMarker: true }),
|
|
75
|
-
marker,
|
|
76
|
-
indent,
|
|
77
|
-
level,
|
|
78
|
-
listType
|
|
63
|
+
const parsed = parseListItem(line, { allowZeroSpaceAfterMarker: true, indentSpaces: 2 });
|
|
64
|
+
if (!parsed) return;
|
|
65
|
+
const level = parsed.outlineLevel ?? parsed.level;
|
|
66
|
+
|
|
67
|
+
items.push({
|
|
68
|
+
line,
|
|
69
|
+
text: parsed.text,
|
|
70
|
+
marker: parsed.marker,
|
|
71
|
+
indent: parsed.indent,
|
|
72
|
+
level,
|
|
73
|
+
listType: parsed.listType
|
|
79
74
|
});
|
|
80
75
|
});
|
|
81
76
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { parseOoxmlSafe } from '../adapters/xml-adapter.js';
|
|
12
12
|
import { NS_W } from '../core/types.js';
|
|
13
13
|
import { getXmlParseError } from '../core/xml-query.js';
|
|
14
|
+
import { isNodeVisibleInRevisionView, readCanonicalRunText } from '../core/paragraph-text.js';
|
|
14
15
|
|
|
15
16
|
function hasParserError(doc) {
|
|
16
17
|
if (!doc || !doc.documentElement) return true;
|
|
@@ -54,34 +55,6 @@ function getWordAttribute(element, names) {
|
|
|
54
55
|
return '';
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
function hasWordAncestorWithin(node, localName, boundary) {
|
|
58
|
-
let cursor = node?.parentNode || null;
|
|
59
|
-
while (cursor && cursor !== boundary) {
|
|
60
|
-
if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W && cursor.localName === localName) {
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
cursor = cursor.parentNode;
|
|
64
|
-
}
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function readRunText(run) {
|
|
69
|
-
let text = '';
|
|
70
|
-
for (const child of Array.from(run?.childNodes || [])) {
|
|
71
|
-
if (!child || child.nodeType !== 1 || child.namespaceURI !== NS_W) continue;
|
|
72
|
-
if (child.localName === 't') {
|
|
73
|
-
text += child.textContent || '';
|
|
74
|
-
} else if (child.localName === 'tab') {
|
|
75
|
-
text += '\t';
|
|
76
|
-
} else if (child.localName === 'br' || child.localName === 'cr') {
|
|
77
|
-
text += '\n';
|
|
78
|
-
} else if (child.localName === 'noBreakHyphen') {
|
|
79
|
-
text += '\u2011';
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return text;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
58
|
function getRunFormatting(run) {
|
|
86
59
|
const rPr = getDirectWordChild(run, 'rPr');
|
|
87
60
|
if (!rPr) return { bold: false, italic: false };
|
|
@@ -99,9 +72,8 @@ function collectParagraphSegments(paragraph) {
|
|
|
99
72
|
const segments = [];
|
|
100
73
|
const runs = Array.from(paragraph?.getElementsByTagNameNS?.(NS_W, 'r') || []);
|
|
101
74
|
for (const run of runs) {
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
const text = readRunText(run);
|
|
75
|
+
if (!isNodeVisibleInRevisionView(run, paragraph, 'accepted')) continue;
|
|
76
|
+
const text = readCanonicalRunText(run, { boundary: paragraph, revisionView: 'accepted' });
|
|
105
77
|
if (!text) continue;
|
|
106
78
|
segments.push({
|
|
107
79
|
text,
|
|
@@ -91,8 +91,11 @@ export function detectNumberingContext(pElement) {
|
|
|
91
91
|
const ilvlEl = getFirstElementByTagNS(numPr, NS_W, 'ilvl');
|
|
92
92
|
if (!numIdEl) return null;
|
|
93
93
|
|
|
94
|
-
const numId = numIdEl.getAttribute('w:val');
|
|
95
|
-
|
|
94
|
+
const numId = numIdEl.getAttribute('w:val');
|
|
95
|
+
// In WordprocessingML, numId="0" explicitly removes numbering inherited
|
|
96
|
+
// from a paragraph style. It is not a reusable list definition.
|
|
97
|
+
if (!/^\d+$/.test(numId) || Number.parseInt(numId, 10) === 0) return null;
|
|
98
|
+
const type = numId === '1' ? 'bullet' : (numId === '2' ? 'numbered' : 'unknown');
|
|
96
99
|
|
|
97
100
|
return {
|
|
98
101
|
numId,
|
|
@@ -329,9 +332,11 @@ function processRun(runElement, startOffset) {
|
|
|
329
332
|
text += '\n';
|
|
330
333
|
} else if (nodeName.endsWith(':tab') || nodeName === 'tab') {
|
|
331
334
|
text += '\t';
|
|
332
|
-
} else if (nodeName.endsWith(':noBreakHyphen') || nodeName === 'noBreakHyphen') {
|
|
333
|
-
text += '\u2011';
|
|
334
|
-
}
|
|
335
|
+
} else if (nodeName.endsWith(':noBreakHyphen') || nodeName === 'noBreakHyphen') {
|
|
336
|
+
text += '\u2011';
|
|
337
|
+
} else if (nodeName.endsWith(':softHyphen') || nodeName === 'softHyphen') {
|
|
338
|
+
text += '\u00ad';
|
|
339
|
+
}
|
|
335
340
|
}
|
|
336
341
|
|
|
337
342
|
if (!text) return null;
|