@usejunior/docx-core 0.10.0 → 0.11.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 (232) hide show
  1. package/LICENSE +202 -21
  2. package/NOTICE +2 -0
  3. package/README.md +2 -2
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/atomizer.d.ts +13 -7
  6. package/dist/atomizer.d.ts.map +1 -1
  7. package/dist/atomizer.js +59 -24
  8. package/dist/atomizer.js.map +1 -1
  9. package/dist/baselines/atomizer/auxiliaryIdCollision.d.ts +99 -0
  10. package/dist/baselines/atomizer/auxiliaryIdCollision.d.ts.map +1 -0
  11. package/dist/baselines/atomizer/auxiliaryIdCollision.js +415 -0
  12. package/dist/baselines/atomizer/auxiliaryIdCollision.js.map +1 -0
  13. package/dist/baselines/atomizer/documentReconstructor.d.ts.map +1 -1
  14. package/dist/baselines/atomizer/documentReconstructor.js +120 -27
  15. package/dist/baselines/atomizer/documentReconstructor.js.map +1 -1
  16. package/dist/baselines/atomizer/inPlaceModifier-wrappers.d.ts +9 -0
  17. package/dist/baselines/atomizer/inPlaceModifier-wrappers.d.ts.map +1 -1
  18. package/dist/baselines/atomizer/inPlaceModifier-wrappers.js +52 -4
  19. package/dist/baselines/atomizer/inPlaceModifier-wrappers.js.map +1 -1
  20. package/dist/baselines/atomizer/pipeline.d.ts +12 -53
  21. package/dist/baselines/atomizer/pipeline.d.ts.map +1 -1
  22. package/dist/baselines/atomizer/pipeline.js +126 -231
  23. package/dist/baselines/atomizer/pipeline.js.map +1 -1
  24. package/dist/baselines/atomizer/trackChangesAcceptorAst.d.ts.map +1 -1
  25. package/dist/baselines/atomizer/trackChangesAcceptorAst.js +153 -23
  26. package/dist/baselines/atomizer/trackChangesAcceptorAst.js.map +1 -1
  27. package/dist/baselines/wmlcomparer/DotnetCli.d.ts.map +1 -1
  28. package/dist/baselines/wmlcomparer/DotnetCli.js +7 -0
  29. package/dist/baselines/wmlcomparer/DotnetCli.js.map +1 -1
  30. package/dist/cli/compare-two.d.ts.map +1 -1
  31. package/dist/cli/compare-two.js +3 -1
  32. package/dist/cli/compare-two.js.map +1 -1
  33. package/dist/cli/conformance-adapter.d.ts +3 -0
  34. package/dist/cli/conformance-adapter.d.ts.map +1 -0
  35. package/dist/cli/conformance-adapter.js +93 -0
  36. package/dist/cli/conformance-adapter.js.map +1 -0
  37. package/dist/cli/index.d.ts.map +1 -1
  38. package/dist/cli/index.js +5 -1
  39. package/dist/cli/index.js.map +1 -1
  40. package/dist/compare-types.d.ts +46 -0
  41. package/dist/compare-types.d.ts.map +1 -1
  42. package/dist/generation/compile.d.ts +21 -0
  43. package/dist/generation/compile.d.ts.map +1 -0
  44. package/dist/generation/compile.js +46 -0
  45. package/dist/generation/compile.js.map +1 -0
  46. package/dist/generation/context.d.ts +42 -0
  47. package/dist/generation/context.d.ts.map +1 -0
  48. package/dist/generation/context.js +65 -0
  49. package/dist/generation/context.js.map +1 -0
  50. package/dist/generation/emit/comments-part.d.ts +36 -0
  51. package/dist/generation/emit/comments-part.d.ts.map +1 -0
  52. package/dist/generation/emit/comments-part.js +116 -0
  53. package/dist/generation/emit/comments-part.js.map +1 -0
  54. package/dist/generation/emit/document-part.d.ts +24 -0
  55. package/dist/generation/emit/document-part.d.ts.map +1 -0
  56. package/dist/generation/emit/document-part.js +60 -0
  57. package/dist/generation/emit/document-part.js.map +1 -0
  58. package/dist/generation/emit/emit-context.d.ts +26 -0
  59. package/dist/generation/emit/emit-context.d.ts.map +1 -0
  60. package/dist/generation/emit/emit-context.js +19 -0
  61. package/dist/generation/emit/emit-context.js.map +1 -0
  62. package/dist/generation/emit/header-footer-part.d.ts +23 -0
  63. package/dist/generation/emit/header-footer-part.d.ts.map +1 -0
  64. package/dist/generation/emit/header-footer-part.js +57 -0
  65. package/dist/generation/emit/header-footer-part.js.map +1 -0
  66. package/dist/generation/emit/numbering-part.d.ts +29 -0
  67. package/dist/generation/emit/numbering-part.d.ts.map +1 -0
  68. package/dist/generation/emit/numbering-part.js +100 -0
  69. package/dist/generation/emit/numbering-part.js.map +1 -0
  70. package/dist/generation/emit/package-parts.d.ts +24 -0
  71. package/dist/generation/emit/package-parts.d.ts.map +1 -0
  72. package/dist/generation/emit/package-parts.js +121 -0
  73. package/dist/generation/emit/package-parts.js.map +1 -0
  74. package/dist/generation/emit/paragraph.d.ts +24 -0
  75. package/dist/generation/emit/paragraph.d.ts.map +1 -0
  76. package/dist/generation/emit/paragraph.js +63 -0
  77. package/dist/generation/emit/paragraph.js.map +1 -0
  78. package/dist/generation/emit/properties.d.ts +34 -0
  79. package/dist/generation/emit/properties.d.ts.map +1 -0
  80. package/dist/generation/emit/properties.js +138 -0
  81. package/dist/generation/emit/properties.js.map +1 -0
  82. package/dist/generation/emit/run.d.ts +15 -0
  83. package/dist/generation/emit/run.d.ts.map +1 -0
  84. package/dist/generation/emit/run.js +71 -0
  85. package/dist/generation/emit/run.js.map +1 -0
  86. package/dist/generation/emit/section.d.ts +29 -0
  87. package/dist/generation/emit/section.d.ts.map +1 -0
  88. package/dist/generation/emit/section.js +117 -0
  89. package/dist/generation/emit/section.js.map +1 -0
  90. package/dist/generation/emit/settings-part.d.ts +13 -0
  91. package/dist/generation/emit/settings-part.d.ts.map +1 -0
  92. package/dist/generation/emit/settings-part.js +24 -0
  93. package/dist/generation/emit/settings-part.js.map +1 -0
  94. package/dist/generation/emit/styles-part.d.ts +16 -0
  95. package/dist/generation/emit/styles-part.d.ts.map +1 -0
  96. package/dist/generation/emit/styles-part.js +80 -0
  97. package/dist/generation/emit/styles-part.js.map +1 -0
  98. package/dist/generation/emit/table.d.ts +26 -0
  99. package/dist/generation/emit/table.d.ts.map +1 -0
  100. package/dist/generation/emit/table.js +196 -0
  101. package/dist/generation/emit/table.js.map +1 -0
  102. package/dist/generation/errors.d.ts +22 -0
  103. package/dist/generation/errors.d.ts.map +1 -0
  104. package/dist/generation/errors.js +29 -0
  105. package/dist/generation/errors.js.map +1 -0
  106. package/dist/generation/index.d.ts +13 -0
  107. package/dist/generation/index.d.ts.map +1 -0
  108. package/dist/generation/index.js +12 -0
  109. package/dist/generation/index.js.map +1 -0
  110. package/dist/generation/ordering.d.ts +46 -0
  111. package/dist/generation/ordering.d.ts.map +1 -0
  112. package/dist/generation/ordering.js +119 -0
  113. package/dist/generation/ordering.js.map +1 -0
  114. package/dist/generation/recipes.d.ts +47 -0
  115. package/dist/generation/recipes.d.ts.map +1 -0
  116. package/dist/generation/recipes.js +84 -0
  117. package/dist/generation/recipes.js.map +1 -0
  118. package/dist/generation/structural-checks.d.ts +24 -0
  119. package/dist/generation/structural-checks.d.ts.map +1 -0
  120. package/dist/generation/structural-checks.js +318 -0
  121. package/dist/generation/structural-checks.js.map +1 -0
  122. package/dist/generation/types.d.ts +217 -0
  123. package/dist/generation/types.d.ts.map +1 -0
  124. package/dist/generation/types.js +16 -0
  125. package/dist/generation/types.js.map +1 -0
  126. package/dist/generation/validate-spec.d.ts +27 -0
  127. package/dist/generation/validate-spec.d.ts.map +1 -0
  128. package/dist/generation/validate-spec.js +307 -0
  129. package/dist/generation/validate-spec.js.map +1 -0
  130. package/dist/index.d.ts +3 -0
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +8 -0
  133. package/dist/index.js.map +1 -1
  134. package/dist/integration/generation-probes.d.ts +15 -0
  135. package/dist/integration/generation-probes.d.ts.map +1 -0
  136. package/dist/integration/generation-probes.js +84 -0
  137. package/dist/integration/generation-probes.js.map +1 -0
  138. package/dist/integration/libreoffice-oracle.d.ts +8 -0
  139. package/dist/integration/libreoffice-oracle.d.ts.map +1 -1
  140. package/dist/integration/libreoffice-oracle.js +14 -6
  141. package/dist/integration/libreoffice-oracle.js.map +1 -1
  142. package/dist/integration/synthetic-docx-fixture.d.ts +72 -0
  143. package/dist/integration/synthetic-docx-fixture.d.ts.map +1 -1
  144. package/dist/integration/synthetic-docx-fixture.js +131 -4
  145. package/dist/integration/synthetic-docx-fixture.js.map +1 -1
  146. package/dist/primitives/accept_changes.d.ts +2 -1
  147. package/dist/primitives/accept_changes.d.ts.map +1 -1
  148. package/dist/primitives/accept_changes.js +153 -12
  149. package/dist/primitives/accept_changes.js.map +1 -1
  150. package/dist/primitives/document.d.ts +38 -0
  151. package/dist/primitives/document.d.ts.map +1 -1
  152. package/dist/primitives/document.js +75 -9
  153. package/dist/primitives/document.js.map +1 -1
  154. package/dist/primitives/document_view-comments.d.ts.map +1 -1
  155. package/dist/primitives/document_view-comments.js +4 -3
  156. package/dist/primitives/document_view-comments.js.map +1 -1
  157. package/dist/primitives/document_view-types.d.ts +15 -0
  158. package/dist/primitives/document_view-types.d.ts.map +1 -1
  159. package/dist/primitives/document_view.d.ts.map +1 -1
  160. package/dist/primitives/document_view.js +21 -13
  161. package/dist/primitives/document_view.js.map +1 -1
  162. package/dist/primitives/formatting_tags.d.ts +1 -0
  163. package/dist/primitives/formatting_tags.d.ts.map +1 -1
  164. package/dist/primitives/formatting_tags.js +16 -10
  165. package/dist/primitives/formatting_tags.js.map +1 -1
  166. package/dist/primitives/index.d.ts +4 -0
  167. package/dist/primitives/index.d.ts.map +1 -1
  168. package/dist/primitives/index.js +4 -0
  169. package/dist/primitives/index.js.map +1 -1
  170. package/dist/primitives/layout.d.ts.map +1 -1
  171. package/dist/primitives/layout.js +13 -0
  172. package/dist/primitives/layout.js.map +1 -1
  173. package/dist/primitives/minimal_save.d.ts +38 -0
  174. package/dist/primitives/minimal_save.d.ts.map +1 -0
  175. package/dist/primitives/minimal_save.js +323 -0
  176. package/dist/primitives/minimal_save.js.map +1 -0
  177. package/dist/primitives/namespaces.d.ts +41 -0
  178. package/dist/primitives/namespaces.d.ts.map +1 -1
  179. package/dist/primitives/namespaces.js +43 -0
  180. package/dist/primitives/namespaces.js.map +1 -1
  181. package/dist/primitives/reject_changes.d.ts +4 -2
  182. package/dist/primitives/reject_changes.d.ts.map +1 -1
  183. package/dist/primitives/reject_changes.js +177 -24
  184. package/dist/primitives/reject_changes.js.map +1 -1
  185. package/dist/primitives/revision-parts.d.ts +7 -0
  186. package/dist/primitives/revision-parts.d.ts.map +1 -0
  187. package/dist/primitives/revision-parts.js +27 -0
  188. package/dist/primitives/revision-parts.js.map +1 -0
  189. package/dist/primitives/revision-vocabulary.d.ts +7 -0
  190. package/dist/primitives/revision-vocabulary.d.ts.map +1 -0
  191. package/dist/primitives/revision-vocabulary.js +39 -0
  192. package/dist/primitives/revision-vocabulary.js.map +1 -0
  193. package/dist/primitives/schema-corpus-capture.d.ts +19 -0
  194. package/dist/primitives/schema-corpus-capture.d.ts.map +1 -0
  195. package/dist/primitives/schema-corpus-capture.js +29 -0
  196. package/dist/primitives/schema-corpus-capture.js.map +1 -0
  197. package/dist/primitives/sectPrAudit.d.ts +19 -0
  198. package/dist/primitives/sectPrAudit.d.ts.map +1 -0
  199. package/dist/primitives/sectPrAudit.js +165 -0
  200. package/dist/primitives/sectPrAudit.js.map +1 -0
  201. package/dist/primitives/semantic_tags.d.ts.map +1 -1
  202. package/dist/primitives/semantic_tags.js +2 -1
  203. package/dist/primitives/semantic_tags.js.map +1 -1
  204. package/dist/primitives/serialize_html.d.ts +1 -0
  205. package/dist/primitives/serialize_html.d.ts.map +1 -1
  206. package/dist/primitives/serialize_html.js +4 -2
  207. package/dist/primitives/serialize_html.js.map +1 -1
  208. package/dist/primitives/styles.d.ts +15 -0
  209. package/dist/primitives/styles.d.ts.map +1 -1
  210. package/dist/primitives/styles.js +11 -0
  211. package/dist/primitives/styles.js.map +1 -1
  212. package/dist/primitives/track-changes-emitter.d.ts +9 -0
  213. package/dist/primitives/track-changes-emitter.d.ts.map +1 -1
  214. package/dist/primitives/track-changes-emitter.js +54 -4
  215. package/dist/primitives/track-changes-emitter.js.map +1 -1
  216. package/dist/primitives/validate_ai_revisions.d.ts +35 -0
  217. package/dist/primitives/validate_ai_revisions.d.ts.map +1 -0
  218. package/dist/primitives/validate_ai_revisions.js +323 -0
  219. package/dist/primitives/validate_ai_revisions.js.map +1 -0
  220. package/dist/primitives/xml.d.ts +5 -0
  221. package/dist/primitives/xml.d.ts.map +1 -1
  222. package/dist/primitives/xml.js +5 -0
  223. package/dist/primitives/xml.js.map +1 -1
  224. package/dist/primitives/zip.d.ts +1 -0
  225. package/dist/primitives/zip.d.ts.map +1 -1
  226. package/dist/primitives/zip.js +21 -3
  227. package/dist/primitives/zip.js.map +1 -1
  228. package/dist/shared/field-structure.d.ts +14 -0
  229. package/dist/shared/field-structure.d.ts.map +1 -0
  230. package/dist/shared/field-structure.js +166 -0
  231. package/dist/shared/field-structure.js.map +1 -0
  232. package/package.json +7 -4
@@ -5,9 +5,11 @@
5
5
  * Integrates atomization, LCS comparison, move detection, format detection,
6
6
  * and document reconstruction.
7
7
  */
8
- import type { CompareResult, ReconstructionMode } from '../../compare-types.js';
9
- import type { MoveDetectionSettings, FormatDetectionSettings } from '../../core-types.js';
8
+ import type { CompareResult, CompareStats, ReconstructionMode } from '../../compare-types.js';
9
+ import type { ComparisonUnitAtom, MoveDetectionSettings, FormatDetectionSettings } from '../../core-types.js';
10
10
  import { type NumberingIntegrationOptions } from './numberingIntegration.js';
11
+ export { hasFldCharInsideDel, validateFieldStructure, type FieldStory, } from '../../shared/field-structure.js';
12
+ import { type FieldStory } from '../../shared/field-structure.js';
11
13
  /**
12
14
  * Options for the atomizer pipeline.
13
15
  */
@@ -40,18 +42,6 @@ export interface AtomizerOptions {
40
42
  */
41
43
  reconstructionMode?: ReconstructionMode;
42
44
  }
43
- /**
44
- * One story (a self-contained complex-field state machine): the main document
45
- * body, an individual footnote entry, or an individual endnote entry. `label`
46
- * is for diagnostics only; `xml` is the serialized fragment that gets parsed
47
- * and walked.
48
- *
49
- * @conformance ECMA-376 edition 5, Part 4 § 17.16.5
50
- */
51
- export interface FieldStory {
52
- label: string;
53
- xml: string;
54
- }
55
45
  /**
56
46
  * Split a docx into per-story XML fragments for field-closure validation.
57
47
  *
@@ -76,45 +66,6 @@ export interface FieldStory {
76
66
  * @see https://github.com/UseJunior/safe-docx/issues/212
77
67
  */
78
68
  export declare function splitStories(documentXml: string, footnotesXmls: ReadonlyArray<string | null>, endnotesXmls: ReadonlyArray<string | null>): FieldStory[];
79
- /**
80
- * Validate field structure integrity across one or more document stories.
81
- *
82
- * Enforces three constraints on complex fields **per story**:
83
- * 1. `w:fldChar` begin/end count balance within the story.
84
- * 2. Every `w:instrText` AND `w:delInstrText` sits inside an open field body
85
- * (between `begin` and `separate`). Orphaned instruction text renders as
86
- * literal text in Word.
87
- * 3. `w:delInstrText` is nested inside a `<w:del>` ancestor (DeletedFieldCode
88
- * schema constraint), and conversely `w:fldChar` is NEVER inside `<w:del>`
89
- * (Word treats this as fatal and discards the field state machine).
90
- *
91
- * Called on both pre-accept/reject combined XML (with track-change wrappers)
92
- * and on post-accept/reject XML (wrappers removed). Both cases must satisfy the
93
- * field placement check; constraint (3) is vacuous post-accept/reject.
94
- *
95
- * Accepts either a single XML string (legacy single-story call) or an array of
96
- * `FieldStory` fragments. Stories are validated independently and short-circuit
97
- * on the first failure.
98
- *
99
- * @conformance ECMA-376 edition 5, Part 4 § 17.16.5
100
- */
101
- /**
102
- * Targeted check for one of the constraints above: `w:fldChar` MUST NOT appear
103
- * inside any `<w:del>` element. Word treats this violation as fatal — the
104
- * field state machine is discarded and the field renders as literal-text
105
- * fallback.
106
- *
107
- * Used as a combined-output safety gate alongside the per-projection
108
- * `validateFieldStructure` checks. Kept narrower than the full structural
109
- * validation so that legacy shapes (e.g. `delInstrText` inside `<w:moveFrom>`)
110
- * don't trigger fallback when the inplace candidate is otherwise sound on its
111
- * accept/reject projections.
112
- *
113
- * @conformance ECMA-376 edition 5, Part 4 § 17.16.5
114
- * @see https://github.com/UseJunior/safe-docx/issues/217
115
- */
116
- export declare function hasFldCharInsideDel(documentXml: string): boolean;
117
- export declare function validateFieldStructure(input: string | FieldStory[]): boolean;
118
69
  /**
119
70
  * Compare two DOCX documents using the atomizer-based approach.
120
71
  *
@@ -141,4 +92,12 @@ export interface AuxiliaryMergeResult {
141
92
  mergedIds: Set<string>;
142
93
  createdPart: boolean;
143
94
  }
95
+ /**
96
+ * Compute comparison statistics from merged atoms.
97
+ *
98
+ * Range counts are contiguous same-status runs in the merged atom stream, scoped
99
+ * to a paragraph. Atom counts remain available under explicit names for callers
100
+ * that need the old granular benchmark signal.
101
+ */
102
+ export declare function computeAtomizerStats(mergedAtoms: ComparisonUnitAtom[]): CompareStats;
144
103
  //# sourceMappingURL=pipeline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../../src/baselines/atomizer/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EACV,aAAa,EAeb,kBAAkB,EACnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAEV,qBAAqB,EACrB,uBAAuB,EAExB,MAAM,qBAAqB,CAAC;AAkC7B,OAAO,EAEL,KAAK,2BAA2B,EAEjC,MAAM,2BAA2B,CAAC;AAGnC;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,8BAA8B;IAC9B,aAAa,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC/C,gCAAgC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACnD,qCAAqC;IACrC,SAAS,CAAC,EAAE,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACjD;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;AAkPD;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAC1B,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,aAAa,CAAC,MAAM,GAAG,IAAI,CAAC,EAC3C,YAAY,EAAE,aAAa,CAAC,MAAM,GAAG,IAAI,CAAC,GACzC,UAAU,EAAE,CA4Bd;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CA4BhE;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,GAAG,OAAO,CAQ5E;AA4LD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAwUxB;AAsBD,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;CACtB"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../../src/baselines/atomizer/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EAeZ,kBAAkB,EACnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EACV,kBAAkB,EAClB,qBAAqB,EACrB,uBAAuB,EAExB,MAAM,qBAAqB,CAAC;AAkC7B,OAAO,EAEL,KAAK,2BAA2B,EAEjC,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,KAAK,UAAU,GAChB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,iCAAiC,CAAC;AAUzC;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,8BAA8B;IAC9B,aAAa,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC/C,gCAAgC;IAChC,eAAe,CAAC,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACnD,qCAAqC;IACrC,SAAS,CAAC,EAAE,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACjD;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;AAkPD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAC1B,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,aAAa,CAAC,MAAM,GAAG,IAAI,CAAC,EAC3C,YAAY,EAAE,aAAa,CAAC,MAAM,GAAG,IAAI,CAAC,GACzC,UAAU,EAAE,CA4Bd;AAqHD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAwWxB;AAYD,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,WAAW,EAAE,OAAO,CAAC;CACtB;AAwgBD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,kBAAkB,EAAE,GAAG,YAAY,CAyDpF"}
@@ -21,6 +21,10 @@ import { modifyRevisedDocument, ContainerResolutionError } from './inPlaceModifi
21
21
  import { acceptAllChanges, rejectAllChanges, extractTextWithParagraphs, compareTexts, } from './trackChangesAcceptorAst.js';
22
22
  import { virtualizeNumberingLabels, DEFAULT_NUMBERING_OPTIONS, } from './numberingIntegration.js';
23
23
  import { premergeAdjacentRuns } from './premergeRuns.js';
24
+ export { hasFldCharInsideDel, validateFieldStructure, } from '../../shared/field-structure.js';
25
+ import { hasFldCharInsideDel, validateFieldStructure, } from '../../shared/field-structure.js';
26
+ import { AUXILIARY_PARTS, parseEntries, renumberCollidingAuxiliaryIds, restampCollidingCommentParaIds, } from './auxiliaryIdCollision.js';
27
+ import { maybeCaptureEmittedDocumentXml } from '../../primitives/schema-corpus-capture.js';
24
28
  function arraysEqual(a, b) {
25
29
  if (a.length !== b.length)
26
30
  return false;
@@ -264,160 +268,6 @@ export function splitStories(documentXml, footnotesXmls, endnotesXmls) {
264
268
  collectEntries(endnotesXmls, 'w:endnote', 'endnote');
265
269
  return stories;
266
270
  }
267
- /**
268
- * Validate field structure integrity across one or more document stories.
269
- *
270
- * Enforces three constraints on complex fields **per story**:
271
- * 1. `w:fldChar` begin/end count balance within the story.
272
- * 2. Every `w:instrText` AND `w:delInstrText` sits inside an open field body
273
- * (between `begin` and `separate`). Orphaned instruction text renders as
274
- * literal text in Word.
275
- * 3. `w:delInstrText` is nested inside a `<w:del>` ancestor (DeletedFieldCode
276
- * schema constraint), and conversely `w:fldChar` is NEVER inside `<w:del>`
277
- * (Word treats this as fatal and discards the field state machine).
278
- *
279
- * Called on both pre-accept/reject combined XML (with track-change wrappers)
280
- * and on post-accept/reject XML (wrappers removed). Both cases must satisfy the
281
- * field placement check; constraint (3) is vacuous post-accept/reject.
282
- *
283
- * Accepts either a single XML string (legacy single-story call) or an array of
284
- * `FieldStory` fragments. Stories are validated independently and short-circuit
285
- * on the first failure.
286
- *
287
- * @conformance ECMA-376 edition 5, Part 4 § 17.16.5
288
- */
289
- /**
290
- * Targeted check for one of the constraints above: `w:fldChar` MUST NOT appear
291
- * inside any `<w:del>` element. Word treats this violation as fatal — the
292
- * field state machine is discarded and the field renders as literal-text
293
- * fallback.
294
- *
295
- * Used as a combined-output safety gate alongside the per-projection
296
- * `validateFieldStructure` checks. Kept narrower than the full structural
297
- * validation so that legacy shapes (e.g. `delInstrText` inside `<w:moveFrom>`)
298
- * don't trigger fallback when the inplace candidate is otherwise sound on its
299
- * accept/reject projections.
300
- *
301
- * @conformance ECMA-376 edition 5, Part 4 § 17.16.5
302
- * @see https://github.com/UseJunior/safe-docx/issues/217
303
- */
304
- export function hasFldCharInsideDel(documentXml) {
305
- const root = parseDocumentXml(documentXml);
306
- let insideDelDepth = 0;
307
- let violation = false;
308
- function scan(node) {
309
- if (violation)
310
- return;
311
- for (let child = node.firstChild; child; child = child.nextSibling) {
312
- if (child.nodeType !== 1)
313
- continue;
314
- const el = child;
315
- const tag = el.tagName;
316
- if (tag === 'w:del') {
317
- insideDelDepth++;
318
- scan(el);
319
- insideDelDepth--;
320
- if (violation)
321
- return;
322
- continue;
323
- }
324
- if (tag === 'w:fldChar' && insideDelDepth > 0) {
325
- violation = true;
326
- return;
327
- }
328
- scan(el);
329
- if (violation)
330
- return;
331
- }
332
- }
333
- scan(root);
334
- return violation;
335
- }
336
- export function validateFieldStructure(input) {
337
- if (typeof input === 'string') {
338
- return validateFieldStructureForStory(input);
339
- }
340
- for (const story of input) {
341
- if (!validateFieldStructureForStory(story.xml))
342
- return false;
343
- }
344
- return true;
345
- }
346
- function validateFieldStructureForStory(documentXml) {
347
- const root = parseDocumentXml(documentXml);
348
- const allFldChars = findAllByTagName(root, 'w:fldChar');
349
- const allInstrTexts = findAllByTagName(root, 'w:instrText');
350
- const allDelInstrTexts = findAllByTagName(root, 'w:delInstrText');
351
- // Constraint (1): global fldChar begin/end balance.
352
- let begins = 0;
353
- let ends = 0;
354
- for (const fc of allFldChars) {
355
- const type = fc.getAttribute('w:fldCharType');
356
- if (type === 'begin')
357
- begins++;
358
- else if (type === 'end')
359
- ends++;
360
- }
361
- if (begins !== ends)
362
- return false;
363
- if (allFldChars.length === 0 &&
364
- allInstrTexts.length === 0 &&
365
- allDelInstrTexts.length === 0) {
366
- return true;
367
- }
368
- // Depth-first scan tracking field nesting (for constraint 2) and <w:del>
369
- // ancestor nesting (for constraint 3).
370
- let depth = 0;
371
- const pastSeparatorAtDepth = [];
372
- let insideDelDepth = 0;
373
- function scan(node) {
374
- for (let child = node.firstChild; child; child = child.nextSibling) {
375
- if (child.nodeType !== 1)
376
- continue;
377
- const el = child;
378
- const tag = el.tagName;
379
- if (tag === 'w:del') {
380
- insideDelDepth++;
381
- const ok = scan(el);
382
- insideDelDepth--;
383
- if (!ok)
384
- return false;
385
- continue;
386
- }
387
- if (tag === 'w:fldChar') {
388
- if (insideDelDepth > 0)
389
- return false;
390
- const type = el.getAttribute('w:fldCharType');
391
- if (type === 'begin') {
392
- depth++;
393
- pastSeparatorAtDepth[depth] = 0;
394
- }
395
- else if (type === 'separate') {
396
- if (depth > 0)
397
- pastSeparatorAtDepth[depth] = 1;
398
- }
399
- else if (type === 'end') {
400
- if (depth > 0)
401
- depth--;
402
- }
403
- }
404
- else if (tag === 'w:instrText') {
405
- if (depth === 0 || pastSeparatorAtDepth[depth])
406
- return false;
407
- }
408
- else if (tag === 'w:delInstrText') {
409
- if (insideDelDepth === 0)
410
- return false;
411
- if (depth === 0 || pastSeparatorAtDepth[depth])
412
- return false;
413
- }
414
- if (!scan(el))
415
- return false;
416
- }
417
- return true;
418
- }
419
- return scan(root);
420
- }
421
271
  function evaluateSafetyChecks(originalTextForRoundTrip, revisedTextForRoundTrip, originalBookmarkDiagnostics, revisedBookmarkDiagnostics, candidateXml, auxiliarySidecars) {
422
272
  const acceptedXml = acceptAllChanges(candidateXml);
423
273
  const rejectedXml = rejectAllChanges(candidateXml);
@@ -526,6 +376,14 @@ export async function compareDocumentsAtomizer(original, revised, options = {})
526
376
  // Step 1: Load DOCX archives
527
377
  const originalArchive = await DocxArchive.load(original);
528
378
  const revisedArchive = await DocxArchive.load(revised);
379
+ // Step 1b: Resolve auxiliary ID collisions. When both sides define
380
+ // different content under the same comment/footnote/endnote w:id or the
381
+ // same comment paraId, rewrite the revised side so no anchor or ancillary
382
+ // row in the merged output can bind to the other document's definition.
383
+ // Must run before any document.xml extraction so every downstream step sees
384
+ // the rewritten archive.
385
+ await renumberCollidingAuxiliaryIds(originalArchive, revisedArchive);
386
+ await restampCollidingCommentParaIds(originalArchive, revisedArchive);
529
387
  // Step 2: Extract document.xml
530
388
  const originalXml = await originalArchive.getDocumentXml();
531
389
  const revisedXml = await revisedArchive.getDocumentXml();
@@ -718,6 +576,23 @@ export async function compareDocumentsAtomizer(original, revised, options = {})
718
576
  else {
719
577
  comparisonResult = runComparisonPass({ atomizeParagraphLevelMarkers: true }, 'rebuild');
720
578
  }
579
+ // Rebuild output gets the same safety screening as inplace attempts, whether
580
+ // rebuild was requested directly or reached via inplace fallback. Rebuild is
581
+ // the terminal strategy, so failures are surfaced in diagnostics rather than
582
+ // blocking the output.
583
+ // @see https://github.com/UseJunior/safe-docx/issues/226
584
+ let rebuildSafetyDiagnostics;
585
+ if (comparisonResult.outputMode === 'rebuild') {
586
+ const safety = evaluateRoundTripSafety(comparisonResult.newDocumentXml);
587
+ if (!safety.safe) {
588
+ rebuildSafetyDiagnostics = {
589
+ checks: safety.checks,
590
+ failedChecks: safety.failedChecks,
591
+ failureDetails: safety.failureDetails,
592
+ firstDiffSummary: safety.failureSummary,
593
+ };
594
+ }
595
+ }
721
596
  const { mergedAtoms, newDocumentXml } = comparisonResult;
722
597
  // Step 12: Clone appropriate archive and update document.xml.
723
598
  // Use the revised archive only for true inplace output.
@@ -729,6 +604,7 @@ export async function compareDocumentsAtomizer(original, revised, options = {})
729
604
  // auxiliary part that the revised side introduced (issue #94).
730
605
  const mergeSourceArchive = comparisonResult.outputMode === 'inplace' ? originalArchive : revisedArchive;
731
606
  const resultArchive = await baseArchive.clone();
607
+ maybeCaptureEmittedDocumentXml(newDocumentXml);
732
608
  resultArchive.setDocumentXml(newDocumentXml);
733
609
  // Step 12b: Merge auxiliary part definitions (footnotes, endnotes, comments).
734
610
  // Reconstruction may insert content (deleted in inplace, added in rebuild)
@@ -740,13 +616,14 @@ export async function compareDocumentsAtomizer(original, revised, options = {})
740
616
  // Gated on root comment IDs in the *result* document (not on what the
741
617
  // generic merge appended), so the pass runs even when the original already
742
618
  // contains the root and revised only adds replies under it (issue #108).
743
- const rootCommentIds = collectReferenceIds(newDocumentXml, 'w:commentReference');
619
+ // Comments anchored on footnote/endnote text count as roots too.
620
+ const rootCommentIds = await collectStoryReferenceIds(resultArchive, newDocumentXml, 'w:commentReference', null);
744
621
  if (rootCommentIds.size > 0) {
745
622
  await mergeCommentAncillaryParts(mergeSourceArchive, resultArchive, rootCommentIds);
746
623
  }
747
624
  // Step 13: Save result and compute stats
748
625
  const resultBuffer = await resultArchive.save();
749
- const stats = computeStats(mergedAtoms);
626
+ const stats = computeAtomizerStats(mergedAtoms);
750
627
  return {
751
628
  document: resultBuffer,
752
629
  stats,
@@ -755,37 +632,29 @@ export async function compareDocumentsAtomizer(original, revised, options = {})
755
632
  reconstructionModeUsed: comparisonResult.outputMode,
756
633
  fallbackReason,
757
634
  fallbackDiagnostics,
635
+ rebuildSafetyDiagnostics,
758
636
  };
759
637
  }
760
- const AUXILIARY_PARTS = [
761
- {
762
- label: 'footnote',
763
- partPath: 'word/footnotes.xml',
764
- referenceTag: 'w:footnoteReference',
765
- entryTag: 'w:footnote',
766
- rootTag: 'w:footnotes',
767
- contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml',
768
- relationshipType: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes',
769
- },
770
- {
771
- label: 'endnote',
772
- partPath: 'word/endnotes.xml',
773
- referenceTag: 'w:endnoteReference',
774
- entryTag: 'w:endnote',
775
- rootTag: 'w:endnotes',
776
- contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml',
777
- relationshipType: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes',
778
- },
779
- {
780
- label: 'comment',
781
- partPath: 'word/comments.xml',
782
- referenceTag: 'w:commentReference',
783
- entryTag: 'w:comment',
784
- rootTag: 'w:comments',
785
- contentType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml',
786
- relationshipType: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments',
787
- },
788
- ];
638
+ /**
639
+ * Collect reference IDs across every result story that can host anchors: the
640
+ * merged document.xml plus the result archive's footnote/endnote parts (Word
641
+ * allows comments anchored on note text). `excludePartPath` skips the part
642
+ * whose own definitions are being merged — entries can't reference
643
+ * themselves.
644
+ */
645
+ async function collectStoryReferenceIds(resultArchive, documentXml, referenceTag, excludePartPath) {
646
+ const ids = collectReferenceIds(documentXml, referenceTag);
647
+ for (const storyPath of ['word/footnotes.xml', 'word/endnotes.xml']) {
648
+ if (storyPath === excludePartPath)
649
+ continue;
650
+ const storyXml = await resultArchive.getFile(storyPath);
651
+ if (!storyXml)
652
+ continue;
653
+ for (const id of collectReferenceIds(storyXml, referenceTag))
654
+ ids.add(id);
655
+ }
656
+ return ids;
657
+ }
789
658
  /**
790
659
  * Collect reference IDs from document.xml using DOM parsing.
791
660
  */
@@ -800,21 +669,6 @@ function collectReferenceIds(documentXml, referenceTag) {
800
669
  }
801
670
  return ids;
802
671
  }
803
- /**
804
- * Parse an auxiliary part and extract entry elements by ID.
805
- */
806
- function parseEntries(xml, entryTag) {
807
- const doc = parseXml(xml);
808
- const entries = new Map();
809
- const elements = doc.getElementsByTagName(entryTag);
810
- for (let i = 0; i < elements.length; i++) {
811
- const el = elements[i];
812
- const id = el.getAttribute('w:id');
813
- if (id)
814
- entries.set(id, el);
815
- }
816
- return { doc, entries };
817
- }
818
672
  /**
819
673
  * Merge auxiliary part definitions (footnotes, endnotes, comments) from the
820
674
  * source archive into the result archive. The source archive is whichever
@@ -824,7 +678,11 @@ function parseEntries(xml, entryTag) {
824
678
  */
825
679
  async function mergeAuxiliaryPartDefinitions(sourceArchive, resultArchive, documentXml, descriptor) {
826
680
  const result = { mergedIds: new Set(), createdPart: false };
827
- const referencedIds = collectReferenceIds(documentXml, descriptor.referenceTag);
681
+ // Anchors may live in the merged body or on note text in the result's
682
+ // footnote/endnote stories. AUXILIARY_PARTS merges notes before comments,
683
+ // so by the comment pass the note stories already carry any merged-in
684
+ // comment anchors.
685
+ const referencedIds = await collectStoryReferenceIds(resultArchive, documentXml, descriptor.referenceTag, descriptor.partPath);
828
686
  if (referencedIds.size === 0)
829
687
  return result;
830
688
  const sourcePartXml = await sourceArchive.getFile(descriptor.partPath);
@@ -1154,6 +1012,7 @@ const COMMENTS_EXTENDED_DESCRIPTOR = {
1154
1012
  rootTag: 'w15:commentsEx',
1155
1013
  contentType: 'application/vnd.ms-word.commentsExtended+xml',
1156
1014
  relationshipType: 'http://schemas.microsoft.com/office/2011/relationships/commentsExtended',
1015
+ idBearingTags: [], // keyed by w15:paraId, not w:id
1157
1016
  };
1158
1017
  const PEOPLE_DESCRIPTOR = {
1159
1018
  label: 'people',
@@ -1163,6 +1022,7 @@ const PEOPLE_DESCRIPTOR = {
1163
1022
  rootTag: 'w15:people',
1164
1023
  contentType: 'application/vnd.ms-word.people+xml',
1165
1024
  relationshipType: 'http://schemas.microsoft.com/office/2011/relationships/people',
1025
+ idBearingTags: [], // keyed by w15:author, not w:id
1166
1026
  };
1167
1027
  async function mergePeople(sourceArchive, resultArchive, mergedAuthors) {
1168
1028
  if (mergedAuthors.size === 0)
@@ -1219,47 +1079,82 @@ async function mergePeople(sourceArchive, resultArchive, mergedAuthors) {
1219
1079
  resultArchive.setFile('word/people.xml', serializer.serializeToString(newDoc));
1220
1080
  await ensureOpcMetadata(resultArchive, PEOPLE_DESCRIPTOR);
1221
1081
  }
1082
+ const fallbackParagraphStatsKeys = new WeakMap();
1083
+ let nextFallbackParagraphStatsKey = 0;
1084
+ function paragraphStatsKey(atom) {
1085
+ if (atom.paragraphIndex !== undefined) {
1086
+ return `${atom.part.uri}:${atom.paragraphIndex}`;
1087
+ }
1088
+ const pAncestor = atom.ancestorElements.find((a) => a.tagName === 'w:p');
1089
+ if (!pAncestor)
1090
+ return undefined;
1091
+ let key = fallbackParagraphStatsKeys.get(pAncestor);
1092
+ if (!key) {
1093
+ key = `${atom.part.uri}:paragraph-ref:${nextFallbackParagraphStatsKey++}`;
1094
+ fallbackParagraphStatsKeys.set(pAncestor, key);
1095
+ }
1096
+ return key;
1097
+ }
1222
1098
  /**
1223
1099
  * Compute comparison statistics from merged atoms.
1100
+ *
1101
+ * Range counts are contiguous same-status runs in the merged atom stream, scoped
1102
+ * to a paragraph. Atom counts remain available under explicit names for callers
1103
+ * that need the old granular benchmark signal.
1224
1104
  */
1225
- function computeStats(mergedAtoms) {
1105
+ export function computeAtomizerStats(mergedAtoms) {
1226
1106
  const reconstructionStats = computeReconstructionStats(mergedAtoms);
1227
- // Count unique paragraphs for modifications
1228
- // A modification is when we have both deleted and inserted atoms in the same paragraph
1229
- const modifiedParagraphs = new Set();
1230
- let currentParagraph = '';
1231
- let hasDeleted = false;
1232
- let hasInserted = false;
1107
+ let insertedRanges = 0;
1108
+ let deletedRanges = 0;
1109
+ let formatChanges = 0;
1110
+ let previousRangeStatus = null;
1111
+ let previousRangeParagraph;
1112
+ const paragraphs = new Map();
1233
1113
  for (const atom of mergedAtoms) {
1234
- // Detect paragraph boundaries
1235
- const pAncestor = atom.ancestorElements.find((a) => a.tagName === 'w:p');
1236
- const paragraphId = pAncestor
1237
- ? `${atom.part.uri}:${atom.ancestorElements.indexOf(pAncestor)}`
1238
- : '';
1239
- if (paragraphId !== currentParagraph) {
1240
- // Check previous paragraph
1241
- if (currentParagraph && hasDeleted && hasInserted) {
1242
- modifiedParagraphs.add(currentParagraph);
1114
+ const paragraphKey = paragraphStatsKey(atom);
1115
+ const status = atom.correlationStatus;
1116
+ const rangeStatus = status === CorrelationStatus.Inserted ||
1117
+ status === CorrelationStatus.Deleted ||
1118
+ status === CorrelationStatus.FormatChanged
1119
+ ? status
1120
+ : null;
1121
+ if (rangeStatus) {
1122
+ if (rangeStatus !== previousRangeStatus || paragraphKey !== previousRangeParagraph) {
1123
+ if (rangeStatus === CorrelationStatus.Inserted)
1124
+ insertedRanges++;
1125
+ if (rangeStatus === CorrelationStatus.Deleted)
1126
+ deletedRanges++;
1127
+ if (rangeStatus === CorrelationStatus.FormatChanged)
1128
+ formatChanges++;
1243
1129
  }
1244
- currentParagraph = paragraphId;
1245
- hasDeleted = false;
1246
- hasInserted = false;
1130
+ previousRangeStatus = rangeStatus;
1131
+ previousRangeParagraph = paragraphKey;
1247
1132
  }
1248
- if (atom.correlationStatus === CorrelationStatus.Deleted) {
1249
- hasDeleted = true;
1133
+ else {
1134
+ previousRangeStatus = null;
1135
+ previousRangeParagraph = undefined;
1250
1136
  }
1251
- else if (atom.correlationStatus === CorrelationStatus.Inserted) {
1252
- hasInserted = true;
1137
+ if (paragraphKey && (status === CorrelationStatus.Deleted || status === CorrelationStatus.Inserted)) {
1138
+ const flags = paragraphs.get(paragraphKey) ?? { hasDeleted: false, hasInserted: false };
1139
+ if (status === CorrelationStatus.Deleted)
1140
+ flags.hasDeleted = true;
1141
+ if (status === CorrelationStatus.Inserted)
1142
+ flags.hasInserted = true;
1143
+ paragraphs.set(paragraphKey, flags);
1253
1144
  }
1254
1145
  }
1255
- // Check last paragraph
1256
- if (currentParagraph && hasDeleted && hasInserted) {
1257
- modifiedParagraphs.add(currentParagraph);
1258
- }
1146
+ const modifiedParagraphs = Array.from(paragraphs.values()).filter((flags) => flags.hasDeleted && flags.hasInserted).length;
1259
1147
  return {
1260
- insertions: reconstructionStats.insertions,
1261
- deletions: reconstructionStats.deletions,
1262
- modifications: modifiedParagraphs.size + reconstructionStats.formatChanges,
1148
+ insertions: insertedRanges,
1149
+ deletions: deletedRanges,
1150
+ modifications: modifiedParagraphs,
1151
+ insertedRanges,
1152
+ deletedRanges,
1153
+ insertedAtoms: reconstructionStats.insertions,
1154
+ deletedAtoms: reconstructionStats.deletions,
1155
+ modifiedParagraphs,
1156
+ formatChanges,
1157
+ formatChangeAtoms: reconstructionStats.formatChanges,
1263
1158
  };
1264
1159
  }
1265
1160
  //# sourceMappingURL=pipeline.js.map