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,370 @@
1
+ /**
2
+ * HyperlinkProcessor - Hyperlink manipulation and API integration
3
+ *
4
+ * Handles:
5
+ * - Hyperlink formatting standardization
6
+ * - PowerAutomate API integration for URL/text updates
7
+ * - Custom replacement rules
8
+ * - Internal hyperlink repair
9
+ * - URL update batch processing
10
+ */
11
+
12
+ import {
13
+ Document,
14
+ Hyperlink,
15
+ Revision,
16
+ } from "docxmlater";
17
+ import type { DetailedHyperlinkInfo, HyperlinkType } from "@/types/hyperlink";
18
+ import type { DocumentChange } from "@/types/session";
19
+ import { logger } from "@/utils/logger";
20
+ import { sanitizeHyperlinkText } from "@/utils/textSanitizer";
21
+ import { extractLookupIds } from "@/utils/urlPatterns";
22
+ import { hyperlinkService } from "../../HyperlinkService";
23
+ import { DocXMLaterProcessor } from "../DocXMLaterProcessor";
24
+ import { documentProcessingComparison } from "../DocumentProcessingComparison";
25
+
26
+ const log = logger.namespace("HyperlinkProcessor");
27
+
28
+ /**
29
+ * Result of URL update batch operation
30
+ */
31
+ export interface UrlUpdateResult {
32
+ updated: number;
33
+ failed: Array<{
34
+ oldUrl: string;
35
+ newUrl: string;
36
+ error: unknown;
37
+ paragraphIndex?: number;
38
+ }>;
39
+ }
40
+
41
+ /**
42
+ * Result of hyperlink processing
43
+ */
44
+ export interface HyperlinkProcessingResult {
45
+ updatedUrls: number;
46
+ updatedDisplayTexts: number;
47
+ standardizedCount: number;
48
+ changes: DocumentChange[];
49
+ processedLinks: Array<{
50
+ id: string;
51
+ url: string;
52
+ displayText: string;
53
+ type: HyperlinkType;
54
+ location: string;
55
+ status: "processed" | "skipped" | "error";
56
+ before: string;
57
+ after: string;
58
+ modifications: string[];
59
+ }>;
60
+ errorMessages: string[];
61
+ }
62
+
63
+ /**
64
+ * Options for hyperlink processing
65
+ */
66
+ export interface HyperlinkProcessingOptions {
67
+ apiEndpoint?: string;
68
+ operations?: {
69
+ fixContentIds?: boolean;
70
+ updateTitles?: boolean;
71
+ processHyperlinks?: boolean;
72
+ standardizeHyperlinkColor?: boolean;
73
+ fixInternalHyperlinks?: boolean;
74
+ };
75
+ trackChanges?: boolean;
76
+ userProfile?: {
77
+ firstName: string;
78
+ lastName: string;
79
+ email: string;
80
+ };
81
+ customReplacements?: Array<{
82
+ find: string;
83
+ replace: string;
84
+ matchType: "contains" | "exact" | "startsWith";
85
+ applyTo: "url" | "text" | "both";
86
+ }>;
87
+ }
88
+
89
+ /**
90
+ * Hyperlink processing service
91
+ */
92
+ export class HyperlinkProcessor {
93
+ private docXMLater: DocXMLaterProcessor;
94
+ private readonly DEBUG = process.env.NODE_ENV !== "production";
95
+
96
+ constructor() {
97
+ this.docXMLater = new DocXMLaterProcessor();
98
+ }
99
+
100
+ /**
101
+ * Standardize hyperlink formatting to Verdana 12pt blue underlined
102
+ */
103
+ async standardizeFormatting(doc: Document): Promise<number> {
104
+ let standardizedCount = 0;
105
+
106
+ try {
107
+ const hyperlinks = await this.docXMLater.extractHyperlinks(doc);
108
+ log.debug(`Found ${hyperlinks.length} hyperlinks to standardize`);
109
+
110
+ for (const { hyperlink, url, text } of hyperlinks) {
111
+ try {
112
+ hyperlink.setFormatting({
113
+ font: "Verdana",
114
+ size: 12,
115
+ color: "0000FF",
116
+ underline: "single",
117
+ bold: false,
118
+ italic: false,
119
+ });
120
+ standardizedCount++;
121
+ log.debug(`Standardized hyperlink: "${text}" (${url})`);
122
+ } catch (error) {
123
+ log.warn(`Failed to standardize hyperlink "${text}": ${error}`);
124
+ }
125
+ }
126
+
127
+ log.info(`Standardized ${standardizedCount} of ${hyperlinks.length} hyperlinks`);
128
+ } catch (error) {
129
+ log.error(`Error standardizing hyperlink formatting: ${error}`);
130
+ throw error;
131
+ }
132
+
133
+ return standardizedCount;
134
+ }
135
+
136
+ /**
137
+ * Apply URL updates to hyperlinks with track changes support
138
+ */
139
+ async applyUrlUpdates(
140
+ doc: Document,
141
+ urlMap: Map<string, string>,
142
+ author: string = "DocHub"
143
+ ): Promise<UrlUpdateResult> {
144
+ if (urlMap.size === 0) {
145
+ return { updated: 0, failed: [] };
146
+ }
147
+
148
+ const failedUrls: UrlUpdateResult["failed"] = [];
149
+ let updatedCount = 0;
150
+ const paragraphs = doc.getAllParagraphs();
151
+ const trackChangesEnabled = doc.isTrackChangesEnabled();
152
+
153
+ log.debug(`Processing ${paragraphs.length} paragraphs for URL updates`);
154
+
155
+ for (let paraIndex = 0; paraIndex < paragraphs.length; paraIndex++) {
156
+ const para = paragraphs[paraIndex];
157
+ const content = para.getContent();
158
+
159
+ for (const item of [...content]) {
160
+ if (item instanceof Hyperlink) {
161
+ const oldUrl = item.getUrl();
162
+
163
+ if (oldUrl && urlMap.has(oldUrl)) {
164
+ const newUrl = urlMap.get(oldUrl)!;
165
+
166
+ if (oldUrl === newUrl) {
167
+ continue;
168
+ }
169
+
170
+ try {
171
+ if (trackChangesEnabled) {
172
+ const oldHyperlink = item.clone();
173
+ item.setUrl(newUrl);
174
+
175
+ const deletion = Revision.createDeletion(author, [oldHyperlink]);
176
+ const insertion = Revision.createInsertion(author, [item]);
177
+
178
+ const replaced = para.replaceContent(item, [deletion, insertion]);
179
+
180
+ if (replaced) {
181
+ const revisionManager = doc.getRevisionManager();
182
+ revisionManager.register(deletion);
183
+ revisionManager.register(insertion);
184
+ log.debug(`Created tracked change for hyperlink URL: ${oldUrl} -> ${newUrl}`);
185
+ }
186
+ } else {
187
+ item.setUrl(newUrl);
188
+ log.debug(`Updated hyperlink URL: ${oldUrl} -> ${newUrl}`);
189
+ }
190
+
191
+ updatedCount++;
192
+ } catch (error) {
193
+ log.error(`Failed to update URL at paragraph ${paraIndex}: ${oldUrl} -> ${newUrl}`, error);
194
+ failedUrls.push({
195
+ oldUrl,
196
+ newUrl,
197
+ error,
198
+ paragraphIndex: paraIndex,
199
+ });
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ if (failedUrls.length > 0) {
207
+ log.warn(`URL update completed with ${failedUrls.length} failures`);
208
+ } else {
209
+ log.info(`Successfully updated ${updatedCount} hyperlink URLs`);
210
+ }
211
+
212
+ return { updated: updatedCount, failed: failedUrls };
213
+ }
214
+
215
+ /**
216
+ * Process custom URL and text replacements
217
+ */
218
+ async processCustomReplacements(
219
+ doc: Document,
220
+ replacements: NonNullable<HyperlinkProcessingOptions["customReplacements"]>
221
+ ): Promise<{ updatedUrls: number; updatedTexts: number }> {
222
+ const hyperlinks = await this.docXMLater.extractHyperlinks(doc);
223
+ let updatedUrls = 0;
224
+ let updatedTexts = 0;
225
+
226
+ for (const { hyperlink, url, text } of hyperlinks) {
227
+ for (const rule of replacements) {
228
+ if (rule.applyTo === "url" || rule.applyTo === "both") {
229
+ if (url && this.matchesPattern(url, rule.find, rule.matchType)) {
230
+ const newUrl = url.replace(rule.find, rule.replace);
231
+ hyperlink.setUrl(newUrl);
232
+ updatedUrls++;
233
+ }
234
+ }
235
+
236
+ if (rule.applyTo === "text" || rule.applyTo === "both") {
237
+ if (this.matchesPattern(text, rule.find, rule.matchType)) {
238
+ const newText = text.replace(rule.find, rule.replace);
239
+ hyperlink.setText(newText);
240
+ updatedTexts++;
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ return { updatedUrls, updatedTexts };
247
+ }
248
+
249
+ /**
250
+ * Fix internal hyperlinks - repair broken bookmarks
251
+ */
252
+ async fixInternalHyperlinks(doc: Document): Promise<number> {
253
+ const hyperlinks = await this.docXMLater.extractHyperlinks(doc);
254
+ let fixedCount = 0;
255
+
256
+ for (const { hyperlink, text } of hyperlinks) {
257
+ const anchor = hyperlink.getAnchor();
258
+ if (!anchor) continue;
259
+
260
+ const bookmarkExists = doc.hasBookmark(anchor);
261
+
262
+ if (!bookmarkExists) {
263
+ if (text) {
264
+ const matchingHeading = this.findHeadingByText(doc, text);
265
+ if (matchingHeading) {
266
+ const newBookmark = this.createBookmarkForHeading(doc, matchingHeading, anchor);
267
+ if (newBookmark) {
268
+ fixedCount++;
269
+ log.info(`Created bookmark "${anchor}" for heading "${text}"`);
270
+ }
271
+ }
272
+ }
273
+ }
274
+ }
275
+
276
+ return fixedCount;
277
+ }
278
+
279
+ /**
280
+ * Find matching API result for a URL
281
+ */
282
+ findMatchingApiResult(url: string, apiResultsMap: Map<string, unknown>): unknown {
283
+ if (!url || !apiResultsMap || apiResultsMap.size === 0) {
284
+ return null;
285
+ }
286
+
287
+ const lookupIds = extractLookupIds(url);
288
+ if (!lookupIds) {
289
+ return null;
290
+ }
291
+
292
+ if (lookupIds.contentId) {
293
+ const result = apiResultsMap.get(lookupIds.contentId);
294
+ if (result) {
295
+ log.debug(`Matched by Content_ID: ${lookupIds.contentId}`);
296
+ return result;
297
+ }
298
+ }
299
+
300
+ if (lookupIds.documentId) {
301
+ const result = apiResultsMap.get(lookupIds.documentId);
302
+ if (result) {
303
+ log.debug(`Matched by Document_ID: ${lookupIds.documentId}`);
304
+ return result;
305
+ }
306
+ }
307
+
308
+ return null;
309
+ }
310
+
311
+ /**
312
+ * Pattern matching helper
313
+ */
314
+ private matchesPattern(
315
+ text: string,
316
+ pattern: string,
317
+ matchType: "contains" | "exact" | "startsWith"
318
+ ): boolean {
319
+ switch (matchType) {
320
+ case "exact":
321
+ return text === pattern;
322
+ case "startsWith":
323
+ return text.startsWith(pattern);
324
+ case "contains":
325
+ default:
326
+ return text.includes(pattern);
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Find heading by text content
332
+ */
333
+ private findHeadingByText(doc: Document, searchText: string): unknown | null {
334
+ const normalizedSearch = searchText.trim().toLowerCase();
335
+ const paragraphs = doc.getAllParagraphs();
336
+
337
+ for (const para of paragraphs) {
338
+ const style = para.getStyle();
339
+
340
+ if (style && (style.startsWith("Heading") || style.includes("Heading"))) {
341
+ const paraText = (para.getText() || "").trim().toLowerCase();
342
+ if (paraText === normalizedSearch) {
343
+ return para;
344
+ }
345
+ }
346
+ }
347
+
348
+ return null;
349
+ }
350
+
351
+ /**
352
+ * Create bookmark for heading
353
+ */
354
+ private createBookmarkForHeading(
355
+ doc: Document,
356
+ heading: unknown,
357
+ bookmarkName: string
358
+ ): boolean {
359
+ try {
360
+ // Implementation depends on docxmlater API
361
+ // This is a placeholder for the actual implementation
362
+ return false;
363
+ } catch (error) {
364
+ log.warn(`Failed to create bookmark: ${error}`);
365
+ return false;
366
+ }
367
+ }
368
+ }
369
+
370
+ export const hyperlinkProcessor = new HyperlinkProcessor();
@@ -0,0 +1,257 @@
1
+ /**
2
+ * ListProcessor - List formatting and bullet/numbering operations
3
+ *
4
+ * Handles:
5
+ * - List bullet settings and uniformity
6
+ * - Numbered list formatting
7
+ * - List prefix standardization (font, size, color)
8
+ * - Indentation configuration
9
+ * - Spacing between list items
10
+ */
11
+
12
+ import { Document, inchesToTwips } from "docxmlater";
13
+ import { logger } from "@/utils/logger";
14
+
15
+ const log = logger.namespace("ListProcessor");
16
+
17
+ /**
18
+ * Configuration for list indentation levels
19
+ */
20
+ export interface ListIndentationLevel {
21
+ level: number;
22
+ symbolIndent: number; // Symbol/bullet position in inches
23
+ textIndent: number; // Text position in inches
24
+ bulletChar?: string;
25
+ numberedFormat?: string;
26
+ }
27
+
28
+ /**
29
+ * List bullet settings
30
+ */
31
+ export interface ListBulletSettings {
32
+ enabled: boolean;
33
+ indentationLevels: ListIndentationLevel[];
34
+ spacingBetweenItems: number;
35
+ }
36
+
37
+ /**
38
+ * Result of list processing
39
+ */
40
+ export interface ListProcessingResult {
41
+ listsUpdated: number;
42
+ levelsProcessed: number;
43
+ }
44
+
45
+ /**
46
+ * List processing service
47
+ */
48
+ export class ListProcessor {
49
+ private readonly DEBUG = process.env.NODE_ENV !== "production";
50
+
51
+ /**
52
+ * Apply list indentation settings to all lists in the document
53
+ */
54
+ async applyListIndentation(
55
+ doc: Document,
56
+ settings: ListBulletSettings
57
+ ): Promise<ListProcessingResult> {
58
+ if (!settings.enabled || !settings.indentationLevels.length) {
59
+ return { listsUpdated: 0, levelsProcessed: 0 };
60
+ }
61
+
62
+ let listsUpdated = 0;
63
+ let levelsProcessed = 0;
64
+ const paragraphs = doc.getAllParagraphs();
65
+
66
+ for (const para of paragraphs) {
67
+ const numbering = para.getNumbering();
68
+ if (!numbering) continue;
69
+
70
+ const level = numbering.level || 0;
71
+ const indentSetting = settings.indentationLevels.find((l) => l.level === level);
72
+
73
+ if (indentSetting) {
74
+ // Validate indentation - symbolIndent must be less than textIndent
75
+ if (indentSetting.symbolIndent >= indentSetting.textIndent) {
76
+ log.warn(
77
+ `Invalid indentation for level ${level}: symbolIndent (${indentSetting.symbolIndent}) ` +
78
+ `must be less than textIndent (${indentSetting.textIndent}). Skipping.`
79
+ );
80
+ continue;
81
+ }
82
+
83
+ try {
84
+ // Apply indentation using individual methods
85
+ para.setLeftIndent(inchesToTwips(indentSetting.textIndent));
86
+ // Hanging indent is implemented as a negative first-line indent
87
+ const hangingTwips = inchesToTwips(indentSetting.textIndent - indentSetting.symbolIndent);
88
+ para.setFirstLineIndent(-hangingTwips);
89
+
90
+ // Apply spacing if configured
91
+ if (settings.spacingBetweenItems > 0) {
92
+ para.setSpaceAfter(settings.spacingBetweenItems * 20); // Convert to twips
93
+ }
94
+
95
+ listsUpdated++;
96
+ levelsProcessed++;
97
+ } catch (error) {
98
+ log.warn(`Failed to apply indentation to list item: ${error}`);
99
+ }
100
+ }
101
+ }
102
+
103
+ log.info(`Applied indentation to ${listsUpdated} list items`);
104
+ return { listsUpdated, levelsProcessed };
105
+ }
106
+
107
+ /**
108
+ * Standardize list prefix formatting (bullet/number font, size, color)
109
+ * Uses NumberingLevel API setters instead of raw XML manipulation.
110
+ */
111
+ async standardizeListPrefixFormatting(doc: Document): Promise<number> {
112
+ let standardizedCount = 0;
113
+
114
+ try {
115
+ const numberingManager = doc.getNumberingManager();
116
+ if (!numberingManager) {
117
+ log.warn("No numbering manager available for list prefix standardization");
118
+ return 0;
119
+ }
120
+
121
+ const abstractNums = numberingManager.getAllAbstractNumberings();
122
+ log.debug(`Found ${abstractNums.length} abstract numberings to process`);
123
+
124
+ // Special bullet fonts that should be preserved (used for special characters like open/closed circles)
125
+ const specialBulletFonts = ["Webdings", "Wingdings", "Symbol", "Wingdings 2", "Wingdings 3", "Courier New"];
126
+
127
+ for (const abstractNum of abstractNums) {
128
+ for (let levelIndex = 0; levelIndex <= 8; levelIndex++) {
129
+ const level = abstractNum.getLevel(levelIndex);
130
+ if (!level) continue;
131
+
132
+ // Check if this level uses a special bullet font that should be preserved
133
+ const currentFont = level.getProperties().font;
134
+ const preserveFont = currentFont && specialBulletFonts.includes(currentFont);
135
+
136
+ // Preserve special bullet fonts, otherwise use Verdana
137
+ const fontToUse = preserveFont ? currentFont : "Verdana";
138
+ level.setFont(fontToUse);
139
+ level.setColor("000000");
140
+ level.setFontSize(24); // 24 half-points = 12pt
141
+ level.setBold(false);
142
+
143
+ standardizedCount++;
144
+
145
+ if (preserveFont) {
146
+ log.debug(`Standardized list level ${levelIndex}: preserved ${currentFont} font, 12pt black`);
147
+ } else {
148
+ log.debug(`Standardized list level ${levelIndex}: Verdana 12pt black`);
149
+ }
150
+ }
151
+ }
152
+
153
+ if (standardizedCount > 0) {
154
+ log.info(`Standardized ${standardizedCount} list prefix levels`);
155
+ }
156
+ } catch (error) {
157
+ log.error(`Error standardizing list prefix formatting: ${error}`);
158
+ throw error;
159
+ }
160
+
161
+ return standardizedCount;
162
+ }
163
+
164
+ /**
165
+ * Apply bullet uniformity - standardize bullet characters
166
+ * Uses NumberingLevel API setters instead of raw XML manipulation.
167
+ */
168
+ async applyBulletUniformity(
169
+ doc: Document,
170
+ settings: ListBulletSettings
171
+ ): Promise<ListProcessingResult> {
172
+ let listsUpdated = 0;
173
+
174
+ try {
175
+ const numberingManager = doc.getNumberingManager();
176
+ if (!numberingManager) {
177
+ return { listsUpdated: 0, levelsProcessed: 0 };
178
+ }
179
+
180
+ const abstractNums = numberingManager.getAllAbstractNumberings();
181
+
182
+ // Update bullet characters based on settings
183
+ for (const levelConfig of settings.indentationLevels) {
184
+ if (!levelConfig.bulletChar) continue;
185
+
186
+ for (const abstractNum of abstractNums) {
187
+ const level = abstractNum.getLevel(levelConfig.level);
188
+ if (!level) continue;
189
+
190
+ // Only update bullet lists, not numbered lists
191
+ if (level.getFormat() !== "bullet") continue;
192
+
193
+ level.setText(levelConfig.bulletChar);
194
+ listsUpdated++;
195
+ }
196
+ }
197
+
198
+ if (listsUpdated > 0) {
199
+ log.info(`Updated bullet characters in ${listsUpdated} list levels`);
200
+ }
201
+ } catch (error) {
202
+ log.error(`Error applying bullet uniformity: ${error}`);
203
+ }
204
+
205
+ return { listsUpdated, levelsProcessed: listsUpdated };
206
+ }
207
+
208
+ /**
209
+ * Check if a paragraph is a bullet list item
210
+ */
211
+ isBulletList(doc: Document, numId: number): boolean {
212
+ try {
213
+ const numberingManager = doc.getNumberingManager();
214
+ const instance = numberingManager?.getNumberingInstance(numId);
215
+ if (!instance) return false;
216
+
217
+ const abstractNumId = instance.getAbstractNumId();
218
+ const abstractNum = numberingManager?.getAbstractNumbering(abstractNumId);
219
+ if (!abstractNum) return false;
220
+
221
+ const level0 = abstractNum.getLevel(0);
222
+ return level0?.getFormat() === "bullet";
223
+ } catch {
224
+ return false;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Check if a paragraph is a numbered list item
230
+ */
231
+ isNumberedList(doc: Document, numId: number): boolean {
232
+ try {
233
+ const numberingManager = doc.getNumberingManager();
234
+ const instance = numberingManager?.getNumberingInstance(numId);
235
+ if (!instance) return false;
236
+
237
+ const abstractNumId = instance.getAbstractNumId();
238
+ const abstractNum = numberingManager?.getAbstractNumbering(abstractNumId);
239
+ if (!abstractNum) return false;
240
+
241
+ const level0 = abstractNum.getLevel(0);
242
+ const format = level0?.getFormat();
243
+ return (
244
+ format === "decimal" ||
245
+ format === "lowerLetter" ||
246
+ format === "upperLetter" ||
247
+ format === "lowerRoman" ||
248
+ format === "upperRoman"
249
+ );
250
+ } catch {
251
+ return false;
252
+ }
253
+ }
254
+
255
+ }
256
+
257
+ export const listProcessor = new ListProcessor();