@ansonlai/docx-redline-js 0.5.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 +69 -13
- package/CHANGELOG.md +1 -1
- package/README.md +1 -1
- package/core/redline-validation.js +11 -5
- package/dist/docx-redline-js.esm.js +305 -48
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +73 -72
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/plans/2026-09-08-cross-author-revision-slicing.md +505 -0
- package/docs/schemas/document-operations.schema.json +1 -1
- package/engine/oxml-engine.js +27 -10
- package/engine/surgical-diff-application.js +189 -17
- package/engine/surgical-mode.js +13 -5
- package/engine/surgical-run-splitting.js +103 -0
- package/index.d.ts +1 -1
- package/node/cli.js +12 -7
- package/package.json +123 -123
- package/scripts/generate-cross-author-slicing-fixtures.ps1 +256 -0
- package/services/batch-operation-orchestrator.js +24 -5
- package/services/document-operation-contract.js +22 -0
- package/services/document-operation-mutations.js +19 -3
- package/services/operation-preflight.js +32 -8
- package/services/revision-comment-management.js +40 -0
package/package.json
CHANGED
|
@@ -1,123 +1,123 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@ansonlai/docx-redline-js",
|
|
3
|
-
"version": "0.5.
|
|
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
|
-
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
3
|
+
|
|
4
|
+
$repoRoot = Split-Path -Parent $PSScriptRoot
|
|
5
|
+
$fixturesDir = Join-Path $repoRoot "tests\fixtures\cross-author-slicing"
|
|
6
|
+
if (-not (Test-Path $fixturesDir)) {
|
|
7
|
+
New-Item -ItemType Directory -Path $fixturesDir -Force | Out-Null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function Extract-DocumentXml($docxPath, $xmlPath) {
|
|
11
|
+
$zip = [System.IO.Compression.ZipFile]::OpenRead($docxPath)
|
|
12
|
+
try {
|
|
13
|
+
$entry = $zip.GetEntry("word/document.xml")
|
|
14
|
+
if ($entry) {
|
|
15
|
+
$reader = New-Object System.IO.StreamReader($entry.Open(), [System.Text.Encoding]::UTF8)
|
|
16
|
+
try {
|
|
17
|
+
$xml = $reader.ReadToEnd()
|
|
18
|
+
[System.IO.File]::WriteAllText($xmlPath, $xml, [System.Text.Encoding]::UTF8)
|
|
19
|
+
} finally {
|
|
20
|
+
$reader.Dispose()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} finally {
|
|
24
|
+
$zip.Dispose()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function Save-Triple($doc, [string]$baseName) {
|
|
29
|
+
$pendingDocx = [string](Join-Path $fixturesDir "$baseName-pending.docx")
|
|
30
|
+
$acceptedDocx = [string](Join-Path $fixturesDir "$baseName-accepted.docx")
|
|
31
|
+
$rejectedDocx = [string](Join-Path $fixturesDir "$baseName-rejected.docx")
|
|
32
|
+
|
|
33
|
+
[object]$pRef = $pendingDocx
|
|
34
|
+
[object]$aRef = $acceptedDocx
|
|
35
|
+
[object]$rRef = $rejectedDocx
|
|
36
|
+
[object]$fmt = 16
|
|
37
|
+
|
|
38
|
+
# Save pending
|
|
39
|
+
$doc.SaveAs2([ref]$pRef, [ref]$fmt)
|
|
40
|
+
|
|
41
|
+
# Accept All
|
|
42
|
+
$doc.Revisions.AcceptAll()
|
|
43
|
+
$doc.SaveAs2([ref]$aRef, [ref]$fmt)
|
|
44
|
+
$doc.Close($false)
|
|
45
|
+
|
|
46
|
+
# Reopen pending and Reject All
|
|
47
|
+
$reopened = $global:word.Documents.Open([ref]$pRef)
|
|
48
|
+
$reopened.Revisions.RejectAll()
|
|
49
|
+
$reopened.SaveAs2([ref]$rRef, [ref]$fmt)
|
|
50
|
+
$reopened.Close($false)
|
|
51
|
+
|
|
52
|
+
# Extract XML
|
|
53
|
+
Extract-DocumentXml $pendingDocx (Join-Path $fixturesDir "$baseName-pending.xml")
|
|
54
|
+
Extract-DocumentXml $acceptedDocx (Join-Path $fixturesDir "$baseName-accepted.xml")
|
|
55
|
+
Extract-DocumentXml $rejectedDocx (Join-Path $fixturesDir "$baseName-rejected.xml")
|
|
56
|
+
Write-Host "Saved: $baseName"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Find-RequiredText($doc, [string]$text, [string]$scenario) {
|
|
60
|
+
$foundPos = $doc.Content.Text.IndexOf($text)
|
|
61
|
+
if ($foundPos -lt 0) {
|
|
62
|
+
throw "Scenario '$scenario' could not find required text '$text'."
|
|
63
|
+
}
|
|
64
|
+
return $foundPos
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
$origUserName = $null
|
|
68
|
+
$origUserInitials = $null
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
$global:word = New-Object -ComObject Word.Application
|
|
72
|
+
$global:word.Visible = $false
|
|
73
|
+
$global:word.DisplayAlerts = 0
|
|
74
|
+
$origUserName = $global:word.UserName
|
|
75
|
+
$origUserInitials = $global:word.UserInitials
|
|
76
|
+
|
|
77
|
+
# -------------------------------------------------------------------------
|
|
78
|
+
# Scenario 1: insert-interior
|
|
79
|
+
# Author A (Barry) inserts: "amended by this Agreement."
|
|
80
|
+
# Author B (Anson) inserts "MASTER " before "Agreement"
|
|
81
|
+
# -------------------------------------------------------------------------
|
|
82
|
+
Write-Host "1. insert-interior"
|
|
83
|
+
$doc = $global:word.Documents.Add()
|
|
84
|
+
$doc.TrackRevisions = $false
|
|
85
|
+
$doc.Range(0, 0).Text = "Contract terms "
|
|
86
|
+
|
|
87
|
+
# Author A insertion
|
|
88
|
+
$global:word.UserName = "Barry Plasteras"
|
|
89
|
+
$global:word.UserInitials = "BP"
|
|
90
|
+
$doc.TrackRevisions = $true
|
|
91
|
+
$endR = $doc.Range($doc.Content.End - 1, $doc.Content.End - 1)
|
|
92
|
+
$endR.Text = "amended by this Agreement."
|
|
93
|
+
|
|
94
|
+
# Author B insertion inside Author A's insertion
|
|
95
|
+
$global:word.UserName = "Anson Lai"
|
|
96
|
+
$global:word.UserInitials = "AL"
|
|
97
|
+
# Find "Agreement" and insert "MASTER " before it
|
|
98
|
+
$targetText = "Agreement."
|
|
99
|
+
$foundPos = Find-RequiredText $doc $targetText "insert-interior"
|
|
100
|
+
$insRange = $doc.Range($foundPos, $foundPos)
|
|
101
|
+
$insRange.Text = "MASTER "
|
|
102
|
+
Save-Triple $doc "insert-interior"
|
|
103
|
+
|
|
104
|
+
# -------------------------------------------------------------------------
|
|
105
|
+
# Scenario 2: delete-interior
|
|
106
|
+
# Author A (Barry) inserts: "The Services will process the Input to generate outputs for Customer."
|
|
107
|
+
# Author B (Anson) deletes: "generate "
|
|
108
|
+
# -------------------------------------------------------------------------
|
|
109
|
+
Write-Host "2. delete-interior"
|
|
110
|
+
$doc = $global:word.Documents.Add()
|
|
111
|
+
$doc.TrackRevisions = $false
|
|
112
|
+
$doc.Range(0, 0).Text = "Background. "
|
|
113
|
+
|
|
114
|
+
# Author A insertion
|
|
115
|
+
$global:word.UserName = "Barry Plasteras"
|
|
116
|
+
$global:word.UserInitials = "BP"
|
|
117
|
+
$doc.TrackRevisions = $true
|
|
118
|
+
$endR = $doc.Range($doc.Content.End - 1, $doc.Content.End - 1)
|
|
119
|
+
$endR.Text = "The Services will process the Input to generate outputs for Customer."
|
|
120
|
+
|
|
121
|
+
# Author B deletion inside Author A's insertion
|
|
122
|
+
$global:word.UserName = "Anson Lai"
|
|
123
|
+
$global:word.UserInitials = "AL"
|
|
124
|
+
$delWord = "generate "
|
|
125
|
+
$foundPos = Find-RequiredText $doc $delWord "delete-interior"
|
|
126
|
+
$delRange = $doc.Range($foundPos, $foundPos + $delWord.Length)
|
|
127
|
+
$delRange.Delete() | Out-Null
|
|
128
|
+
Save-Triple $doc "delete-interior"
|
|
129
|
+
|
|
130
|
+
# -------------------------------------------------------------------------
|
|
131
|
+
# Scenario 3: delete-boundary-start
|
|
132
|
+
# Author A (Barry) inserts: "Notwithstanding the foregoing, the NDA remains in effect."
|
|
133
|
+
# Author B (Anson) deletes: "Notwithstanding the foregoing, "
|
|
134
|
+
# -------------------------------------------------------------------------
|
|
135
|
+
Write-Host "3. delete-boundary-start"
|
|
136
|
+
$doc = $global:word.Documents.Add()
|
|
137
|
+
$doc.TrackRevisions = $false
|
|
138
|
+
$doc.Range(0, 0).Text = "Section 1. "
|
|
139
|
+
|
|
140
|
+
# Author A insertion
|
|
141
|
+
$global:word.UserName = "Barry Plasteras"
|
|
142
|
+
$global:word.UserInitials = "BP"
|
|
143
|
+
$doc.TrackRevisions = $true
|
|
144
|
+
$endR = $doc.Range($doc.Content.End - 1, $doc.Content.End - 1)
|
|
145
|
+
$endR.Text = "Notwithstanding the foregoing, the NDA remains in effect."
|
|
146
|
+
|
|
147
|
+
# Author B deletion at start of insertion
|
|
148
|
+
$global:word.UserName = "Anson Lai"
|
|
149
|
+
$global:word.UserInitials = "AL"
|
|
150
|
+
$delWord = "Notwithstanding the foregoing, "
|
|
151
|
+
$foundPos = Find-RequiredText $doc $delWord "delete-boundary-start"
|
|
152
|
+
$delRange = $doc.Range($foundPos, $foundPos + $delWord.Length)
|
|
153
|
+
$delRange.Delete() | Out-Null
|
|
154
|
+
Save-Triple $doc "delete-boundary-start"
|
|
155
|
+
|
|
156
|
+
# -------------------------------------------------------------------------
|
|
157
|
+
# Scenario 4: delete-boundary-end
|
|
158
|
+
# Author A (Barry) inserts: "subject to Section 2.8 and applicable law."
|
|
159
|
+
# Author B (Anson) deletes: " and applicable law."
|
|
160
|
+
# -------------------------------------------------------------------------
|
|
161
|
+
Write-Host "4. delete-boundary-end"
|
|
162
|
+
$doc = $global:word.Documents.Add()
|
|
163
|
+
$doc.TrackRevisions = $false
|
|
164
|
+
$doc.Range(0, 0).Text = "Compliance: "
|
|
165
|
+
|
|
166
|
+
# Author A insertion
|
|
167
|
+
$global:word.UserName = "Barry Plasteras"
|
|
168
|
+
$global:word.UserInitials = "BP"
|
|
169
|
+
$doc.TrackRevisions = $true
|
|
170
|
+
$endR = $doc.Range($doc.Content.End - 1, $doc.Content.End - 1)
|
|
171
|
+
$endR.Text = "subject to Section 2.8 and applicable law."
|
|
172
|
+
|
|
173
|
+
# Author B deletion at end of insertion
|
|
174
|
+
$global:word.UserName = "Anson Lai"
|
|
175
|
+
$global:word.UserInitials = "AL"
|
|
176
|
+
$delWord = " and applicable law."
|
|
177
|
+
$foundPos = Find-RequiredText $doc $delWord "delete-boundary-end"
|
|
178
|
+
$delRange = $doc.Range($foundPos, $foundPos + $delWord.Length)
|
|
179
|
+
$delRange.Delete() | Out-Null
|
|
180
|
+
Save-Triple $doc "delete-boundary-end"
|
|
181
|
+
|
|
182
|
+
# -------------------------------------------------------------------------
|
|
183
|
+
# Scenario 5: delete-straddle-baseline-insertion
|
|
184
|
+
# Baseline: "Baseline start "
|
|
185
|
+
# Author A (Barry) inserts: "inserted finish."
|
|
186
|
+
# Author B (Anson) deletes: "start inserted" (straddling baseline and insertion)
|
|
187
|
+
# -------------------------------------------------------------------------
|
|
188
|
+
Write-Host "5. delete-straddle-baseline-insertion"
|
|
189
|
+
$doc = $global:word.Documents.Add()
|
|
190
|
+
$doc.TrackRevisions = $false
|
|
191
|
+
$doc.Range(0, 0).Text = "Baseline start "
|
|
192
|
+
|
|
193
|
+
# Author A insertion
|
|
194
|
+
$global:word.UserName = "Barry Plasteras"
|
|
195
|
+
$global:word.UserInitials = "BP"
|
|
196
|
+
$doc.TrackRevisions = $true
|
|
197
|
+
$endR = $doc.Range($doc.Content.End - 1, $doc.Content.End - 1)
|
|
198
|
+
$endR.Text = "inserted finish."
|
|
199
|
+
|
|
200
|
+
# Author B deletion straddling baseline and insertion
|
|
201
|
+
$global:word.UserName = "Anson Lai"
|
|
202
|
+
$global:word.UserInitials = "AL"
|
|
203
|
+
$delWord = "start inserted"
|
|
204
|
+
$foundPos = Find-RequiredText $doc $delWord "delete-straddle-baseline-insertion"
|
|
205
|
+
$delRange = $doc.Range($foundPos, $foundPos + $delWord.Length)
|
|
206
|
+
$delRange.Delete() | Out-Null
|
|
207
|
+
Save-Triple $doc "delete-straddle-baseline-insertion"
|
|
208
|
+
|
|
209
|
+
# -------------------------------------------------------------------------
|
|
210
|
+
# Scenario 6: multi-author-stacked
|
|
211
|
+
# Baseline: "Provision "
|
|
212
|
+
# Author A (Barry) inserts: "first draft of the proposal with initial metrics."
|
|
213
|
+
# Author B (Anson) deletes: "of the proposal "
|
|
214
|
+
# Author C (Chris) deletes: "initial " from the remaining text
|
|
215
|
+
# -------------------------------------------------------------------------
|
|
216
|
+
Write-Host "6. multi-author-stacked"
|
|
217
|
+
$doc = $global:word.Documents.Add()
|
|
218
|
+
$doc.TrackRevisions = $false
|
|
219
|
+
$doc.Range(0, 0).Text = "Provision "
|
|
220
|
+
|
|
221
|
+
# Author A insertion
|
|
222
|
+
$global:word.UserName = "Barry Plasteras"
|
|
223
|
+
$global:word.UserInitials = "BP"
|
|
224
|
+
$doc.TrackRevisions = $true
|
|
225
|
+
$endR = $doc.Range($doc.Content.End - 1, $doc.Content.End - 1)
|
|
226
|
+
$endR.Text = "first draft of the proposal with initial metrics."
|
|
227
|
+
|
|
228
|
+
# Author B deletion inside Author A's insertion
|
|
229
|
+
$global:word.UserName = "Anson Lai"
|
|
230
|
+
$global:word.UserInitials = "AL"
|
|
231
|
+
$delWordB = "of the proposal "
|
|
232
|
+
$foundPosB = Find-RequiredText $doc $delWordB "multi-author-stacked (Author B)"
|
|
233
|
+
$delRangeB = $doc.Range($foundPosB, $foundPosB + $delWordB.Length)
|
|
234
|
+
$delRangeB.Delete() | Out-Null
|
|
235
|
+
|
|
236
|
+
# Author C deletion inside Author A's remaining insertion
|
|
237
|
+
$global:word.UserName = "Chris Davis"
|
|
238
|
+
$global:word.UserInitials = "CD"
|
|
239
|
+
$delWordC = "initial "
|
|
240
|
+
$foundPosC = Find-RequiredText $doc $delWordC "multi-author-stacked (Author C)"
|
|
241
|
+
$delRangeC = $doc.Range($foundPosC, $foundPosC + $delWordC.Length)
|
|
242
|
+
$delRangeC.Delete() | Out-Null
|
|
243
|
+
Save-Triple $doc "multi-author-stacked"
|
|
244
|
+
|
|
245
|
+
Write-Host "All 6 fixture triples successfully generated!"
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
if ($null -ne $origUserName -and $null -ne $global:word) {
|
|
249
|
+
$global:word.UserName = $origUserName
|
|
250
|
+
$global:word.UserInitials = $origUserInitials
|
|
251
|
+
}
|
|
252
|
+
if ($null -ne $global:word) {
|
|
253
|
+
$global:word.Quit()
|
|
254
|
+
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($global:word) | Out-Null
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createSerializer, parseOoxmlSafe } from '../adapters/xml-adapter.js';
|
|
2
2
|
import { getDefaultAuthor } from '../adapters/config.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
isExistingRevisionsPolicy,
|
|
5
|
+
normalizeDocumentOperation,
|
|
5
6
|
resolveDocumentOperationAuthor
|
|
6
7
|
} from './document-operation-contract.js';
|
|
7
8
|
import {
|
|
@@ -183,15 +184,33 @@ function mergeCommentsXml(existingXml, incomingXml) {
|
|
|
183
184
|
return serializer.serializeToString(existingDoc);
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
export async function applyOperationsToDocumentXml(documentXml, operations, author, runtimeContext = null, options = {}) {
|
|
187
|
+
export async function applyOperationsToDocumentXml(documentXml, operations, author, runtimeContext = null, options = {}) {
|
|
187
188
|
const sourceOperations = Array.isArray(operations) ? operations : [];
|
|
188
189
|
const defaultAuthor = getDefaultAuthor();
|
|
189
|
-
const emptyReceipts = () => sourceOperations.map((op, i) => createEmptyReceipt(
|
|
190
|
+
const emptyReceipts = () => sourceOperations.map((op, i) => createEmptyReceipt(
|
|
190
191
|
i + 1,
|
|
191
192
|
op?.operationId,
|
|
192
193
|
resolveDocumentOperationAuthor(op, author, defaultAuthor),
|
|
193
194
|
'not_attempted'
|
|
194
|
-
));
|
|
195
|
+
));
|
|
196
|
+
|
|
197
|
+
if (options.existingRevisions != null && !isExistingRevisionsPolicy(options.existingRevisions)) {
|
|
198
|
+
return {
|
|
199
|
+
documentXml,
|
|
200
|
+
hasChanges: false,
|
|
201
|
+
commentsXml: null,
|
|
202
|
+
numberingXmlParts: [],
|
|
203
|
+
results: [],
|
|
204
|
+
receipts: emptyReceipts(),
|
|
205
|
+
executionOrder: [],
|
|
206
|
+
authorsUsed: [],
|
|
207
|
+
status: 'error',
|
|
208
|
+
error: {
|
|
209
|
+
code: 'INVALID_OPERATION',
|
|
210
|
+
message: `Unsupported existingRevisions policy: "${String(options.existingRevisions)}".`
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
195
214
|
|
|
196
215
|
if (options?.expectedRevision) {
|
|
197
216
|
const tokenValidation = validateRevisionToken(options.expectedRevision);
|
|
@@ -20,6 +20,18 @@ const SUPPORTED_OPERATION_TYPES = new Set([
|
|
|
20
20
|
'highlight'
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
|
+
export const EXISTING_REVISIONS_POLICIES = Object.freeze([
|
|
24
|
+
'merge-same-author',
|
|
25
|
+
'slice-cross-author',
|
|
26
|
+
'reject-input',
|
|
27
|
+
'accept-all-first',
|
|
28
|
+
'accept-all-first-keep-normalized'
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export function isExistingRevisionsPolicy(value) {
|
|
32
|
+
return EXISTING_REVISIONS_POLICIES.includes(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
function isRecord(value) {
|
|
24
36
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
25
37
|
}
|
|
@@ -203,6 +215,16 @@ export function validateDocumentOperation(operation) {
|
|
|
203
215
|
};
|
|
204
216
|
}
|
|
205
217
|
|
|
218
|
+
if (normalized.existingRevisions != null && !isExistingRevisionsPolicy(normalized.existingRevisions)) {
|
|
219
|
+
return {
|
|
220
|
+
valid: false,
|
|
221
|
+
error: {
|
|
222
|
+
code: 'INVALID_OPERATION',
|
|
223
|
+
message: `existingRevisions must be one of: ${EXISTING_REVISIONS_POLICIES.join(', ')}.`
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
206
228
|
if (normalized.insertionAffinity != null) {
|
|
207
229
|
if (!isRecord(normalized.insertionAffinity)) {
|
|
208
230
|
return {
|
|
@@ -860,11 +860,11 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
860
860
|
&& existingPolicy !== 'accept-all-first'
|
|
861
861
|
&& existingPolicy !== 'accept-all-first-keep-normalized'
|
|
862
862
|
) {
|
|
863
|
-
if (existingPolicy === 'merge-same-author') {
|
|
863
|
+
if (existingPolicy === 'merge-same-author' || existingPolicy === 'slice-cross-author') {
|
|
864
864
|
const authors = getTrackedChangeAuthors(targetParagraph);
|
|
865
865
|
const opAuthor = String(author || '').trim().toLowerCase();
|
|
866
866
|
const allSame = authors.length > 0 && authors.every(a => a.trim().toLowerCase() === opAuthor);
|
|
867
|
-
if (!allSame) {
|
|
867
|
+
if (!allSame && existingPolicy === 'merge-same-author') {
|
|
868
868
|
return {
|
|
869
869
|
documentXml,
|
|
870
870
|
hasChanges: false,
|
|
@@ -876,7 +876,7 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
876
876
|
}
|
|
877
877
|
};
|
|
878
878
|
}
|
|
879
|
-
const mergeCommentIds = getCommentIdsInElement(targetParagraph);
|
|
879
|
+
const mergeCommentIds = allSame ? getCommentIdsInElement(targetParagraph) : [];
|
|
880
880
|
if (mergeCommentIds.length > 0) {
|
|
881
881
|
const comments = commentDetailsForIds(mergeCommentIds, options._existingCommentDetails);
|
|
882
882
|
return {
|
|
@@ -892,6 +892,22 @@ export async function applyToParagraphByExactText(documentXml, targetText, modif
|
|
|
892
892
|
}
|
|
893
893
|
};
|
|
894
894
|
}
|
|
895
|
+
if (!allSame) {
|
|
896
|
+
const hasMove = targetParagraph.getElementsByTagNameNS(NS_W, 'moveFrom').length > 0
|
|
897
|
+
|| targetParagraph.getElementsByTagNameNS(NS_W, 'moveTo').length > 0;
|
|
898
|
+
if (hasMove) {
|
|
899
|
+
return {
|
|
900
|
+
documentXml,
|
|
901
|
+
hasChanges: false,
|
|
902
|
+
numberingXml: null,
|
|
903
|
+
status: 'error',
|
|
904
|
+
error: {
|
|
905
|
+
code: 'UNSAFE_REVISION_NESTING',
|
|
906
|
+
message: 'Cross-author slicing does not support pending move revisions.'
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
}
|
|
895
911
|
} else {
|
|
896
912
|
const hasDel = targetParagraph.getElementsByTagNameNS(NS_W, 'del').length > 0;
|
|
897
913
|
const hasMove = targetParagraph.getElementsByTagNameNS(NS_W, 'moveFrom').length > 0
|