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,658 @@
1
+ /**
2
+ * BlankLineManager - Rule-based blank line management engine.
3
+ *
4
+ * Replaces the old phase-based "remove all, then add back" approach with
5
+ * a rule-based system that applies explicit rules first, then preserves
6
+ * original blank lines where no rule matched.
7
+ *
8
+ * Processing order:
9
+ * 1. Remove SDT wrappers
10
+ * 2. Apply REMOVAL rules (absolute constraints)
11
+ * 3. Apply ADDITION rules (absolute requirements)
12
+ * 4. Apply PRESERVATION fallback (keep original if no rule matched)
13
+ * 5. Apply INDENTATION rules
14
+ * 6. Dedup (safety net - remove adjacent blanks)
15
+ * 7. Normalize blank line styles to Normal
16
+ */
17
+
18
+ import { Document, Paragraph, Table, TableCell } from "docxmlater";
19
+ import { logger } from "@/utils/logger";
20
+ import { clearCustom } from "./helpers/clearCustom";
21
+ import { isParagraphBlank } from "./helpers/paragraphChecks";
22
+ import { createBlankParagraph } from "./helpers/blankLineInsertion";
23
+ import { getImageRunFromParagraph, isImageSmall } from "./helpers/imageChecks";
24
+ import { tableHasNestedContent } from "./helpers/tableGuards";
25
+ import type { BlankLineSnapshot } from "./helpers/blankLineSnapshot";
26
+ import {
27
+ wasOriginallyBlankAtBody,
28
+ wasOriginallyBlankInCell,
29
+ } from "./helpers/blankLineSnapshot";
30
+ import { removalRules } from "./rules/removalRules";
31
+ import { additionRules } from "./rules/additionRules";
32
+ import { applyIndentationRules } from "./rules/indentationRules";
33
+ import type {
34
+ RuleContext,
35
+ BlankLineProcessingOptions,
36
+ RuleEngineResult,
37
+ BlankLineRule,
38
+ } from "./rules/ruleTypes";
39
+ import type { BlankLineOptions } from "./types";
40
+
41
+ const log = logger.namespace("BlankLineManager");
42
+
43
+ function buildBlankLineOptions(
44
+ normalStyle?: BlankLineProcessingOptions['normalStyleFormatting']
45
+ ): BlankLineOptions {
46
+ return {
47
+ spacingAfter: normalStyle?.spaceAfter ?? 120,
48
+ spacingBefore: normalStyle?.spaceBefore,
49
+ lineSpacing: normalStyle?.lineSpacing,
50
+ fontSize: normalStyle?.fontSize,
51
+ fontFamily: normalStyle?.fontFamily,
52
+ markAsPreserved: true,
53
+ style: "Normal",
54
+ };
55
+ }
56
+
57
+ export class BlankLineManager {
58
+ /**
59
+ * Primary entry point for rule-based blank line processing.
60
+ * Must be called AFTER list normalization is complete.
61
+ */
62
+ processBlankLines(
63
+ doc: Document,
64
+ snapshot: BlankLineSnapshot,
65
+ options: BlankLineProcessingOptions
66
+ ): RuleEngineResult {
67
+ const result: RuleEngineResult = {
68
+ removed: 0,
69
+ added: 0,
70
+ preserved: 0,
71
+ indentationFixed: 0,
72
+ };
73
+
74
+ const blankOpts = buildBlankLineOptions(options.normalStyleFormatting);
75
+
76
+ // Step 1: Remove SDT wrappers
77
+ clearCustom(doc);
78
+
79
+ // NOTE: Small indent removal (< 0.25") now runs earlier in WordDocumentProcessor,
80
+ // before applyListContinuationIndentation, so that trivially-indented paragraphs
81
+ // are normalized to zero before continuation indentation logic evaluates them.
82
+
83
+ // Step 2: Apply removal rules (walk body + cells, remove blanks where rules match)
84
+ result.removed += this.applyRemovalRulesBody(doc);
85
+ result.removed += this.applyRemovalRulesCells(doc);
86
+
87
+ // Step 3: Apply addition rules (walk body + cells, add blanks where rules match)
88
+ result.added += this.applyAdditionRulesBody(doc, options, blankOpts);
89
+ result.added += this.applyAdditionRulesCells(doc, options, blankOpts);
90
+
91
+ // Step 4: Apply preservation fallback (keep original blanks where no rule matched)
92
+ result.preserved += this.applyPreservationFallbackBody(doc, snapshot, blankOpts);
93
+ result.preserved += this.applyPreservationFallbackCells(doc, snapshot, blankOpts);
94
+
95
+ // Step 5: Apply indentation rules
96
+ result.indentationFixed = applyIndentationRules(doc, options);
97
+
98
+ // Step 6: Final dedup pass (remove adjacent blank lines)
99
+ const dedupRemoved = this.dedup(doc);
100
+ result.removed += dedupRemoved;
101
+
102
+ // Step 7: Normalize all blank line styles to Normal
103
+ this.normalizeBlankLineStyles(doc, blankOpts);
104
+
105
+ log.info(
106
+ `Rule engine complete: ${result.removed} removed, ${result.added} added, ` +
107
+ `${result.preserved} preserved, ${result.indentationFixed} indentation fixes`
108
+ );
109
+
110
+ return result;
111
+ }
112
+
113
+ /**
114
+ * Apply removal rules to body-level blank paragraphs.
115
+ * Iterates backwards to safely remove elements.
116
+ */
117
+ private applyRemovalRulesBody(doc: Document): number {
118
+ let removed = 0;
119
+
120
+ for (let i = doc.getBodyElementCount() - 1; i >= 0; i--) {
121
+ const element = doc.getBodyElementAt(i);
122
+ if (!(element instanceof Paragraph)) continue;
123
+ if (!isParagraphBlank(element)) continue;
124
+ if (element.isPreserved()) continue; // Protect field paragraphs (TOC)
125
+
126
+ const ctx = this.buildBodyContext(doc, i);
127
+ const matchedRule = this.findMatchingRule(removalRules, ctx, "body");
128
+
129
+ if (matchedRule) {
130
+ log.debug(`Removal rule "${matchedRule.id}" matched at body index ${i}`);
131
+ doc.removeBodyElementAt(i);
132
+ removed++;
133
+ }
134
+ }
135
+
136
+ return removed;
137
+ }
138
+
139
+ /**
140
+ * Apply removal rules to blank paragraphs inside table cells.
141
+ * Iterates backwards within each cell for safe removal.
142
+ */
143
+ private applyRemovalRulesCells(doc: Document): number {
144
+ let removed = 0;
145
+
146
+ for (const table of doc.getAllTables()) {
147
+ if (tableHasNestedContent(table)) continue;
148
+
149
+ for (const row of table.getRows()) {
150
+ for (const cell of row.getCells()) {
151
+ let paras = cell.getParagraphs();
152
+
153
+ for (let ci = paras.length - 1; ci >= 0; ci--) {
154
+ // Keep at least one paragraph per cell (ECMA-376)
155
+ if (paras.length <= 1) break;
156
+
157
+ const para = paras[ci];
158
+ if (!para || !isParagraphBlank(para)) continue;
159
+ if (para.isPreserved()) continue;
160
+
161
+ const ctx = this.buildCellContext(doc, cell, ci, paras, table);
162
+ const matchedRule = this.findMatchingRule(removalRules, ctx, "cell");
163
+
164
+ if (matchedRule) {
165
+ log.debug(`Removal rule "${matchedRule.id}" matched in cell at index ${ci}`);
166
+ cell.removeParagraph(ci);
167
+ removed++;
168
+ paras = cell.getParagraphs();
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ return removed;
176
+ }
177
+
178
+ /**
179
+ * Apply addition rules to body-level positions.
180
+ * Iterates forward, inserting blanks where addition rules match.
181
+ */
182
+ private applyAdditionRulesBody(
183
+ doc: Document,
184
+ options: BlankLineProcessingOptions,
185
+ blankOpts: BlankLineOptions
186
+ ): number {
187
+ let added = 0;
188
+
189
+ for (let i = 0; i < doc.getBodyElementCount(); i++) {
190
+ const element = doc.getBodyElementAt(i);
191
+ const ctx = this.buildBodyContext(doc, i);
192
+ const matchedRule = this.findMatchingRule(additionRules, ctx, "body");
193
+
194
+ if (matchedRule) {
195
+ // Clear indentation from navigation hyperlink paragraphs
196
+ if (matchedRule.id === "add-above-top-of-doc-hyperlink") {
197
+ const targetPara = ctx.nextElement;
198
+ if (targetPara instanceof Paragraph) {
199
+ const indent = targetPara.getFormatting()?.indentation?.left;
200
+ if (indent && indent > 0) {
201
+ targetPara.setLeftIndent(0);
202
+ }
203
+ }
204
+ }
205
+
206
+ // Determine if this rule wants a blank BEFORE the next element or AFTER the current
207
+ const isBefore =
208
+ matchedRule.id === "add-above-top-of-doc-hyperlink" ||
209
+ matchedRule.id === "add-above-warning" ||
210
+ matchedRule.id === "add-before-first-1x1-table" ||
211
+ matchedRule.id === "add-above-bold-colon-no-indent";
212
+
213
+ if (isBefore) {
214
+ // These rules want a blank BEFORE the next element
215
+ // Check if a blank already exists between current and next
216
+ const nextIdx = i + 1;
217
+ if (nextIdx < doc.getBodyElementCount()) {
218
+ const nextEl = doc.getBodyElementAt(nextIdx);
219
+ if (nextEl instanceof Paragraph && isParagraphBlank(nextEl)) {
220
+ continue; // Already a blank
221
+ }
222
+ const blankPara = createBlankParagraph(blankOpts);
223
+ doc.insertBodyElementAt(nextIdx, blankPara);
224
+ added++;
225
+ i++; // Skip past inserted blank
226
+ }
227
+ } else {
228
+ // Standard "after" rules - ensure blank after current element
229
+ const nextIdx = i + 1;
230
+ if (nextIdx < doc.getBodyElementCount()) {
231
+ const nextEl = doc.getBodyElementAt(nextIdx);
232
+ if (nextEl instanceof Paragraph && isParagraphBlank(nextEl)) {
233
+ continue; // Already a blank
234
+ }
235
+ }
236
+ const blankPara = createBlankParagraph(blankOpts);
237
+ doc.insertBodyElementAt(i + 1, blankPara);
238
+ added++;
239
+ i++; // Skip past inserted blank
240
+ }
241
+ }
242
+
243
+ // Special: large images need blank ABOVE as well
244
+ if (element instanceof Paragraph && !isParagraphBlank(element)) {
245
+ const imageRun = getImageRunFromParagraph(element);
246
+ if (imageRun) {
247
+ const image = imageRun.getImageElement();
248
+ if (!isImageSmall(image) && i > 0) {
249
+ const prevEl = doc.getBodyElementAt(i - 1);
250
+ if (!(prevEl instanceof Paragraph && isParagraphBlank(prevEl))) {
251
+ // Don't add blank above image if previous is centered text
252
+ const isCenteredText =
253
+ prevEl instanceof Paragraph &&
254
+ prevEl.getAlignment() === "center" &&
255
+ !!prevEl.getText()?.trim();
256
+ if (!isCenteredText) {
257
+ const blankPara = createBlankParagraph(blankOpts);
258
+ doc.insertBodyElementAt(i, blankPara);
259
+ added++;
260
+ i++; // Skip past inserted blank
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ return added;
269
+ }
270
+
271
+ /**
272
+ * Apply addition rules to table cell positions.
273
+ */
274
+ private applyAdditionRulesCells(
275
+ doc: Document,
276
+ options: BlankLineProcessingOptions,
277
+ blankOpts: BlankLineOptions
278
+ ): number {
279
+ let added = 0;
280
+
281
+ for (const table of doc.getAllTables()) {
282
+ if (tableHasNestedContent(table)) continue;
283
+
284
+ for (const row of table.getRows()) {
285
+ for (const cell of row.getCells()) {
286
+ let paras = cell.getParagraphs();
287
+
288
+ for (let ci = 0; ci < paras.length; ci++) {
289
+ paras = cell.getParagraphs();
290
+ const para = paras[ci];
291
+ if (!para) continue;
292
+
293
+ const ctx = this.buildCellContext(doc, cell, ci, paras, table);
294
+ const matchedRule = this.findMatchingRule(additionRules, ctx, "cell");
295
+
296
+ if (matchedRule) {
297
+ const isLastInCell = ci === paras.length - 1;
298
+ if (isLastInCell) continue; // Don't add blank at end of cell
299
+
300
+ const nextPara = paras[ci + 1];
301
+ if (nextPara && isParagraphBlank(nextPara)) continue; // Already has blank
302
+
303
+ const blankPara = createBlankParagraph(blankOpts);
304
+ cell.addParagraphAt(ci + 1, blankPara);
305
+ added++;
306
+ ci++; // Skip past inserted blank
307
+ paras = cell.getParagraphs();
308
+ }
309
+ }
310
+ }
311
+ }
312
+ }
313
+
314
+ return added;
315
+ }
316
+
317
+ /**
318
+ * Preservation fallback for body-level positions.
319
+ * For positions where no rule matched, if the original document had a blank there,
320
+ * insert one to preserve the original spacing.
321
+ */
322
+ private applyPreservationFallbackBody(
323
+ doc: Document,
324
+ snapshot: BlankLineSnapshot,
325
+ blankOpts: BlankLineOptions
326
+ ): number {
327
+ let preserved = 0;
328
+
329
+ for (let i = 0; i < doc.getBodyElementCount() - 1; i++) {
330
+ const next = doc.getBodyElementAt(i + 1);
331
+
332
+ // If there's already a blank at the next position, skip
333
+ if (next instanceof Paragraph && isParagraphBlank(next)) continue;
334
+
335
+ // Check if a blank existed here originally
336
+ if (wasOriginallyBlankAtBody(snapshot, doc, i + 1)) {
337
+ // Check if any removal rule would explicitly remove a blank here
338
+ const removalMatch = this.findMatchingRemovalForPosition(doc, i, i + 1);
339
+ if (removalMatch) continue;
340
+
341
+ // No rule matched and original had a blank - preserve it
342
+ const blankPara = createBlankParagraph(blankOpts);
343
+ doc.insertBodyElementAt(i + 1, blankPara);
344
+ preserved++;
345
+ i++; // Skip past inserted blank
346
+ }
347
+ }
348
+
349
+ return preserved;
350
+ }
351
+
352
+ /**
353
+ * Preservation fallback for cell-level positions.
354
+ */
355
+ private applyPreservationFallbackCells(
356
+ doc: Document,
357
+ snapshot: BlankLineSnapshot,
358
+ blankOpts: BlankLineOptions
359
+ ): number {
360
+ let preserved = 0;
361
+ let tableIndex = 0;
362
+
363
+ for (const table of doc.getAllTables()) {
364
+ if (tableHasNestedContent(table)) {
365
+ tableIndex++;
366
+ continue;
367
+ }
368
+
369
+ let firstCellText = "";
370
+ try {
371
+ const firstCell = table.getCell(0, 0);
372
+ if (firstCell) {
373
+ firstCellText = firstCell
374
+ .getParagraphs()
375
+ .map((p) => p.getText())
376
+ .join(" ")
377
+ .substring(0, 20);
378
+ }
379
+ } catch {
380
+ // Skip
381
+ }
382
+
383
+ const rows = table.getRows();
384
+ for (let ri = 0; ri < rows.length; ri++) {
385
+ const cells = rows[ri].getCells();
386
+ for (let colIdx = 0; colIdx < cells.length; colIdx++) {
387
+ const cell = cells[colIdx];
388
+ let paras = cell.getParagraphs();
389
+ const cellId = `t${tableIndex}_r${ri}_c${colIdx}_${firstCellText.substring(0, 20)}`;
390
+
391
+ for (let ci = 0; ci < paras.length - 1; ci++) {
392
+ const nextPara = paras[ci + 1];
393
+ if (!nextPara || isParagraphBlank(nextPara)) continue;
394
+
395
+ if (wasOriginallyBlankInCell(snapshot, cell, ci + 1, cellId)) {
396
+ // Don't preserve blank at very end of cell
397
+ if (ci + 1 >= paras.length - 1) continue;
398
+
399
+ // Check if a removal rule would remove this blank
400
+ const removalMatch = this.findMatchingCellRemovalForPosition(
401
+ doc, cell, paras, ci, ci + 1, table
402
+ );
403
+ if (removalMatch) continue;
404
+
405
+ const blankPara = createBlankParagraph(blankOpts);
406
+ cell.addParagraphAt(ci + 1, blankPara);
407
+ preserved++;
408
+ ci++;
409
+ paras = cell.getParagraphs();
410
+ }
411
+ }
412
+ }
413
+ }
414
+ tableIndex++;
415
+ }
416
+
417
+ return preserved;
418
+ }
419
+
420
+ /**
421
+ * Check if any removal rule would match at a hypothetical blank at the given position.
422
+ */
423
+ private findMatchingRemovalForPosition(
424
+ doc: Document,
425
+ prevIndex: number,
426
+ blankIndex: number
427
+ ): BlankLineRule | null {
428
+ const prev = doc.getBodyElementAt(prevIndex);
429
+ const next =
430
+ blankIndex < doc.getBodyElementCount()
431
+ ? doc.getBodyElementAt(blankIndex)
432
+ : undefined;
433
+
434
+ const ctx: RuleContext = {
435
+ doc,
436
+ currentIndex: blankIndex,
437
+ currentElement: Paragraph.create(), // Simulate a blank paragraph
438
+ prevElement:
439
+ prev instanceof Paragraph || prev instanceof Table ? prev : undefined,
440
+ nextElement:
441
+ next instanceof Paragraph || next instanceof Table ? next : undefined,
442
+ scope: "body",
443
+ };
444
+
445
+ for (const rule of removalRules) {
446
+ if (rule.scope !== "body" && rule.scope !== "both") continue;
447
+ if (rule.matches(ctx)) return rule;
448
+ }
449
+ return null;
450
+ }
451
+
452
+ /**
453
+ * Check if any removal rule would match a hypothetical blank at a cell position.
454
+ */
455
+ private findMatchingCellRemovalForPosition(
456
+ doc: Document,
457
+ cell: TableCell,
458
+ paras: Paragraph[],
459
+ prevIndex: number,
460
+ nextIndex: number,
461
+ parentTable: Table
462
+ ): BlankLineRule | null {
463
+ const ctx: RuleContext = {
464
+ doc,
465
+ currentIndex: prevIndex + 1,
466
+ currentElement: Paragraph.create(), // Simulate a blank paragraph
467
+ prevElement: prevIndex >= 0 ? paras[prevIndex] : undefined,
468
+ nextElement: nextIndex < paras.length ? paras[nextIndex] : undefined,
469
+ scope: "cell",
470
+ cell,
471
+ cellParagraphs: paras,
472
+ cellParaIndex: prevIndex + 1,
473
+ parentTable,
474
+ };
475
+
476
+ for (const rule of removalRules) {
477
+ if (rule.scope !== "cell" && rule.scope !== "both") continue;
478
+ if (rule.matches(ctx)) return rule;
479
+ }
480
+ return null;
481
+ }
482
+
483
+ /**
484
+ * Remove adjacent blank paragraphs (dedup safety net).
485
+ */
486
+ private dedup(doc: Document): number {
487
+ let removed = 0;
488
+
489
+ // Body dedup (iterate backwards)
490
+ for (let i = doc.getBodyElementCount() - 1; i > 0; i--) {
491
+ const current = doc.getBodyElementAt(i);
492
+ const prev = doc.getBodyElementAt(i - 1);
493
+
494
+ if (
495
+ current instanceof Paragraph &&
496
+ prev instanceof Paragraph &&
497
+ isParagraphBlank(current) &&
498
+ isParagraphBlank(prev)
499
+ ) {
500
+ doc.removeBodyElementAt(i);
501
+ removed++;
502
+ }
503
+ }
504
+
505
+ // Cell dedup
506
+ for (const table of doc.getAllTables()) {
507
+ if (tableHasNestedContent(table)) continue;
508
+
509
+ for (const row of table.getRows()) {
510
+ for (const cell of row.getCells()) {
511
+ let paras = cell.getParagraphs();
512
+
513
+ // Remove adjacent blanks
514
+ for (let ci = paras.length - 1; ci > 0; ci--) {
515
+ if (paras.length <= 1) break;
516
+
517
+ const current = paras[ci];
518
+ const prev = paras[ci - 1];
519
+
520
+ if (current && prev && isParagraphBlank(current) && isParagraphBlank(prev)) {
521
+ cell.removeParagraph(ci);
522
+ removed++;
523
+ paras = cell.getParagraphs();
524
+ }
525
+ }
526
+
527
+ // Remove trailing blanks (no blank between last visual element and cell end)
528
+ while (paras.length > 1 && isParagraphBlank(paras[paras.length - 1])) {
529
+ cell.removeParagraph(paras.length - 1);
530
+ removed++;
531
+ paras = cell.getParagraphs();
532
+ }
533
+ }
534
+ }
535
+ }
536
+
537
+ if (removed > 0) {
538
+ log.debug(`Dedup removed ${removed} adjacent blank paragraphs`);
539
+ }
540
+
541
+ return removed;
542
+ }
543
+
544
+ /**
545
+ * Ensure all blank paragraphs have Normal style with correct formatting.
546
+ */
547
+ private normalizeBlankLineStyles(doc: Document, opts: BlankLineOptions): void {
548
+ const applyFormatting = (para: Paragraph) => {
549
+ para.setStyle(opts.style);
550
+ if (opts.spacingBefore !== undefined) {
551
+ para.setSpaceBefore(opts.spacingBefore);
552
+ }
553
+ para.setSpaceAfter(opts.spacingAfter);
554
+ if (opts.lineSpacing !== undefined) {
555
+ para.setLineSpacing(opts.lineSpacing);
556
+ }
557
+ // Apply font/size to existing runs (paragraph mark formatting)
558
+ if (opts.fontSize || opts.fontFamily) {
559
+ const runs = para.getRuns();
560
+ for (const run of runs) {
561
+ if (opts.fontSize) run.setSize(opts.fontSize);
562
+ if (opts.fontFamily) run.setFont(opts.fontFamily);
563
+ }
564
+ }
565
+ };
566
+
567
+ // Body
568
+ for (let i = 0; i < doc.getBodyElementCount(); i++) {
569
+ const element = doc.getBodyElementAt(i);
570
+ if (element instanceof Paragraph && isParagraphBlank(element)) {
571
+ applyFormatting(element);
572
+ }
573
+ }
574
+
575
+ // Cells
576
+ for (const table of doc.getAllTables()) {
577
+ for (const row of table.getRows()) {
578
+ for (const cell of row.getCells()) {
579
+ for (const para of cell.getParagraphs()) {
580
+ if (isParagraphBlank(para)) {
581
+ applyFormatting(para);
582
+ }
583
+ }
584
+ }
585
+ }
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Build a RuleContext for a body-level element.
591
+ */
592
+ private buildBodyContext(doc: Document, index: number): RuleContext {
593
+ const element = doc.getBodyElementAt(index);
594
+ const prev = index > 0 ? doc.getBodyElementAt(index - 1) : undefined;
595
+ const next =
596
+ index < doc.getBodyElementCount() - 1
597
+ ? doc.getBodyElementAt(index + 1)
598
+ : undefined;
599
+
600
+ return {
601
+ doc,
602
+ currentIndex: index,
603
+ currentElement:
604
+ element instanceof Paragraph || element instanceof Table
605
+ ? element
606
+ : (element as any),
607
+ prevElement:
608
+ prev instanceof Paragraph || prev instanceof Table ? prev : undefined,
609
+ nextElement:
610
+ next instanceof Paragraph || next instanceof Table ? next : undefined,
611
+ scope: "body",
612
+ };
613
+ }
614
+
615
+ /**
616
+ * Build a RuleContext for a cell-level paragraph.
617
+ */
618
+ private buildCellContext(
619
+ doc: Document,
620
+ cell: TableCell,
621
+ paraIndex: number,
622
+ paragraphs: Paragraph[],
623
+ parentTable: Table
624
+ ): RuleContext {
625
+ return {
626
+ doc,
627
+ currentIndex: paraIndex,
628
+ currentElement: paragraphs[paraIndex],
629
+ prevElement: paraIndex > 0 ? paragraphs[paraIndex - 1] : undefined,
630
+ nextElement:
631
+ paraIndex < paragraphs.length - 1
632
+ ? paragraphs[paraIndex + 1]
633
+ : undefined,
634
+ scope: "cell",
635
+ cell,
636
+ cellParagraphs: paragraphs,
637
+ cellParaIndex: paraIndex,
638
+ parentTable,
639
+ };
640
+ }
641
+
642
+ /**
643
+ * Find the first matching rule for the given context and scope.
644
+ */
645
+ private findMatchingRule(
646
+ rules: BlankLineRule[],
647
+ ctx: RuleContext,
648
+ scope: "body" | "cell"
649
+ ): BlankLineRule | null {
650
+ for (const rule of rules) {
651
+ if (rule.scope !== scope && rule.scope !== "both") continue;
652
+ if (rule.matches(ctx)) return rule;
653
+ }
654
+ return null;
655
+ }
656
+ }
657
+
658
+ export const blankLineManager = new BlankLineManager();