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,272 @@
1
+ import { vi, describe, it, expect, beforeEach, type Mocked } from "vitest";
2
+ import { Run, ImageRun, Image } from "docxmlater";
3
+ import { normalizeRunWhitespace } from "../whitespace";
4
+
5
+ vi.mock("docxmlater");
6
+ vi.mock("../../blanklines", () => ({
7
+ isImageSmall: vi.fn(),
8
+ }));
9
+
10
+ import { isImageSmall } from "../../blanklines";
11
+ const mockIsImageSmall = isImageSmall as ReturnType<typeof vi.fn>;
12
+
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ });
16
+
17
+ // ── Helpers ──────────────────────────────────────────────
18
+
19
+ function createTextRun(text: string): Mocked<Run> {
20
+ let currentText = text;
21
+ return {
22
+ getText: vi.fn().mockImplementation(() => currentText),
23
+ setText: vi.fn().mockImplementation((t: string) => { currentText = t; }),
24
+ } as unknown as Mocked<Run>;
25
+ }
26
+
27
+ function createImageRun(small: boolean): Mocked<ImageRun> {
28
+ const mockImage = {} as Image;
29
+ const imgRun = Object.create(ImageRun.prototype);
30
+ imgRun.getText = vi.fn().mockReturnValue(null);
31
+ imgRun.setText = vi.fn();
32
+ imgRun.getImageElement = vi.fn().mockReturnValue(mockImage);
33
+
34
+ mockIsImageSmall.mockImplementation((img: Image) =>
35
+ img === mockImage ? small : false
36
+ );
37
+
38
+ return imgRun as unknown as Mocked<ImageRun>;
39
+ }
40
+
41
+ // ── Tests ────────────────────────────────────────────────
42
+
43
+ describe("normalizeRunWhitespace", () => {
44
+ // ── Basic whitespace collapse ──
45
+
46
+ describe("double-space collapse", () => {
47
+ it("should collapse multiple spaces to single", () => {
48
+ const run = createTextRun("Hello world");
49
+ const count = normalizeRunWhitespace([run]);
50
+ expect(count).toBe(1);
51
+ expect(run.setText).toHaveBeenCalledWith("Hello world");
52
+ });
53
+
54
+ it("should collapse many spaces", () => {
55
+ const run = createTextRun("Text with spaces");
56
+ const count = normalizeRunWhitespace([run]);
57
+ expect(count).toBe(1);
58
+ expect(run.setText).toHaveBeenCalledWith("Text with spaces");
59
+ });
60
+
61
+ it("should not modify text without extra spaces", () => {
62
+ const run = createTextRun("Normal text");
63
+ const count = normalizeRunWhitespace([run]);
64
+ expect(count).toBe(0);
65
+ expect(run.setText).not.toHaveBeenCalled();
66
+ });
67
+ });
68
+
69
+ // ── Leading space stripping ──
70
+
71
+ describe("leading space stripping", () => {
72
+ it("should strip leading spaces at paragraph start", () => {
73
+ const run = createTextRun(" Hello");
74
+ const count = normalizeRunWhitespace([run]);
75
+ expect(count).toBe(1);
76
+ expect(run.setText).toHaveBeenCalledWith("Hello");
77
+ });
78
+
79
+ it("should strip leading spaces and collapse inner spaces", () => {
80
+ const run = createTextRun(" Hello world");
81
+ const count = normalizeRunWhitespace([run]);
82
+ expect(count).toBe(1);
83
+ expect(run.setText).toHaveBeenCalledWith("Hello world");
84
+ });
85
+
86
+ it("should strip across whitespace-only first run", () => {
87
+ const run0 = createTextRun(" ");
88
+ const run1 = createTextRun(" Content");
89
+ const count = normalizeRunWhitespace([run0, run1]);
90
+ expect(count).toBe(2);
91
+ expect(run0.setText).toHaveBeenCalledWith("");
92
+ expect(run1.setText).toHaveBeenCalledWith("Content");
93
+ });
94
+
95
+ it("should NOT strip leading spaces from mid-paragraph runs", () => {
96
+ const run0 = createTextRun("Hello");
97
+ const run1 = createTextRun(" world");
98
+ const count = normalizeRunWhitespace([run0, run1]);
99
+ expect(count).toBe(1);
100
+ expect(run0.setText).not.toHaveBeenCalled();
101
+ expect(run1.setText).toHaveBeenCalledWith(" world");
102
+ });
103
+ });
104
+
105
+ // ── Tab preservation ──
106
+
107
+ describe("tab preservation", () => {
108
+ it("should not strip leading tabs from paragraph start", () => {
109
+ const run = createTextRun("\tHello world");
110
+ const count = normalizeRunWhitespace([run]);
111
+ expect(count).toBe(0);
112
+ expect(run.setText).not.toHaveBeenCalled();
113
+ });
114
+ });
115
+
116
+ // ── Cross-run double space ──
117
+
118
+ describe("cross-run double space", () => {
119
+ it("should trim trailing space when next run starts with space", () => {
120
+ const run0 = createTextRun("hello ");
121
+ const run1 = createTextRun(" world");
122
+ const count = normalizeRunWhitespace([run0, run1]);
123
+ expect(count).toBe(1);
124
+ expect(run0.setText).toHaveBeenCalledWith("hello");
125
+ expect(run1.setText).not.toHaveBeenCalled();
126
+ });
127
+ });
128
+
129
+ // ── Empty / null runs ──
130
+
131
+ describe("empty runs", () => {
132
+ it("should handle empty text run", () => {
133
+ const run = createTextRun("");
134
+ const count = normalizeRunWhitespace([run]);
135
+ expect(count).toBe(0);
136
+ });
137
+
138
+ it("should handle null text run", () => {
139
+ const run = { getText: vi.fn().mockReturnValue(null), setText: vi.fn() } as unknown as Mocked<Run>;
140
+ const count = normalizeRunWhitespace([run]);
141
+ expect(count).toBe(0);
142
+ });
143
+ });
144
+
145
+ // ── ImageRun interactions ──
146
+
147
+ describe("image run - space insertion", () => {
148
+ it("should insert space after small image when text has none", () => {
149
+ const imgRun = createImageRun(true);
150
+ const textRun = createTextRun("CC: supervisor");
151
+ const count = normalizeRunWhitespace([imgRun, textRun] as Run[]);
152
+ expect(count).toBe(1);
153
+ expect(textRun.setText).toHaveBeenCalledWith(" CC: supervisor");
154
+ });
155
+
156
+ it("should NOT insert space after small image when text already has one", () => {
157
+ const imgRun = createImageRun(true);
158
+ const textRun = createTextRun(" CC: supervisor");
159
+ const count = normalizeRunWhitespace([imgRun, textRun] as Run[]);
160
+ expect(count).toBe(0);
161
+ expect(textRun.setText).not.toHaveBeenCalled();
162
+ });
163
+
164
+ it("should collapse multiple spaces after small image via regex on next iteration", () => {
165
+ const imgRun = createImageRun(true);
166
+ // Text starts with multiple spaces — no insertion needed, regex collapses
167
+ const textRun = createTextRun(" CC: supervisor");
168
+ const count = normalizeRunWhitespace([imgRun, textRun] as Run[]);
169
+ // The space starts with " " so no insertion; regex collapse handles it
170
+ expect(count).toBe(1);
171
+ expect(textRun.setText).toHaveBeenCalledWith(" CC: supervisor");
172
+ });
173
+
174
+ it("should NOT insert space after large image", () => {
175
+ const imgRun = createImageRun(false);
176
+ const textRun = createTextRun("Caption");
177
+ const count = normalizeRunWhitespace([imgRun, textRun] as Run[]);
178
+ expect(count).toBe(0);
179
+ expect(textRun.setText).not.toHaveBeenCalled();
180
+ });
181
+
182
+ it("should NOT insert space when image is at end of paragraph", () => {
183
+ const imgRun = createImageRun(true);
184
+ const count = normalizeRunWhitespace([imgRun] as Run[]);
185
+ expect(count).toBe(0);
186
+ });
187
+
188
+ it("should handle consecutive images then text", () => {
189
+ const img1 = createImageRun(true);
190
+ const img2 = createImageRun(true);
191
+ const textRun = createTextRun("text");
192
+
193
+ // img1's next is img2 (getText returns null) — no insertion
194
+ // img2's next is textRun ("text" no leading space) — insertion
195
+ const count = normalizeRunWhitespace([img1, img2, textRun] as Run[]);
196
+ expect(count).toBe(1);
197
+ expect(textRun.setText).toHaveBeenCalledWith(" text");
198
+ });
199
+
200
+ it("should NOT insert space after small image when text starts with tab", () => {
201
+ const imgRun = createImageRun(true);
202
+ const textRun = createTextRun("\tsome text");
203
+ const count = normalizeRunWhitespace([imgRun, textRun] as Run[]);
204
+ expect(count).toBe(0);
205
+ expect(textRun.setText).not.toHaveBeenCalled();
206
+ });
207
+
208
+ it("should preserve seenTextInParagraph after image (no leading strip)", () => {
209
+ const imgRun = createImageRun(false);
210
+ const textRun = createTextRun(" text after image");
211
+ const count = normalizeRunWhitespace([imgRun, textRun] as Run[]);
212
+ // The leading space should be preserved because image set seenTextInParagraph
213
+ expect(count).toBe(0);
214
+ expect(textRun.setText).not.toHaveBeenCalled();
215
+ });
216
+ });
217
+
218
+ // ── Variant space stripping after small images ──
219
+
220
+ describe("image run - variant space stripping", () => {
221
+ it("should strip en space and ensure one regular space", () => {
222
+ const imgRun = createImageRun(true);
223
+ const textRun = createTextRun("\u2002CC: supervisor");
224
+ const count = normalizeRunWhitespace([imgRun, textRun] as Run[]);
225
+ expect(count).toBe(1);
226
+ expect(textRun.setText).toHaveBeenCalledWith(" CC: supervisor");
227
+ });
228
+
229
+ it("should strip em space and ensure one regular space", () => {
230
+ const imgRun = createImageRun(true);
231
+ const textRun = createTextRun("\u2003CC: supervisor");
232
+ const count = normalizeRunWhitespace([imgRun, textRun] as Run[]);
233
+ expect(count).toBe(1);
234
+ expect(textRun.setText).toHaveBeenCalledWith(" CC: supervisor");
235
+ });
236
+
237
+ it("should strip non-breaking space and ensure one regular space", () => {
238
+ const imgRun = createImageRun(true);
239
+ const textRun = createTextRun("\u00A0CC: supervisor");
240
+ const count = normalizeRunWhitespace([imgRun, textRun] as Run[]);
241
+ expect(count).toBe(1);
242
+ expect(textRun.setText).toHaveBeenCalledWith(" CC: supervisor");
243
+ });
244
+
245
+ it("should strip mixed variant and regular spaces to one regular space", () => {
246
+ const imgRun = createImageRun(true);
247
+ const textRun = createTextRun(" \u2003 \u2002CC: supervisor");
248
+ const count = normalizeRunWhitespace([imgRun, textRun] as Run[]);
249
+ expect(count).toBe(1);
250
+ expect(textRun.setText).toHaveBeenCalledWith(" CC: supervisor");
251
+ });
252
+
253
+ it("should clear intermediate variant-space-only run and add space to text run", () => {
254
+ const imgRun = createImageRun(true);
255
+ const spaceRun = createTextRun("\u2003");
256
+ const textRun = createTextRun("CC: supervisor");
257
+ const count = normalizeRunWhitespace([imgRun, spaceRun, textRun] as Run[]);
258
+ expect(count).toBe(2);
259
+ expect(spaceRun.setText).toHaveBeenCalledWith("");
260
+ expect(textRun.setText).toHaveBeenCalledWith(" CC: supervisor");
261
+ });
262
+
263
+ it("should not strip variant spaces from mid-text positions", () => {
264
+ const imgRun = createImageRun(true);
265
+ const textRun = createTextRun("Hello\u2003world");
266
+ const count = normalizeRunWhitespace([imgRun, textRun] as Run[]);
267
+ expect(count).toBe(1);
268
+ // Only leading space added; the mid-text em space is preserved
269
+ expect(textRun.setText).toHaveBeenCalledWith(" Hello\u2003world");
270
+ });
271
+ });
272
+ });
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Whitespace normalization for paragraph runs.
3
+ *
4
+ * Extracted from WordDocumentProcessor for testability.
5
+ * Handles: double-space collapse, leading-space stripping,
6
+ * cross-run double-space removal, and space insertion after small images.
7
+ */
8
+
9
+ import { Run, ImageRun } from "docxmlater";
10
+ import { isImageSmall } from "../blanklines";
11
+
12
+ /**
13
+ * Normalize whitespace within a single paragraph's runs.
14
+ *
15
+ * - Collapses multiple consecutive spaces to one
16
+ * - Strips leading spaces from the paragraph start
17
+ * - Removes cross-run double spaces
18
+ * - Inserts exactly one space after small inline images (<100x100px)
19
+ * when the following text has none
20
+ *
21
+ * @returns Number of runs modified
22
+ */
23
+ export function normalizeRunWhitespace(runs: Run[]): number {
24
+ let cleanedCount = 0;
25
+ let seenTextInParagraph = false;
26
+
27
+ for (let i = 0; i < runs.length; i++) {
28
+ const run = runs[i];
29
+ const text = run.getText();
30
+ if (!text) {
31
+ // ImageRuns have no text but ARE visible content —
32
+ // text after them is not at paragraph start and its leading
33
+ // space must be preserved (e.g., image + " CC: your supervisor...")
34
+ if (run instanceof ImageRun) {
35
+ seenTextInParagraph = true;
36
+
37
+ // Ensure exactly one regular space after small inline images (<100x100px).
38
+ // Strip special/variant space characters (en space, em space, NBSP, etc.)
39
+ // from between the image and text before ensuring the single space.
40
+ const image = run.getImageElement();
41
+ if (isImageSmall(image) && i < runs.length - 1) {
42
+ // Matches regular space + all Unicode space variants (NOT tabs or newlines)
43
+ const LEADING_SPACES = /^[ \u00A0\u2002-\u200A\u202F\u205F\u3000]+/;
44
+
45
+ for (let j = i + 1; j < runs.length; j++) {
46
+ const nextRun = runs[j];
47
+ const nextText = nextRun.getText();
48
+ if (!nextText) continue; // Skip non-text runs (e.g., another ImageRun)
49
+
50
+ // Preserve tabs (intentional formatting)
51
+ if (nextText.startsWith("\t")) break;
52
+
53
+ // Strip all leading space characters (regular + variant)
54
+ const stripped = nextText.replace(LEADING_SPACES, "");
55
+
56
+ if (stripped.length === 0) {
57
+ // Run was only spaces — clear it and check next run
58
+ if (nextText.length > 0) {
59
+ nextRun.setText("");
60
+ cleanedCount++;
61
+ }
62
+ continue;
63
+ }
64
+
65
+ // Found actual text — ensure exactly one regular space before it
66
+ const desired = " " + stripped;
67
+ if (nextText !== desired) {
68
+ nextRun.setText(desired);
69
+ cleanedCount++;
70
+ }
71
+ break;
72
+ }
73
+ }
74
+ }
75
+ continue;
76
+ }
77
+
78
+ // Step 1: Collapse multiple consecutive SPACES only
79
+ // Preserve tabs (\t) and newlines (\n) as they represent intentional formatting (<w:tab/> and <w:br/>)
80
+ let cleaned = text.replace(/ {2,}/g, " ");
81
+
82
+ // Step 1.5: Strip leading spaces from paragraph start
83
+ // Word uses setLeftIndent() for proper indentation, not literal spaces
84
+ // Only strip space characters (U+0020), NOT tabs or other whitespace
85
+ if (!seenTextInParagraph) {
86
+ cleaned = cleaned.replace(/^ +/, "");
87
+ if (cleaned.length > 0) {
88
+ seenTextInParagraph = true;
89
+ }
90
+ }
91
+
92
+ // Step 2: Trim trailing space if next run starts with space (cross-run double space)
93
+ if (i < runs.length - 1) {
94
+ const nextRun = runs[i + 1];
95
+ const nextText = nextRun?.getText() || "";
96
+ if (cleaned.endsWith(" ") && nextText.startsWith(" ")) {
97
+ cleaned = cleaned.trimEnd();
98
+ }
99
+ }
100
+
101
+ // Step 3: Trim leading space if previous run ends with space (cross-run double space)
102
+ if (i > 0) {
103
+ const prevRun = runs[i - 1];
104
+ const prevText = prevRun?.getText() || "";
105
+ if (cleaned.startsWith(" ") && prevText.endsWith(" ")) {
106
+ cleaned = cleaned.trimStart();
107
+ }
108
+ }
109
+
110
+ if (cleaned !== text) {
111
+ run.setText(cleaned);
112
+ cleanedCount++;
113
+ }
114
+ }
115
+
116
+ return cleanedCount;
117
+ }