documentation-hub 5.7.2

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 (271) hide show
  1. package/.eslintrc.json +43 -0
  2. package/.github/workflows/build.yml +64 -0
  3. package/.github/workflows/ci.yml +39 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/Current.md +97 -0
  6. package/DocHub_Image.png +0 -0
  7. package/README.md +666 -0
  8. package/USER_GUIDE.md +1173 -0
  9. package/Updater.md +311 -0
  10. package/build/256x256.png +0 -0
  11. package/build/512x512.png +0 -0
  12. package/build/app-update.yml +4 -0
  13. package/build/create-icon.js +208 -0
  14. package/build/icon.ico +0 -0
  15. package/build/icon.png +0 -0
  16. package/build/icon_1024x1024.png +0 -0
  17. package/dist/assets/Analytics-BpsG9895.js +1 -0
  18. package/dist/assets/Card-IAZin8kp.js +1 -0
  19. package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
  20. package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
  21. package/dist/assets/Documents-CqZ25axS.js +1 -0
  22. package/dist/assets/Input-l89xwXBi.js +1 -0
  23. package/dist/assets/Reporting-DqdHJY_a.js +1 -0
  24. package/dist/assets/Search-XNbu5z_3.js +1 -0
  25. package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
  26. package/dist/assets/Sessions-ClZOPYNc.js +1 -0
  27. package/dist/assets/Settings-DUEHGURa.js +11 -0
  28. package/dist/assets/index-8xUe8ptc.js +24 -0
  29. package/dist/assets/index-RYyJqF7O.css +1 -0
  30. package/dist/assets/path-BkOl0AGO.js +1 -0
  31. package/dist/assets/promises-ID_B9S-h.js +1 -0
  32. package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
  33. package/dist/assets/useToast-yRSO1dkm.js +1 -0
  34. package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
  35. package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
  36. package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
  37. package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
  38. package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
  39. package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
  40. package/dist/electron/main-CXkNtyv-.js +19789 -0
  41. package/dist/electron/main.js +5 -0
  42. package/dist/electron/preload.js +1 -0
  43. package/dist/icon.png +0 -0
  44. package/dist/index.html +27 -0
  45. package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
  46. package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
  47. package/docs/README.md +115 -0
  48. package/docs/TOC_WIRING_GUIDE.md +344 -0
  49. package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
  50. package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
  51. package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
  52. package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
  53. package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
  54. package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
  55. package/docs/analysis/List_Implementation.md +206 -0
  56. package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
  57. package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
  58. package/docs/analysis/RefactorStyles.md +852 -0
  59. package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
  60. package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
  61. package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
  62. package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
  63. package/docs/analysis/github-issues-to-create.md +237 -0
  64. package/docs/api/API_README.md +538 -0
  65. package/docs/api/API_REFERENCE.md +751 -0
  66. package/docs/api/TYPE_DEFINITIONS.md +869 -0
  67. package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
  68. package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
  69. package/docs/docxmlater-readme.md +1341 -0
  70. package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
  71. package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
  72. package/docs/fixes/README.md +37 -0
  73. package/docs/github-issues/issue-1-body.md +125 -0
  74. package/docs/github-issues/issue-10-body.md +850 -0
  75. package/docs/github-issues/issue-2-body.md +200 -0
  76. package/docs/github-issues/issue-3-body.md +270 -0
  77. package/docs/github-issues/issue-4-body.md +169 -0
  78. package/docs/github-issues/issue-5-body.md +173 -0
  79. package/docs/github-issues/issue-6-body.md +158 -0
  80. package/docs/github-issues/issue-7-body.md +171 -0
  81. package/docs/github-issues/issue-8-body.md +407 -0
  82. package/docs/github-issues/issue-9-body.md +515 -0
  83. package/docs/github-issues/issue-tracker.md +274 -0
  84. package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
  85. package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
  86. package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
  87. package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
  88. package/docs/implementation/RefactorStyles.md +588 -0
  89. package/docs/implementation/implement-plan.md +489 -0
  90. package/docs/implementation/missing-helpers-implementation.md +391 -0
  91. package/docs/implementation/refactor-plan.md +520 -0
  92. package/docs/implementation/session-implementation-complete.md +233 -0
  93. package/docs/implementation/session-management-plan.md +250 -0
  94. package/docs/setup-checklist.md +77 -0
  95. package/docs/versions/changelog.md +345 -0
  96. package/electron/customUpdater.ts +656 -0
  97. package/electron/main.ts +2441 -0
  98. package/electron/memoryConfig.ts +187 -0
  99. package/electron/preload.ts +394 -0
  100. package/electron/proxyConfig.ts +340 -0
  101. package/electron/services/BackupService.ts +452 -0
  102. package/electron/services/DictionaryService.ts +402 -0
  103. package/electron/services/LocalDictionaryLookupService.ts +147 -0
  104. package/electron/services/PowerAutomateApiService.ts +231 -0
  105. package/electron/services/SharePointSyncService.ts +474 -0
  106. package/electron/windowsCertStore.ts +427 -0
  107. package/electron/zscalerConfig.ts +381 -0
  108. package/eslint.config.js +92 -0
  109. package/jest.config.js +52 -0
  110. package/package.json +214 -0
  111. package/postcss.config.mjs +6 -0
  112. package/public/icon.png +0 -0
  113. package/publish-release.ps1 +5 -0
  114. package/renovate.json +30 -0
  115. package/src/App.tsx +216 -0
  116. package/src/__mocks__/p-limit.js +12 -0
  117. package/src/__mocks__/styleMock.js +1 -0
  118. package/src/components/common/BugReportButton.tsx +44 -0
  119. package/src/components/common/BugReportDialog.tsx +193 -0
  120. package/src/components/common/Button.tsx +153 -0
  121. package/src/components/common/Card.tsx +86 -0
  122. package/src/components/common/ColorPickerDialog.tsx +177 -0
  123. package/src/components/common/ConfirmDialog.tsx +96 -0
  124. package/src/components/common/DebugConsole.tsx +275 -0
  125. package/src/components/common/EmptyState.tsx +183 -0
  126. package/src/components/common/ErrorBoundary.tsx +98 -0
  127. package/src/components/common/ErrorDetailsDialog.tsx +153 -0
  128. package/src/components/common/ErrorFallback.tsx +218 -0
  129. package/src/components/common/Input.tsx +109 -0
  130. package/src/components/common/Skeleton.tsx +184 -0
  131. package/src/components/common/SplashScreen.tsx +81 -0
  132. package/src/components/common/Toast.tsx +155 -0
  133. package/src/components/common/Tooltip.tsx +79 -0
  134. package/src/components/common/UpdateNotification.tsx +320 -0
  135. package/src/components/comparison/ComparisonWindow.tsx +374 -0
  136. package/src/components/comparison/SideBySideDiff.tsx +486 -0
  137. package/src/components/comparison/index.ts +8 -0
  138. package/src/components/document/DocumentUploader.tsx +288 -0
  139. package/src/components/document/HyperlinkPreview.tsx +430 -0
  140. package/src/components/document/HyperlinkService.md +1484 -0
  141. package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
  142. package/src/components/document/InlineChangesView.tsx +707 -0
  143. package/src/components/document/ProcessingProgress.tsx +303 -0
  144. package/src/components/document/ProcessingResults.tsx +256 -0
  145. package/src/components/document/TrackedChangesDetail.tsx +530 -0
  146. package/src/components/document/TrackedChangesPanel.tsx +546 -0
  147. package/src/components/document/VirtualDocumentList.tsx +240 -0
  148. package/src/components/editor/DocumentEditor.tsx +723 -0
  149. package/src/components/editor/DocumentEditorModal.tsx +640 -0
  150. package/src/components/editor/EditorQuickActions.tsx +502 -0
  151. package/src/components/editor/EditorToolbar.tsx +312 -0
  152. package/src/components/editor/TableEditor.tsx +926 -0
  153. package/src/components/editor/index.ts +18 -0
  154. package/src/components/layout/Header.tsx +190 -0
  155. package/src/components/layout/Sidebar.tsx +313 -0
  156. package/src/components/layout/TitleBar.tsx +190 -0
  157. package/src/components/navigation/CommandPalette.tsx +233 -0
  158. package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
  159. package/src/components/sessions/ChangeItem.tsx +408 -0
  160. package/src/components/sessions/ChangeViewer.tsx +1155 -0
  161. package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
  162. package/src/components/sessions/ProcessingOptions.tsx +297 -0
  163. package/src/components/sessions/ReplacementsTab.tsx +438 -0
  164. package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
  165. package/src/components/sessions/SessionManager.tsx +188 -0
  166. package/src/components/sessions/StylesEditor.tsx +1335 -0
  167. package/src/components/sessions/TabContainer.tsx +151 -0
  168. package/src/components/sessions/VirtualSessionList.tsx +157 -0
  169. package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
  170. package/src/components/settings/CertificateManager.tsx +410 -0
  171. package/src/components/settings/SegmentedControl.tsx +88 -0
  172. package/src/components/settings/SettingRow.tsx +52 -0
  173. package/src/contexts/GlobalStatsContext.tsx +396 -0
  174. package/src/contexts/SessionContext.tsx +2129 -0
  175. package/src/contexts/ThemeContext.tsx +428 -0
  176. package/src/contexts/UserSettingsContext.tsx +290 -0
  177. package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
  178. package/src/global.d.ts +273 -0
  179. package/src/hooks/useDocumentQueue.tsx +210 -0
  180. package/src/hooks/useToast.tsx +55 -0
  181. package/src/main.tsx +10 -0
  182. package/src/pages/Analytics.tsx +386 -0
  183. package/src/pages/CurrentSession.tsx +1174 -0
  184. package/src/pages/Dashboard.tsx +319 -0
  185. package/src/pages/Documents.tsx +317 -0
  186. package/src/pages/Projects.tsx +250 -0
  187. package/src/pages/Reporting.tsx +386 -0
  188. package/src/pages/Search.tsx +349 -0
  189. package/src/pages/Sessions.tsx +285 -0
  190. package/src/pages/Settings.tsx +2662 -0
  191. package/src/services/HyperlinkService.ts +1085 -0
  192. package/src/services/document/DocXMLaterProcessor.ts +617 -0
  193. package/src/services/document/DocumentProcessingComparison.ts +856 -0
  194. package/src/services/document/DocumentSnapshotService.ts +575 -0
  195. package/src/services/document/WordDocumentProcessor.ts +10509 -0
  196. package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
  197. package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
  198. package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
  199. package/src/services/document/blanklines/BlankLineManager.ts +658 -0
  200. package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
  201. package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
  202. package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
  203. package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
  204. package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
  205. package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
  206. package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
  207. package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
  208. package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
  209. package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
  210. package/src/services/document/blanklines/index.ts +67 -0
  211. package/src/services/document/blanklines/rules/additionRules.ts +337 -0
  212. package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
  213. package/src/services/document/blanklines/rules/removalRules.ts +362 -0
  214. package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
  215. package/src/services/document/blanklines/types.ts +29 -0
  216. package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
  217. package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
  218. package/src/services/document/helpers/whitespace.ts +117 -0
  219. package/src/services/document/list/ListNormalizer.ts +947 -0
  220. package/src/services/document/list/index.ts +45 -0
  221. package/src/services/document/list/list-detection.ts +275 -0
  222. package/src/services/document/list/list-types.ts +162 -0
  223. package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
  224. package/src/services/document/processors/ListProcessor.ts +257 -0
  225. package/src/services/document/processors/StructureProcessor.ts +176 -0
  226. package/src/services/document/processors/StyleProcessor.ts +389 -0
  227. package/src/services/document/processors/TableProcessor.ts +2238 -0
  228. package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
  229. package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
  230. package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
  231. package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
  232. package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
  233. package/src/services/document/processors/index.ts +28 -0
  234. package/src/services/document/types/docx-processing.ts +310 -0
  235. package/src/services/editor/EditorActionHandlers.ts +901 -0
  236. package/src/services/editor/index.ts +13 -0
  237. package/src/setupTests.ts +47 -0
  238. package/src/styles/global.css +782 -0
  239. package/src/types/backup.ts +132 -0
  240. package/src/types/dictionary.ts +125 -0
  241. package/src/types/document-processing.ts +331 -0
  242. package/src/types/docxmlater-augments.d.ts +142 -0
  243. package/src/types/editor.ts +280 -0
  244. package/src/types/electron.ts +340 -0
  245. package/src/types/globalStats.ts +155 -0
  246. package/src/types/hyperlink.ts +471 -0
  247. package/src/types/operations.ts +354 -0
  248. package/src/types/session.ts +427 -0
  249. package/src/types/settings.ts +112 -0
  250. package/src/utils/MemoryMonitor.ts +248 -0
  251. package/src/utils/cn.ts +6 -0
  252. package/src/utils/colorConvert.ts +306 -0
  253. package/src/utils/diffUtils.ts +347 -0
  254. package/src/utils/documentUtils.ts +202 -0
  255. package/src/utils/electronGuard.ts +62 -0
  256. package/src/utils/indexedDB.ts +915 -0
  257. package/src/utils/logger.ts +717 -0
  258. package/src/utils/pathSecurity.ts +232 -0
  259. package/src/utils/pathValidator.ts +236 -0
  260. package/src/utils/processingTimeEstimator.ts +153 -0
  261. package/src/utils/safeJsonParse.ts +62 -0
  262. package/src/utils/textSanitizer.ts +162 -0
  263. package/src/utils/urlHelpers.ts +304 -0
  264. package/src/utils/urlPatterns.ts +198 -0
  265. package/src/utils/urlSanitizer.ts +152 -0
  266. package/src/vite-env.d.ts +11 -0
  267. package/tsconfig.electron.json +19 -0
  268. package/tsconfig.json +36 -0
  269. package/tsconfig.node.json +12 -0
  270. package/typedoc.json +45 -0
  271. package/vite.config.ts +152 -0
@@ -0,0 +1,856 @@
1
+ /**
2
+ * DocumentProcessingComparison - Compare document before and after processing
3
+ *
4
+ * This service captures the state of a document before processing,
5
+ * tracks all changes made during processing, and generates a comparison
6
+ * showing exactly what was modified.
7
+ *
8
+ * Used to show users what changes were made to their document during:
9
+ * - Hyperlink processing
10
+ * - Style application
11
+ * - Content ID appending
12
+ * - PowerAutomate API updates
13
+ */
14
+
15
+ import { Document, Paragraph, Hyperlink } from 'docxmlater';
16
+ import { diffWords, Change } from 'diff';
17
+ import { sanitizeHyperlinkText, isTextCorrupted } from '../../utils/textSanitizer';
18
+ import { logger } from '../../utils/logger';
19
+
20
+ // Create namespaced logger for this module
21
+ const log = logger.namespace('DocumentProcessingComparison');
22
+
23
+ export interface ProcessingChange {
24
+ id: string;
25
+ type:
26
+ | 'hyperlink_url'
27
+ | 'hyperlink_text'
28
+ | 'style'
29
+ | 'content_added'
30
+ | 'content_removed'
31
+ | 'formatting';
32
+ location: string; // e.g., "Paragraph 5, Hyperlink 2"
33
+ before: any;
34
+ after: any;
35
+ timestamp: Date;
36
+ operation: string; // e.g., "PowerAutomate API Update", "Content ID Append"
37
+ }
38
+
39
+ export interface ProcessingComparison {
40
+ documentPath: string;
41
+ processingStartTime: Date;
42
+ processingEndTime?: Date;
43
+ originalBuffer: Buffer;
44
+ processedBuffer?: Buffer;
45
+ changes: ProcessingChange[];
46
+ statistics: ProcessingStatistics;
47
+ hyperlinkChanges: HyperlinkChange[];
48
+ styleChanges: StyleChange[];
49
+ contentChanges: ContentChange[];
50
+ }
51
+
52
+ export interface ProcessingStatistics {
53
+ totalChanges: number;
54
+ hyperlinksModified: number;
55
+ urlsChanged: number;
56
+ displayTextsChanged: number;
57
+ stylesApplied: number;
58
+ contentIdsAppended: number;
59
+ processingDurationMs: number;
60
+ }
61
+
62
+ export interface HyperlinkChange {
63
+ paragraphIndex: number;
64
+ hyperlinkIndex: number;
65
+ originalUrl: string;
66
+ modifiedUrl: string;
67
+ originalText: string;
68
+ modifiedText: string;
69
+ changeReason: string; // e.g., "Content ID appended", "URL updated by API"
70
+ /** Status of the hyperlink source - 'not_found' for theSource links that couldn't be resolved */
71
+ status?: 'updated' | 'not_found' | 'expired';
72
+ /** Content ID associated with this hyperlink change (for theSource links) */
73
+ contentId?: string;
74
+ }
75
+
76
+ export interface StyleChange {
77
+ paragraphIndex: number;
78
+ styleId: string;
79
+ styleName: string;
80
+ properties: Record<string, any>;
81
+ }
82
+
83
+ export interface ContentChange {
84
+ paragraphIndex: number;
85
+ type: 'added' | 'removed' | 'modified';
86
+ originalContent?: string;
87
+ modifiedContent?: string;
88
+ diff?: Change[];
89
+ }
90
+
91
+ /**
92
+ * Service for tracking and comparing document changes during processing
93
+ */
94
+ export class DocumentProcessingComparison {
95
+ private comparison: ProcessingComparison | null = null;
96
+ private originalHyperlinks: Map<string, HyperlinkSnapshot> = new Map();
97
+ private originalStyles: Map<number, any> = new Map();
98
+ private changeIdCounter = 0;
99
+
100
+ /**
101
+ * Start tracking a document for comparison
102
+ */
103
+ async startTracking(documentPath: string, document: Document): Promise<void> {
104
+ const startTime = new Date();
105
+
106
+ // Capture original state
107
+ const originalBuffer = await document.toBuffer();
108
+
109
+ // Capture hyperlink snapshots
110
+ this.captureHyperlinks(document);
111
+
112
+ // Capture style snapshots
113
+ this.captureStyles(document);
114
+
115
+ // Initialize comparison
116
+ this.comparison = {
117
+ documentPath,
118
+ processingStartTime: startTime,
119
+ originalBuffer,
120
+ changes: [],
121
+ statistics: {
122
+ totalChanges: 0,
123
+ hyperlinksModified: 0,
124
+ urlsChanged: 0,
125
+ displayTextsChanged: 0,
126
+ stylesApplied: 0,
127
+ contentIdsAppended: 0,
128
+ processingDurationMs: 0,
129
+ },
130
+ hyperlinkChanges: [],
131
+ styleChanges: [],
132
+ contentChanges: [],
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Capture current hyperlink state
138
+ */
139
+ private captureHyperlinks(document: Document): void {
140
+ this.originalHyperlinks.clear();
141
+ const paragraphs = document.getAllParagraphs();
142
+
143
+ paragraphs.forEach((para, paraIndex) => {
144
+ const content = para.getContent();
145
+ let hyperlinkIndex = 0;
146
+
147
+ for (const item of content) {
148
+ if (item instanceof Hyperlink) {
149
+ const key = `${paraIndex}-${hyperlinkIndex}`;
150
+ const rawText = item.getText();
151
+
152
+ // Log if XML corruption detected
153
+ if (isTextCorrupted(rawText)) {
154
+ log.warn(
155
+ `XML corruption detected in hyperlink text at paragraph ${paraIndex}, hyperlink ${hyperlinkIndex}:`,
156
+ rawText
157
+ );
158
+ }
159
+
160
+ this.originalHyperlinks.set(key, {
161
+ paragraphIndex: paraIndex,
162
+ hyperlinkIndex,
163
+ url: item.getUrl() || '',
164
+ text: sanitizeHyperlinkText(rawText),
165
+ });
166
+ hyperlinkIndex++;
167
+ }
168
+ }
169
+ });
170
+ }
171
+
172
+ /**
173
+ * Capture current style state
174
+ */
175
+ private captureStyles(document: Document): void {
176
+ this.originalStyles.clear();
177
+ const paragraphs = document.getAllParagraphs();
178
+
179
+ paragraphs.forEach((para, index) => {
180
+ const formatting = para.getFormatting?.() || {};
181
+ this.originalStyles.set(index, formatting);
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Record a hyperlink URL change
187
+ */
188
+ recordHyperlinkUrlChange(
189
+ paragraphIndex: number,
190
+ hyperlinkIndex: number,
191
+ originalUrl: string,
192
+ newUrl: string,
193
+ reason: string
194
+ ): void {
195
+ if (!this.comparison) return;
196
+
197
+ const changeId = `change-${++this.changeIdCounter}`;
198
+ const location = `Paragraph ${paragraphIndex + 1}, Hyperlink ${hyperlinkIndex + 1}`;
199
+
200
+ // Add to general changes
201
+ this.comparison.changes.push({
202
+ id: changeId,
203
+ type: 'hyperlink_url',
204
+ location,
205
+ before: originalUrl,
206
+ after: newUrl,
207
+ timestamp: new Date(),
208
+ operation: reason,
209
+ });
210
+
211
+ // Find if there's an existing hyperlink change for this position
212
+ const existingChange = this.comparison.hyperlinkChanges.find(
213
+ (c) => c.paragraphIndex === paragraphIndex && c.hyperlinkIndex === hyperlinkIndex
214
+ );
215
+
216
+ if (existingChange) {
217
+ existingChange.modifiedUrl = newUrl;
218
+ existingChange.changeReason += `, ${reason}`;
219
+ } else {
220
+ // Get original state
221
+ const key = `${paragraphIndex}-${hyperlinkIndex}`;
222
+ const original = this.originalHyperlinks.get(key);
223
+
224
+ this.comparison.hyperlinkChanges.push({
225
+ paragraphIndex,
226
+ hyperlinkIndex,
227
+ originalUrl: original?.url || originalUrl,
228
+ modifiedUrl: newUrl,
229
+ originalText: original?.text || '',
230
+ modifiedText: original?.text || '',
231
+ changeReason: reason,
232
+ });
233
+ }
234
+
235
+ // Update statistics
236
+ this.comparison.statistics.urlsChanged++;
237
+ this.comparison.statistics.hyperlinksModified++;
238
+
239
+ if (reason.includes('Content ID')) {
240
+ this.comparison.statistics.contentIdsAppended++;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Record a hyperlink text change
246
+ * @param status - Optional status for theSource links ('not_found', 'expired')
247
+ * @param contentId - Optional content ID associated with this hyperlink
248
+ */
249
+ recordHyperlinkTextChange(
250
+ paragraphIndex: number,
251
+ hyperlinkIndex: number,
252
+ originalText: string,
253
+ newText: string,
254
+ reason: string,
255
+ status?: 'updated' | 'not_found' | 'expired',
256
+ contentId?: string
257
+ ): void {
258
+ if (!this.comparison) return;
259
+
260
+ const changeId = `change-${++this.changeIdCounter}`;
261
+ const location = `Paragraph ${paragraphIndex + 1}, Hyperlink ${hyperlinkIndex + 1}`;
262
+
263
+ // Add to general changes
264
+ this.comparison.changes.push({
265
+ id: changeId,
266
+ type: 'hyperlink_text',
267
+ location,
268
+ before: originalText,
269
+ after: newText,
270
+ timestamp: new Date(),
271
+ operation: reason,
272
+ });
273
+
274
+ // Update hyperlink changes
275
+ const existingChange = this.comparison.hyperlinkChanges.find(
276
+ (c) => c.paragraphIndex === paragraphIndex && c.hyperlinkIndex === hyperlinkIndex
277
+ );
278
+
279
+ if (existingChange) {
280
+ existingChange.modifiedText = newText;
281
+ existingChange.changeReason += `, ${reason}`;
282
+ if (status) existingChange.status = status;
283
+ if (contentId) existingChange.contentId = contentId;
284
+ } else {
285
+ const key = `${paragraphIndex}-${hyperlinkIndex}`;
286
+ const original = this.originalHyperlinks.get(key);
287
+
288
+ this.comparison.hyperlinkChanges.push({
289
+ paragraphIndex,
290
+ hyperlinkIndex,
291
+ originalUrl: original?.url || '',
292
+ modifiedUrl: original?.url || '',
293
+ originalText: original?.text || originalText,
294
+ modifiedText: newText,
295
+ changeReason: reason,
296
+ status,
297
+ contentId,
298
+ });
299
+ }
300
+
301
+ // Update statistics
302
+ this.comparison.statistics.displayTextsChanged++;
303
+ if (!existingChange) {
304
+ this.comparison.statistics.hyperlinksModified++;
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Record a style change
310
+ */
311
+ recordStyleChange(
312
+ paragraphIndex: number,
313
+ styleId: string,
314
+ styleName: string,
315
+ properties: Record<string, any>
316
+ ): void {
317
+ if (!this.comparison) return;
318
+
319
+ const changeId = `change-${++this.changeIdCounter}`;
320
+ const location = `Paragraph ${paragraphIndex + 1}`;
321
+
322
+ // Add to general changes
323
+ this.comparison.changes.push({
324
+ id: changeId,
325
+ type: 'style',
326
+ location,
327
+ before: this.originalStyles.get(paragraphIndex) || {},
328
+ after: { styleId, ...properties },
329
+ timestamp: new Date(),
330
+ operation: `Applied style: ${styleName}`,
331
+ });
332
+
333
+ // Add to style changes
334
+ this.comparison.styleChanges.push({
335
+ paragraphIndex,
336
+ styleId,
337
+ styleName,
338
+ properties,
339
+ });
340
+
341
+ // Update statistics
342
+ this.comparison.statistics.stylesApplied++;
343
+ }
344
+
345
+ /**
346
+ * Complete tracking and generate final comparison
347
+ */
348
+ async completeTracking(processedDocument: Document): Promise<ProcessingComparison | null> {
349
+ if (!this.comparison) return null;
350
+
351
+ // Capture processed state
352
+ this.comparison.processedBuffer = await processedDocument.toBuffer();
353
+ this.comparison.processingEndTime = new Date();
354
+
355
+ // Calculate processing duration
356
+ this.comparison.statistics.processingDurationMs =
357
+ this.comparison.processingEndTime.getTime() - this.comparison.processingStartTime.getTime();
358
+
359
+ // Compare final hyperlink state
360
+ this.compareFinalHyperlinks(processedDocument);
361
+
362
+ // Update total changes
363
+ this.comparison.statistics.totalChanges = this.comparison.changes.length;
364
+
365
+ return this.comparison;
366
+ }
367
+
368
+ /**
369
+ * Compare final hyperlink state with original
370
+ */
371
+ private compareFinalHyperlinks(document: Document): void {
372
+ if (!this.comparison) return;
373
+
374
+ const paragraphs = document.getAllParagraphs();
375
+
376
+ paragraphs.forEach((para, paraIndex) => {
377
+ const content = para.getContent();
378
+ let hyperlinkIndex = 0;
379
+
380
+ for (const item of content) {
381
+ if (item instanceof Hyperlink) {
382
+ const key = `${paraIndex}-${hyperlinkIndex}`;
383
+ const original = this.originalHyperlinks.get(key);
384
+
385
+ if (original) {
386
+ const currentUrl = item.getUrl() || '';
387
+ const rawCurrentText = item.getText();
388
+
389
+ // Log if XML corruption detected
390
+ if (isTextCorrupted(rawCurrentText)) {
391
+ log.warn(
392
+ `XML corruption detected in current hyperlink text at paragraph ${paraIndex}, hyperlink ${hyperlinkIndex}:`,
393
+ rawCurrentText
394
+ );
395
+ }
396
+
397
+ const currentText = sanitizeHyperlinkText(rawCurrentText);
398
+
399
+ // Check if this change was already recorded
400
+ const recorded = this.comparison!.hyperlinkChanges.find(
401
+ (c) => c.paragraphIndex === paraIndex && c.hyperlinkIndex === hyperlinkIndex
402
+ );
403
+
404
+ if (!recorded) {
405
+ // Detect untracked changes
406
+ if (currentUrl !== original.url || currentText !== original.text) {
407
+ this.comparison!.hyperlinkChanges.push({
408
+ paragraphIndex: paraIndex,
409
+ hyperlinkIndex,
410
+ originalUrl: original.url,
411
+ modifiedUrl: currentUrl,
412
+ originalText: original.text,
413
+ modifiedText: currentText,
414
+ changeReason: 'Automatic change detected',
415
+ });
416
+
417
+ if (currentUrl !== original.url) {
418
+ this.comparison!.statistics.urlsChanged++;
419
+ }
420
+ if (currentText !== original.text) {
421
+ this.comparison!.statistics.displayTextsChanged++;
422
+ }
423
+ this.comparison!.statistics.hyperlinksModified++;
424
+ }
425
+ }
426
+ }
427
+
428
+ hyperlinkIndex++;
429
+ }
430
+ }
431
+ });
432
+ }
433
+
434
+ /**
435
+ * Generate HTML report of changes
436
+ */
437
+ generateHTMLReport(comparison: ProcessingComparison): string {
438
+ const html = `
439
+ <!DOCTYPE html>
440
+ <html>
441
+ <head>
442
+ <title>Document Processing Changes</title>
443
+ <style>
444
+ * { margin: 0; padding: 0; box-sizing: border-box; }
445
+ body {
446
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
447
+ background: #f8f9fa;
448
+ color: #212529;
449
+ }
450
+ .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
451
+
452
+ .header {
453
+ background: white;
454
+ border-radius: 12px;
455
+ padding: 24px;
456
+ margin-bottom: 24px;
457
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
458
+ }
459
+
460
+ .header h1 {
461
+ font-size: 24px;
462
+ font-weight: 600;
463
+ margin-bottom: 16px;
464
+ color: #0969da;
465
+ }
466
+
467
+ .meta-info {
468
+ display: grid;
469
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
470
+ gap: 12px;
471
+ color: #656d76;
472
+ font-size: 14px;
473
+ }
474
+
475
+ .statistics {
476
+ background: white;
477
+ border-radius: 12px;
478
+ padding: 20px;
479
+ margin-bottom: 24px;
480
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
481
+ }
482
+
483
+ .stat-grid {
484
+ display: grid;
485
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
486
+ gap: 16px;
487
+ margin-top: 16px;
488
+ }
489
+
490
+ .stat-card {
491
+ background: #f6f8fa;
492
+ padding: 12px 16px;
493
+ border-radius: 8px;
494
+ border-left: 3px solid #0969da;
495
+ }
496
+
497
+ .stat-card .value {
498
+ font-size: 24px;
499
+ font-weight: 600;
500
+ color: #0969da;
501
+ }
502
+
503
+ .stat-card .label {
504
+ font-size: 12px;
505
+ color: #656d76;
506
+ text-transform: uppercase;
507
+ letter-spacing: 0.5px;
508
+ }
509
+
510
+ .changes-section {
511
+ background: white;
512
+ border-radius: 12px;
513
+ padding: 20px;
514
+ margin-bottom: 24px;
515
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
516
+ }
517
+
518
+ .changes-section h2 {
519
+ font-size: 18px;
520
+ font-weight: 600;
521
+ margin-bottom: 16px;
522
+ color: #24292f;
523
+ }
524
+
525
+ .change-item {
526
+ background: #f6f8fa;
527
+ border-radius: 8px;
528
+ padding: 16px;
529
+ margin-bottom: 12px;
530
+ border: 1px solid #d1d9e0;
531
+ transition: all 0.2s;
532
+ }
533
+
534
+ .change-item:hover {
535
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
536
+ transform: translateY(-1px);
537
+ }
538
+
539
+ .change-location {
540
+ font-size: 12px;
541
+ color: #656d76;
542
+ margin-bottom: 8px;
543
+ font-weight: 500;
544
+ }
545
+
546
+ .change-content {
547
+ display: grid;
548
+ gap: 8px;
549
+ }
550
+
551
+ .before, .after {
552
+ padding: 8px 12px;
553
+ border-radius: 6px;
554
+ font-family: 'SF Mono', Monaco, monospace;
555
+ font-size: 13px;
556
+ word-break: break-all;
557
+ }
558
+
559
+ .before {
560
+ background: #ffebe9;
561
+ color: #cf222e;
562
+ border: 1px solid #ff8182;
563
+ }
564
+
565
+ .after {
566
+ background: #dafbe1;
567
+ color: #1a7f37;
568
+ border: 1px solid #7ee787;
569
+ }
570
+
571
+ .change-reason {
572
+ font-size: 12px;
573
+ color: #0969da;
574
+ margin-top: 8px;
575
+ font-style: italic;
576
+ }
577
+
578
+ .empty-state {
579
+ text-align: center;
580
+ padding: 40px;
581
+ color: #656d76;
582
+ }
583
+
584
+ .processing-time {
585
+ position: fixed;
586
+ bottom: 20px;
587
+ right: 20px;
588
+ background: #0969da;
589
+ color: white;
590
+ padding: 8px 16px;
591
+ border-radius: 20px;
592
+ font-size: 12px;
593
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
594
+ }
595
+ </style>
596
+ </head>
597
+ <body>
598
+ <div class="container">
599
+ <div class="header">
600
+ <h1>📄 Document Processing Changes Report</h1>
601
+ <div class="meta-info">
602
+ <div>📁 <strong>File:</strong> ${comparison.documentPath}</div>
603
+ <div>🕐 <strong>Start:</strong> ${comparison.processingStartTime.toLocaleString()}</div>
604
+ <div>🕑 <strong>End:</strong> ${comparison.processingEndTime?.toLocaleString() || 'In Progress'}</div>
605
+ <div>⚡ <strong>Duration:</strong> ${comparison.statistics.processingDurationMs}ms</div>
606
+ </div>
607
+ </div>
608
+
609
+ <div class="statistics">
610
+ <h2>Processing Statistics</h2>
611
+ <div class="stat-grid">
612
+ <div class="stat-card">
613
+ <div class="value">${comparison.statistics.totalChanges}</div>
614
+ <div class="label">Total Changes</div>
615
+ </div>
616
+ <div class="stat-card">
617
+ <div class="value">${comparison.statistics.hyperlinksModified}</div>
618
+ <div class="label">Hyperlinks Modified</div>
619
+ </div>
620
+ <div class="stat-card">
621
+ <div class="value">${comparison.statistics.urlsChanged}</div>
622
+ <div class="label">URLs Changed</div>
623
+ </div>
624
+ <div class="stat-card">
625
+ <div class="value">${comparison.statistics.displayTextsChanged}</div>
626
+ <div class="label">Texts Updated</div>
627
+ </div>
628
+ <div class="stat-card">
629
+ <div class="value">${comparison.statistics.contentIdsAppended}</div>
630
+ <div class="label">Content IDs Added</div>
631
+ </div>
632
+ <div class="stat-card">
633
+ <div class="value">${comparison.statistics.stylesApplied}</div>
634
+ <div class="label">Styles Applied</div>
635
+ </div>
636
+ </div>
637
+ </div>
638
+
639
+ ${this.renderHyperlinkChanges(comparison)}
640
+ ${this.renderStyleChanges(comparison)}
641
+ ${this.renderContentChanges(comparison)}
642
+ </div>
643
+
644
+ <div class="processing-time">
645
+ Processing took ${comparison.statistics.processingDurationMs}ms
646
+ </div>
647
+ </body>
648
+ </html>
649
+ `;
650
+
651
+ return html;
652
+ }
653
+
654
+ /**
655
+ * Render hyperlink changes section
656
+ */
657
+ private renderHyperlinkChanges(comparison: ProcessingComparison): string {
658
+ if (comparison.hyperlinkChanges.length === 0) {
659
+ return `
660
+ <div class="changes-section">
661
+ <h2>🔗 Hyperlink Changes</h2>
662
+ <div class="empty-state">No hyperlink changes detected</div>
663
+ </div>
664
+ `;
665
+ }
666
+
667
+ const changes = comparison.hyperlinkChanges
668
+ .map(
669
+ (change) => `
670
+ <div class="change-item">
671
+ <div class="change-location">📍 Paragraph ${change.paragraphIndex + 1}, Hyperlink ${change.hyperlinkIndex + 1}</div>
672
+ <div class="change-content">
673
+ ${
674
+ change.originalUrl !== change.modifiedUrl
675
+ ? `
676
+ <div><strong>URL:</strong></div>
677
+ <div class="before">🔴 ${this.escapeHtml(change.originalUrl)}</div>
678
+ <div class="after">🟢 ${this.escapeHtml(change.modifiedUrl)}</div>
679
+ `
680
+ : ''
681
+ }
682
+ ${
683
+ change.originalText !== change.modifiedText
684
+ ? `
685
+ <div><strong>Display Text:</strong></div>
686
+ <div class="before">🔴 ${this.escapeHtml(change.originalText)}</div>
687
+ <div class="after">🟢 ${this.escapeHtml(change.modifiedText)}</div>
688
+ `
689
+ : ''
690
+ }
691
+ </div>
692
+ <div class="change-reason">💡 ${change.changeReason}</div>
693
+ </div>
694
+ `
695
+ )
696
+ .join('');
697
+
698
+ return `
699
+ <div class="changes-section">
700
+ <h2>🔗 Hyperlink Changes (${comparison.hyperlinkChanges.length})</h2>
701
+ ${changes}
702
+ </div>
703
+ `;
704
+ }
705
+
706
+ /**
707
+ * Render style changes section
708
+ */
709
+ private renderStyleChanges(comparison: ProcessingComparison): string {
710
+ if (comparison.styleChanges.length === 0) {
711
+ return `
712
+ <div class="changes-section">
713
+ <h2>🎨 Style Changes</h2>
714
+ <div class="empty-state">No style changes detected</div>
715
+ </div>
716
+ `;
717
+ }
718
+
719
+ const changes = comparison.styleChanges
720
+ .map(
721
+ (change) => `
722
+ <div class="change-item">
723
+ <div class="change-location">📍 Paragraph ${change.paragraphIndex + 1}</div>
724
+ <div class="change-content">
725
+ <div><strong>Applied Style:</strong> ${change.styleName} (${change.styleId})</div>
726
+ ${Object.entries(change.properties)
727
+ .map(([key, value]) => `<div style="margin-left: 20px;">• ${key}: ${value}</div>`)
728
+ .join('')}
729
+ </div>
730
+ </div>
731
+ `
732
+ )
733
+ .join('');
734
+
735
+ return `
736
+ <div class="changes-section">
737
+ <h2>🎨 Style Changes (${comparison.styleChanges.length})</h2>
738
+ ${changes}
739
+ </div>
740
+ `;
741
+ }
742
+
743
+ /**
744
+ * Render content changes section
745
+ */
746
+ private renderContentChanges(comparison: ProcessingComparison): string {
747
+ if (comparison.contentChanges.length === 0) {
748
+ return `
749
+ <div class="changes-section">
750
+ <h2>📝 Content Changes</h2>
751
+ <div class="empty-state">No content changes detected</div>
752
+ </div>
753
+ `;
754
+ }
755
+
756
+ const changes = comparison.contentChanges
757
+ .map(
758
+ (change) => `
759
+ <div class="change-item">
760
+ <div class="change-location">📍 Paragraph ${change.paragraphIndex + 1}</div>
761
+ <div class="change-content">
762
+ ${
763
+ change.type === 'added'
764
+ ? `
765
+ <div class="after">🟢 Added: ${this.escapeHtml(change.modifiedContent || '')}</div>
766
+ `
767
+ : ''
768
+ }
769
+ ${
770
+ change.type === 'removed'
771
+ ? `
772
+ <div class="before">🔴 Removed: ${this.escapeHtml(change.originalContent || '')}</div>
773
+ `
774
+ : ''
775
+ }
776
+ ${change.type === 'modified' && change.diff ? this.renderDiff(change.diff) : ''}
777
+ </div>
778
+ </div>
779
+ `
780
+ )
781
+ .join('');
782
+
783
+ return `
784
+ <div class="changes-section">
785
+ <h2>📝 Content Changes (${comparison.contentChanges.length})</h2>
786
+ ${changes}
787
+ </div>
788
+ `;
789
+ }
790
+
791
+ /**
792
+ * Render diff with inline changes
793
+ */
794
+ private renderDiff(changes: Change[]): string {
795
+ let html = '<div style="line-height: 1.6;">';
796
+
797
+ for (const change of changes) {
798
+ if (change.added) {
799
+ html += `<span style="background: #dafbe1; color: #1a7f37; padding: 2px 4px; border-radius: 3px;">${this.escapeHtml(change.value)}</span>`;
800
+ } else if (change.removed) {
801
+ html += `<span style="background: #ffebe9; color: #cf222e; padding: 2px 4px; text-decoration: line-through; border-radius: 3px;">${this.escapeHtml(change.value)}</span>`;
802
+ } else {
803
+ html += this.escapeHtml(change.value);
804
+ }
805
+ }
806
+
807
+ html += '</div>';
808
+ return html;
809
+ }
810
+
811
+ /**
812
+ * Escape HTML for safe rendering
813
+ */
814
+ private escapeHtml(text: string): string {
815
+ const div = document?.createElement ? document.createElement('div') : null;
816
+ if (div) {
817
+ div.textContent = text;
818
+ return div.innerHTML;
819
+ }
820
+ // Fallback for Node.js environment
821
+ return text
822
+ .replace(/&/g, '&amp;')
823
+ .replace(/</g, '&lt;')
824
+ .replace(/>/g, '&gt;')
825
+ .replace(/"/g, '&quot;')
826
+ .replace(/'/g, '&#039;');
827
+ }
828
+
829
+ /**
830
+ * Reset tracking for new document
831
+ */
832
+ reset(): void {
833
+ this.comparison = null;
834
+ this.originalHyperlinks.clear();
835
+ this.originalStyles.clear();
836
+ this.changeIdCounter = 0;
837
+ }
838
+
839
+ /**
840
+ * Get current comparison
841
+ */
842
+ getCurrentComparison(): ProcessingComparison | null {
843
+ return this.comparison;
844
+ }
845
+ }
846
+
847
+ // Export singleton instance
848
+ export const documentProcessingComparison = new DocumentProcessingComparison();
849
+
850
+ // Type for hyperlink snapshot
851
+ interface HyperlinkSnapshot {
852
+ paragraphIndex: number;
853
+ hyperlinkIndex: number;
854
+ url: string;
855
+ text: string;
856
+ }