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,317 @@
1
+ /**
2
+ * Indentation Rules - Fix indentation on lines after list items.
3
+ *
4
+ * These rules adjust paragraph indentation for continuation text
5
+ * that follows list items, ensuring visual alignment.
6
+ *
7
+ * Also includes removeSmallIndents which strips indentation < 0.25"
8
+ * from non-list paragraphs before blank line and indentation rules run.
9
+ */
10
+
11
+ import { Document, Paragraph, Table, TableCell } from "docxmlater";
12
+ import { isParagraphBlank } from "../helpers/paragraphChecks";
13
+ import { detectTypedPrefix } from "@/services/document/list";
14
+ import type { BlankLineProcessingOptions } from "./ruleTypes";
15
+ import { logger } from "@/utils/logger";
16
+
17
+ const log = logger.namespace("IndentationRules");
18
+
19
+ /** Conversion: 1 inch = 1440 twips */
20
+ const TWIPS_PER_INCH = 1440;
21
+
22
+ /** Threshold below which indentation is removed for non-list paragraphs (0.25 inch) */
23
+ const SMALL_INDENT_THRESHOLD_TWIPS = 360; // 0.25 * 1440
24
+
25
+ function inchesToTwips(inches: number): number {
26
+ return Math.round(inches * TWIPS_PER_INCH);
27
+ }
28
+
29
+ /**
30
+ * Check whether a paragraph is a list element (Word list or typed prefix).
31
+ */
32
+ function isListElement(para: Paragraph): boolean {
33
+ // Real Word list item
34
+ const numbering = para.getNumbering();
35
+ if (numbering && numbering.numId !== undefined && numbering.numId !== 0) {
36
+ return true;
37
+ }
38
+
39
+ // Typed list prefix in text (e.g. "1. ", "a) ", "• ")
40
+ const text = para.getText()?.trim() || "";
41
+ if (text.length > 0) {
42
+ const typed = detectTypedPrefix(text);
43
+ if (typed.prefix) {
44
+ return true;
45
+ }
46
+ }
47
+
48
+ return false;
49
+ }
50
+
51
+ /**
52
+ * Remove small indentation (< 0.25") from non-list paragraphs.
53
+ *
54
+ * This runs BEFORE blank line rules and indentation rules to ensure
55
+ * that trivially-indented body text and table cell text is normalized
56
+ * to zero indent. This prevents downstream rules from misinterpreting
57
+ * small indents as intentional formatting and incorrectly adding or
58
+ * removing blank lines.
59
+ *
60
+ * Handles both body-level paragraphs and paragraphs inside table cells
61
+ * (including multi-column rows).
62
+ */
63
+ export function removeSmallIndents(doc: Document): number {
64
+ let fixed = 0;
65
+
66
+ // Process body-level paragraphs
67
+ for (let i = 0; i < doc.getBodyElementCount(); i++) {
68
+ const element = doc.getBodyElementAt(i);
69
+ if (!(element instanceof Paragraph)) continue;
70
+ if (isParagraphBlank(element)) continue;
71
+
72
+ const indent = element.getFormatting()?.indentation?.left;
73
+ if (!indent || indent <= 0) continue;
74
+ if (indent >= SMALL_INDENT_THRESHOLD_TWIPS) continue;
75
+
76
+ // Skip list elements - their indentation is intentional
77
+ if (isListElement(element)) continue;
78
+
79
+ log.debug(
80
+ `Removing small indent (${indent} twips / ${(indent / TWIPS_PER_INCH).toFixed(2)}") ` +
81
+ `from body paragraph: "${element.getText()?.substring(0, 40)}..."`
82
+ );
83
+ element.setLeftIndent(0);
84
+ fixed++;
85
+ }
86
+
87
+ // Process table cell paragraphs (all columns in all rows)
88
+ for (const table of doc.getAllTables()) {
89
+ for (const row of table.getRows()) {
90
+ for (const cell of row.getCells()) {
91
+ const paras = cell.getParagraphs();
92
+
93
+ for (let ci = 0; ci < paras.length; ci++) {
94
+ const para = paras[ci];
95
+ if (!para) continue;
96
+ if (isParagraphBlank(para)) continue;
97
+
98
+ const indent = para.getFormatting()?.indentation?.left;
99
+ if (!indent || indent <= 0) continue;
100
+ if (indent >= SMALL_INDENT_THRESHOLD_TWIPS) continue;
101
+
102
+ // Skip list elements
103
+ if (isListElement(para)) continue;
104
+
105
+ log.debug(
106
+ `Removing small indent (${indent} twips / ${(indent / TWIPS_PER_INCH).toFixed(2)}") ` +
107
+ `from table cell paragraph: "${para.getText()?.substring(0, 40)}..."`
108
+ );
109
+ para.setLeftIndent(0);
110
+ fixed++;
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ if (fixed > 0) {
117
+ log.info(`Removed small indentation (< 0.25") from ${fixed} non-list paragraphs`);
118
+ }
119
+
120
+ return fixed;
121
+ }
122
+
123
+ /**
124
+ * Find the nearest preceding list item and return its level.
125
+ * Scans backwards from the given index in the body.
126
+ */
127
+ function findPrecedingListItem(
128
+ doc: Document,
129
+ bodyIndex: number
130
+ ): { level: number; paragraph: Paragraph } | null {
131
+ for (let i = bodyIndex - 1; i >= 0; i--) {
132
+ const el = doc.getBodyElementAt(i);
133
+ if (el instanceof Table) return null; // Stop at table boundaries
134
+ if (!(el instanceof Paragraph)) continue;
135
+ if (isParagraphBlank(el)) continue;
136
+
137
+ const numbering = el.getNumbering();
138
+ if (numbering) {
139
+ return { level: numbering.level ?? 0, paragraph: el };
140
+ }
141
+
142
+ // If we hit non-indented, non-list text, stop looking
143
+ const indent = el.getFormatting()?.indentation?.left;
144
+ if (!indent || indent <= 0) return null;
145
+ }
146
+ return null;
147
+ }
148
+
149
+ /**
150
+ * Find the nearest preceding list item in a cell.
151
+ */
152
+ function findPrecedingListItemInCell(
153
+ paragraphs: Paragraph[],
154
+ paraIndex: number
155
+ ): { level: number; paragraph: Paragraph } | null {
156
+ for (let i = paraIndex - 1; i >= 0; i--) {
157
+ const para = paragraphs[i];
158
+ if (!para) continue;
159
+ if (isParagraphBlank(para)) continue;
160
+
161
+ const numbering = para.getNumbering();
162
+ if (numbering) {
163
+ return { level: numbering.level ?? 0, paragraph: para };
164
+ }
165
+
166
+ // If we hit non-indented text, stop
167
+ const indent = para.getFormatting()?.indentation?.left;
168
+ if (!indent || indent <= 0) return null;
169
+ }
170
+ return null;
171
+ }
172
+
173
+ /**
174
+ * Get the text indentation in twips for a given list level.
175
+ */
176
+ function getTextIndentForLevel(
177
+ options: BlankLineProcessingOptions,
178
+ level: number
179
+ ): number | null {
180
+ if (!options.listBulletSettings?.indentationLevels) return null;
181
+
182
+ const levels = options.listBulletSettings.indentationLevels;
183
+ const levelConfig = levels.find((l) => l.level === level);
184
+
185
+ if (levelConfig) {
186
+ return inchesToTwips(levelConfig.textIndent);
187
+ }
188
+
189
+ // If no exact level match, use the last configured level
190
+ if (levels.length > 0) {
191
+ const lastLevel = levels[levels.length - 1];
192
+ return inchesToTwips(lastLevel.textIndent);
193
+ }
194
+
195
+ return null;
196
+ }
197
+
198
+ /**
199
+ * Get the level-0 text indentation in twips.
200
+ */
201
+ function getLevel0TextIndent(options: BlankLineProcessingOptions): number | null {
202
+ return getTextIndentForLevel(options, 0);
203
+ }
204
+
205
+ /**
206
+ * Applies indentation rules to the document body.
207
+ *
208
+ * Rule 1: Indented text after a list item should match the text indentation
209
+ * of the list item's level.
210
+ *
211
+ * Rule 2: If consecutive indented lines exist and the line above is a list item,
212
+ * match the list item's text indentation. If not a list item, match
213
+ * the level-0 bullet text indentation.
214
+ */
215
+ export function applyIndentationRules(
216
+ doc: Document,
217
+ options: BlankLineProcessingOptions
218
+ ): number {
219
+ if (!options.listBulletSettings?.indentationLevels) {
220
+ return 0;
221
+ }
222
+
223
+ let fixed = 0;
224
+
225
+ // Process body-level paragraphs
226
+ for (let i = 0; i < doc.getBodyElementCount(); i++) {
227
+ const element = doc.getBodyElementAt(i);
228
+ if (!(element instanceof Paragraph)) continue;
229
+ if (isParagraphBlank(element)) continue;
230
+ if (element.getNumbering()) continue; // Skip list items themselves
231
+
232
+ const indent = element.getFormatting()?.indentation?.left;
233
+ if (!indent || indent <= 0) continue; // Only process indented paragraphs
234
+
235
+ // Find preceding list item
236
+ const listItem = findPrecedingListItem(doc, i);
237
+
238
+ if (listItem) {
239
+ // Rule 1: Match the list item's text indentation level
240
+ const targetIndent = getTextIndentForLevel(options, listItem.level);
241
+ if (targetIndent !== null && indent !== targetIndent) {
242
+ element.setLeftIndent(targetIndent);
243
+ fixed++;
244
+ }
245
+ } else {
246
+ // Rule 2: No preceding list item - check if previous line is indented
247
+ const prevElement = doc.getBodyElementAt(i - 1);
248
+ if (
249
+ prevElement instanceof Paragraph &&
250
+ !isParagraphBlank(prevElement) &&
251
+ !prevElement.getNumbering()
252
+ ) {
253
+ const prevIndent = prevElement.getFormatting()?.indentation?.left;
254
+ if (prevIndent && prevIndent > 0) {
255
+ // Consecutive indented lines - match level-0 text indent
256
+ const level0Indent = getLevel0TextIndent(options);
257
+ if (level0Indent !== null && indent !== level0Indent) {
258
+ element.setLeftIndent(level0Indent);
259
+ fixed++;
260
+ }
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ // Process table cell paragraphs
267
+ for (const table of doc.getAllTables()) {
268
+ for (const row of table.getRows()) {
269
+ for (const cell of row.getCells()) {
270
+ const paras = cell.getParagraphs();
271
+
272
+ for (let ci = 0; ci < paras.length; ci++) {
273
+ const para = paras[ci];
274
+ if (!para) continue;
275
+ if (isParagraphBlank(para)) continue;
276
+ if (para.getNumbering()) continue;
277
+
278
+ const indent = para.getFormatting()?.indentation?.left;
279
+ if (!indent || indent <= 0) continue;
280
+
281
+ const listItem = findPrecedingListItemInCell(paras, ci);
282
+
283
+ if (listItem) {
284
+ const targetIndent = getTextIndentForLevel(options, listItem.level);
285
+ if (targetIndent !== null && indent !== targetIndent) {
286
+ para.setLeftIndent(targetIndent);
287
+ fixed++;
288
+ }
289
+ } else {
290
+ // Consecutive indented lines without a list item
291
+ const prevPara = paras[ci - 1];
292
+ if (
293
+ prevPara &&
294
+ !isParagraphBlank(prevPara) &&
295
+ !prevPara.getNumbering()
296
+ ) {
297
+ const prevIndent = prevPara.getFormatting()?.indentation?.left;
298
+ if (prevIndent && prevIndent > 0) {
299
+ const level0Indent = getLevel0TextIndent(options);
300
+ if (level0Indent !== null && indent !== level0Indent) {
301
+ para.setLeftIndent(level0Indent);
302
+ fixed++;
303
+ }
304
+ }
305
+ }
306
+ }
307
+ }
308
+ }
309
+ }
310
+ }
311
+
312
+ if (fixed > 0) {
313
+ log.info(`Fixed indentation on ${fixed} paragraphs`);
314
+ }
315
+
316
+ return fixed;
317
+ }
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Removal Rules - Define when blank lines must be REMOVED.
3
+ *
4
+ * If any removal rule matches a blank paragraph, it is removed regardless
5
+ * of addition rules or preservation fallback.
6
+ */
7
+
8
+ import { Paragraph, Table } from "docxmlater";
9
+ import type { BlankLineRule, RuleContext } from "./ruleTypes";
10
+ import { isParagraphBlank, getEffectiveLeftIndent, hasNavigationHyperlink } from "../helpers/paragraphChecks";
11
+ import { isSmallImageParagraph, getImageRunFromParagraph, isImageSmall } from "../helpers/imageChecks";
12
+ import { tableHasNestedContent } from "../helpers/tableGuards";
13
+
14
+ /**
15
+ * Remove blank line above Heading 1 style text.
16
+ */
17
+ export const aboveHeading1Rule: BlankLineRule = {
18
+ id: "remove-above-heading1",
19
+ action: "remove",
20
+ scope: "body",
21
+ matches(ctx: RuleContext): boolean {
22
+ if (ctx.scope !== "body") return false;
23
+ if (!(ctx.currentElement instanceof Paragraph)) return false;
24
+ if (!isParagraphBlank(ctx.currentElement)) return false;
25
+
26
+ // Check if the element after this blank is Heading 1
27
+ if (ctx.nextElement instanceof Paragraph) {
28
+ const style = ctx.nextElement.getStyle();
29
+ if (style === "Heading1" && ctx.nextElement.getText().trim() !== "") {
30
+ return true;
31
+ }
32
+ }
33
+ return false;
34
+ },
35
+ };
36
+
37
+ /**
38
+ * Remove blank line if it's the first line of a multi-row cell.
39
+ * Only applies if the cell has more than one row in its parent table.
40
+ */
41
+ export const firstLineOfMultiRowCellRule: BlankLineRule = {
42
+ id: "remove-first-line-multi-row-cell",
43
+ action: "remove",
44
+ scope: "cell",
45
+ matches(ctx: RuleContext): boolean {
46
+ if (ctx.scope !== "cell") return false;
47
+ if (ctx.cellParaIndex !== 0) return false;
48
+ if (!ctx.cellParagraphs || ctx.cellParagraphs.length === 0) return false;
49
+ if (!(ctx.currentElement instanceof Paragraph)) return false;
50
+ if (!isParagraphBlank(ctx.currentElement)) return false;
51
+
52
+ // Only remove if this table has more than one row
53
+ if (ctx.parentTable) {
54
+ const rowCount = ctx.parentTable.getRowCount();
55
+ if (rowCount > 1) {
56
+ return true;
57
+ }
58
+ }
59
+ return false;
60
+ },
61
+ };
62
+
63
+ /**
64
+ * Remove blank line above tables larger than 1x1.
65
+ */
66
+ export const aboveLargeTableRule: BlankLineRule = {
67
+ id: "remove-above-large-table",
68
+ action: "remove",
69
+ scope: "body",
70
+ matches(ctx: RuleContext): boolean {
71
+ if (ctx.scope !== "body") return false;
72
+ if (!(ctx.currentElement instanceof Paragraph)) return false;
73
+ if (!isParagraphBlank(ctx.currentElement)) return false;
74
+
75
+ if (ctx.nextElement instanceof Table) {
76
+ const rowCount = ctx.nextElement.getRowCount();
77
+ const colCount = ctx.nextElement.getColumnCount();
78
+ if (rowCount > 1 || colCount > 1) {
79
+ return true;
80
+ }
81
+ }
82
+ return false;
83
+ },
84
+ };
85
+
86
+ /**
87
+ * Remove blank line between list items, even if they are different level
88
+ * bullet lists or numbered list items.
89
+ */
90
+ export const betweenListItemsRule: BlankLineRule = {
91
+ id: "remove-between-list-items",
92
+ action: "remove",
93
+ scope: "both",
94
+ matches(ctx: RuleContext): boolean {
95
+ if (!(ctx.currentElement instanceof Paragraph)) return false;
96
+ if (!isParagraphBlank(ctx.currentElement)) return false;
97
+
98
+ const prevIsListItem =
99
+ ctx.prevElement instanceof Paragraph && !!ctx.prevElement.getNumbering();
100
+ const nextIsListItem =
101
+ ctx.nextElement instanceof Paragraph && !!ctx.nextElement.getNumbering();
102
+
103
+ return prevIsListItem && nextIsListItem;
104
+ },
105
+ };
106
+
107
+ /**
108
+ * Remove blank line between a list item and the next line if that next line is:
109
+ * - indented text
110
+ * - a list item
111
+ * - indented text with a small image (<100x100) followed by text
112
+ */
113
+ export const listItemToIndentedContentRule: BlankLineRule = {
114
+ id: "remove-list-to-indented",
115
+ action: "remove",
116
+ scope: "both",
117
+ matches(ctx: RuleContext): boolean {
118
+ if (!(ctx.currentElement instanceof Paragraph)) return false;
119
+ if (!isParagraphBlank(ctx.currentElement)) return false;
120
+
121
+ const prevIsListItem =
122
+ ctx.prevElement instanceof Paragraph && !!ctx.prevElement.getNumbering();
123
+ if (!prevIsListItem) return false;
124
+
125
+ if (ctx.nextElement instanceof Paragraph) {
126
+ // Next is a list item
127
+ if (ctx.nextElement.getNumbering()) return true;
128
+
129
+ // Don't remove blank before centered images
130
+ if (ctx.nextElement.getAlignment() === "center") {
131
+ const imageRun = getImageRunFromParagraph(ctx.nextElement);
132
+ if (imageRun) return false;
133
+ }
134
+
135
+ // Next is indented text (check both direct and style-inherited indentation)
136
+ const nextIndent = getEffectiveLeftIndent(ctx.nextElement, ctx.doc);
137
+ if (nextIndent > 0) return true;
138
+
139
+ // Next is a small image followed by text (check if small image with indentation)
140
+ if (isSmallImageParagraph(ctx.nextElement)) {
141
+ const nextIndentImg = getEffectiveLeftIndent(ctx.nextElement, ctx.doc);
142
+ if (nextIndentImg > 0) return true;
143
+ }
144
+ }
145
+ return false;
146
+ },
147
+ };
148
+
149
+ /**
150
+ * Before the very first list item in a sequence, if the preceding line is
151
+ * non-indented text, do not have a blank line between them.
152
+ */
153
+ export const beforeFirstListItemRule: BlankLineRule = {
154
+ id: "remove-before-first-list-item",
155
+ action: "remove",
156
+ scope: "both",
157
+ matches(ctx: RuleContext): boolean {
158
+ if (!(ctx.currentElement instanceof Paragraph)) return false;
159
+ if (!isParagraphBlank(ctx.currentElement)) return false;
160
+
161
+ // Next element must be a list item
162
+ if (!(ctx.nextElement instanceof Paragraph) || !ctx.nextElement.getNumbering()) {
163
+ return false;
164
+ }
165
+
166
+ // Previous element must be non-indented text (not a list item)
167
+ if (!(ctx.prevElement instanceof Paragraph)) return false;
168
+ if (ctx.prevElement.getNumbering()) return false;
169
+ if (isParagraphBlank(ctx.prevElement)) return false;
170
+
171
+ const prevIndent = ctx.prevElement.getFormatting()?.indentation?.left;
172
+ if (prevIndent && prevIndent > 0) return false;
173
+
174
+ // Check that the next element IS the first list item (prev is not a list item)
175
+ return true;
176
+ },
177
+ };
178
+
179
+ /**
180
+ * If current line has bold text with a colon, no indentation, and the next
181
+ * line is indented or a list item, never have a blank line between them.
182
+ *
183
+ * This rule checks for a blank line between bold+colon and indented/list content.
184
+ */
185
+ export const boldColonToIndentedRule: BlankLineRule = {
186
+ id: "remove-bold-colon-to-indented",
187
+ action: "remove",
188
+ scope: "both",
189
+ matches(ctx: RuleContext): boolean {
190
+ if (!(ctx.currentElement instanceof Paragraph)) return false;
191
+ if (!isParagraphBlank(ctx.currentElement)) return false;
192
+
193
+ // Previous must be bold+colon with no indentation
194
+ if (!(ctx.prevElement instanceof Paragraph)) return false;
195
+
196
+ const prevContent = ctx.prevElement.getContent();
197
+ if (!prevContent || prevContent.length === 0) return false;
198
+
199
+ // Check for bold first run with colon
200
+ const { Run } = require("docxmlater");
201
+ const firstRun = prevContent.find((item: any) => item instanceof Run) as any;
202
+ if (!firstRun) return false;
203
+ const formatting = firstRun.getFormatting() as any;
204
+ if (!formatting.bold) return false;
205
+ const fullText = ctx.prevElement.getText();
206
+ if (!fullText || !fullText.substring(0, 55).includes(":")) return false;
207
+
208
+ // Must have no indentation
209
+ const prevIndent = ctx.prevElement.getFormatting()?.indentation?.left;
210
+ if (prevIndent && prevIndent > 0) return false;
211
+
212
+ // Next must be indented or a list item
213
+ if (!(ctx.nextElement instanceof Paragraph)) return false;
214
+
215
+ if (ctx.nextElement.getNumbering()) return true;
216
+
217
+ const nextIndent = ctx.nextElement.getFormatting()?.indentation?.left;
218
+ if (nextIndent && nextIndent > 0) return true;
219
+
220
+ return false;
221
+ },
222
+ };
223
+
224
+ /**
225
+ * Remove blank line BELOW navigation hyperlinks (text starts with "Top of" or "Return to").
226
+ */
227
+ export const afterTopOfDocHyperlinkRule: BlankLineRule = {
228
+ id: "remove-after-top-of-doc-hyperlink",
229
+ action: "remove",
230
+ scope: "body",
231
+ matches(ctx: RuleContext): boolean {
232
+ if (ctx.scope !== "body") return false;
233
+ if (!(ctx.currentElement instanceof Paragraph)) return false;
234
+ if (!isParagraphBlank(ctx.currentElement)) return false;
235
+ if (!(ctx.prevElement instanceof Paragraph)) return false;
236
+ return hasNavigationHyperlink(ctx.prevElement);
237
+ },
238
+ };
239
+
240
+ /**
241
+ * Never have a blank line between the last line in a cell and the element before it,
242
+ * unless the element before is a nested or floating table.
243
+ */
244
+ export const lastLineInCellRule: BlankLineRule = {
245
+ id: "remove-last-line-in-cell",
246
+ action: "remove",
247
+ scope: "cell",
248
+ matches(ctx: RuleContext): boolean {
249
+ if (ctx.scope !== "cell") return false;
250
+ if (!ctx.cellParagraphs || !ctx.cell) return false;
251
+ if (!(ctx.currentElement instanceof Paragraph)) return false;
252
+ if (!isParagraphBlank(ctx.currentElement)) return false;
253
+
254
+ const paraIndex = ctx.cellParaIndex ?? -1;
255
+ const isLastBlankBeforeEnd =
256
+ paraIndex >= 0 && paraIndex === ctx.cellParagraphs.length - 1;
257
+
258
+ // If this blank is the last paragraph, remove it (unless preceded by nested table)
259
+ if (isLastBlankBeforeEnd && ctx.cellParagraphs.length > 1) {
260
+ // Check if the cell has nested tables that the blank might be separating
261
+ if (ctx.cell.hasNestedTables()) {
262
+ return false; // Don't remove - might be needed for nested table spacing
263
+ }
264
+ return true;
265
+ }
266
+
267
+ // Also check for blanks that are second-to-last where last is blank
268
+ // (trailing blanks in cells)
269
+ if (
270
+ paraIndex >= 1 &&
271
+ paraIndex === ctx.cellParagraphs.length - 2 &&
272
+ isParagraphBlank(ctx.cellParagraphs[ctx.cellParagraphs.length - 1])
273
+ ) {
274
+ if (ctx.cell.hasNestedTables()) return false;
275
+ return true;
276
+ }
277
+
278
+ return false;
279
+ },
280
+ };
281
+
282
+ /**
283
+ * Remove blank line after images >100x100 if the image is the last element in a table cell.
284
+ */
285
+ export const largeImageLastInCellRule: BlankLineRule = {
286
+ id: "remove-large-image-last-in-cell",
287
+ action: "remove",
288
+ scope: "cell",
289
+ matches(ctx: RuleContext): boolean {
290
+ if (ctx.scope !== "cell") return false;
291
+ if (!ctx.cellParagraphs || !ctx.cell) return false;
292
+ if (!(ctx.currentElement instanceof Paragraph)) return false;
293
+ if (!isParagraphBlank(ctx.currentElement)) return false;
294
+
295
+ const paraIndex = ctx.cellParaIndex ?? -1;
296
+ if (paraIndex < 1) return false;
297
+
298
+ // Check if previous paragraph is a large image
299
+ const prevPara = ctx.cellParagraphs[paraIndex - 1];
300
+ if (!prevPara) return false;
301
+
302
+ const imageRun = getImageRunFromParagraph(prevPara);
303
+ if (!imageRun) return false;
304
+
305
+ const image = imageRun.getImageElement();
306
+ if (isImageSmall(image)) return false; // Small image, rule doesn't apply
307
+
308
+ // Large image - check if it's effectively the last content in the cell
309
+ // (current blank is after the image and is the last or near-last element)
310
+ const isNearEnd = paraIndex >= ctx.cellParagraphs.length - 1;
311
+ if (isNearEnd) return true;
312
+
313
+ // Also if all remaining paragraphs after this are blank
314
+ for (let i = paraIndex + 1; i < ctx.cellParagraphs.length; i++) {
315
+ if (!isParagraphBlank(ctx.cellParagraphs[i])) return false;
316
+ }
317
+ return true;
318
+ },
319
+ };
320
+
321
+ /**
322
+ * Remove blank line between centered text and a following image paragraph.
323
+ * e.g., "AETNA:" (centered) → blank → image → remove the blank.
324
+ */
325
+ export const centeredTextToImageRule: BlankLineRule = {
326
+ id: "remove-centered-text-to-image",
327
+ action: "remove",
328
+ scope: "both",
329
+ matches(ctx: RuleContext): boolean {
330
+ if (!(ctx.currentElement instanceof Paragraph)) return false;
331
+ if (!isParagraphBlank(ctx.currentElement)) return false;
332
+
333
+ // Previous must be centered text paragraph
334
+ if (!(ctx.prevElement instanceof Paragraph)) return false;
335
+ if (ctx.prevElement.getAlignment() !== "center") return false;
336
+ if (!ctx.prevElement.getText()?.trim()) return false;
337
+
338
+ // Next must be a paragraph containing an image
339
+ if (!(ctx.nextElement instanceof Paragraph)) return false;
340
+ const imageRun = getImageRunFromParagraph(ctx.nextElement);
341
+ if (!imageRun) return false;
342
+
343
+ return true;
344
+ },
345
+ };
346
+
347
+ /**
348
+ * All removal rules in priority order.
349
+ */
350
+ export const removalRules: BlankLineRule[] = [
351
+ aboveHeading1Rule,
352
+ firstLineOfMultiRowCellRule,
353
+ aboveLargeTableRule,
354
+ betweenListItemsRule,
355
+ listItemToIndentedContentRule,
356
+ beforeFirstListItemRule,
357
+ boldColonToIndentedRule,
358
+ afterTopOfDocHyperlinkRule,
359
+ lastLineInCellRule,
360
+ largeImageLastInCellRule,
361
+ centeredTextToImageRule,
362
+ ];