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.
- package/.eslintrc.json +43 -0
- package/.github/workflows/build.yml +64 -0
- package/.github/workflows/ci.yml +39 -0
- package/.vscode/extensions.json +3 -0
- package/Current.md +97 -0
- package/DocHub_Image.png +0 -0
- package/README.md +666 -0
- package/USER_GUIDE.md +1173 -0
- package/Updater.md +311 -0
- package/build/256x256.png +0 -0
- package/build/512x512.png +0 -0
- package/build/app-update.yml +4 -0
- package/build/create-icon.js +208 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/icon_1024x1024.png +0 -0
- package/dist/assets/Analytics-BpsG9895.js +1 -0
- package/dist/assets/Card-IAZin8kp.js +1 -0
- package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
- package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
- package/dist/assets/Documents-CqZ25axS.js +1 -0
- package/dist/assets/Input-l89xwXBi.js +1 -0
- package/dist/assets/Reporting-DqdHJY_a.js +1 -0
- package/dist/assets/Search-XNbu5z_3.js +1 -0
- package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
- package/dist/assets/Sessions-ClZOPYNc.js +1 -0
- package/dist/assets/Settings-DUEHGURa.js +11 -0
- package/dist/assets/index-8xUe8ptc.js +24 -0
- package/dist/assets/index-RYyJqF7O.css +1 -0
- package/dist/assets/path-BkOl0AGO.js +1 -0
- package/dist/assets/promises-ID_B9S-h.js +1 -0
- package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
- package/dist/assets/useToast-yRSO1dkm.js +1 -0
- package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
- package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
- package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
- package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
- package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
- package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
- package/dist/electron/main-CXkNtyv-.js +19789 -0
- package/dist/electron/main.js +5 -0
- package/dist/electron/preload.js +1 -0
- package/dist/icon.png +0 -0
- package/dist/index.html +27 -0
- package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
- package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
- package/docs/README.md +115 -0
- package/docs/TOC_WIRING_GUIDE.md +344 -0
- package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
- package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
- package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
- package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
- package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
- package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
- package/docs/analysis/List_Implementation.md +206 -0
- package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
- package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
- package/docs/analysis/RefactorStyles.md +852 -0
- package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
- package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
- package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
- package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
- package/docs/analysis/github-issues-to-create.md +237 -0
- package/docs/api/API_README.md +538 -0
- package/docs/api/API_REFERENCE.md +751 -0
- package/docs/api/TYPE_DEFINITIONS.md +869 -0
- package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
- package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
- package/docs/docxmlater-readme.md +1341 -0
- package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
- package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
- package/docs/fixes/README.md +37 -0
- package/docs/github-issues/issue-1-body.md +125 -0
- package/docs/github-issues/issue-10-body.md +850 -0
- package/docs/github-issues/issue-2-body.md +200 -0
- package/docs/github-issues/issue-3-body.md +270 -0
- package/docs/github-issues/issue-4-body.md +169 -0
- package/docs/github-issues/issue-5-body.md +173 -0
- package/docs/github-issues/issue-6-body.md +158 -0
- package/docs/github-issues/issue-7-body.md +171 -0
- package/docs/github-issues/issue-8-body.md +407 -0
- package/docs/github-issues/issue-9-body.md +515 -0
- package/docs/github-issues/issue-tracker.md +274 -0
- package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
- package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
- package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
- package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
- package/docs/implementation/RefactorStyles.md +588 -0
- package/docs/implementation/implement-plan.md +489 -0
- package/docs/implementation/missing-helpers-implementation.md +391 -0
- package/docs/implementation/refactor-plan.md +520 -0
- package/docs/implementation/session-implementation-complete.md +233 -0
- package/docs/implementation/session-management-plan.md +250 -0
- package/docs/setup-checklist.md +77 -0
- package/docs/versions/changelog.md +345 -0
- package/electron/customUpdater.ts +656 -0
- package/electron/main.ts +2441 -0
- package/electron/memoryConfig.ts +187 -0
- package/electron/preload.ts +394 -0
- package/electron/proxyConfig.ts +340 -0
- package/electron/services/BackupService.ts +452 -0
- package/electron/services/DictionaryService.ts +402 -0
- package/electron/services/LocalDictionaryLookupService.ts +147 -0
- package/electron/services/PowerAutomateApiService.ts +231 -0
- package/electron/services/SharePointSyncService.ts +474 -0
- package/electron/windowsCertStore.ts +427 -0
- package/electron/zscalerConfig.ts +381 -0
- package/eslint.config.js +92 -0
- package/jest.config.js +52 -0
- package/package.json +214 -0
- package/postcss.config.mjs +6 -0
- package/public/icon.png +0 -0
- package/publish-release.ps1 +5 -0
- package/renovate.json +30 -0
- package/src/App.tsx +216 -0
- package/src/__mocks__/p-limit.js +12 -0
- package/src/__mocks__/styleMock.js +1 -0
- package/src/components/common/BugReportButton.tsx +44 -0
- package/src/components/common/BugReportDialog.tsx +193 -0
- package/src/components/common/Button.tsx +153 -0
- package/src/components/common/Card.tsx +86 -0
- package/src/components/common/ColorPickerDialog.tsx +177 -0
- package/src/components/common/ConfirmDialog.tsx +96 -0
- package/src/components/common/DebugConsole.tsx +275 -0
- package/src/components/common/EmptyState.tsx +183 -0
- package/src/components/common/ErrorBoundary.tsx +98 -0
- package/src/components/common/ErrorDetailsDialog.tsx +153 -0
- package/src/components/common/ErrorFallback.tsx +218 -0
- package/src/components/common/Input.tsx +109 -0
- package/src/components/common/Skeleton.tsx +184 -0
- package/src/components/common/SplashScreen.tsx +81 -0
- package/src/components/common/Toast.tsx +155 -0
- package/src/components/common/Tooltip.tsx +79 -0
- package/src/components/common/UpdateNotification.tsx +320 -0
- package/src/components/comparison/ComparisonWindow.tsx +374 -0
- package/src/components/comparison/SideBySideDiff.tsx +486 -0
- package/src/components/comparison/index.ts +8 -0
- package/src/components/document/DocumentUploader.tsx +288 -0
- package/src/components/document/HyperlinkPreview.tsx +430 -0
- package/src/components/document/HyperlinkService.md +1484 -0
- package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
- package/src/components/document/InlineChangesView.tsx +707 -0
- package/src/components/document/ProcessingProgress.tsx +303 -0
- package/src/components/document/ProcessingResults.tsx +256 -0
- package/src/components/document/TrackedChangesDetail.tsx +530 -0
- package/src/components/document/TrackedChangesPanel.tsx +546 -0
- package/src/components/document/VirtualDocumentList.tsx +240 -0
- package/src/components/editor/DocumentEditor.tsx +723 -0
- package/src/components/editor/DocumentEditorModal.tsx +640 -0
- package/src/components/editor/EditorQuickActions.tsx +502 -0
- package/src/components/editor/EditorToolbar.tsx +312 -0
- package/src/components/editor/TableEditor.tsx +926 -0
- package/src/components/editor/index.ts +18 -0
- package/src/components/layout/Header.tsx +190 -0
- package/src/components/layout/Sidebar.tsx +313 -0
- package/src/components/layout/TitleBar.tsx +190 -0
- package/src/components/navigation/CommandPalette.tsx +233 -0
- package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
- package/src/components/sessions/ChangeItem.tsx +408 -0
- package/src/components/sessions/ChangeViewer.tsx +1155 -0
- package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
- package/src/components/sessions/ProcessingOptions.tsx +297 -0
- package/src/components/sessions/ReplacementsTab.tsx +438 -0
- package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
- package/src/components/sessions/SessionManager.tsx +188 -0
- package/src/components/sessions/StylesEditor.tsx +1335 -0
- package/src/components/sessions/TabContainer.tsx +151 -0
- package/src/components/sessions/VirtualSessionList.tsx +157 -0
- package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
- package/src/components/settings/CertificateManager.tsx +410 -0
- package/src/components/settings/SegmentedControl.tsx +88 -0
- package/src/components/settings/SettingRow.tsx +52 -0
- package/src/contexts/GlobalStatsContext.tsx +396 -0
- package/src/contexts/SessionContext.tsx +2129 -0
- package/src/contexts/ThemeContext.tsx +428 -0
- package/src/contexts/UserSettingsContext.tsx +290 -0
- package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
- package/src/global.d.ts +273 -0
- package/src/hooks/useDocumentQueue.tsx +210 -0
- package/src/hooks/useToast.tsx +55 -0
- package/src/main.tsx +10 -0
- package/src/pages/Analytics.tsx +386 -0
- package/src/pages/CurrentSession.tsx +1174 -0
- package/src/pages/Dashboard.tsx +319 -0
- package/src/pages/Documents.tsx +317 -0
- package/src/pages/Projects.tsx +250 -0
- package/src/pages/Reporting.tsx +386 -0
- package/src/pages/Search.tsx +349 -0
- package/src/pages/Sessions.tsx +285 -0
- package/src/pages/Settings.tsx +2662 -0
- package/src/services/HyperlinkService.ts +1085 -0
- package/src/services/document/DocXMLaterProcessor.ts +617 -0
- package/src/services/document/DocumentProcessingComparison.ts +856 -0
- package/src/services/document/DocumentSnapshotService.ts +575 -0
- package/src/services/document/WordDocumentProcessor.ts +10509 -0
- package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
- package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
- package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
- package/src/services/document/blanklines/BlankLineManager.ts +658 -0
- package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
- package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
- package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
- package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
- package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
- package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
- package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
- package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
- package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
- package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
- package/src/services/document/blanklines/index.ts +67 -0
- package/src/services/document/blanklines/rules/additionRules.ts +337 -0
- package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
- package/src/services/document/blanklines/rules/removalRules.ts +362 -0
- package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
- package/src/services/document/blanklines/types.ts +29 -0
- package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
- package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
- package/src/services/document/helpers/whitespace.ts +117 -0
- package/src/services/document/list/ListNormalizer.ts +947 -0
- package/src/services/document/list/index.ts +45 -0
- package/src/services/document/list/list-detection.ts +275 -0
- package/src/services/document/list/list-types.ts +162 -0
- package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
- package/src/services/document/processors/ListProcessor.ts +257 -0
- package/src/services/document/processors/StructureProcessor.ts +176 -0
- package/src/services/document/processors/StyleProcessor.ts +389 -0
- package/src/services/document/processors/TableProcessor.ts +2238 -0
- package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
- package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
- package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
- package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
- package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
- package/src/services/document/processors/index.ts +28 -0
- package/src/services/document/types/docx-processing.ts +310 -0
- package/src/services/editor/EditorActionHandlers.ts +901 -0
- package/src/services/editor/index.ts +13 -0
- package/src/setupTests.ts +47 -0
- package/src/styles/global.css +782 -0
- package/src/types/backup.ts +132 -0
- package/src/types/dictionary.ts +125 -0
- package/src/types/document-processing.ts +331 -0
- package/src/types/docxmlater-augments.d.ts +142 -0
- package/src/types/editor.ts +280 -0
- package/src/types/electron.ts +340 -0
- package/src/types/globalStats.ts +155 -0
- package/src/types/hyperlink.ts +471 -0
- package/src/types/operations.ts +354 -0
- package/src/types/session.ts +427 -0
- package/src/types/settings.ts +112 -0
- package/src/utils/MemoryMonitor.ts +248 -0
- package/src/utils/cn.ts +6 -0
- package/src/utils/colorConvert.ts +306 -0
- package/src/utils/diffUtils.ts +347 -0
- package/src/utils/documentUtils.ts +202 -0
- package/src/utils/electronGuard.ts +62 -0
- package/src/utils/indexedDB.ts +915 -0
- package/src/utils/logger.ts +717 -0
- package/src/utils/pathSecurity.ts +232 -0
- package/src/utils/pathValidator.ts +236 -0
- package/src/utils/processingTimeEstimator.ts +153 -0
- package/src/utils/safeJsonParse.ts +62 -0
- package/src/utils/textSanitizer.ts +162 -0
- package/src/utils/urlHelpers.ts +304 -0
- package/src/utils/urlPatterns.ts +198 -0
- package/src/utils/urlSanitizer.ts +152 -0
- package/src/vite-env.d.ts +11 -0
- package/tsconfig.electron.json +19 -0
- package/tsconfig.json +36 -0
- package/tsconfig.node.json +12 -0
- package/typedoc.json +45 -0
- 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
|
+
];
|