@ansonlai/docx-redline-js 0.2.1 → 0.4.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.
Files changed (86) hide show
  1. package/AGENTS.md +36 -10
  2. package/README.md +83 -6
  3. package/adapters/xml-adapter.js +73 -10
  4. package/core/list-targeting.js +3 -0
  5. package/core/paragraph-targeting.js +33 -7
  6. package/core/redline-validation.js +22 -0
  7. package/core/types.js +122 -27
  8. package/core/xml-query.js +3 -1
  9. package/dist/docx-redline-js.esm.js +1124 -545
  10. package/dist/docx-redline-js.esm.js.map +4 -4
  11. package/dist/docx-redline-js.esm.min.js +79 -78
  12. package/dist/docx-redline-js.esm.min.js.map +4 -4
  13. package/docs/TESTING.md +687 -0
  14. package/docs/VALIDATION.md +81 -2
  15. package/docs/WORD-MANUAL-REVIEW.md +138 -0
  16. package/docs/plans/2026-08-30-reliability-testing-improvements.md +488 -0
  17. package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +210 -0
  18. package/docs/plans/{2026-03-01-release-0.1.4-design.md → completed/2026-03-01-release-0.1.4-design.md} +2 -0
  19. package/docs/plans/{2026-03-01-release-0.1.4.md → completed/2026-03-01-release-0.1.4.md} +5 -3
  20. package/docs/plans/{2026-05-31-architectural changes.md → completed/2026-05-31-architectural changes.md } +2 -0
  21. package/docs/plans/completed/2026-08-02-reliability-improvements.md +1155 -0
  22. package/docs/test-comparison-dashboard.html +95 -0
  23. package/docs/validation-reports/2026-08-30-phase-1-word-visual-preflight.md +22 -0
  24. package/docs/validation-reports/2026-08-30-phase-2-word-visual-preflight.md +24 -0
  25. package/docs/validation-reports/2026-08-30-phase-3-coverage.md +73 -0
  26. package/docs/validation-reports/2026-09-02-multilevel-bullets-visual-review.md +82 -0
  27. package/docs/validation-reports/2026-09-02-multimodal-visual-samples.md +114 -0
  28. package/docs/validation-reports/2026-09-02-visual-failures-preflight.md +79 -0
  29. package/engine/format-extraction.js +1 -1
  30. package/engine/formatting-removal.js +84 -99
  31. package/engine/oxml-engine.js +176 -83
  32. package/engine/reconstruction-mapper.js +276 -79
  33. package/engine/reconstruction-mode.js +20 -6
  34. package/engine/reconstruction-writer.js +117 -72
  35. package/engine/run-builders.js +3 -3
  36. package/engine/surgical-mode.js +3 -2
  37. package/engine/table-mode.js +27 -16
  38. package/index.d.ts +95 -3
  39. package/index.js +14 -13
  40. package/orchestration/list-structural-fallback.js +16 -39
  41. package/package.json +22 -4
  42. package/pipeline/diff-engine.js +174 -55
  43. package/pipeline/ingestion-export.js +39 -24
  44. package/pipeline/ingestion-paragraph.js +7 -5
  45. package/pipeline/list-generation.js +27 -18
  46. package/pipeline/patching.js +2 -3
  47. package/pipeline/pipeline.js +65 -36
  48. package/pipeline/serialization.js +13 -5
  49. package/scripts/build-test-dashboard.mjs +43 -0
  50. package/scripts/check-types.mjs +16 -24
  51. package/scripts/export-validation-fixtures.mjs +191 -45
  52. package/scripts/fetch-superdoc-corpus.mjs +61 -0
  53. package/scripts/generate-test-dashboard.mjs +199 -0
  54. package/scripts/inspect-visual-evidence.mjs +271 -0
  55. package/scripts/lib/minimal-zip.mjs +199 -18
  56. package/scripts/lib/word-coverage-catalogue.mjs +207 -0
  57. package/scripts/lib/word-coverage-metadata.mjs +93 -0
  58. package/scripts/lib/zip-reader.mjs +64 -0
  59. package/scripts/package-superdoc-word-fixtures.ps1 +64 -0
  60. package/scripts/prepare-corpus-word-visual-review.mjs +84 -0
  61. package/scripts/prepare-superdoc-word-corpus.mjs +284 -0
  62. package/scripts/prepare-word-review.mjs +77 -0
  63. package/scripts/prepare-word-visual-review.mjs +90 -0
  64. package/scripts/render-agenda-multilevel.mjs +70 -0
  65. package/scripts/render-case22.mjs +73 -0
  66. package/scripts/render-case40.ps1 +35 -0
  67. package/scripts/render-multilevel-bullet-images.py +58 -0
  68. package/scripts/render-multilevel-bullet-visual.ps1 +32 -0
  69. package/scripts/render-multilevel-cases.mjs +80 -0
  70. package/scripts/report-coverage-gaps.mjs +103 -0
  71. package/scripts/report-word-coverage.mjs +71 -0
  72. package/scripts/sample-multimodal-visual-check.mjs +221 -0
  73. package/scripts/test-multilevel-bullet-visual.mjs +187 -0
  74. package/scripts/word-com-corpus-suite.ps1 +43 -0
  75. package/scripts/word-com-corpus-visual-suite.ps1 +116 -0
  76. package/scripts/word-com-differential.ps1 +158 -16
  77. package/scripts/word-com-suite.ps1 +19 -0
  78. package/scripts/word-com-visual-suite.ps1 +132 -0
  79. package/services/comment-engine.js +51 -46
  80. package/services/comment-locator.js +0 -1
  81. package/services/comment-package.js +11 -10
  82. package/services/numbering-service.js +1 -1
  83. package/services/revision-comment-management.js +31 -10
  84. package/services/standalone-docx-plumbing.js +45 -34
  85. package/services/standalone-operation-runner.js +315 -75
  86. package/services/table-reconciliation.js +23 -11
@@ -0,0 +1,22 @@
1
+ # Phase 1 Microsoft Word visual preflight — 2026-08-30
2
+
3
+ - Review type: AI visual preflight (advisory; not human release sign-off)
4
+ - Word version/build: 16.0 / 16.0.20326
5
+ - Automated synthetic differential: 28/28 passed
6
+ - Automated reviewed SuperDoc differential: 20/20 passed
7
+ - Render method: Microsoft Word `ExportAsFixedFormat`, once as final document
8
+ content and once as document-with-markup; every generated page image was
9
+ inspected.
10
+ - Screenshot storage: ignored
11
+ `tmp/word-validation/visual-review/*-page-1.png`
12
+
13
+ | Case | Views inspected | Result | Notes |
14
+ |---|---|---|---|
15
+ | `administrative-tab-aligned-status` | Final, markup | Pass | Two tab stops retain visible column alignment; the Draft/Final replacement is localized. |
16
+ | `administrative-boundary-tabs-preserved` | Final, markup | Pass | Leading indentation remains visible; the trailing tab remains structurally asserted; replacement markup is localized. |
17
+ | `legal-locked-field-adjacent-replacement` | Final, markup | Pass after fix | Initial markup exposed a duplicated PAGE result caused by moving the cached result outside the field. Zero-width field sentinels were corrected; repeat rendering shows one unrevised page number and only `amended` inserted. |
18
+
19
+ Word COM independently performed Accept All and Reject All for every synthetic
20
+ case and compared exact resulting text. This preflight reviewed final and markup
21
+ appearance; it does not replace the three-view human release checklist in
22
+ `docs/WORD-MANUAL-REVIEW.md`.
@@ -0,0 +1,24 @@
1
+ # Phase 2 Microsoft Word visual preflight — 2026-08-30
2
+
3
+ - Review type: AI visual preflight (advisory; not human release sign-off)
4
+ - Word version/build: 16.0 / 16.0.20326
5
+ - Automated synthetic differential: 33/33 passed
6
+ - Automated reviewed SuperDoc differential: 20/20 passed
7
+ - Render method: Microsoft Word `ExportAsFixedFormat`, as final content and as
8
+ document-with-markup; every generated page image was inspected.
9
+ - Screenshot storage: ignored
10
+ `tmp/word-validation/phase2-visual-review/*-page-1.png`
11
+
12
+ | Case | Views inspected | Result | Notes |
13
+ |---|---|---|---|
14
+ | `administrative-comment-anchor-adjacent-replacement` | Final, markup | Pass | Comment text renders in a connected balloon anchored to “Agency decision”; replacement markup remains localized. |
15
+ | `administrative-footnote-adjacent-deadline` | Final, markup | Pass after fixture refinement | Footnote separator, superscript reference, and note body render cleanly; Friday/Monday replacement remains adjacent without losing the reference. |
16
+ | `legal-endnote-adjacent-duration` | Final, markup | Pass after fixture refinement | Word renders its default roman endnote reference and note body; two/three replacement remains localized. |
17
+ | `administrative-header-footer-package` | Final, markup | Pass | Header and footer both render in their expected page regions and remain unaffected by the body revision. |
18
+ | `legal-external-hyperlink-adjacent-replacement` | Final, markup | Pass | Hyperlink remains blue and underlined; ten/fifteen replacement does not absorb or restyle it. |
19
+
20
+ The first pass showed baseline note-reference characters because the minimal
21
+ package deliberately has no styles part. The fixtures were changed to carry
22
+ direct superscript formatting and visible note-body references, then rerendered.
23
+ Word COM independently verified Accept All and Reject All. This report does not
24
+ replace the human release checklist in `docs/WORD-MANUAL-REVIEW.md`.
@@ -0,0 +1,73 @@
1
+ # Phase 3 behavior-focused coverage report — 2026-08-30
2
+
3
+ Phase 3 used `coverage/coverage-final.json` and `npm run coverage:gaps` to count
4
+ production functions and branches. The checked baseline is
5
+ `tests/coverage-data/phase3-baseline.json`; retained P0/P1 gaps are classified in
6
+ `tests/coverage-data/phase3-reviewed-gaps.json`.
7
+
8
+ ## Outcome
9
+
10
+ - Production functions: **437/540 → 496/542**. At least 59 previously
11
+ unexecuted functions now have behavior-driven hits, exceeding the 40-function
12
+ milestone. V8 discovered two additional nested callback functions when their
13
+ containing paths first executed.
14
+ - Production branches: **2333/3351 → 2919/3956**. Covered branches increased by
15
+ 586. V8 exposes additional branch sites when previously cold functions run,
16
+ so both numerator and denominator grow; the final production ratio is 73.79%.
17
+ - Full c8 snapshot: **87.92% statements/lines, 73.90% branches, 91.80%
18
+ functions**.
19
+ - No target file lost covered functions or branches relative to the baseline.
20
+ - No production defect was discovered, so Phase 3 required no compatibility or
21
+ behavior change.
22
+
23
+ ## Target files
24
+
25
+ | Priority | File | Functions before → after | Covered branches before → after |
26
+ |---|---|---:|---:|
27
+ | P0 | `services/numbering-helpers.js` | 0/18 → 18/18 | 1 → 109 |
28
+ | P0 | `orchestration/route-plan.js` | 0/6 → 6/6 | 1 → 37 |
29
+ | P0 | `orchestration/list-markdown.js` | 0/6 → 6/6 | 1 → 39 |
30
+ | P0 | `pipeline/patching.js` | 6/12 → 12/12 | 36 → 121 |
31
+ | P0 | `engine/format-span-application.js` | 2/5 → 5/5 | 10 → 40 |
32
+ | P1 | `orchestration/list-structural-fallback.js` | 8/17 → 17/17 | 44 → 162 |
33
+ | P1 | `engine/table-mode.js` | 2/3 → 3/3 | 6 → 13 |
34
+ | P1 | `core/table-targeting.js` | 7/11 → 11/11 | 22 → 99 |
35
+ | P1 | `services/standalone-operation-runner.js` | 41/50 → 42/52 | 255 → 284 |
36
+ | P1 | `pipeline/pipeline.js` | 7/12 → 12/12 | 28 → 57 |
37
+
38
+ Branch totals are intentionally omitted from the last column because V8 did
39
+ not expose all cold-function branch sites at baseline. The checked regression
40
+ gate compares covered counts, while the JSON report retains current totals.
41
+
42
+ ## Behavior matrices added
43
+
44
+ - Numbering: missing/malformed parts, identifier collisions, preferred-range
45
+ overflow, independent-document determinism, pair reservation, paragraph
46
+ reference overwrite/extraction, explicit starts, payload remapping, schema
47
+ ordering, duplicate idempotence, and malformed merge fallback.
48
+ - Routing and list markdown: every route kind, list/table precedence, literal
49
+ escapes, empty and block inputs, ordered/unordered lists, nesting/outdenting,
50
+ marker stripping, decimal/alpha/Roman styles, and alpha rollover past Z.
51
+ - Patching and format spans: offset zero/end, run boundaries, whitespace,
52
+ hyperlink/structural runs, insert/delete/equal, containers, multiline list
53
+ insertion, style selection, overlapping hints, empty spans, and final-character
54
+ formatting with tracked/untracked output assertions.
55
+ - Structural list fallback: detection/rejection, existing numbering, explicit
56
+ sequence reuse/reset, binding cleanup/preservation, injected generation
57
+ failure, start overrides, and trailing paragraph cleanup.
58
+ - Tables and pipeline: paragraph-block inference, symmetric row insertion,
59
+ ambiguity/no-mutation, nested tables, table no-ops, validation modes, web
60
+ yielding, wrapping, indentation, and valid/invalid table generation.
61
+ - Standalone runner: highlighting, localized preservation, stop-on-error,
62
+ atomic artifact rollback, non-atomic retention, and malformed-document errors.
63
+ Existing tests continue to cover mixed ordering, comments, numbering artifacts,
64
+ target snapshot invalidation, and full continue-on-error batches.
65
+
66
+ ## Reviewed retained gaps
67
+
68
+ All P0 functions are covered. The ten remaining P1 function entries are nine
69
+ empty default logging callbacks and one last-resort paragraph constructor whose
70
+ failure prerequisite cannot be injected through a supported API. They contain
71
+ no untested routing or identifier behavior and are retained for diagnostics and
72
+ content-preserving defense. Exact locations and classifications are stored in
73
+ `tests/coverage-data/phase3-reviewed-gaps.json`.
@@ -0,0 +1,82 @@
1
+ # Multi-Level Bullets & Numbered Lists Visual & Non-Visual Review — 2026-09-02
2
+
3
+ - **Evaluation Type:** Multi-Level List Verification (Visual + Non-Visual Semantic Suite)
4
+ - **Target Structure:** Bullets and Numbered Lists with Multiple Changes Across Multiple Levels (Levels 0, 1, 2)
5
+ - **Renderer:** Microsoft Word 16.0 (Build 16.0.20326) via COM `ExportAsFixedFormat`
6
+ - **Non-Visual Test Suite:** [`tests/multilevel_bullet_tests.mjs`](../../tests/multilevel_bullet_tests.mjs) (5/5 passing)
7
+ - **Visual Evidence Directory:** `tmp/multilevel-bullet-visual/`
8
+
9
+ ---
10
+
11
+ ## Executive Summary
12
+
13
+ Nested lists with changes at multiple levels represent one of the highest-complexity challenges in OOXML tracked changes. Potential failure modes include:
14
+ 1. **Level Flattening:** Edits inside nested items (Level 1 or 2) dropping `w:ilvl`, causing Word to render the item at the margin (Level 0).
15
+ 2. **Double Promotion:** Child bullets indented with leading spaces receiving an additional level bump, placing them at Level 3 instead of Level 2.
16
+ 3. **Ghost Bullets:** Deleting or inserting items leaving orphaned `<w:pPr>` paragraph markers that render as empty bullets in Word.
17
+ 4. **Numbering Stream Disruption:** Inserting a sub-item disrupting the sequential numbering of sibling items or outer parent items.
18
+
19
+ To thoroughly address this, we implemented a dual validation strategy:
20
+ - **Non-Visual Suite:** 5 automated tests in [`tests/multilevel_bullet_tests.mjs`](../../tests/multilevel_bullet_tests.mjs) asserting on exact OOXML `w:numPr` (`w:numId`, `w:ilvl`), paragraph mark tracking, format boundary isolation, and accept/reject symmetry.
21
+ - **Visual Suite:** Direct desktop Microsoft Word COM rendering across 3 layout-sensitive multi-level test cases (both synthetic and authentic legal/administrative documents), inspected with multimodal vision across `allMarkup`, `acceptAll`, and `rejectAll` views.
22
+
23
+ ---
24
+
25
+ ## Non-Visual Semantic Test Suite
26
+
27
+ All 5 tests in [`tests/multilevel_bullet_tests.mjs`](../../tests/multilevel_bullet_tests.mjs) execute as part of `npm test`:
28
+
29
+ | Test Name | Scenario & Coverage | Assertions & Invariants | Result |
30
+ |---|---|---|---|
31
+ | `testNestedChildInsertionAtMultipleLevels` | Inserting a Level 2 bullet under a Level 1 parent item. | Verifies `ilvl="2"` assigned to child; `acceptAll` yields 7 paragraphs with levels `[0, 1, 2, 2, 2, 1, 0]`; `rejectAll` restores original 6 paragraphs. | **PASS** |
32
+ | `testConcurrentEditsAcrossAllThreeLevels` | Batch of concurrent edits modifying Level 0, Level 1, and two Level 2 items simultaneously. | Verifies all 3 levels retain exact `ilvl` without level drift (`[0, 1, 2, 2, 1, 0]`); text replacement exact; zero formatting leakage. | **PASS** |
33
+ | `testLevelFlatteningGuard` | Directly guards against the visual defect of level flattening when editing a nested item. | Asserts that modifying a Level 2 item strictly maintains `w:ilvl w:val="2"` and never omits `w:ilvl`. | **PASS** |
34
+ | `testFormattingPreservationAcrossLevels` | Applying bold markdown (`**text**`) to a Level 2 item. | Verifies Level 2 item retains `ilvl="2"`, while parent Level 1 and sibling Level 2 items contain no bold run properties. | **PASS** |
35
+ | `testMultiLevelRangeDeletionAndAcceptance` | Consolidating two adjacent Level 2 items into a single modified item. | Verifies paragraph count reduces from 6 to 5; remaining consolidated item retains `ilvl="2"`; rejection cleanly restores both items. | **PASS** |
36
+
37
+ ### Core Engine Improvement: Child Promotion Guard
38
+ During development, we identified and fixed a subtle double-promotion bug in [`core/list-targeting.js`](../../core/list-targeting.js#L123): when an author provided markdown input that was *already* indented with leading spaces (e.g. ` - Sub-item`), `resolveInsertionLevel` placed it at `anchorLevel + 1`. The subsequent `shouldPromoteBulletInsertionsToChildDepth` check added a second level increment, pushing it to `anchorLevel + 2` (`ilvl=3`). Adding an `alreadyIndented` check prevents this double bump, ensuring child bullets consistently settle at `ilvl=2`.
39
+
40
+ ---
41
+
42
+ ## Visual Word COM Evidence & Multimodal Inspection
43
+
44
+ We rendered three layout-sensitive multi-level list documents in desktop Microsoft Word 16.0:
45
+
46
+ ### Case 1: Synthetic Multi-Level List (`administrative-list-change-nested-child`)
47
+ - **Structure:** 2-level numbered outline (Level 0 items `1.`, `2.`, and Level 1 sub-items `1.`, `2.`, `3.`).
48
+ - **Operation:** Inserting a new child item (`Escalate unresolved notifications.`) directly below `Disclosure obligations.`
49
+ - **Visual Observations:**
50
+ - **All Markup:** Red underlined insertion appears at the exact child indent; sibling items below (`Notify affected parties.`, `Preserve supporting records.`) shift down smoothly with renumbering strikes (`1.2.`, `2.3.`). Margin revision bar is present.
51
+ - **Accept All:** Sub-items cleanly renumber `1.`, `2.`, `3.` under Item 1; Item 2 (`Remediation obligations.`) remains at the outer margin.
52
+ - **Reject All:** Exactly restores original 4 paragraphs without blank space or ghost markers.
53
+ - **Verdict:** **PASS**
54
+
55
+ ### Case 2: Municipal Board Agenda (`superdoc:administrative-list-change-board-agenda-multiple-children`)
56
+ - **Structure:** Mixed hierarchy with lettered sections (`A.`, `B.`, `C.`, `D.`), numbered reports (`1.`, `2.`, `3.`, `4.`, `5.`), and dash/bullet sub-items.
57
+ - **Operation:** Inserting two new bullet children (`Review meter replacement progress.`, `Confirm hydrant inspection dates.`) under `2. Water District Report`.
58
+ - **Visual Observations:**
59
+ - **All Markup:** The two newly inserted bullets appear with red underline and bullet glyphs (`•`) at hanging indent 0.5 in. Margin bar indicates change.
60
+ - **Accept All:** Bullets line up with sub-item indent; numbered items `3. Code Enforcement`, `4. WWTP Report`, `5. Tax Collector Report` maintain their outer alignment and numbering.
61
+ - **Reject All:** Returns to exact 5 numbered report items with zero extra bullets.
62
+ - **Verdict:** **PASS**
63
+
64
+ ### Case 3: Corporate Bylaws Multi-Level Batch (`superdoc:legal-bylaws-nested-list-batch`)
65
+ - **Structure:** 7-page legal bylaws document containing Roman numeral Articles (`I.`, `II.`, `III.`), lettered sections (`A. Officers`, `B. Time of Election`), and bullet items.
66
+ - **Operation:** Concurrent multi-level edits on Page 2:
67
+ - Level 0: Section II title text replacement (`comprised` -> `composed`).
68
+ - Level 1: Four bullet item edits under Section II removing parenthesized numbers and updating city representation text.
69
+ - Level 1: Subsection A title text update (`Vice-Chair` hyphenation).
70
+ - **Visual Observations:**
71
+ - **All Markup:** Red strikethroughs and additions render inline within the bullets; hanging indents for Roman numerals (`II.`, `III.`) and bullet points remain completely aligned.
72
+ - **Accept All:** Text flows seamlessly; bullet points retain uniform left margin; subsection letters `A.` and `B.` align with Section III text.
73
+ - **Pagination:** Exactly 7 pages across all 3 views (`allMarkup`, `acceptAll`, `rejectAll`).
74
+ - **Verdict:** **PASS**
75
+
76
+ ---
77
+
78
+ ## Conclusion
79
+
80
+ The dual test suite confirms that `@ansonlai/docx-redline-js` robustly handles multi-level lists with concurrent edits across multiple levels:
81
+ 1. `w:ilvl` values are strictly preserved across single-item edits, subtree additions, and range consolidations.
82
+ 2. Microsoft Word renders the resulting OOXML with exact typography, proper hanging indents, and zero ghost markers or level collapsing.
@@ -0,0 +1,114 @@
1
+ # Multimodal LLM Visual Inspection Samples Report — 2026-09-02
2
+
3
+ - **Evaluation Type:** Multimodal LLM Visual Inspection (Real-Document Spot Check)
4
+ - **Source Corpus:** SuperDoc Legal & Administrative Corpus
5
+ - **Evaluator:** Multimodal Vision AI Model
6
+ - **Target Samples:** 3 Real Documents (Municipal Board Agenda, Parish Council Financial Minutes, City Zoning Ordinance)
7
+ - **Renderer:** Microsoft Word 16.0 (Build 16.0.20326) via `ExportAsFixedFormat`
8
+ - **Sample Generation Command:** `npm run test:visual:sample` / `node scripts/sample-multimodal-visual-check.mjs`
9
+
10
+ ---
11
+
12
+ ## Executive Summary
13
+
14
+ As defined in [`docs/TESTING.md`](../TESTING.md#multimodal-llm-visual-inspection-real-document-spot-checks), multimodal LLM visual inspection is an on-demand, sampled evaluation conducted on real-world documents to detect subtle layout, table, list, and typographical regressions that programmatic string/XML diffs cannot observe.
15
+
16
+ This report documents a complete end-to-end execution of the multimodal visual spot-check workflow across three authentic documents representing different layout complexities:
17
+
18
+ | Sample | Scenario Identity | Category & Shape | Document Size | Visual Evaluation |
19
+ |---|---|---|---|---|
20
+ | **Sample 1** | `superdoc:administrative-multi-bullet-board-agenda` | Administrative / List (Logo & Mixed Hierarchy) | 1 Page | **PASS** — Flawless list levels, zero ghost markers, image header intact |
21
+ | **Sample 2** | `superdoc:administrative-multi-table-council-minutes` | Administrative / Table (Financial Grid) | 2 Pages | **PASS** — Shaded table header, grid lines, and column widths preserved |
22
+ | **Sample 3** | `superdoc:legal-complex-zoning-list-table-batch` | Legal / Multi-Page Complex Ordinance | 25 Pages | **PASS** — Page 4 sub-clauses aligned, font size stable, 25-page count preserved |
23
+
24
+ ---
25
+
26
+ ## Detailed Sample Evaluations
27
+
28
+ ### Sample 1: Municipal Board Agenda (`administrative-multi-bullet-board-agenda`)
29
+
30
+ - **Document Type:** Town of Prattsville Board Agenda (Public Meeting)
31
+ - **Visual Features:** Graphic logo header (`PRATTSVILLE Est. 1824`), lettered agenda sections (A., B., C., D.), dash sub-bullets, and numbered report sections (1–5, 1–8).
32
+ - **Operations Evaluated:**
33
+ - Item A: Insertion of `THE` (`CALL THE MEETING TO ORDER`).
34
+ - Item C: Replacement of `FINANCIALS` with `FINANCIAL` (`APPROVAL OF THE MONTHLY FINANCIAL REPORT`).
35
+ - Item D: Replacement of `/ CORRESPONDENCE FROM THE COMMUNITY` with `AND COMMUNITY CORRESPONDENCE`.
36
+
37
+ #### Multimodal Visual Checklist:
38
+
39
+ 1. **Markup Visibility (`allMarkup`):**
40
+ - Insertions (`THE`, `FINANCIAL`, `AND COMMUNITY CORRESPONDENCE`) appear cleanly in red underline.
41
+ - Deletions (`FINANCIALS`, `/ CORRESPONDENCE FROM THE COMMUNITY`) show distinct strikethrough without clipping surrounding text.
42
+ - Revision change bars are correctly rendered in the left margin.
43
+ 2. **Accepted State (`acceptAll`):**
44
+ - Text flows naturally with correct single spacing between words.
45
+ - Line wrapping on Item D (`D. COMMENTS FROM THE FLOOR AND COMMUNITY\nCORRESPONDENCE`) conforms cleanly to document margins.
46
+ - Zero stray revision artifacts or misplaced spaces.
47
+ 3. **Rejected State (`rejectAll`):**
48
+ - Accurately restores the exact original wording and layout.
49
+ - Dash bullets (`- Pledge of Allegiance`, `- Roll Call`) and numbered items remain in place without marker displacement.
50
+ 4. **Structural Integrity:**
51
+ - The graphic logo and centered meeting header remain in identical coordinate positions.
52
+ - **Verdict:** **PASS**
53
+
54
+ ---
55
+
56
+ ### Sample 2: Parish Council Financial Minutes (`administrative-multi-table-council-minutes`)
57
+
58
+ - **Document Type:** Cholmondeley & Chorley Parish Council Minutes
59
+ - **Visual Features:** Multi-table document with light-blue shaded header rows, grid borders, numeric currency columns (`Amount`), multi-line descriptions, and two-column attendee headers.
60
+ - **Operations Evaluated:**
61
+ - Table Header: Replacement of `date` with `Date` (`Invoice Date`).
62
+ - Row 1: Replacement of `Payroll` with `Payroll,` (`Payroll, April 2023-March 2024`).
63
+ - Row 3: Replacement of `Jan-March2024tax,` with `, January-March 2024` (`HMRC tax, January-March 2024`).
64
+
65
+ #### Multimodal Visual Checklist:
66
+
67
+ 1. **Markup Visibility (`allMarkup`):**
68
+ - Strikethroughs and red underlines inside table cells remain strictly contained within their respective cells.
69
+ - No text overflow into adjacent columns (`Account name`, `Amount`).
70
+ 2. **Table Grid & Cell Properties:**
71
+ - Header shading (`fill="#EBF1F5"`) remains unbroken across all four columns.
72
+ - Column widths remain stable; the narrow `Amount` column does not expand or collapse.
73
+ - Outer border and interior cell gridlines render continuously without hairline gaps.
74
+ 3. **Accepted State (`acceptAll`):**
75
+ - "Invoice Date" renders with proper capitalization and bold formatting.
76
+ - Punctuation and spacing around currency amounts and date ranges are clean and readable.
77
+ 4. **Page Layout:**
78
+ - Two-column layout at top (`PRESENT:` vs `Vice Chair`, `APPOLOGIES:` vs `Chairman`) is undisturbed.
79
+ - Document length is exactly 2 pages with centered footer (`Page: 1`, `Page: 2`).
80
+ - **Verdict:** **PASS**
81
+
82
+ ---
83
+
84
+ ### Sample 3: Municipal Zoning Ordinance Resolution (`legal-complex-zoning-list-table-batch`)
85
+
86
+ - **Document Type:** City of Cupertino Planning Commission Draft Resolution (25 Pages)
87
+ - **Visual Features:** Formal legal resolution apparatus with double-line borders, legal definitions, nested statutory citations, and zoning schedule tables.
88
+ - **Operations Evaluated (Page 4):**
89
+ - Definition "Accessory dwelling unit", sub-item 1: Insertion of `the California` (`...Section 17958.1 of the California Health and Safety Code.`).
90
+ - Definition "Accessory dwelling unit", sub-item 2: Insertion of `California` (`...Section 18007 of the California Health and Safety Code.`).
91
+
92
+ #### Multimodal Visual Checklist:
93
+
94
+ 1. **Markup Visibility (`allMarkup`):**
95
+ - Red underlined additions appear precisely adjacent to statutory section numbers on page 4.
96
+ - Vertical revision bars appear cleanly in the left margin.
97
+ 2. **Accepted State (`acceptAll`):**
98
+ - Statutory phrases flow seamlessly into the legal definitions.
99
+ - List numbering (`1.`, `2.`) maintains uniform hanging indent alignment.
100
+ - Font family and point size remain completely uniform with the surrounding body text (no unstyled font fallback).
101
+ 3. **Document-Wide Pagination Stability:**
102
+ - Across the entire 25-page document, page counts are identical (25 pages in `allMarkup`, 25 pages in `acceptAll`, 25 pages in `rejectAll`).
103
+ - Centered footer (`- 4 -`) and top section headings (`SECTION 1. Section 19.08.030...`) remain aligned.
104
+ - **Verdict:** **PASS**
105
+
106
+ ---
107
+
108
+ ## Conclusion & Recommendations
109
+
110
+ The multimodal visual spot checks confirmed that desktop Microsoft Word renders the engine's OOXML output with complete layout, typographical, and structural fidelity:
111
+ - Table grids, borders, and cell background shading are preserved.
112
+ - Mixed list hierarchies (dashes, numbers, letters) maintain exact hanging indents without ghost markers.
113
+ - Long-document pagination (up to 25 pages) does not drift across revision states.
114
+ - Running `npm run test:visual:sample` provides a fast, repeatable mechanism to package and inspect real-document spot checks on demand.
@@ -0,0 +1,79 @@
1
+ # Microsoft Word Visual Failures & Preflight Report — 2026-09-02
2
+
3
+ - **Review type:** AI visual preflight (advisory; not human release sign-off)
4
+ - **Word version/build:** 16.0 / 16.0.20326
5
+ - **Test execution date:** 2026-09-02
6
+ - **Coverage scope:**
7
+ - Automated semantic visual failure regression suite ([`tests/visual_failure_regression_tests.mjs`](file:///c:/Users/Phara/Desktop/Projects/Docx%20Redline%20JS/tests/visual_failure_regression_tests.mjs)): 9/9 passed
8
+ - Synthetic layout-sensitive fixtures rendered: 30 cases (90 PDFs across `allMarkup`, `acceptAll`, `rejectAll`)
9
+ - SuperDoc real-document scenarios rendered: 40 cases (120 PDFs across `allMarkup`, `acceptAll`, `rejectAll`)
10
+ - Automated visual evidence inspection ([`scripts/inspect-visual-evidence.mjs`](file:///c:/Users/Phara/Desktop/Projects/Docx%20Redline%20JS/scripts/inspect-visual-evidence.mjs)): 70/70 rendered, 70/70 valid, 0 anomalies
11
+ - Contact sheets & page renders generated in `tmp/word-visual-review/inspected-sheets/`
12
+
13
+ ---
14
+
15
+ ## 1. Automated Semantic Visual Failure Regressions
16
+
17
+ To prevent visual failures before documents reach Word, [`tests/visual_failure_regression_tests.mjs`](file:///c:/Users/Phara/Desktop/Projects/Docx%20Redline%20JS/tests/visual_failure_regression_tests.mjs) tests the OOXML properties that govern Word layout and rendering:
18
+
19
+ | Visual Failure Mode | Failure Mechanism in Word | Semantic Guardrail Assertion | Status |
20
+ |---|---|---|---|
21
+ | **Footnote Superscript Bleed** | Replacement adjacent to footnote reference inherits `w:vertAlign="superscript"`, shrinking and raising normal text. | Insertion runs explicitly forbid `w:vertAlign`. | **Pass** |
22
+ | **Highlight Bleed** | Edits adjacent to highlighted runs bleed highlight onto plain text. | Plain insertion runs forbid `w:highlight`. | **Pass** |
23
+ | **Underline Bleed** | Edits adjacent to underlined titles or terms bleed underline into surrounding text. | Trailing insertion runs forbid `w:u`. | **Pass** |
24
+ | **Font & Size Reset (Hyperlink Boundary)** | Edits crossing hyperlinks lose run properties, causing Word to fall back from 14pt Georgia to 12pt Normal default. | Insertion runs retain explicit `w:sz="28"` and `w:rFonts w:ascii="Georgia"`. | **Pass** |
25
+ | **Heading Style Reset** | Reconstructing a heading drops `w:pStyle="Heading1"` or bold styling, causing headings to render as body text. | Paragraph preserves `w:pStyle="Heading1"` and insertion retains bold and 16pt size. | **Pass** |
26
+ | **Ghost Markers (List Insertion Rejection)** | Rejecting an inserted list item that tracked only text leaves an untracked paragraph mark, rendering an empty bullet/number in Word. | Paragraph count returns to exact original (1 paragraph); no empty marker remains. | **Pass** |
27
+ | **Ghost Markers (List Item Deletion)** | Deleting a list item without tracking the paragraph mark leaves an empty bullet in Word. | Accepted deletion leaves exactly the remaining paragraphs without empty markers. | **Pass** |
28
+ | **List Numbering & Level Corruption** | Adding a list item to an existing numbered list assigns wrong `w:numId` or `w:ilvl`, breaking sequence in Word. | Inserted item retains original `w:numId` and `w:ilvl`. | **Pass** |
29
+ | **Table Cell Property Destruction** | Editing cell text strips `w:tcPr`, losing cell width, borders, background shading, and vertical alignment. | `w:tcPr` (`tcW`, `tcBorders`, `shd`, `vAlign`) survives document editing intact. | **Pass** |
30
+
31
+ ---
32
+
33
+ ## 2. Microsoft Word Visual Inspection (Sample of 20 Representative Cases)
34
+
35
+ Word COM rendered all three views (`allMarkup`, `acceptAll`, `rejectAll`) via `ExportAsFixedFormat`. Contact sheets and individual pages were visually inspected:
36
+
37
+ | Case Identity | Category & Shape | Views Inspected | Visual Result | Notes |
38
+ |---|---|---|---|---|
39
+ | `synthetic:administrative-header-footer-package` | Administrative / header-footer | Markup, Accept, Reject | **Pass** | Header and footer render cleanly in top/bottom margins; body text revision is completely isolated. |
40
+ | `synthetic:administrative-tab-aligned-status` | Administrative / tab-break | Markup, Accept, Reject | **Pass** | Column alignment at tab stops is preserved across revisions. |
41
+ | `synthetic:administrative-boundary-tabs-preserved` | Administrative / tab-break | Markup, Accept, Reject | **Pass** | Leading tab indentation preserved; trailing tab remains intact. |
42
+ | `synthetic:administrative-comment-anchor-adjacent-replacement` | Administrative / comment | Markup, Accept, Reject | **Pass** | Comment balloon anchors cleanly to "Agency decision"; replacement text localized. |
43
+ | `synthetic:administrative-footnote-adjacent-deadline` | Administrative / note | Markup, Accept, Reject | **Pass** | Superscript footnote reference and note separator render cleanly without displacement. |
44
+ | `synthetic:legal-endnote-adjacent-duration` | Legal / note | Markup, Accept, Reject | **Pass** | Endnote reference renders cleanly; replacement remains adjacent. |
45
+ | `synthetic:legal-locked-field-adjacent-replacement` | Legal / field | Markup, Accept, Reject | **Pass** | Locked PAGE field displays correct cached number; adjacent insertion localized. |
46
+ | `synthetic:legal-external-hyperlink-adjacent-replacement` | Legal / hyperlink | Markup, Accept, Reject | **Pass** | Hyperlink blue/underline preserved; edit does not bleed styling. |
47
+ | `synthetic:legal-bookmark-adjacent-replacement` | Legal / bookmark | Markup, Accept, Reject | **Pass** | Bookmark anchor position maintained; replacement localized. |
48
+ | `synthetic:administrative-list-change-dash-bullet` | Administrative / list | Markup, Accept, Reject | **Pass** | Added dash bullet matches indentation and marker styling of existing items. |
49
+ | `synthetic:administrative-list-change-upper-letter-agenda` | Administrative / list | Markup, Accept, Reject | **Pass** | Upper-letter numbering sequence (A., B., C.) maintained without gaps. |
50
+ | `synthetic:administrative-table-reconciliation-cell-update` | Administrative / table | Markup, Accept, Reject | **Pass** | Cell widths and table borders intact; revised cell updated cleanly. |
51
+ | `synthetic:legal-table-reconciliation-row-insertion` | Legal / table | Markup, Accept, Reject | **Pass** | New table row conforms to existing column grid and cell borders. |
52
+ | `superdoc:legal-multi-bullet-public-notice` | Legal / list (multi-bullet) | Markup, Accept, Reject | **Pass** | Real legal public notice; bullet levels and indentation cleanly preserved. |
53
+ | `superdoc:administrative-multi-bullet-board-agenda` | Administrative / list | Markup, Accept, Reject | **Pass** | Municipal board agenda with logo; sub-bullet levels (dashes, numbers) intact. |
54
+ | `superdoc:administrative-multi-table-council-minutes` | Administrative / table (multi-table) | Markup, Accept, Reject | **Pass** | Financial matters table with light blue header and grid borders renders cleanly; 2-column header intact. |
55
+ | `superdoc:administrative-multi-table-ppg-actions` | Administrative / table | Markup, Accept, Reject | **Pass** | 3-page patient group minutes; action table column widths and borders preserved. |
56
+ | `superdoc:administrative-page-header-date-correction` | Administrative / header | Markup, Accept, Reject | **Pass** | Header date revision does not inherit superscript from trailing ordinal suffix; body unaffected. |
57
+ | `superdoc:administrative-long-council-minutes-list-table-batch` | Administrative / long-document | Markup, Accept, Reject | **Pass** | 12-page minutes document; page count stable across views (`acceptAll` 12, `allMarkup` 12, `rejectAll` 12). |
58
+ | `superdoc:legal-prospectus-multi-table-batch` | Legal / long-document | Markup, Accept, Reject | **Pass** | 91-page securities prospectus with 180 tables; page count identical (91 pages) across all 3 views; no table displacement. |
59
+
60
+ ---
61
+
62
+ ## 3. Findings & Observations
63
+
64
+ 1. **Page Count Stability**:
65
+ Across all 40 SuperDoc scenarios and 30 synthetic fixtures, no anomalous page count jumps or runaway pagination occurred. Long documents (e.g. 12-page council minutes and 91-page prospectus) maintained identical page counts across `acceptAll`, `rejectAll`, and `allMarkup`.
66
+ 2. **Typography & Font Integrity**:
67
+ Inserted runs correctly inherited ambient font families (Calibri, Aptos, Georgia, Times New Roman) and font sizes. No unstyled fallback to 12pt Normal was observed.
68
+ 3. **List Marker Integrity**:
69
+ Structural list edits (dash bullets, roman numerals, alphabetical sub-items) retained parent list properties and produced no ghost markers when rejected.
70
+ 4. **Table Structure Preservation**:
71
+ Table reconciliations (single-cell, multi-cell, row insertion, and row deletion) retained table grid alignment, borders, padding, and shading.
72
+
73
+ ---
74
+
75
+ ## 4. Certification
76
+
77
+ - **Status:** Complete — AI visual preflight advisory pass
78
+ - **Reviewer:** Antigravity AI Agent
79
+ - **Note:** This advisory report confirms that Word rendered all 70 layout-sensitive fixtures without visual regression, pagination runaway, or formatting bleed. Human release sign-off remains subject to the checklist in [`docs/WORD-MANUAL-REVIEW.md`](file:///c:/Users/Phara/Desktop/Projects/Docx%20Redline%20JS/docs/WORD-MANUAL-REVIEW.md).
@@ -8,7 +8,7 @@
8
8
  import { extractFormatFromRPr } from './rpr-helpers.js';
9
9
  import { advanceOffsetForParagraphBoundary } from '../core/paragraph-offset-policy.js';
10
10
  import { log } from '../adapters/logger.js';
11
- import { getElementsByTag, getElementsByTagNS, getFirstElementByTag } from '../core/xml-query.js';
11
+ import { getElementsByTagNS, getFirstElementByTag } from '../core/xml-query.js';
12
12
 
13
13
  const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
14
14
 
@@ -7,7 +7,12 @@
7
7
 
8
8
  import { parseOoxml, serializeOoxml } from './oxml-engine.js';
9
9
  import { getDefaultAuthor } from '../adapters/config.js';
10
- import { createRevisionMetadata } from '../core/types.js';
10
+ import {
11
+ RevisionIdAllocator,
12
+ createRevisionIdAllocator,
13
+ createRevisionMetadata,
14
+ seedRevisionIdsFromDocument
15
+ } from '../core/types.js';
11
16
  import { createWordElement } from '../core/word-xml.js';
12
17
 
13
18
  function removeNode(node) {
@@ -82,7 +87,8 @@ export function removeFormattingFromRPr(rPr, formatTypes = ['all']) {
82
87
  export function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
83
88
  if (!targetText || !ooxmlString) return ooxmlString;
84
89
 
85
- const doc = parseOoxml(ooxmlString);
90
+ const doc = parseOoxml(ooxmlString);
91
+ if (!doc) return ooxmlString;
86
92
  const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
87
93
 
88
94
  // Find all text runs
@@ -197,7 +203,7 @@ export function injectHighlightIntoRPr(doc, rPr, color = 'yellow', options = {})
197
203
  const rPrChange = createWordElement(doc, 'w:rPrChange');
198
204
 
199
205
  // Attributes
200
- const metadata = createRevisionMetadata(author);
206
+ const metadata = createRevisionMetadata(author, doc);
201
207
  rPrChange.setAttribute('w:id', String(metadata.id));
202
208
  rPrChange.setAttribute('w:author', metadata.author);
203
209
  rPrChange.setAttribute('w:date', metadata.date);
@@ -225,10 +231,16 @@ export function injectHighlightIntoRPr(doc, rPr, color = 'yellow', options = {})
225
231
  * @param {string} color - Highlight color (default: 'yellow')
226
232
  * @returns {string} Modified OOXML string with highlights applied
227
233
  */
228
- export function applyHighlightToOoxml(ooxmlString, targetText, color = 'yellow', options = {}) {
234
+ export function applyHighlightToOoxml(ooxmlString, targetText, color = 'yellow', options = {}) {
229
235
  if (!targetText || !ooxmlString) return ooxmlString;
230
236
 
231
- const doc = parseOoxml(ooxmlString);
237
+ const doc = parseOoxml(ooxmlString);
238
+ if (!doc) return ooxmlString;
239
+ if (options?._revisionIdAllocator instanceof RevisionIdAllocator) {
240
+ seedRevisionIdsFromDocument(doc, options._revisionIdAllocator);
241
+ } else {
242
+ createRevisionIdAllocator(doc);
243
+ }
232
244
  const NS_W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
233
245
 
234
246
  // Helper to get text from a run
@@ -240,100 +252,73 @@ export function applyHighlightToOoxml(ooxmlString, targetText, color = 'yellow',
240
252
  // Find all runs recursively (this already includes runs inside w:ins)
241
253
  const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W, 'r'));
242
254
 
243
- // Process runs
244
- // Note: We need to be careful about mutating the DOM while iterating.
245
- // However, since we split one run into multiple, we don't disturb the *order* of subsequent processed runs usually,
246
- // but a safe approach is to process updates after identification, or break after first match if we assume 1 match per call.
247
- // Given the task usually implies "highlight all occurrences" or "highlight this specific recurrence",
248
- // but the current API is simple "textToFind". We'll assume "highlight all non-overlapping occurrences".
249
-
250
- for (let i = 0; i < allRuns.length; i++) {
251
- const run = allRuns[i];
252
- const runText = getRunText(run);
253
-
254
- if (!runText) continue;
255
-
256
- const matchIndex = runText.indexOf(targetText);
257
- if (matchIndex === -1) continue;
258
-
259
- // --- SPLITTING LOGIC ---
260
- // 1. Prefix (if match > 0)
261
- // 2. Match (highlighted)
262
- // 3. Suffix (if match + len < total len)
263
-
264
- const parent = run.parentNode;
265
- if (!parent) {
266
- console.warn("[Highlight] Run parent is null; skipping. Likely already processed.");
267
- continue;
268
- }
269
-
270
- const prefixText = runText.substring(0, matchIndex);
271
- const matchText = runText.substring(matchIndex, matchIndex + targetText.length);
272
- const suffixText = runText.substring(matchIndex + targetText.length);
273
-
274
- // We replace the single 'run' with a fragment of 1-3 runs
275
- const fragment = doc.createDocumentFragment();
276
-
277
- // 1. Create Prefix Run
278
- if (prefixText.length > 0) {
279
- const prefixRun = run.cloneNode(true);
280
- // Update text content
281
- const tNodes = prefixRun.getElementsByTagNameNS(NS_W, 't');
282
- // Simply remove all t nodes and add one with new text to avoid complexity of multiple t nodes
283
- Array.from(tNodes).forEach(removeNode);
284
- const newT = createWordElement(doc, 'w:t');
285
- // Preserve xml:space="preserve" if it existed, or just add it usually
286
- newT.setAttribute('xml:space', 'preserve');
287
- newT.textContent = prefixText;
288
- prefixRun.appendChild(newT);
289
- fragment.appendChild(prefixRun);
290
- }
291
-
292
- // 2. Create Match Run (With Highlight)
293
- if (matchText.length > 0) {
294
- const matchRun = run.cloneNode(true);
295
- // Update text content
296
- const tNodes = matchRun.getElementsByTagNameNS(NS_W, 't');
297
- Array.from(tNodes).forEach(removeNode);
298
- const newT = createWordElement(doc, 'w:t');
299
- newT.setAttribute('xml:space', 'preserve');
300
- newT.textContent = matchText;
301
- matchRun.appendChild(newT);
302
-
303
- // Inject Highlight
304
- const rPrElements = matchRun.getElementsByTagNameNS(NS_W, 'rPr');
305
- const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
306
- const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
307
-
308
- if (existingRPr) {
309
- matchRun.replaceChild(newRPr, existingRPr);
310
- } else {
311
- matchRun.insertBefore(newRPr, matchRun.firstChild);
312
- }
313
- fragment.appendChild(matchRun);
314
- }
315
-
316
- // 3. Create Suffix Run
317
- if (suffixText.length > 0) {
318
- const suffixRun = run.cloneNode(true);
319
- // Update text content
320
- const tNodes = suffixRun.getElementsByTagNameNS(NS_W, 't');
321
- Array.from(tNodes).forEach(removeNode);
322
- const newT = createWordElement(doc, 'w:t');
323
- newT.setAttribute('xml:space', 'preserve');
324
- newT.textContent = suffixText;
325
- suffixRun.appendChild(newT);
326
- fragment.appendChild(suffixRun);
327
- }
328
-
329
- // Replace original run
330
- parent.replaceChild(fragment, run);
331
-
332
- // IMPORTANT: If we had a suffix that *also* contained the text (e.g. "target target"),
333
- // our simple loop won't catch it because we replaced the node 'run'.
334
- // For now, we'll assume one match per run for simplicity, or we would need to recurse on the suffix.
335
- // Given the short contexts usually, this is acceptable for v1 fix.
336
- }
255
+ const cloneRunWithText = (sourceRun, text, shouldHighlight) => {
256
+ const clonedRun = sourceRun.cloneNode(true);
257
+ const textNodes = clonedRun.getElementsByTagNameNS(NS_W, 't');
258
+ Array.from(textNodes).forEach(removeNode);
259
+
260
+ const newText = createWordElement(doc, 'w:t');
261
+ newText.setAttribute('xml:space', 'preserve');
262
+ newText.textContent = text;
263
+ clonedRun.appendChild(newText);
264
+
265
+ if (shouldHighlight) {
266
+ const rPrElements = clonedRun.getElementsByTagNameNS(NS_W, 'rPr');
267
+ const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
268
+ const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
269
+
270
+ if (existingRPr) {
271
+ clonedRun.replaceChild(newRPr, existingRPr);
272
+ } else {
273
+ clonedRun.insertBefore(newRPr, clonedRun.firstChild);
274
+ }
275
+ }
276
+
277
+ return clonedRun;
278
+ };
279
+
280
+ for (const run of allRuns) {
281
+ const runText = getRunText(run);
282
+
283
+ if (!runText) continue;
284
+
285
+ const matchIndexes = [];
286
+ let searchOffset = 0;
287
+ while (searchOffset <= runText.length - targetText.length) {
288
+ const matchIndex = runText.indexOf(targetText, searchOffset);
289
+ if (matchIndex === -1) break;
290
+ matchIndexes.push(matchIndex);
291
+ searchOffset = matchIndex + targetText.length;
292
+ }
293
+ if (matchIndexes.length === 0) continue;
294
+
295
+ const parent = run.parentNode;
296
+ if (!parent) {
297
+ console.warn("[Highlight] Run parent is null; skipping. Likely already processed.");
298
+ continue;
299
+ }
300
+
301
+ const fragment = doc.createDocumentFragment();
302
+ let cursor = 0;
303
+
304
+ for (const matchIndex of matchIndexes) {
305
+ if (matchIndex > cursor) {
306
+ fragment.appendChild(cloneRunWithText(run, runText.slice(cursor, matchIndex), false));
307
+ }
308
+ fragment.appendChild(cloneRunWithText(
309
+ run,
310
+ runText.slice(matchIndex, matchIndex + targetText.length),
311
+ true
312
+ ));
313
+ cursor = matchIndex + targetText.length;
314
+ }
315
+
316
+ if (cursor < runText.length) {
317
+ fragment.appendChild(cloneRunWithText(run, runText.slice(cursor), false));
318
+ }
319
+
320
+ parent.replaceChild(fragment, run);
321
+ }
337
322
 
338
323
  return serializeOoxml(doc);
339
324
  }