@ansonlai/docx-redline-js 0.1.4 → 0.1.6

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.
Files changed (42) hide show
  1. package/AGENTS.md +53 -4
  2. package/ARCHITECTURE.md +57 -9
  3. package/README.md +47 -3
  4. package/core/types.js +35 -8
  5. package/core/word-xml.js +90 -0
  6. package/dist/docx-redline-js.esm.js +3073 -2594
  7. package/dist/docx-redline-js.esm.js.map +4 -4
  8. package/dist/docx-redline-js.esm.min.js +71 -67
  9. package/dist/docx-redline-js.esm.min.js.map +4 -4
  10. package/docs/VALIDATION.md +48 -0
  11. package/docs/plans/2026-03-01-release-0.1.4-design.md +31 -0
  12. package/docs/plans/2026-03-01-release-0.1.4.md +108 -0
  13. package/engine/format-application.js +13 -14
  14. package/engine/format-span-application.js +7 -6
  15. package/engine/formatting-removal.js +15 -12
  16. package/engine/oxml-engine.js +146 -55
  17. package/engine/reconstruction-mapper.js +35 -8
  18. package/engine/reconstruction-mode.js +14 -13
  19. package/engine/reconstruction-writer.js +97 -78
  20. package/engine/rpr-helpers.js +34 -32
  21. package/engine/run-builders.js +150 -39
  22. package/engine/surgical-diff-application.js +216 -0
  23. package/engine/surgical-mode.js +84 -519
  24. package/engine/surgical-run-splitting.js +96 -0
  25. package/engine/surgical-spans.js +169 -0
  26. package/engine/table-cell-context.js +15 -13
  27. package/engine/table-mode.js +39 -35
  28. package/index.d.ts +148 -0
  29. package/index.js +15 -13
  30. package/package.json +8 -1
  31. package/pipeline/ingestion-export.js +1 -0
  32. package/pipeline/ingestion-paragraph.js +37 -12
  33. package/pipeline/ingestion-table.js +11 -8
  34. package/scripts/build.mjs +35 -0
  35. package/scripts/check-types.mjs +28 -0
  36. package/scripts/export-validation-fixtures.mjs +68 -0
  37. package/scripts/run-tests.mjs +43 -0
  38. package/scripts/word-com-smoke.ps1 +48 -0
  39. package/services/comment-locator.js +10 -9
  40. package/services/revision-comment-management.js +115 -1
  41. package/services/standalone-operation-runner.js +119 -69
  42. package/services/table-reconciliation.js +7 -8
package/AGENTS.md CHANGED
@@ -16,10 +16,12 @@ Input: (paragraph OOXML, original text, modified text, options)
16
16
  Engine routes to: format-only | surgical | reconstruction | list | table mode
17
17
  |
18
18
  v
19
- Output: { oxml: string, hasChanges: boolean, warnings?: string[] }
19
+ Output: { oxml: string, hasChanges: boolean, status?: string, error?: object, warnings?: string[] }
20
20
  ```
21
21
 
22
- The engine works at paragraph scope. For full-document operations, callers iterate paragraph targets or use the standalone operation runner.
22
+ The engine usually works at paragraph/range/table scope. For full-document
23
+ operations, use the standalone operation runner so the result is safe to write
24
+ back to `word/document.xml`.
23
25
 
24
26
  ## Entry Point
25
27
 
@@ -45,10 +47,15 @@ Browsers have native DOM APIs, so no provider injection is typically needed.
45
47
  ```js
46
48
  const result = await applyRedlineToOxml(oxml, originalText, modifiedText, {
47
49
  generateRedlines: true,
48
- author: 'Agent Name'
50
+ author: 'Agent Name',
51
+ existingRevisions: 'reject-input'
49
52
  });
50
53
  ```
51
54
 
55
+ `existingRevisions` defaults to `'reject-input'`. Use `'accept-all-first'` only
56
+ when the caller intentionally wants to accept prior tracked changes before
57
+ applying a new edit.
58
+
52
59
  ### Apply a text edit without tracked changes
53
60
 
54
61
  ```js
@@ -90,6 +97,9 @@ const rejectedMine = rejectTrackedChangesInOoxml(documentXml, { author: 'Agent'
90
97
  const rejectedAll = rejectTrackedChangesInOoxml(documentXml, { allAuthors: true });
91
98
  ```
92
99
 
100
+ Move revisions are consumed too: accept removes `w:moveFrom` and unwraps
101
+ `w:moveTo`; reject unwraps `w:moveFrom` and removes `w:moveTo`.
102
+
93
103
  ### Delete comments from one user (or all users)
94
104
 
95
105
  ```js
@@ -105,6 +115,15 @@ import { applyOperationToDocumentXml } from '@ansonlai/docx-redline-js/services/
105
115
  const result = await applyOperationToDocumentXml(documentXml, operation, options);
106
116
  ```
107
117
 
118
+ Use `result.documentXml` from this API when replacing full `word/document.xml`.
119
+
120
+ ### Detect existing tracked changes
121
+
122
+ ```js
123
+ import { containsTrackedChanges } from '@ansonlai/docx-redline-js';
124
+ const hasTrackedChanges = containsTrackedChanges(xmlDoc);
125
+ ```
126
+
108
127
  ### Convert paragraph text into a Word list
109
128
 
110
129
  ```js
@@ -130,15 +149,21 @@ adapters/
130
149
  logger.js
131
150
  core/
132
151
  types.js
152
+ word-xml.js
133
153
  paragraph-targeting.js
134
154
  list-targeting.js
135
155
  table-targeting.js
136
156
  engine/
137
157
  oxml-engine.js
138
158
  surgical-mode.js
159
+ surgical-run-splitting.js
160
+ surgical-diff-application.js
161
+ surgical-spans.js
139
162
  reconstruction-mode.js
163
+ reconstruction-writer.js
140
164
  format-application.js
141
165
  formatting-removal.js
166
+ run-builders.js
142
167
  table-mode.js
143
168
  pipeline/
144
169
  pipeline.js
@@ -169,7 +194,8 @@ orchestration/
169
194
  ```js
170
195
  {
171
196
  generateRedlines: true,
172
- author: 'Name'
197
+ author: 'Name',
198
+ existingRevisions: 'reject-input'
173
199
  }
174
200
  ```
175
201
 
@@ -179,12 +205,17 @@ orchestration/
179
205
  {
180
206
  oxml: string,
181
207
  hasChanges: boolean,
208
+ status?: 'ok' | 'no-op' | 'error',
209
+ error?: { code: string, message: string },
182
210
  warnings?: string[],
183
211
  numberingXml?: string,
184
212
  useNativeApi?: boolean
185
213
  }
186
214
  ```
187
215
 
216
+ Known error codes include `PARSE_ERROR`, `TARGET_NOT_FOUND`, and
217
+ `EXISTING_REVISIONS`.
218
+
188
219
  ### OOXML wrapping for Word insertOoxml scenarios
189
220
 
190
221
  ```js
@@ -211,3 +242,21 @@ directly into `word/document.xml`.
211
242
  5. `useNativeApi: true` means standalone mode cannot fully handle that operation path.
212
243
  6. `deleteCommentsByAuthorInOoxml` removes matching `comments.xml` entries and linked comment anchors/references in the document.
213
244
  7. If output begins with `<pkg:package`, treat it as package-level OOXML and normalize it before writing anything back to `word/document.xml`.
245
+ 8. Existing revisions are rejected by default; pass `existingRevisions: 'accept-all-first'` only when that is desired.
246
+ 9. Hyperlinks, bookmarks, comment markers, tabs/breaks, and footnote/endnote references are structural OOXML and should survive adjacent redline edits.
247
+ 10. Internally, create Word elements through `createWordElement` and tracked-change metadata through `createRevisionMetadata`.
248
+
249
+ ## Validation Commands
250
+
251
+ ```bash
252
+ npm test
253
+ npm run test:isolation
254
+ npm run check:types
255
+ node scripts/export-validation-fixtures.mjs
256
+ ```
257
+
258
+ Optional Windows/Word smoke test for a completed `.docx`:
259
+
260
+ ```bash
261
+ npm run smoke:word -- path/to/file.docx
262
+ ```
package/ARCHITECTURE.md CHANGED
@@ -13,7 +13,8 @@ This repository contains only the publishable package surface:
13
13
  - `services/`
14
14
  - `orchestration/`
15
15
  - `index.js`
16
- - `standalone.js`
16
+ - `index.d.ts`
17
+ - `dist/`
17
18
 
18
19
  No Word add-in entrypoints or host-specific integration layers are part of this package.
19
20
 
@@ -22,6 +23,8 @@ No Word add-in entrypoints or host-specific integration layers are part of this
22
23
  - Preserve Word-compatible redlines by editing OOXML directly.
23
24
  - Keep core logic host-independent (no Office.js globals, no Word API calls).
24
25
  - Reuse the same engine in browser, Node.js, and other JavaScript runtimes.
26
+ - Keep generated OOXML schema-safe through shared Word element creation and
27
+ centralized revision metadata helpers.
25
28
 
26
29
  ## Folder Layout
27
30
 
@@ -32,16 +35,23 @@ No Word add-in entrypoints or host-specific integration layers are part of this
32
35
  │ ├── logger.js
33
36
  │ └── xml-adapter.js
34
37
  ├── core/
38
+ │ └── word-xml.js
35
39
  ├── engine/
40
+ │ ├── oxml-engine.js
41
+ │ ├── surgical-mode.js
42
+ │ ├── reconstruction-mode.js
43
+ │ ├── run-builders.js
36
44
  │ └── formatting-removal.js
37
45
  ├── orchestration/
38
46
  ├── pipeline/
39
47
  ├── services/
48
+ │ ├── comment-engine.js
40
49
  │ ├── numbering-helpers.js
41
50
  │ ├── revision-comment-management.js
42
51
  │ ├── standalone-docx-plumbing.js
43
52
  │ └── standalone-operation-runner.js
44
- └── index.js
53
+ ├── index.js
54
+ └── index.d.ts
45
55
  ```
46
56
 
47
57
  ## Entry Points
@@ -58,20 +68,30 @@ No Word add-in entrypoints or host-specific integration layers are part of this
58
68
  - Runtime logger injection and shared logging methods.
59
69
  - `core/*`
60
70
  - Shared types, OOXML identity helpers, target resolution, list/table targeting heuristics, and XML query helpers.
71
+ - `core/word-xml.js`
72
+ - Namespace-safe Word element creation, tracked-change detection, and OOXML payload source-shape helpers.
73
+ - `core/types.js`
74
+ - Shared model enums/types plus revision metadata generation and document-aware revision ID seeding.
61
75
  - `engine/oxml-engine.js`
62
- - Main reconciliation router and mode selection.
76
+ - Main reconciliation router, mode selection, existing-revision policy gate, and status/error result handling.
77
+ - `engine/run-builders.js`
78
+ - Shared builders for insertion/deletion wrappers, paragraph-mark revisions, visible run content, and run-property changes.
79
+ - `engine/surgical-*.js`
80
+ - Surgical run splitting, diff application, and span helpers for localized edits that preserve surrounding markup.
63
81
  - `engine/formatting-removal.js`
64
82
  - Shared formatting removal and highlight helpers.
65
83
  - `pipeline/*`
66
- - Ingestion, markdown preprocessing, diffing, patching, and serialization stages.
84
+ - Ingestion, markdown preprocessing, diffing, patching, and serialization stages. Ingestion treats deleted and moved-from content as non-visible text and inserted/moved-to content as visible text.
85
+ - `services/comment-engine.js`
86
+ - Comment creation and package-level comment XML handling.
67
87
  - `services/numbering-helpers.js`
68
88
  - Dynamic numbering ID allocation, numbering payload remapping, and schema-order-safe numbering merges.
69
89
  - `services/standalone-docx-plumbing.js`
70
90
  - Package-level extraction/wiring/validation for `word/document.xml`, `word/numbering.xml`, and `word/comments.xml`.
71
91
  - `services/revision-comment-management.js`
72
- - OOXML transforms for accepting/rejecting tracked changes by author/all-authors and deleting comments by author/all-authors.
92
+ - OOXML transforms for accepting/rejecting insertion, deletion, move, paragraph-mark, and property-change revisions by author/all-authors, plus deleting comments by author/all-authors.
73
93
  - `services/standalone-operation-runner.js`
74
- - Host-agnostic operation bridge for `redline`, `highlight`, and `comment` workflows.
94
+ - Host-agnostic operation bridge for full-document `redline`, `highlight`, and `comment` workflows.
75
95
  - `orchestration/*`
76
96
  - Route planning and list fallback orchestration utilities.
77
97
 
@@ -81,14 +101,33 @@ No Word add-in entrypoints or host-specific integration layers are part of this
81
101
  2. Caller configures XML provider/logger/defaults when needed via `adapters/*`.
82
102
  3. Caller invokes reconciliation APIs (`applyRedlineToOxml`, operation runner, ingestion/export helpers).
83
103
  4. `engine/oxml-engine.js` routes to format, table, list, surgical, or reconstruction flows.
84
- 5. Pipeline/services return OOXML and optional package artifacts (`numberingXml`, comments payloads).
85
- 6. Optional revision/comment management transforms can accept/reject revisions or delete comments by author.
104
+ 5. Pipeline/services return OOXML, optional package artifacts (`numberingXml`, comments payloads), and non-breaking `status`/`error` fields where applicable.
105
+ 6. Optional revision/comment management transforms can accept/reject revisions, including move revisions, or delete comments by author.
86
106
  7. Caller writes resulting XML back to package/document boundaries.
87
107
 
88
108
  ## Public Surfaces
89
109
 
90
110
  - Primary: `index.js`
91
- Keep exports centralized through `index.js`.
111
+ - Types: `index.d.ts`
112
+
113
+ Keep public exports centralized through `index.js`; deep imports are supported by
114
+ the package `exports` map for advanced consumers, but new public APIs should
115
+ still be re-exported from `index.js`.
116
+
117
+ ## Reliability Guardrails
118
+
119
+ - Create Word namespace elements with `createWordElement(xmlDoc, 'w:...')`.
120
+ Avoid direct `document.createElement('w:*')` or `createElementNS(NS_W, 'w:*')`
121
+ outside `core/word-xml.js`.
122
+ - Generate tracked-change metadata through `createRevisionMetadata(author)` so
123
+ `w:id`, `w:author`, and `w:date` stay consistent and document-unique.
124
+ - Seed revision IDs from parsed input with `seedRevisionIdsFromDocument(xmlDoc)`
125
+ before emitting new tracked changes.
126
+ - Use `containsTrackedChanges(xmlDoc)` before redlining existing revisions unless
127
+ the caller explicitly chooses the `existingRevisions: 'accept-all-first'` policy.
128
+ - Do not write unknown `result.oxml` payloads directly into `word/document.xml`;
129
+ normalize with `extractReplacementNodesFromOoxml(...)` or use
130
+ `applyOperationToDocumentXml(...).documentXml` for full-document replacement.
92
131
 
93
132
 
94
133
  ## Build Output
@@ -108,6 +147,12 @@ The bundle inlines `diff-match-patch` and keeps `@xmldom/xmldom` external.
108
147
  - Runs the package test runner (`scripts/run-tests.mjs`) against all `tests/*.mjs` except setup helpers.
109
148
  - `npm run test:isolation`
110
149
  - Runs boundary checks for Word API markers and dependency-graph isolation.
150
+ - `npm run check:types`
151
+ - Smoke-checks `index.d.ts`.
152
+ - `node scripts/export-validation-fixtures.mjs`
153
+ - Writes release-time validation fixtures to `tmp/validation-docx/`.
154
+ - `npm run smoke:word -- path/to/file.docx`
155
+ - Optional Windows/Word COM smoke test for a completed `.docx`.
111
156
 
112
157
  Use these checks before publishing or tagging.
113
158
 
@@ -121,3 +166,6 @@ Use this sequence to understand or modify behavior without reading everything:
121
166
  4. For package wiring issues, inspect `services/standalone-docx-plumbing.js`.
122
167
  5. For revision/comment cleanup behavior, inspect `services/revision-comment-management.js`.
123
168
  6. For numbering/list issues, inspect `services/numbering-helpers.js` and orchestration list-fallback modules.
169
+ 7. For reliability regressions, start with `tests/roundtrip_invariant_tests.mjs`,
170
+ `tests/engine_reliability_tests.mjs`, `tests/paragraph_mark_revision_tests.mjs`,
171
+ `tests/move_revision_tests.mjs`, and `tests/hardening_status_tests.mjs`.
package/README.md CHANGED
@@ -11,12 +11,14 @@ Converts AI-generated or programmatic text/markdown edits into valid Office Open
11
11
  - Lists: generate and edit real Word lists (`w:numPr`) from markdown
12
12
  - Tables: virtual-grid diffing for cell-level edits with merge safety
13
13
  - Comments: inject OOXML comments anchored to text ranges
14
- - Revision management: accept/reject tracked changes by author or for all authors
14
+ - Revision management: detect existing revisions, consume move revisions, and accept/reject tracked changes by author or for all authors
15
15
  - Comment management: delete comments by author or for all authors
16
16
  - Highlights: apply highlight colors to runs
17
17
  - Markdown and OOXML conversion in both directions
18
+ - Status/error result fields for parse, targeting, and existing-revision failures
18
19
  - Package plumbing helpers for numbering.xml, comments.xml, content types, and relationships
19
20
  - Zero host dependencies: works in Node.js, browsers, Deno, and similar JS runtimes with DOM parsing support
21
+ - TypeScript declarations included via `index.d.ts`
20
22
 
21
23
  ## Install
22
24
 
@@ -112,6 +114,21 @@ const result = await applyRedlineToOxml(oxml, original, modified, {
112
114
  | `applyRedlineToOxmlWithListFallback(oxml, original, modified, options)` | Core engine with automatic single-line list structural fallback. |
113
115
  | `reconcileMarkdownTableOoxml(oxml, original, markdownTable, options)` | Table-specific reconciliation helper. |
114
116
 
117
+ Common `applyRedlineToOxml` options:
118
+
119
+ | Option | Purpose |
120
+ |--------|---------|
121
+ | `generateRedlines` | When `true`, emit Word-native tracked changes; when `false`, apply clean text changes. |
122
+ | `author` | Track-change author used for generated revisions. |
123
+ | `existingRevisions` | Policy for source OOXML that already contains tracked changes: `'reject-input'` (default) returns `status: 'error'` with code `EXISTING_REVISIONS`; `'accept-all-first'` accepts existing revisions before applying the new edit. |
124
+
125
+ Common result fields:
126
+
127
+ | Field | Purpose |
128
+ |-------|---------|
129
+ | `status` | Optional non-breaking status: `'ok'`, `'no-op'`, or `'error'`. |
130
+ | `error` | Present when `status === 'error'`; includes a stable `code` such as `PARSE_ERROR`, `TARGET_NOT_FOUND`, or `EXISTING_REVISIONS`. |
131
+
115
132
  ### Pipeline (lower-level access)
116
133
 
117
134
  | Function | Purpose |
@@ -121,14 +138,15 @@ const result = await applyRedlineToOxml(oxml, original, modified, {
121
138
  | `ingestWordOoxmlToMarkdown(oxml)` | Convert OOXML to markdown. |
122
139
  | `ingestOoxml(oxml)` | Flatten OOXML into an internal run model with offsets. |
123
140
  | `preprocessMarkdown(text)` | Normalize markdown and extract format hints. |
141
+ | `containsTrackedChanges(xmlDoc)` | Detect `w:ins`, `w:del`, move revisions, property changes, and paragraph-mark revision markup in a parsed OOXML document/fragment. |
124
142
 
125
143
  ### Services
126
144
 
127
145
  | Function | Purpose |
128
146
  |----------|---------|
129
147
  | `injectCommentsIntoOoxml(oxml, comments, options)` | Add comments anchored to text ranges. |
130
- | `acceptTrackedChangesInOoxml(oxml, { author?, allAuthors? })` | Accept `w:ins` / `w:del` / `*PrChange` revisions for one author or all authors. |
131
- | `rejectTrackedChangesInOoxml(oxml, { author?, allAuthors? })` | Reject `w:ins` / `w:del` / `*PrChange` revisions for one author or all authors. |
148
+ | `acceptTrackedChangesInOoxml(oxml, { author?, allAuthors? })` | Accept `w:ins` / `w:del` / `w:moveFrom` / `w:moveTo` / `*PrChange` revisions for one author or all authors. |
149
+ | `rejectTrackedChangesInOoxml(oxml, { author?, allAuthors? })` | Reject `w:ins` / `w:del` / `w:moveFrom` / `w:moveTo` / `*PrChange` revisions for one author or all authors. |
132
150
  | `deleteCommentsByAuthorInOoxml(oxml, { author?, allAuthors? })` | Delete comments and matching anchors/references for one author or all authors. |
133
151
  | `generateTableOoxml(headers, rows, options)` | Generate a `w:tbl` from tabular data. |
134
152
  | `createDynamicNumberingIdState(numberingXml)` | Allocate numbering IDs without collisions. |
@@ -160,6 +178,8 @@ Different APIs return different OOXML shapes. Use this as a packaging safety che
160
178
  ### Do / Don't for Packaging
161
179
 
162
180
  - Do use `applyOperationToDocumentXml(...).documentXml` when your intent is to replace `word/document.xml`.
181
+ - Redline application now strips non-visible field scaffolding (`w:fldChar`, `w:instrText`) and proofing markers (`w:proofErr`) from the matched target paragraph before diffing, while preserving the visible field result text. This avoids a class of Word-open failures caused by tracked changes spanning hidden field instruction runs.
182
+ - Hyperlinks, bookmarks, comment range markers, tabs/breaks, and footnote/endnote references are treated as structural OOXML that should survive adjacent redline edits instead of being orphaned or wrapped in deletions.
163
183
  - Do use `extractReplacementNodesFromOoxml(...)` when you are consuming `result.oxml` from paragraph/range/table APIs.
164
184
  - Do merge numbering/comments artifacts with `ensureNumberingArtifactsInZip(...)` and `ensureCommentsArtifactsInZip(...)` when those parts are present.
165
185
  - Don't write payloads that start with `<pkg:package` directly into `word/document.xml`.
@@ -217,8 +237,32 @@ await validateDocxPackage(zip);
217
237
  const output = await zip.generateAsync({ type: 'nodebuffer' });
218
238
  ```
219
239
 
240
+ ## Validating Output
241
+
242
+ Run the automated package checks:
243
+
244
+ ```bash
245
+ npm test
246
+ npm run test:isolation
247
+ npm run check:types
248
+ ```
249
+
250
+ For release-time fixture export:
251
+
252
+ ```bash
253
+ node scripts/export-validation-fixtures.mjs
254
+ ```
255
+
256
+ On Windows with desktop Word installed, you can smoke-test a completed `.docx`:
257
+
258
+ ```bash
259
+ npm run smoke:word -- path/to/file.docx
260
+ ```
261
+
220
262
  ## Architecture
221
263
 
222
264
  See [ARCHITECTURE.md](./ARCHITECTURE.md) for module layout, data flow, and contributor guidance.
223
265
 
224
266
  See [AGENTS.md](./AGENTS.md) for a concise reference for AI coding agents.
267
+
268
+ See [docs/VALIDATION.md](./docs/VALIDATION.md) for release-time validation steps.
package/core/types.js CHANGED
@@ -120,12 +120,13 @@ export const NumberSuffix = Object.freeze({
120
120
  * @property {FormatHint[]} formatHints - Position-based format information
121
121
  */
122
122
 
123
- /**
124
- * @typedef {Object} ReconciliationResult
125
- * @property {string} ooxml - The reconciled OOXML output
126
- * @property {boolean} isValid - Whether validation passed
127
- * @property {string[]} warnings - Any warnings during processing
128
- */
123
+ /**
124
+ * @typedef {Object} ReconciliationResult
125
+ * @property {string} ooxml - The reconciled OOXML output
126
+ * @property {boolean} isValid - Whether validation passed
127
+ * @property {string[]} warnings - Any warnings during processing
128
+ * @property {'package'|'document'|'fragment'} [sourceType] - Shape of the OOXML payload when known
129
+ */
129
130
 
130
131
  /**
131
132
  * @typedef {Object} SerializationOptions
@@ -194,8 +195,34 @@ export function createRevisionMetadata(author) {
194
195
  date: getRevisionTimestamp()
195
196
  };
196
197
  }
197
-
198
- /**
198
+
199
+ /**
200
+ * Seeds the revision ID counter above any existing Word revision/comment id values.
201
+ *
202
+ * @param {Document|Element} xmlDoc - Parsed OOXML document or element
203
+ * @returns {number} Next revision id after seeding
204
+ */
205
+ export function seedRevisionIdsFromDocument(xmlDoc) {
206
+ let maxFound = -1;
207
+ const elements = Array.from(xmlDoc?.getElementsByTagName?.('*') || []);
208
+
209
+ for (const element of elements) {
210
+ for (const attr of Array.from(element.attributes || [])) {
211
+ if ((attr.localName || '').toLowerCase() !== 'id') continue;
212
+ const parsed = Number.parseInt(attr.value, 10);
213
+ if (Number.isFinite(parsed)) {
214
+ maxFound = Math.max(maxFound, parsed);
215
+ }
216
+ }
217
+ }
218
+
219
+ if (maxFound >= revisionIdCounter) {
220
+ revisionIdCounter = maxFound + 1;
221
+ }
222
+ return revisionIdCounter;
223
+ }
224
+
225
+ /**
199
226
  * Resets the revision ID counter (for testing)
200
227
  * @param {number} [startValue=1000] - Value to reset to
201
228
  */
@@ -0,0 +1,90 @@
1
+ import { NS_W } from './types.js';
2
+
3
+ /**
4
+ * Returns true when a node is a WordprocessingML element with the given local name.
5
+ *
6
+ * @param {Node|null|undefined} node - Candidate node
7
+ * @param {string} localName - Word local name, for example `r` or `tbl`
8
+ * @returns {boolean}
9
+ */
10
+ export function isWordElement(node, localName) {
11
+ if (!node || node.nodeType !== 1) return false;
12
+ if (node.namespaceURI === NS_W && node.localName === localName) return true;
13
+ const nodeName = String(node.nodeName || '');
14
+ return nodeName === `w:${localName}` || nodeName === localName;
15
+ }
16
+
17
+ /**
18
+ * Creates a WordprocessingML element using namespace-aware DOM APIs when available.
19
+ *
20
+ * @param {Document} xmlDoc - Target document
21
+ * @param {string} qualifiedName - Qualified name, for example `w:r`
22
+ * @returns {Element}
23
+ */
24
+ export function createWordElement(xmlDoc, qualifiedName) {
25
+ return typeof xmlDoc.createElementNS === 'function'
26
+ ? xmlDoc.createElementNS(NS_W, qualifiedName)
27
+ : xmlDoc.createElement(qualifiedName);
28
+ }
29
+
30
+ function wordElementsByLocalName(xmlDoc, localName) {
31
+ const namespaced = Array.from(xmlDoc?.getElementsByTagNameNS?.(NS_W, localName) || []);
32
+ if (namespaced.length > 0) return namespaced;
33
+ return Array.from(xmlDoc?.getElementsByTagName?.('*') || []).filter(node => isWordElement(node, localName));
34
+ }
35
+
36
+ /**
37
+ * Returns true if a document or fragment contains Word tracked-change markup.
38
+ *
39
+ * @param {Document|Element} xmlDoc - Parsed OOXML document or element
40
+ * @returns {boolean}
41
+ */
42
+ export function containsTrackedChanges(xmlDoc) {
43
+ const trackedChangeNames = [
44
+ 'ins',
45
+ 'del',
46
+ 'moveFrom',
47
+ 'moveTo',
48
+ 'moveFromRangeStart',
49
+ 'moveFromRangeEnd',
50
+ 'moveToRangeStart',
51
+ 'moveToRangeEnd',
52
+ 'rPrChange',
53
+ 'pPrChange',
54
+ 'cellIns',
55
+ 'cellDel'
56
+ ];
57
+
58
+ return trackedChangeNames.some(localName => wordElementsByLocalName(xmlDoc, localName).length > 0);
59
+ }
60
+
61
+ /**
62
+ * Classifies the shape of an OOXML payload.
63
+ *
64
+ * @param {string} oxml - OOXML payload
65
+ * @returns {'package'|'document'|'fragment'}
66
+ */
67
+ export function classifyOoxmlSourceType(oxml) {
68
+ const trimmed = String(oxml || '').trim();
69
+ if (/^<\?xml\b[^>]*>\s*<pkg:package\b/i.test(trimmed) || /^<pkg:package\b/i.test(trimmed)) {
70
+ return 'package';
71
+ }
72
+ if (/^<\?xml\b[^>]*>\s*<(?:w:)?document\b/i.test(trimmed) || /^<(?:w:)?document\b/i.test(trimmed)) {
73
+ return 'document';
74
+ }
75
+ return 'fragment';
76
+ }
77
+
78
+ /**
79
+ * Adds `sourceType` metadata to OOXML result objects without changing payloads.
80
+ *
81
+ * @template T
82
+ * @param {T & { oxml?: string, sourceType?: 'package'|'document'|'fragment' }} result - Result object
83
+ * @returns {T & { sourceType?: 'package'|'document'|'fragment' }}
84
+ */
85
+ export function withOoxmlSourceType(result) {
86
+ if (!result || typeof result !== 'object' || result.sourceType || typeof result.oxml !== 'string') {
87
+ return result;
88
+ }
89
+ return { ...result, sourceType: classifyOoxmlSourceType(result.oxml) };
90
+ }