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,2238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TableProcessor - Table formatting and uniformity operations
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Table uniformity (borders, shading, cell formatting)
|
|
6
|
+
* - Header2 table detection and formatting
|
|
7
|
+
* - 1x1 table special handling
|
|
8
|
+
* - Smart table detection and formatting
|
|
9
|
+
* - Table shading color configuration
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Document, Table, Paragraph, ImageRun, Image, Run, Hyperlink, PreservedElement, NumberingLevel, WORD_NATIVE_BULLETS, pointsToTwips, inchesToTwips } from "docxmlater";
|
|
13
|
+
import { logger } from "@/utils/logger";
|
|
14
|
+
|
|
15
|
+
const log = logger.namespace("TableProcessor");
|
|
16
|
+
|
|
17
|
+
// ═══════════════════════════════════════════════════════════
|
|
18
|
+
// HLP (High Level Process) Table Constants
|
|
19
|
+
// ═══════════════════════════════════════════════════════════
|
|
20
|
+
const HLP_HEADER_COLOR = 'FFC000'; // Orange header shading
|
|
21
|
+
const HLP_HEADER_TEXT = 'high level process'; // Case-insensitive match target
|
|
22
|
+
const HLP_TIPS_COLOR = 'FFF2CC'; // Light yellow tips column
|
|
23
|
+
const HLP_BORDER_SIZE = 18; // 2.25pt in eighths of a point
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* HLP table layout variant
|
|
27
|
+
*/
|
|
28
|
+
export type HLPVariant = 'single-column' | 'two-column';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Detailed HLP table analysis result
|
|
32
|
+
*/
|
|
33
|
+
export interface HLPTableAnalysis {
|
|
34
|
+
isHLP: boolean;
|
|
35
|
+
variant: HLPVariant | null;
|
|
36
|
+
columnCount: number;
|
|
37
|
+
rowCount: number;
|
|
38
|
+
hasTipsColumn: boolean;
|
|
39
|
+
headerText: string;
|
|
40
|
+
headerCellSpan: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Result of HLP table processing
|
|
45
|
+
*/
|
|
46
|
+
export interface HLPTableProcessingResult {
|
|
47
|
+
tablesFound: number;
|
|
48
|
+
headersStyled: number;
|
|
49
|
+
singleColumnTables: number;
|
|
50
|
+
twoColumnTables: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Table shading configuration
|
|
55
|
+
*/
|
|
56
|
+
export interface TableShadingSettings {
|
|
57
|
+
header2Shading: string; // Hex color for Header 2 / 1x1 table cells
|
|
58
|
+
otherShading: string; // Hex color for other table cells
|
|
59
|
+
preserveBold?: boolean; // If true, preserve original bold formatting in table cells
|
|
60
|
+
heading2FontFamily?: string; // Font family for Heading 2 / 1x1 table cells
|
|
61
|
+
heading2FontSize?: number; // Font size in points for Heading 2 / 1x1 table cells
|
|
62
|
+
// Normal style properties for shaded cells and first row cells
|
|
63
|
+
normalFontFamily?: string; // Font family (default "Verdana")
|
|
64
|
+
normalFontSize?: number; // Font size in points (default 12)
|
|
65
|
+
normalAlignment?: "left" | "center" | "right" | "justify"; // Alignment (default "center")
|
|
66
|
+
preserveCenterAlignment?: boolean; // If true, preserve existing center alignment
|
|
67
|
+
normalSpaceBefore?: number; // Space before in points (default 3)
|
|
68
|
+
normalSpaceAfter?: number; // Space after in points (default 3)
|
|
69
|
+
normalLineSpacing?: number; // Line spacing multiplier (default 1.0)
|
|
70
|
+
// Table cell padding in inches
|
|
71
|
+
padding1x1Top?: number; // default: 0
|
|
72
|
+
padding1x1Bottom?: number; // default: 0
|
|
73
|
+
padding1x1Left?: number; // default: 0.08
|
|
74
|
+
padding1x1Right?: number; // default: 0.08
|
|
75
|
+
paddingOtherTop?: number; // default: 0
|
|
76
|
+
paddingOtherBottom?: number; // default: 0
|
|
77
|
+
paddingOtherLeft?: number; // default: 0.08
|
|
78
|
+
paddingOtherRight?: number; // default: 0.08
|
|
79
|
+
// List indentation levels for HLP table content (from session ListBulletSettings)
|
|
80
|
+
listIndentationLevels?: Array<{
|
|
81
|
+
level: number;
|
|
82
|
+
symbolIndent: number; // inches
|
|
83
|
+
textIndent: number; // inches
|
|
84
|
+
}>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Result of table formatting operation
|
|
89
|
+
*/
|
|
90
|
+
export interface TableFormattingResult {
|
|
91
|
+
tablesProcessed: number;
|
|
92
|
+
cellsRecolored: number;
|
|
93
|
+
bordersApplied: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Result of Header2 table validation
|
|
98
|
+
*/
|
|
99
|
+
export interface Header2TableValidationResult {
|
|
100
|
+
count: number;
|
|
101
|
+
tablesFixed: string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Table processing service
|
|
106
|
+
*/
|
|
107
|
+
export class TableProcessor {
|
|
108
|
+
private readonly DEBUG = process.env.NODE_ENV !== "production";
|
|
109
|
+
|
|
110
|
+
/** Colors used by HLP tables — preserved during table uniformity operations. */
|
|
111
|
+
private static readonly HLP_PRESERVED_COLORS = [HLP_HEADER_COLOR, HLP_TIPS_COLOR];
|
|
112
|
+
|
|
113
|
+
/** Cached HLP table analysis results, populated before applyStyles() overwrites FFC000 shading. */
|
|
114
|
+
private _hlpTableCache: Map<Table, HLPTableAnalysis> | null = null;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Apply Normal-style spacing settings to a paragraph.
|
|
118
|
+
* Shared by table uniformity (header rows, shaded cells) and HLP content formatting.
|
|
119
|
+
*/
|
|
120
|
+
private applyNormalSpacing(
|
|
121
|
+
para: Paragraph,
|
|
122
|
+
settings?: { normalSpaceBefore?: number; normalSpaceAfter?: number; normalLineSpacing?: number }
|
|
123
|
+
): void {
|
|
124
|
+
if (settings?.normalSpaceBefore !== undefined) {
|
|
125
|
+
para.setSpaceBefore(pointsToTwips(settings.normalSpaceBefore));
|
|
126
|
+
}
|
|
127
|
+
if (settings?.normalSpaceAfter !== undefined) {
|
|
128
|
+
para.setSpaceAfter(pointsToTwips(settings.normalSpaceAfter));
|
|
129
|
+
}
|
|
130
|
+
if (settings?.normalLineSpacing !== undefined) {
|
|
131
|
+
para.setLineSpacing(pointsToTwips(settings.normalLineSpacing * 12));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Detect if a cell has visual shading from any source
|
|
137
|
+
*
|
|
138
|
+
* Checks direct cell-level shading (from w:tcPr) to determine if a cell appears shaded:
|
|
139
|
+
* 1. Direct cell shading fill color (non-white, non-auto)
|
|
140
|
+
* 2. Direct cell pattern shading (non-clear, non-nil patterns like pct50, solid, diagStripe)
|
|
141
|
+
*
|
|
142
|
+
* Does NOT check table style inheritance — that was intentionally removed to prevent
|
|
143
|
+
* applyTableUniformity() from incorrectly formatting cells with conditional style shading.
|
|
144
|
+
*
|
|
145
|
+
* @returns Object with hasShading boolean and optional fill color
|
|
146
|
+
*/
|
|
147
|
+
private getResolvedCellShading(
|
|
148
|
+
cell: ReturnType<ReturnType<Table["getRows"]>[number]["getCells"]>[number],
|
|
149
|
+
table: Table,
|
|
150
|
+
doc: Document,
|
|
151
|
+
debugContext?: { tableIndex?: number; rowIndex?: number; cellIndex?: number }
|
|
152
|
+
): { hasShading: boolean; fill?: string } {
|
|
153
|
+
const formatting = cell.getFormatting();
|
|
154
|
+
|
|
155
|
+
// 1. Check direct cell shading fill
|
|
156
|
+
const directFill = formatting.shading?.fill?.toUpperCase();
|
|
157
|
+
|
|
158
|
+
// DEBUG: Log shading detection details
|
|
159
|
+
if (debugContext) {
|
|
160
|
+
log.debug(
|
|
161
|
+
`[Table ${debugContext.tableIndex}] Cell (${debugContext.rowIndex},${debugContext.cellIndex}): ` +
|
|
162
|
+
`shading.fill="${formatting.shading?.fill || 'undefined'}", ` +
|
|
163
|
+
`shading.pattern="${formatting.shading?.pattern || 'undefined'}", ` +
|
|
164
|
+
`directFill="${directFill || 'undefined'}"`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (directFill && directFill !== "AUTO" && directFill !== "FFFFFF") {
|
|
169
|
+
if (debugContext) {
|
|
170
|
+
log.debug(` → hasShading=TRUE (fill: ${directFill})`);
|
|
171
|
+
}
|
|
172
|
+
return { hasShading: true, fill: directFill };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 2. Check direct cell pattern shading (e.g., pct50, solid, diagStripe)
|
|
176
|
+
// Uses docxmlater's proper API — only detects direct cell shading from w:tcPr,
|
|
177
|
+
// NOT inherited table style conditionals (banded rows, firstCol, etc.)
|
|
178
|
+
const pattern = formatting.shading?.pattern;
|
|
179
|
+
if (pattern && pattern !== "clear" && pattern !== "nil") {
|
|
180
|
+
if (debugContext) {
|
|
181
|
+
log.debug(` → hasShading=TRUE (pattern: ${pattern})`);
|
|
182
|
+
}
|
|
183
|
+
return { hasShading: true, fill: directFill };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 3. Table style inheritance check REMOVED
|
|
187
|
+
// Previously this checked table style for inherited cell shading, but now that
|
|
188
|
+
// docxmlater properly parses table styles (w:tblStyle), ALL cells with any
|
|
189
|
+
// table style conditional shading (banded rows, firstCol, etc.) were being detected.
|
|
190
|
+
// This caused applyTableUniformity() to center and bold ALL cells incorrectly.
|
|
191
|
+
// Now we only detect cells with DIRECT shading, preserving original table styling.
|
|
192
|
+
|
|
193
|
+
// 4. No shading detected
|
|
194
|
+
if (debugContext) {
|
|
195
|
+
log.debug(` → hasShading=FALSE`);
|
|
196
|
+
}
|
|
197
|
+
return { hasShading: false };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if a cell contains ANY image (not just large ones).
|
|
202
|
+
* Images can appear as Image or ImageRun instances in paragraph content.
|
|
203
|
+
*
|
|
204
|
+
* @param cell - The table cell to check
|
|
205
|
+
* @returns True if the cell contains any image
|
|
206
|
+
*/
|
|
207
|
+
private cellContainsAnyImage(
|
|
208
|
+
cell: ReturnType<ReturnType<Table["getRows"]>[number]["getCells"]>[number]
|
|
209
|
+
): boolean {
|
|
210
|
+
for (const para of cell.getParagraphs()) {
|
|
211
|
+
const content = para.getContent();
|
|
212
|
+
for (const item of content) {
|
|
213
|
+
if (item instanceof Image || item instanceof ImageRun) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Count the number of text lines in a cell.
|
|
223
|
+
* Each paragraph counts as 1 line, plus any soft line breaks (\n) within paragraphs.
|
|
224
|
+
*
|
|
225
|
+
* @param cell - The table cell to check
|
|
226
|
+
* @returns Number of text lines in the cell
|
|
227
|
+
*/
|
|
228
|
+
private countCellTextLines(
|
|
229
|
+
cell: ReturnType<ReturnType<Table["getRows"]>[number]["getCells"]>[number]
|
|
230
|
+
): number {
|
|
231
|
+
let lineCount = 0;
|
|
232
|
+
for (const para of cell.getParagraphs()) {
|
|
233
|
+
lineCount += 1;
|
|
234
|
+
const text = para.getText() || "";
|
|
235
|
+
lineCount += (text.match(/\n/g) || []).length;
|
|
236
|
+
}
|
|
237
|
+
return lineCount;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if text starts with a typed list prefix (bullet character or number).
|
|
242
|
+
* Used as fallback detection when Word list formatting is not detected.
|
|
243
|
+
*
|
|
244
|
+
* @param text - The text to check
|
|
245
|
+
* @returns True if the text starts with a list prefix
|
|
246
|
+
*/
|
|
247
|
+
private hasTypedListPrefix(text: string): boolean {
|
|
248
|
+
if (!text) return false;
|
|
249
|
+
// Bullet characters (including dash variants)
|
|
250
|
+
if (/^[•●○◦▪▫‣⁃\-–—]\s/.test(text)) return true;
|
|
251
|
+
// Numbered: "1.", "1)", "(1)", "a.", "a)", "(a)", "i.", etc.
|
|
252
|
+
if (/^(\d+[\.\):]|\(\d+\)|[a-zA-Z][\.\):]|\([a-zA-Z]\)|[ivxIVX]+[\.\):])/.test(text)) return true;
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Check if a table cell contains any list items (bullets or numbered lists).
|
|
258
|
+
* Uses multiple detection methods for robustness:
|
|
259
|
+
* 1. Word list formatting via getNumbering() / hasNumbering()
|
|
260
|
+
* 2. Typed list prefixes (bullet characters or numbers in text)
|
|
261
|
+
*
|
|
262
|
+
* @param cell - The table cell to check
|
|
263
|
+
* @returns True if the cell contains any list formatting
|
|
264
|
+
*/
|
|
265
|
+
private cellContainsAnyList(
|
|
266
|
+
cell: ReturnType<ReturnType<Table["getRows"]>[number]["getCells"]>[number]
|
|
267
|
+
): boolean {
|
|
268
|
+
const paragraphs = cell.getParagraphs();
|
|
269
|
+
log.debug(`cellContainsAnyList: checking ${paragraphs.length} paragraphs`);
|
|
270
|
+
|
|
271
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
272
|
+
const para = paragraphs[i];
|
|
273
|
+
const numbering = para.getNumbering();
|
|
274
|
+
const text = para.getText()?.trim() || '';
|
|
275
|
+
|
|
276
|
+
// Method 1: Check Word list formatting via getNumbering()
|
|
277
|
+
if (numbering && numbering.numId) {
|
|
278
|
+
log.debug(` Para ${i}: FOUND LIST via getNumbering() numId=${numbering.numId}, text="${text.substring(0, 40)}..."`);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Method 2: Check Word list formatting via hasNumbering() (handles edge cases)
|
|
283
|
+
if (typeof para.hasNumbering === 'function' && para.hasNumbering()) {
|
|
284
|
+
log.debug(` Para ${i}: FOUND LIST via hasNumbering(), text="${text.substring(0, 40)}..."`);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Method 3: Check for typed list prefixes (fallback)
|
|
289
|
+
if (this.hasTypedListPrefix(text)) {
|
|
290
|
+
log.debug(` Para ${i}: FOUND LIST via typed prefix, text="${text.substring(0, 40)}..."`);
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
log.debug(` -> NO LISTS FOUND in ${paragraphs.length} paragraphs`);
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Check if a 1x1 table should be excluded from Heading 2 styling and shading.
|
|
301
|
+
* Excluded if the cell has more than 2 lines of text.
|
|
302
|
+
*
|
|
303
|
+
* @param cell - The single cell of a 1x1 table
|
|
304
|
+
* @returns True if the table should be excluded from styling/shading
|
|
305
|
+
*/
|
|
306
|
+
private should1x1TableBeExcluded(
|
|
307
|
+
cell: ReturnType<ReturnType<Table["getRows"]>[number]["getCells"]>[number]
|
|
308
|
+
): boolean {
|
|
309
|
+
const lineCount = this.countCellTextLines(cell);
|
|
310
|
+
if (lineCount > 2) {
|
|
311
|
+
log.debug(`should1x1TableBeExcluded: ${lineCount} lines (>2) -> EXCLUDED`);
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
log.debug(`should1x1TableBeExcluded: ${lineCount} lines (<=2) -> NOT EXCLUDED`);
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Apply uniform formatting to all tables in the document
|
|
320
|
+
*/
|
|
321
|
+
async applyTableUniformity(
|
|
322
|
+
doc: Document,
|
|
323
|
+
shadingSettings?: TableShadingSettings
|
|
324
|
+
): Promise<TableFormattingResult> {
|
|
325
|
+
const tables = doc.getTables();
|
|
326
|
+
let tablesProcessed = 0;
|
|
327
|
+
let cellsRecolored = 0;
|
|
328
|
+
|
|
329
|
+
const header2Shading = shadingSettings?.header2Shading || "BFBFBF";
|
|
330
|
+
const otherShading = shadingSettings?.otherShading || "DFDFDF";
|
|
331
|
+
const preserveBold = shadingSettings?.preserveBold ?? true; // Default to preserve
|
|
332
|
+
|
|
333
|
+
// Normal style properties for shaded cells and first row cells
|
|
334
|
+
// Note: Bold and center alignment are ALWAYS applied to shaded/first row cells (hardcoded)
|
|
335
|
+
// Only font family, font size, and spacing are configurable from Normal style
|
|
336
|
+
const normalFontFamily = shadingSettings?.normalFontFamily ?? "Verdana";
|
|
337
|
+
const normalFontSize = shadingSettings?.normalFontSize ?? 12;
|
|
338
|
+
|
|
339
|
+
log.info(`Processing ${tables.length} tables for uniformity`);
|
|
340
|
+
log.debug(`Shading colors: Header2=${header2Shading}, Other=${otherShading}`);
|
|
341
|
+
log.debug(`preserveBold=${preserveBold} (from shadingSettings?.preserveBold=${shadingSettings?.preserveBold})`);
|
|
342
|
+
|
|
343
|
+
let tableIndex = 0;
|
|
344
|
+
for (const table of tables) {
|
|
345
|
+
try {
|
|
346
|
+
// Skip floating tables and tables containing nested tables
|
|
347
|
+
if (this.shouldSkipTable(table)) {
|
|
348
|
+
log.debug(`[Table ${tableIndex}] Skipping floating/nested table`);
|
|
349
|
+
tableIndex++;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Skip HLP tables — they have their own formatting in processHLPTables()
|
|
354
|
+
if (this.isHLPTable(table)) {
|
|
355
|
+
log.debug(`[Table ${tableIndex}] Skipping HLP table (handled by processHLPTables)`);
|
|
356
|
+
tableIndex++;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const rows = table.getRows();
|
|
361
|
+
const rowCount = rows.length;
|
|
362
|
+
|
|
363
|
+
if (rowCount === 0) {
|
|
364
|
+
tableIndex++;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Detect if this is a 1x1 table
|
|
369
|
+
const is1x1Table = rowCount === 1 && rows[0].getCells().length === 1;
|
|
370
|
+
|
|
371
|
+
log.debug(`[Table ${tableIndex}] Type: ${is1x1Table ? "1x1" : `${rowCount}x${rows[0].getCells().length}`}`);
|
|
372
|
+
|
|
373
|
+
if (is1x1Table) {
|
|
374
|
+
// Apply shading and font formatting to 1x1 tables
|
|
375
|
+
// EXCEPTION: Skip if cell has >2 lines of text
|
|
376
|
+
const singleCell = rows[0].getCells()[0];
|
|
377
|
+
if (singleCell) {
|
|
378
|
+
// Check if this 1x1 table should be excluded from styling
|
|
379
|
+
if (this.should1x1TableBeExcluded(singleCell)) {
|
|
380
|
+
const lineCount = this.countCellTextLines(singleCell);
|
|
381
|
+
log.debug(`[Table ${tableIndex}] 1x1 table: Skipping styling (${lineCount} lines)`);
|
|
382
|
+
|
|
383
|
+
tablesProcessed++;
|
|
384
|
+
tableIndex++;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const { hasShading } = this.getResolvedCellShading(singleCell, table, doc, { tableIndex, rowIndex: 0, cellIndex: 0 });
|
|
389
|
+
|
|
390
|
+
// Also check if any paragraph has Heading 2 style
|
|
391
|
+
const hasHeading2Style = singleCell.getParagraphs().some(para => {
|
|
392
|
+
const style = para.getStyle();
|
|
393
|
+
return style === "Heading2" || style === "Heading 2";
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Apply shading if EITHER has existing shading OR has Heading 2 style
|
|
397
|
+
if (hasShading || hasHeading2Style) {
|
|
398
|
+
singleCell.setShading({ fill: header2Shading, pattern: "clear", color: "auto" });
|
|
399
|
+
cellsRecolored++;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Format text in 1x1 tables using Heading 2 style configuration
|
|
403
|
+
// Using TableCell convenience methods for cleaner code
|
|
404
|
+
const h2FontFamily = shadingSettings?.heading2FontFamily ?? "Verdana";
|
|
405
|
+
const h2FontSize = shadingSettings?.heading2FontSize ?? 14; // 14pt default for Heading 2
|
|
406
|
+
singleCell.setAllRunsFont(h2FontFamily);
|
|
407
|
+
singleCell.setAllRunsSize(h2FontSize);
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
// Handle multi-cell tables
|
|
411
|
+
// - First row (header): ALWAYS shade with "Other Table Shading" + bold + center
|
|
412
|
+
// - Data rows WITH existing shading: Apply "Other Table Shading" + bold + center
|
|
413
|
+
// - Data rows WITHOUT shading: Preserve original formatting (don't change bold)
|
|
414
|
+
let rowIndex = 0;
|
|
415
|
+
for (const row of rows) {
|
|
416
|
+
const isFirstRow = rowIndex === 0;
|
|
417
|
+
let cellIndex = 0;
|
|
418
|
+
|
|
419
|
+
for (const cell of row.getCells()) {
|
|
420
|
+
// Check if cell has visual shading using resolved detection (direct fill only)
|
|
421
|
+
const { hasShading, fill: existingFill } = this.getResolvedCellShading(cell, table, doc, { tableIndex, rowIndex, cellIndex });
|
|
422
|
+
|
|
423
|
+
if (isFirstRow) {
|
|
424
|
+
// HEADER ROW: ALWAYS apply "Other Table Shading" + bold + center
|
|
425
|
+
// (HLP tables are already skipped above, so no preservation check needed)
|
|
426
|
+
log.debug(`[Table ${tableIndex}] HEADER cell (${rowIndex},${cellIndex}): Applying shading #${otherShading}, bold=true, center=true`);
|
|
427
|
+
cell.setShading({ fill: otherShading, pattern: "clear", color: "auto" });
|
|
428
|
+
cellsRecolored++;
|
|
429
|
+
|
|
430
|
+
for (const para of cell.getParagraphs()) {
|
|
431
|
+
const isListItem = !!para.getNumbering();
|
|
432
|
+
for (const run of para.getRuns()) {
|
|
433
|
+
run.setFont(normalFontFamily);
|
|
434
|
+
run.setSize(normalFontSize);
|
|
435
|
+
// Don't force bold on list items - respect preserveBold setting
|
|
436
|
+
if (!isListItem) {
|
|
437
|
+
run.setBold(true);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// Apply alignment (skip list items) - ALWAYS center for header row cells
|
|
441
|
+
if (!isListItem) {
|
|
442
|
+
para.setAlignment("center");
|
|
443
|
+
}
|
|
444
|
+
this.applyNormalSpacing(para, shadingSettings);
|
|
445
|
+
}
|
|
446
|
+
} else if (hasShading) {
|
|
447
|
+
// DATA ROW WITH SHADING: Apply "Other Table Shading" + bold + center
|
|
448
|
+
// EXCEPTION: Preserve HLP table shading colors (orange/yellow)
|
|
449
|
+
const shouldPreserveShading = existingFill && TableProcessor.HLP_PRESERVED_COLORS.includes(existingFill.toUpperCase());
|
|
450
|
+
|
|
451
|
+
if (shouldPreserveShading) {
|
|
452
|
+
log.debug(`[Table ${tableIndex}] DATA cell (${rowIndex},${cellIndex}): Preserving original shading #${existingFill}`);
|
|
453
|
+
} else {
|
|
454
|
+
log.debug(`[Table ${tableIndex}] DATA cell WITH shading (${rowIndex},${cellIndex}): Applying shading #${otherShading}, bold=true`);
|
|
455
|
+
cell.setShading({ fill: otherShading, pattern: "clear", color: "auto" });
|
|
456
|
+
cellsRecolored++;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Apply formatting regardless of shading preservation
|
|
460
|
+
for (const para of cell.getParagraphs()) {
|
|
461
|
+
const isListItem = !!para.getNumbering();
|
|
462
|
+
for (const run of para.getRuns()) {
|
|
463
|
+
run.setFont(normalFontFamily);
|
|
464
|
+
run.setSize(normalFontSize);
|
|
465
|
+
// Don't force bold on list items - respect preserveBold setting
|
|
466
|
+
if (!isListItem) {
|
|
467
|
+
run.setBold(true);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Apply alignment (skip list items) - ALWAYS center for shaded cells
|
|
471
|
+
if (!isListItem) {
|
|
472
|
+
para.setAlignment("center");
|
|
473
|
+
}
|
|
474
|
+
this.applyNormalSpacing(para, shadingSettings);
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
// DATA ROW WITHOUT SHADING: Preserve original formatting
|
|
478
|
+
// Don't change bold - preserve original bold state
|
|
479
|
+
log.debug(`[Table ${tableIndex}] DATA cell WITHOUT shading (${rowIndex},${cellIndex}): Preserving formatting (no shading, no bold change)`);
|
|
480
|
+
for (const para of cell.getParagraphs()) {
|
|
481
|
+
// Skip list items
|
|
482
|
+
if (!para.getNumbering()) {
|
|
483
|
+
// Check for images - ImageRun extends Run and contains the actual image
|
|
484
|
+
const content = para.getContent();
|
|
485
|
+
const hasImage = content.some(
|
|
486
|
+
(item) => item instanceof ImageRun || item instanceof Image
|
|
487
|
+
);
|
|
488
|
+
if (!hasImage) {
|
|
489
|
+
for (const run of para.getRuns()) {
|
|
490
|
+
run.setFont(normalFontFamily);
|
|
491
|
+
run.setSize(normalFontSize);
|
|
492
|
+
// Note: NOT setting bold here - preserves original
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
cellIndex++;
|
|
499
|
+
}
|
|
500
|
+
rowIndex++;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
tablesProcessed++;
|
|
505
|
+
} catch (error) {
|
|
506
|
+
log.warn(`Failed to process table: ${error}`);
|
|
507
|
+
}
|
|
508
|
+
tableIndex++;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
log.info(
|
|
512
|
+
`Table uniformity complete: ${tablesProcessed} tables, ${cellsRecolored} cells recolored`
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
tablesProcessed,
|
|
517
|
+
cellsRecolored,
|
|
518
|
+
bordersApplied: 0,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Apply smart table formatting based on content detection
|
|
524
|
+
*/
|
|
525
|
+
async applySmartTableFormatting(doc: Document): Promise<number> {
|
|
526
|
+
const tables = doc.getTables();
|
|
527
|
+
let formattedCount = 0;
|
|
528
|
+
|
|
529
|
+
for (const table of tables) {
|
|
530
|
+
try {
|
|
531
|
+
// Skip floating tables and tables containing nested tables
|
|
532
|
+
if (this.shouldSkipTable(table)) {
|
|
533
|
+
log.debug(`Skipping floating/nested table in smart formatting`);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const rows = table.getRows();
|
|
538
|
+
if (rows.length === 0) continue;
|
|
539
|
+
|
|
540
|
+
// Check for header row
|
|
541
|
+
const firstRow = rows[0];
|
|
542
|
+
const firstRowCells = firstRow.getCells();
|
|
543
|
+
|
|
544
|
+
// Detect if first row is a header (often has bold text or different shading)
|
|
545
|
+
let isHeaderRow = false;
|
|
546
|
+
for (const cell of firstRowCells) {
|
|
547
|
+
const paras = cell.getParagraphs();
|
|
548
|
+
for (const para of paras) {
|
|
549
|
+
const runs = para.getRuns();
|
|
550
|
+
for (const run of runs) {
|
|
551
|
+
const formatting = run.getFormatting();
|
|
552
|
+
if (formatting?.bold) {
|
|
553
|
+
isHeaderRow = true;
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Apply formatting based on detection
|
|
561
|
+
if (isHeaderRow) {
|
|
562
|
+
// Apply header row shading
|
|
563
|
+
for (const cell of firstRowCells) {
|
|
564
|
+
cell.setShading({ fill: "BFBFBF", pattern: "clear", color: "auto" });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
formattedCount++;
|
|
569
|
+
} catch (error) {
|
|
570
|
+
log.warn(`Failed to apply smart formatting to table: ${error}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return formattedCount;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Validate and fix Header2 styling in tables
|
|
579
|
+
*/
|
|
580
|
+
async validateHeader2Tables(
|
|
581
|
+
doc: Document,
|
|
582
|
+
header2Style: {
|
|
583
|
+
fontFamily: string;
|
|
584
|
+
fontSize: number;
|
|
585
|
+
bold: boolean;
|
|
586
|
+
italic: boolean;
|
|
587
|
+
alignment: string;
|
|
588
|
+
spaceBefore: number;
|
|
589
|
+
spaceAfter: number;
|
|
590
|
+
preserveBold?: boolean;
|
|
591
|
+
preserveItalic?: boolean;
|
|
592
|
+
preserveUnderline?: boolean;
|
|
593
|
+
}
|
|
594
|
+
): Promise<Header2TableValidationResult> {
|
|
595
|
+
const tables = doc.getTables();
|
|
596
|
+
let cellsFixed = 0;
|
|
597
|
+
const affectedCells: string[] = [];
|
|
598
|
+
|
|
599
|
+
for (const table of tables) {
|
|
600
|
+
// Skip floating tables and tables containing nested tables
|
|
601
|
+
if (this.shouldSkipTable(table)) {
|
|
602
|
+
log.debug(`Skipping floating/nested table in Header2 validation`);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const rows = table.getRows();
|
|
607
|
+
|
|
608
|
+
for (const row of rows) {
|
|
609
|
+
for (const cell of row.getCells()) {
|
|
610
|
+
const paragraphs = cell.getParagraphs();
|
|
611
|
+
|
|
612
|
+
for (const para of paragraphs) {
|
|
613
|
+
const currentStyle = para.getStyle();
|
|
614
|
+
|
|
615
|
+
if (currentStyle === "Heading2" || currentStyle === "Heading 2") {
|
|
616
|
+
// Validate and fix Header2 formatting in table cell
|
|
617
|
+
const runs = para.getRuns();
|
|
618
|
+
|
|
619
|
+
for (const run of runs) {
|
|
620
|
+
const runFormatting = run.getFormatting();
|
|
621
|
+
let needsUpdate = false;
|
|
622
|
+
|
|
623
|
+
if (runFormatting.font !== header2Style.fontFamily) {
|
|
624
|
+
run.setFont(header2Style.fontFamily);
|
|
625
|
+
needsUpdate = true;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (runFormatting.size !== header2Style.fontSize) {
|
|
629
|
+
run.setSize(header2Style.fontSize);
|
|
630
|
+
needsUpdate = true;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (!header2Style.preserveBold && runFormatting.bold !== header2Style.bold) {
|
|
634
|
+
run.setBold(header2Style.bold);
|
|
635
|
+
needsUpdate = true;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (!header2Style.preserveItalic && runFormatting.italic !== header2Style.italic) {
|
|
639
|
+
run.setItalic(header2Style.italic);
|
|
640
|
+
needsUpdate = true;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (needsUpdate) {
|
|
644
|
+
cellsFixed++;
|
|
645
|
+
const cellText = para.getText();
|
|
646
|
+
if (cellText && !affectedCells.includes(cellText)) {
|
|
647
|
+
affectedCells.push(cellText);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (cellsFixed > 0) {
|
|
658
|
+
log.info(`Fixed Header2 styling in ${cellsFixed} table cells`);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return {
|
|
662
|
+
count: cellsFixed,
|
|
663
|
+
tablesFixed: affectedCells,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Detect 1x1 tables in the document
|
|
669
|
+
*/
|
|
670
|
+
detect1x1Tables(doc: Document): Table[] {
|
|
671
|
+
const tables = doc.getTables();
|
|
672
|
+
return tables.filter((table) => {
|
|
673
|
+
// Skip floating tables and tables containing nested tables
|
|
674
|
+
if (this.shouldSkipTable(table)) return false;
|
|
675
|
+
const rows = table.getRows();
|
|
676
|
+
return rows.length === 1 && rows[0].getCells().length === 1;
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Check if a table contains Header2 styled content
|
|
682
|
+
*/
|
|
683
|
+
tableHasHeader2Content(table: Table): boolean {
|
|
684
|
+
const rows = table.getRows();
|
|
685
|
+
|
|
686
|
+
for (const row of rows) {
|
|
687
|
+
for (const cell of row.getCells()) {
|
|
688
|
+
const paragraphs = cell.getParagraphs();
|
|
689
|
+
|
|
690
|
+
for (const para of paragraphs) {
|
|
691
|
+
const style = para.getStyle();
|
|
692
|
+
if (style === "Heading2" || style === "Heading 2") {
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Center-align cells containing only numeric content (step numbers like "1", "2", "3")
|
|
704
|
+
* This improves readability in procedural documents with numbered steps
|
|
705
|
+
*/
|
|
706
|
+
async centerNumericCells(doc: Document): Promise<number> {
|
|
707
|
+
const tables = doc.getTables();
|
|
708
|
+
let centeredCount = 0;
|
|
709
|
+
|
|
710
|
+
for (const table of tables) {
|
|
711
|
+
try {
|
|
712
|
+
// Skip floating tables and tables containing nested tables
|
|
713
|
+
if (this.shouldSkipTable(table)) {
|
|
714
|
+
log.debug(`Skipping floating/nested table in numeric centering`);
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Skip HLP tables — handled by processHLPTables()
|
|
719
|
+
if (this.isHLPTable(table)) continue;
|
|
720
|
+
|
|
721
|
+
const rows = table.getRows();
|
|
722
|
+
|
|
723
|
+
// Skip 1x1 tables (usually headers)
|
|
724
|
+
if (rows.length === 1 && rows[0].getCells().length === 1) {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
for (const row of rows) {
|
|
729
|
+
for (const cell of row.getCells()) {
|
|
730
|
+
const paragraphs = cell.getParagraphs();
|
|
731
|
+
|
|
732
|
+
// Check if cell has only one paragraph with numeric-only content
|
|
733
|
+
if (paragraphs.length === 1) {
|
|
734
|
+
const para = paragraphs[0];
|
|
735
|
+
const text = para.getText()?.trim() || "";
|
|
736
|
+
|
|
737
|
+
// Check if text is purely numeric (digits, periods, commas for decimals)
|
|
738
|
+
// Also match common step formats like "1.", "1)", "Step 1", etc.
|
|
739
|
+
if (this.isNumericContent(text)) {
|
|
740
|
+
para.setAlignment("center");
|
|
741
|
+
centeredCount++;
|
|
742
|
+
log.debug(`Centered numeric cell: "${text}"`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
} catch (error) {
|
|
748
|
+
log.warn(`Failed to process table for numeric centering: ${error}`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (centeredCount > 0) {
|
|
753
|
+
log.info(`Centered ${centeredCount} numeric table cells`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return centeredCount;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Check if text content is numeric-only (suitable for centering)
|
|
761
|
+
* Matches: "1", "2", "10", "1.", "1)", "(1)", "Step 1", etc.
|
|
762
|
+
*/
|
|
763
|
+
private isNumericContent(text: string): boolean {
|
|
764
|
+
if (!text || text.length === 0) {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Skip if too long (likely not a step number)
|
|
769
|
+
if (text.length > 10) {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Pure number: "1", "2", "10", "100"
|
|
774
|
+
if (/^\d+$/.test(text)) {
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Number with period: "1.", "2.", "10."
|
|
779
|
+
if (/^\d+\.$/.test(text)) {
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Number with parenthesis: "1)", "(1)", "(1"
|
|
784
|
+
if (/^\d+\)$/.test(text) || /^\(\d+\)$/.test(text) || /^\(\d+$/.test(text)) {
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Letter step indicators: "a.", "b.", "A.", "B."
|
|
789
|
+
if (/^[a-zA-Z]\.$/.test(text) || /^[a-zA-Z]\)$/.test(text)) {
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Remove specified row heights from all tables, allowing rows to auto-size based on content.
|
|
798
|
+
* This fixes inconsistent row appearance when some rows have specified heights.
|
|
799
|
+
*
|
|
800
|
+
* @param doc - The document to process
|
|
801
|
+
* @returns Number of rows with heights removed
|
|
802
|
+
*/
|
|
803
|
+
async removeSpecifiedRowHeights(doc: Document): Promise<number> {
|
|
804
|
+
const tables = doc.getTables();
|
|
805
|
+
let rowsFixed = 0;
|
|
806
|
+
|
|
807
|
+
for (const table of tables) {
|
|
808
|
+
try {
|
|
809
|
+
// Skip floating tables and tables containing nested tables
|
|
810
|
+
if (this.shouldSkipTable(table)) continue;
|
|
811
|
+
|
|
812
|
+
for (const row of table.getRows()) {
|
|
813
|
+
const formatting = row.getFormatting();
|
|
814
|
+
// Remove specified height - let row auto-size
|
|
815
|
+
if (formatting.height !== undefined) {
|
|
816
|
+
row.clearHeight();
|
|
817
|
+
rowsFixed++;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
} catch (error) {
|
|
821
|
+
log.warn(`Failed to remove row height: ${error}`);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (rowsFixed > 0) {
|
|
826
|
+
log.info(`Removed specified heights from ${rowsFixed} table rows`);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return rowsFixed;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Standardize cell margins and enable text wrapping on all table cells.
|
|
834
|
+
* Sets 0" top/bottom margins, 0.08" left/right margins, and enables text wrapping.
|
|
835
|
+
*
|
|
836
|
+
* @param doc - The document to process
|
|
837
|
+
* @returns Number of cells with margins standardized
|
|
838
|
+
*/
|
|
839
|
+
async standardizeCellMargins(doc: Document): Promise<number> {
|
|
840
|
+
const MARGIN_LEFT_RIGHT = 115; // 0.08 inches in twips (1440 * 0.08)
|
|
841
|
+
const MARGIN_TOP_BOTTOM = 0; // 0 inches
|
|
842
|
+
|
|
843
|
+
const tables = doc.getTables();
|
|
844
|
+
let cellsFixed = 0;
|
|
845
|
+
|
|
846
|
+
for (const table of tables) {
|
|
847
|
+
try {
|
|
848
|
+
// Skip floating tables and tables containing nested tables
|
|
849
|
+
if (this.shouldSkipTable(table)) continue;
|
|
850
|
+
|
|
851
|
+
for (const row of table.getRows()) {
|
|
852
|
+
for (const cell of row.getCells()) {
|
|
853
|
+
// Set standard margins: 0" top/bottom, 0.08" left/right
|
|
854
|
+
cell.setMargins({
|
|
855
|
+
top: MARGIN_TOP_BOTTOM,
|
|
856
|
+
bottom: MARGIN_TOP_BOTTOM,
|
|
857
|
+
left: MARGIN_LEFT_RIGHT,
|
|
858
|
+
right: MARGIN_LEFT_RIGHT,
|
|
859
|
+
});
|
|
860
|
+
// Enable text wrapping (noWrap=false)
|
|
861
|
+
cell.setNoWrap(false);
|
|
862
|
+
cellsFixed++;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
} catch (error) {
|
|
866
|
+
log.warn(`Failed to standardize cell margins: ${error}`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (cellsFixed > 0) {
|
|
871
|
+
log.info(`Standardized margins for ${cellsFixed} table cells`);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return cellsFixed;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Apply custom cell padding to all tables based on table type (1x1 vs other).
|
|
879
|
+
* Uses padding values from TableShadingSettings.
|
|
880
|
+
*
|
|
881
|
+
* @param doc - The document to process
|
|
882
|
+
* @param paddingSettings - Padding settings from session configuration
|
|
883
|
+
* @returns Number of cells with padding applied
|
|
884
|
+
*/
|
|
885
|
+
async applyTablePadding(
|
|
886
|
+
doc: Document,
|
|
887
|
+
paddingSettings?: TableShadingSettings
|
|
888
|
+
): Promise<number> {
|
|
889
|
+
// Default padding values in inches
|
|
890
|
+
const padding1x1Top = paddingSettings?.padding1x1Top ?? 0;
|
|
891
|
+
const padding1x1Bottom = paddingSettings?.padding1x1Bottom ?? 0;
|
|
892
|
+
const padding1x1Left = paddingSettings?.padding1x1Left ?? 0.08;
|
|
893
|
+
const padding1x1Right = paddingSettings?.padding1x1Right ?? 0.08;
|
|
894
|
+
const paddingOtherTop = paddingSettings?.paddingOtherTop ?? 0;
|
|
895
|
+
const paddingOtherBottom = paddingSettings?.paddingOtherBottom ?? 0;
|
|
896
|
+
const paddingOtherLeft = paddingSettings?.paddingOtherLeft ?? 0.08;
|
|
897
|
+
const paddingOtherRight = paddingSettings?.paddingOtherRight ?? 0.08;
|
|
898
|
+
|
|
899
|
+
const tables = doc.getTables();
|
|
900
|
+
let cellsProcessed = 0;
|
|
901
|
+
let emptyTablesSkipped = 0;
|
|
902
|
+
|
|
903
|
+
log.info(`Applying custom table padding to ${tables.length} tables`);
|
|
904
|
+
log.debug(`1x1 Tables: top=${padding1x1Top}", bottom=${padding1x1Bottom}", left=${padding1x1Left}", right=${padding1x1Right}"`);
|
|
905
|
+
log.debug(`Other Tables: top=${paddingOtherTop}", bottom=${paddingOtherBottom}", left=${paddingOtherLeft}", right=${paddingOtherRight}"`);
|
|
906
|
+
|
|
907
|
+
for (const table of tables) {
|
|
908
|
+
try {
|
|
909
|
+
// Skip floating tables and tables containing nested tables
|
|
910
|
+
if (this.shouldSkipTable(table)) continue;
|
|
911
|
+
|
|
912
|
+
// Skip HLP tables — handled by processHLPTables()
|
|
913
|
+
if (this.isHLPTable(table)) continue;
|
|
914
|
+
|
|
915
|
+
const rows = table.getRows();
|
|
916
|
+
if (rows.length === 0) {
|
|
917
|
+
emptyTablesSkipped++;
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Detect if this is a 1x1 table
|
|
922
|
+
const is1x1Table = rows.length === 1 && rows[0].getCells().length === 1;
|
|
923
|
+
|
|
924
|
+
// Select padding values based on table type
|
|
925
|
+
const top = is1x1Table ? padding1x1Top : paddingOtherTop;
|
|
926
|
+
const bottom = is1x1Table ? padding1x1Bottom : paddingOtherBottom;
|
|
927
|
+
const left = is1x1Table ? padding1x1Left : paddingOtherLeft;
|
|
928
|
+
const right = is1x1Table ? padding1x1Right : paddingOtherRight;
|
|
929
|
+
|
|
930
|
+
// Convert inches to twips
|
|
931
|
+
const topTwips = inchesToTwips(top);
|
|
932
|
+
const bottomTwips = inchesToTwips(bottom);
|
|
933
|
+
const leftTwips = inchesToTwips(left);
|
|
934
|
+
const rightTwips = inchesToTwips(right);
|
|
935
|
+
|
|
936
|
+
for (const row of rows) {
|
|
937
|
+
for (const cell of row.getCells()) {
|
|
938
|
+
cell.setMargins({
|
|
939
|
+
top: topTwips,
|
|
940
|
+
bottom: bottomTwips,
|
|
941
|
+
left: leftTwips,
|
|
942
|
+
right: rightTwips,
|
|
943
|
+
});
|
|
944
|
+
// Enable text wrapping (noWrap=false)
|
|
945
|
+
cell.setNoWrap(false);
|
|
946
|
+
cellsProcessed++;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
} catch (error) {
|
|
950
|
+
log.warn(`Failed to apply padding to table: ${error}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (cellsProcessed > 0) {
|
|
955
|
+
log.info(`Applied custom padding to ${cellsProcessed} table cells`);
|
|
956
|
+
}
|
|
957
|
+
if (emptyTablesSkipped > 0) {
|
|
958
|
+
log.debug(`Skipped ${emptyTablesSkipped} empty tables during padding application`);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return cellsProcessed;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Detect tables with "Step" column and set column width to 1 inch.
|
|
966
|
+
*
|
|
967
|
+
* A "Step" column is detected when:
|
|
968
|
+
* - First column header text equals "Step" (case-insensitive, trimmed)
|
|
969
|
+
* - Cells below contain numeric content (step numbers)
|
|
970
|
+
*
|
|
971
|
+
* @param doc - The document to process
|
|
972
|
+
* @returns Number of tables with Step columns adjusted
|
|
973
|
+
*/
|
|
974
|
+
async applyStepColumnWidth(doc: Document): Promise<number> {
|
|
975
|
+
const STEP_COLUMN_WIDTH = 1440; // 1 inch in twips
|
|
976
|
+
const tables = doc.getTables();
|
|
977
|
+
let adjustedCount = 0;
|
|
978
|
+
|
|
979
|
+
for (const table of tables) {
|
|
980
|
+
try {
|
|
981
|
+
// Skip floating tables and tables containing nested tables
|
|
982
|
+
if (this.shouldSkipTable(table)) continue;
|
|
983
|
+
|
|
984
|
+
const rows = table.getRows();
|
|
985
|
+
if (rows.length < 2) continue; // Need header + at least one data row
|
|
986
|
+
|
|
987
|
+
// Get first cell of first row (header)
|
|
988
|
+
const headerCell = table.getCell(0, 0);
|
|
989
|
+
if (!headerCell) continue;
|
|
990
|
+
|
|
991
|
+
const headerText = headerCell.getText().trim().toLowerCase();
|
|
992
|
+
|
|
993
|
+
// Check if header is "Step"
|
|
994
|
+
if (headerText !== "step") continue;
|
|
995
|
+
|
|
996
|
+
// Check if header cell is shaded
|
|
997
|
+
const { hasShading } = this.getResolvedCellShading(headerCell, table, doc);
|
|
998
|
+
if (!hasShading) continue;
|
|
999
|
+
|
|
1000
|
+
// Verify cells below contain numbers
|
|
1001
|
+
let hasNumericContent = false;
|
|
1002
|
+
for (let rowIndex = 1; rowIndex < rows.length; rowIndex++) {
|
|
1003
|
+
const cell = table.getCell(rowIndex, 0);
|
|
1004
|
+
if (!cell) continue;
|
|
1005
|
+
|
|
1006
|
+
const cellText = cell.getText().trim();
|
|
1007
|
+
// Check if cell contains a number (possibly with period, e.g., "1." or "1")
|
|
1008
|
+
if (/^\d+\.?$/.test(cellText)) {
|
|
1009
|
+
hasNumericContent = true;
|
|
1010
|
+
break;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (!hasNumericContent) continue;
|
|
1015
|
+
|
|
1016
|
+
// Set first column width to 1 inch (grid-level)
|
|
1017
|
+
table.setColumnWidth(0, STEP_COLUMN_WIDTH);
|
|
1018
|
+
|
|
1019
|
+
// Also update individual cell widths (tcW) for column 0 in all rows.
|
|
1020
|
+
// Word uses tcW to determine the actual rendered column width,
|
|
1021
|
+
// overriding tblGrid/gridCol when present. Without this, cells retain
|
|
1022
|
+
// their original wider tcW values from the source document.
|
|
1023
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
1024
|
+
const cell = table.getCell(rowIndex, 0);
|
|
1025
|
+
if (cell && cell.getColumnSpan() <= 1) {
|
|
1026
|
+
cell.setWidthType(STEP_COLUMN_WIDTH, "dxa");
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
adjustedCount++;
|
|
1031
|
+
|
|
1032
|
+
log.debug(`Set Step column width to 1 inch for table (grid + cell widths)`);
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
log.warn(`Failed to apply Step column width: ${error}`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (adjustedCount > 0) {
|
|
1039
|
+
log.info(`Adjusted ${adjustedCount} Step column widths to 1 inch`);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
return adjustedCount;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Ensure all paragraphs in 1x1 tables have Heading 2 style.
|
|
1047
|
+
* This should be called after style application to enforce 1x1 table content as headers.
|
|
1048
|
+
*
|
|
1049
|
+
* @param doc - The document to process
|
|
1050
|
+
* @returns Number of paragraphs updated to Heading 2 style
|
|
1051
|
+
*/
|
|
1052
|
+
async ensureHeading2StyleIn1x1Tables(doc: Document): Promise<number> {
|
|
1053
|
+
const tables = doc.getTables();
|
|
1054
|
+
let paragraphsUpdated = 0;
|
|
1055
|
+
|
|
1056
|
+
for (const table of tables) {
|
|
1057
|
+
try {
|
|
1058
|
+
// Skip floating tables and tables containing nested tables
|
|
1059
|
+
if (this.shouldSkipTable(table)) continue;
|
|
1060
|
+
|
|
1061
|
+
const rows = table.getRows();
|
|
1062
|
+
|
|
1063
|
+
// Only process 1x1 tables
|
|
1064
|
+
if (rows.length !== 1 || rows[0].getCells().length !== 1) {
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const cell = rows[0].getCells()[0];
|
|
1069
|
+
|
|
1070
|
+
// Skip excluded 1x1 tables (>2 lines of text)
|
|
1071
|
+
if (this.should1x1TableBeExcluded(cell)) {
|
|
1072
|
+
log.debug(`Skipping Heading 2 style for 1x1 table (>2 lines)`);
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const paragraphs = cell.getParagraphs();
|
|
1077
|
+
|
|
1078
|
+
for (const para of paragraphs) {
|
|
1079
|
+
const currentStyle = para.getStyle();
|
|
1080
|
+
|
|
1081
|
+
// Set to Heading 2 if not already
|
|
1082
|
+
if (currentStyle !== "Heading2" && currentStyle !== "Heading 2") {
|
|
1083
|
+
para.setStyle("Heading2");
|
|
1084
|
+
paragraphsUpdated++;
|
|
1085
|
+
log.debug(`Set 1x1 table paragraph to Heading 2 style`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
} catch (error) {
|
|
1089
|
+
log.warn(`Failed to set Heading 2 style in 1x1 table: ${error}`);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (paragraphsUpdated > 0) {
|
|
1094
|
+
log.info(`Set ${paragraphsUpdated} paragraphs in 1x1 tables to Heading 2 style`);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return paragraphsUpdated;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// ═══════════════════════════════════════════════════════════
|
|
1101
|
+
// HLP (High Level Process) Table Detection and Formatting
|
|
1102
|
+
// ═══════════════════════════════════════════════════════════
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Analyze a table to determine if it is an HLP (High Level Process) table
|
|
1106
|
+
* and classify its variant (single-column or two-column with tips).
|
|
1107
|
+
*
|
|
1108
|
+
* Detection signals:
|
|
1109
|
+
* - Gate check: First row, first cell must have FFC000 (orange) shading
|
|
1110
|
+
* - Minimum 2 rows (header + at least 1 data row)
|
|
1111
|
+
* - Variant determined by column count in data rows
|
|
1112
|
+
* - Tips column detected by FFF2CC shading in last cell of data rows
|
|
1113
|
+
*
|
|
1114
|
+
* @param table - Table to analyze
|
|
1115
|
+
* @returns Full analysis including variant, column count, tips detection
|
|
1116
|
+
*/
|
|
1117
|
+
/**
|
|
1118
|
+
* Cache-aware HLP table analysis.
|
|
1119
|
+
* If the cache is populated (pre-applyStyles), returns cached results.
|
|
1120
|
+
* Otherwise falls back to live analysis (for tests or pre-cache calls).
|
|
1121
|
+
*/
|
|
1122
|
+
analyzeHLPTable(table: Table): HLPTableAnalysis {
|
|
1123
|
+
if (this._hlpTableCache) {
|
|
1124
|
+
const cached = this._hlpTableCache.get(table);
|
|
1125
|
+
if (cached) return cached;
|
|
1126
|
+
// Table not in cache = was analyzed and didn't match
|
|
1127
|
+
return {
|
|
1128
|
+
isHLP: false, variant: null, columnCount: 0, rowCount: 0,
|
|
1129
|
+
hasTipsColumn: false, headerText: '', headerCellSpan: 1,
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
return this._analyzeHLPTableLive(table);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Populate HLP table cache before applyStyles() overwrites FFC000 shading.
|
|
1137
|
+
* Must be called while original document shading is still intact.
|
|
1138
|
+
*/
|
|
1139
|
+
cacheHLPTables(tables: Table[]): void {
|
|
1140
|
+
this._hlpTableCache = new Map();
|
|
1141
|
+
for (const table of tables) {
|
|
1142
|
+
// Don't apply shouldSkipTable() here — HLP tables may have nested content
|
|
1143
|
+
// (e.g., SDTs or structured elements) that triggers the nested table check,
|
|
1144
|
+
// or may be floating. HLP detection uses dual-check (FFC000 shading + "High
|
|
1145
|
+
// Level Process" header text), which is safe to run on any table.
|
|
1146
|
+
const analysis = this._analyzeHLPTableLive(table);
|
|
1147
|
+
if (analysis.isHLP) {
|
|
1148
|
+
this._hlpTableCache.set(table, analysis);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
log.debug(`Cached ${this._hlpTableCache.size} HLP tables for detection`);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Clear the HLP table cache after processing completes.
|
|
1156
|
+
*/
|
|
1157
|
+
clearHLPTableCache(): void {
|
|
1158
|
+
this._hlpTableCache = null;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Live HLP table analysis — reads shading directly from the document XML.
|
|
1163
|
+
* Used by cacheHLPTables() and as fallback when no cache is populated.
|
|
1164
|
+
*/
|
|
1165
|
+
private _analyzeHLPTableLive(table: Table): HLPTableAnalysis {
|
|
1166
|
+
const defaultResult: HLPTableAnalysis = {
|
|
1167
|
+
isHLP: false, variant: null, columnCount: 0, rowCount: 0,
|
|
1168
|
+
hasTipsColumn: false, headerText: '', headerCellSpan: 1,
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
const rows = table.getRows();
|
|
1172
|
+
if (rows.length < 2) return defaultResult; // Need header + at least 1 data row
|
|
1173
|
+
|
|
1174
|
+
const firstRow = rows[0];
|
|
1175
|
+
const firstRowCells = firstRow.getCells();
|
|
1176
|
+
if (firstRowCells.length === 0) return defaultResult;
|
|
1177
|
+
|
|
1178
|
+
const firstCell = firstRowCells[0];
|
|
1179
|
+
const formatting = firstCell.getFormatting();
|
|
1180
|
+
const fill = formatting.shading?.fill?.toUpperCase();
|
|
1181
|
+
|
|
1182
|
+
// Gate check: Must have FFC000 header shading
|
|
1183
|
+
if (fill !== HLP_HEADER_COLOR) return defaultResult;
|
|
1184
|
+
|
|
1185
|
+
const headerText = (firstCell.getText() ?? '').trim();
|
|
1186
|
+
|
|
1187
|
+
// Gate check: Header text must contain "High Level Process" (case-insensitive)
|
|
1188
|
+
if (!headerText.toLowerCase().includes(HLP_HEADER_TEXT)) return defaultResult;
|
|
1189
|
+
const headerCellSpan = firstCell.getColumnSpan() || 1;
|
|
1190
|
+
|
|
1191
|
+
// Determine column count from data rows (more reliable than header row)
|
|
1192
|
+
const dataRow = rows[1];
|
|
1193
|
+
const dataRowCells = dataRow.getCells();
|
|
1194
|
+
const columnCount = dataRowCells.length;
|
|
1195
|
+
|
|
1196
|
+
// Detect tips column: check if any data row has FFF2CC in the last cell
|
|
1197
|
+
let hasTipsColumn = false;
|
|
1198
|
+
if (columnCount >= 2) {
|
|
1199
|
+
for (let i = 1; i < rows.length; i++) {
|
|
1200
|
+
const cells = rows[i].getCells();
|
|
1201
|
+
const lastCell = cells[cells.length - 1];
|
|
1202
|
+
if (lastCell) {
|
|
1203
|
+
const cellFill = lastCell.getFormatting().shading?.fill?.toUpperCase();
|
|
1204
|
+
if (cellFill === HLP_TIPS_COLOR) {
|
|
1205
|
+
hasTipsColumn = true;
|
|
1206
|
+
break;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const variant: HLPVariant = columnCount <= 1 ? 'single-column' : 'two-column';
|
|
1213
|
+
|
|
1214
|
+
return {
|
|
1215
|
+
isHLP: true,
|
|
1216
|
+
variant,
|
|
1217
|
+
columnCount,
|
|
1218
|
+
rowCount: rows.length,
|
|
1219
|
+
hasTipsColumn,
|
|
1220
|
+
headerText,
|
|
1221
|
+
headerCellSpan,
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Detect if a table is an HLP (High Level Process) table.
|
|
1227
|
+
* Delegates to analyzeHLPTable() for backward compatibility.
|
|
1228
|
+
*
|
|
1229
|
+
* @param table - Table to check
|
|
1230
|
+
* @returns true if table is an HLP table
|
|
1231
|
+
*/
|
|
1232
|
+
isHLPTable(table: Table): boolean {
|
|
1233
|
+
return this.analyzeHLPTable(table).isHLP;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Apply HLP-specific table-level borders, variant-aware.
|
|
1238
|
+
*
|
|
1239
|
+
* Single-column: Table-level borders on all 4 sides, no inside borders.
|
|
1240
|
+
* Two-column: Clear table-level borders (cell-level borders handle it).
|
|
1241
|
+
*/
|
|
1242
|
+
private applyHLPTableBorders(table: Table, analysis: HLPTableAnalysis): void {
|
|
1243
|
+
if (analysis.variant === 'single-column') {
|
|
1244
|
+
// Option_2 pattern: table-level borders on all 4 sides
|
|
1245
|
+
table.setBorders({
|
|
1246
|
+
top: { style: 'single', size: HLP_BORDER_SIZE, color: HLP_HEADER_COLOR },
|
|
1247
|
+
bottom: { style: 'single', size: HLP_BORDER_SIZE, color: HLP_HEADER_COLOR },
|
|
1248
|
+
left: { style: 'single', size: HLP_BORDER_SIZE, color: HLP_HEADER_COLOR },
|
|
1249
|
+
right: { style: 'single', size: HLP_BORDER_SIZE, color: HLP_HEADER_COLOR },
|
|
1250
|
+
insideH: { style: 'none', size: 0, color: 'auto' },
|
|
1251
|
+
insideV: { style: 'none', size: 0, color: 'auto' },
|
|
1252
|
+
});
|
|
1253
|
+
} else {
|
|
1254
|
+
// Option_1 pattern: no table-level borders; cell-level borders handle everything
|
|
1255
|
+
table.setBorders({
|
|
1256
|
+
top: { style: 'none', size: 0, color: 'auto' },
|
|
1257
|
+
bottom: { style: 'none', size: 0, color: 'auto' },
|
|
1258
|
+
left: { style: 'none', size: 0, color: 'auto' },
|
|
1259
|
+
right: { style: 'none', size: 0, color: 'auto' },
|
|
1260
|
+
insideH: { style: 'none', size: 0, color: 'auto' },
|
|
1261
|
+
insideV: { style: 'none', size: 0, color: 'auto' },
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Apply Heading 2 style to all paragraphs in the HLP header row.
|
|
1268
|
+
*
|
|
1269
|
+
* @param table - HLP table to format
|
|
1270
|
+
* @returns Number of paragraphs styled
|
|
1271
|
+
*/
|
|
1272
|
+
private applyHLPHeaderStyle(table: Table): number {
|
|
1273
|
+
const rows = table.getRows();
|
|
1274
|
+
if (rows.length === 0) return 0;
|
|
1275
|
+
|
|
1276
|
+
let paragraphsStyled = 0;
|
|
1277
|
+
const headerRow = rows[0];
|
|
1278
|
+
|
|
1279
|
+
for (const cell of headerRow.getCells()) {
|
|
1280
|
+
for (const para of cell.getParagraphs()) {
|
|
1281
|
+
para.setStyle('Heading2');
|
|
1282
|
+
paragraphsStyled++;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
return paragraphsStyled;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
/**
|
|
1290
|
+
* Apply explicit run formatting to HLP header row paragraphs.
|
|
1291
|
+
* Sets h2Font/h2Size/bold/center on all runs so the header looks correct
|
|
1292
|
+
* regardless of Heading2 style definition.
|
|
1293
|
+
*/
|
|
1294
|
+
private applyHLPHeaderRunFormatting(
|
|
1295
|
+
table: Table,
|
|
1296
|
+
settings?: TableShadingSettings,
|
|
1297
|
+
): void {
|
|
1298
|
+
const h2Font = settings?.heading2FontFamily ?? 'Verdana';
|
|
1299
|
+
const h2Size = settings?.heading2FontSize ?? 14;
|
|
1300
|
+
const rows = table.getRows();
|
|
1301
|
+
if (rows.length === 0) return;
|
|
1302
|
+
|
|
1303
|
+
const headerRow = rows[0];
|
|
1304
|
+
for (const cell of headerRow.getCells()) {
|
|
1305
|
+
for (const para of cell.getParagraphs()) {
|
|
1306
|
+
para.setAlignment('left');
|
|
1307
|
+
for (const run of para.getRuns()) {
|
|
1308
|
+
run.setFont(h2Font);
|
|
1309
|
+
run.setSize(h2Size);
|
|
1310
|
+
run.setBold(true);
|
|
1311
|
+
}
|
|
1312
|
+
this.applyNormalSpacing(para, settings);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* Apply formatting to the HLP header row cells (shading, margins, cell borders).
|
|
1319
|
+
*/
|
|
1320
|
+
private applyHLPHeaderCellFormatting(table: Table, analysis: HLPTableAnalysis): void {
|
|
1321
|
+
const rows = table.getRows();
|
|
1322
|
+
if (rows.length === 0) return;
|
|
1323
|
+
|
|
1324
|
+
const headerRow = rows[0];
|
|
1325
|
+
for (const cell of headerRow.getCells()) {
|
|
1326
|
+
// Ensure FFC000 shading
|
|
1327
|
+
cell.setShading({ fill: HLP_HEADER_COLOR, pattern: "clear", color: "auto" });
|
|
1328
|
+
|
|
1329
|
+
// Set cell margins: top=0, bottom=0
|
|
1330
|
+
cell.setMargins({ top: 0, bottom: 0, left: 115, right: 115 });
|
|
1331
|
+
|
|
1332
|
+
// For two-column variant, apply cell-level borders on header
|
|
1333
|
+
if (analysis.variant === 'two-column') {
|
|
1334
|
+
cell.setBorders({
|
|
1335
|
+
top: { style: 'single', size: HLP_BORDER_SIZE, color: HLP_HEADER_COLOR },
|
|
1336
|
+
left: { style: 'single', size: HLP_BORDER_SIZE, color: HLP_HEADER_COLOR },
|
|
1337
|
+
right: { style: 'single', size: HLP_BORDER_SIZE, color: HLP_HEADER_COLOR },
|
|
1338
|
+
bottom: { style: 'none', size: 0, color: 'auto' },
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/**
|
|
1345
|
+
* Apply HLP-specific cell-level borders for data rows, variant-aware.
|
|
1346
|
+
*
|
|
1347
|
+
* Single-column: Clear cell-level borders (table borders handle it).
|
|
1348
|
+
* Two-column: Left column gets left border, right gets right, last row gets bottom.
|
|
1349
|
+
*/
|
|
1350
|
+
private applyHLPCellBorders(table: Table, analysis: HLPTableAnalysis): void {
|
|
1351
|
+
const rows = table.getRows();
|
|
1352
|
+
const noBorder = { style: 'none' as const, size: 0, color: 'auto' };
|
|
1353
|
+
|
|
1354
|
+
if (analysis.variant === 'single-column') {
|
|
1355
|
+
// Clear any cell-level borders on data rows
|
|
1356
|
+
for (let rowIndex = 1; rowIndex < rows.length; rowIndex++) {
|
|
1357
|
+
for (const cell of rows[rowIndex].getCells()) {
|
|
1358
|
+
cell.setBorders({ top: noBorder, bottom: noBorder, left: noBorder, right: noBorder });
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Two-column variant: cell-level borders for seamless appearance
|
|
1365
|
+
for (let rowIndex = 1; rowIndex < rows.length; rowIndex++) {
|
|
1366
|
+
const row = rows[rowIndex];
|
|
1367
|
+
const cells = row.getCells();
|
|
1368
|
+
const isLastRow = rowIndex === rows.length - 1;
|
|
1369
|
+
const orangeBorder = { style: 'single' as const, size: HLP_BORDER_SIZE, color: HLP_HEADER_COLOR };
|
|
1370
|
+
|
|
1371
|
+
for (let cellIndex = 0; cellIndex < cells.length; cellIndex++) {
|
|
1372
|
+
const cell = cells[cellIndex];
|
|
1373
|
+
const isLeftColumn = cellIndex === 0;
|
|
1374
|
+
const isRightColumn = cellIndex === cells.length - 1;
|
|
1375
|
+
|
|
1376
|
+
cell.setBorders({
|
|
1377
|
+
top: noBorder,
|
|
1378
|
+
left: isLeftColumn ? orangeBorder : noBorder,
|
|
1379
|
+
right: isRightColumn ? orangeBorder : noBorder,
|
|
1380
|
+
bottom: isLastRow ? orangeBorder : noBorder,
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* Ensure tips column cells have FFF2CC shading in two-column variant.
|
|
1388
|
+
*/
|
|
1389
|
+
private applyHLPTipsColumnShading(table: Table, analysis: HLPTableAnalysis): void {
|
|
1390
|
+
if (!analysis.hasTipsColumn || analysis.variant !== 'two-column') return;
|
|
1391
|
+
|
|
1392
|
+
const rows = table.getRows();
|
|
1393
|
+
for (let rowIndex = 1; rowIndex < rows.length; rowIndex++) {
|
|
1394
|
+
const cells = rows[rowIndex].getCells();
|
|
1395
|
+
// Skip single-cell rows (full-width merged) — they aren't tips column rows
|
|
1396
|
+
if (cells.length < 2) continue;
|
|
1397
|
+
const lastCell = cells[cells.length - 1];
|
|
1398
|
+
// Also skip if the last cell spans multiple columns (another merge pattern)
|
|
1399
|
+
if (lastCell && lastCell.getColumnSpan() <= 1) {
|
|
1400
|
+
lastCell.setShading({ fill: HLP_TIPS_COLOR, pattern: "clear", color: "auto" });
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/**
|
|
1406
|
+
* Discover the main numbered-list numId used by HLP content cells.
|
|
1407
|
+
* This is the numId shared by all level-0 action items (1., 2., 3., etc.).
|
|
1408
|
+
* Sub-items (a., b., c.) use their own separate abstractNums with different numIds.
|
|
1409
|
+
*
|
|
1410
|
+
* Only returns a numId if the paragraph is at level 0 AND the abstractNum's
|
|
1411
|
+
* level 0 has decimal format. This prevents returning a sub-item numId
|
|
1412
|
+
* (e.g., lowerLetter at level 0) which would corrupt main item formatting.
|
|
1413
|
+
*
|
|
1414
|
+
* @returns The numId of the first decimal level-0 paragraph found, or null
|
|
1415
|
+
*/
|
|
1416
|
+
private discoverHLPMainNumId(table: Table, _analysis: HLPTableAnalysis, doc?: Document): number | null {
|
|
1417
|
+
const manager = doc?.getNumberingManager();
|
|
1418
|
+
const rows = table.getRows();
|
|
1419
|
+
|
|
1420
|
+
// Pass 1: Find a level-0 paragraph whose abstractNum level 0 is decimal (1., 2., 3.)
|
|
1421
|
+
for (let ri = 1; ri < rows.length; ri++) {
|
|
1422
|
+
const cells = rows[ri].getCells();
|
|
1423
|
+
const contentCell = cells[0];
|
|
1424
|
+
if (!contentCell) continue;
|
|
1425
|
+
for (const p of contentCell.getParagraphs()) {
|
|
1426
|
+
const num = p.getNumbering();
|
|
1427
|
+
if (!num || !num.numId) continue;
|
|
1428
|
+
|
|
1429
|
+
if (manager) {
|
|
1430
|
+
const level = num.level ?? 0;
|
|
1431
|
+
if (level !== 0) {
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
const instance = manager.getInstance(num.numId);
|
|
1435
|
+
if (instance) {
|
|
1436
|
+
const absId = instance.getAbstractNumId();
|
|
1437
|
+
const abstractNum = manager.getAbstractNumbering(absId);
|
|
1438
|
+
const level0 = abstractNum?.getLevel(0);
|
|
1439
|
+
const format = level0?.getFormat();
|
|
1440
|
+
if (format === 'decimal') {
|
|
1441
|
+
return num.numId;
|
|
1442
|
+
}
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// Fallback if no manager available: return first numId found (legacy behavior)
|
|
1448
|
+
return num.numId;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// Pass 2: No level-0 decimal found. This happens when ALL content paragraphs
|
|
1453
|
+
// are sub-items (e.g. ilvl=1, lowerLetter a./b./c.) and no main item exists.
|
|
1454
|
+
// Find the most common numId among all numbered content paragraphs so we don't
|
|
1455
|
+
// trigger the unnecessary fallback list creation path.
|
|
1456
|
+
if (manager) {
|
|
1457
|
+
const numIdCounts = new Map<number, number>();
|
|
1458
|
+
for (let ri = 1; ri < rows.length; ri++) {
|
|
1459
|
+
const contentCell = rows[ri].getCells()[0];
|
|
1460
|
+
if (!contentCell) continue;
|
|
1461
|
+
for (const p of contentCell.getParagraphs()) {
|
|
1462
|
+
const num = p.getNumbering();
|
|
1463
|
+
if (!num || !num.numId) continue;
|
|
1464
|
+
numIdCounts.set(num.numId, (numIdCounts.get(num.numId) ?? 0) + 1);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (numIdCounts.size > 0) {
|
|
1468
|
+
// Return the most common numId
|
|
1469
|
+
let bestNumId = 0;
|
|
1470
|
+
let bestCount = 0;
|
|
1471
|
+
for (const [numId, count] of numIdCounts) {
|
|
1472
|
+
if (count > bestCount) {
|
|
1473
|
+
bestNumId = numId;
|
|
1474
|
+
bestCount = count;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
log.debug(`discoverHLPMainNumId: no level-0 decimal found, using most common numId=${bestNumId} (${bestCount} occurrences)`);
|
|
1478
|
+
return bestNumId;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* Apply text formatting to HLP table content: font/size from session settings,
|
|
1487
|
+
* bold on list items, blue (#0000FF) + underline on hyperlinks.
|
|
1488
|
+
*
|
|
1489
|
+
* Content column (left / only column):
|
|
1490
|
+
* - Level-0 items (all decimal numbered): heading2 font/size, bold, explicit numbering
|
|
1491
|
+
* - Sub-items (level 1+): normal font/size, level-based indent
|
|
1492
|
+
* - Hyperlinks within level-0: also bold
|
|
1493
|
+
*
|
|
1494
|
+
* Tips column (right, FFF2CC):
|
|
1495
|
+
* - P[0] (label): normal font/size, bold
|
|
1496
|
+
* - P[1+] (body): normal font/size
|
|
1497
|
+
* - Alignment forced to left
|
|
1498
|
+
*/
|
|
1499
|
+
private applyHLPContentFormatting(
|
|
1500
|
+
table: Table,
|
|
1501
|
+
analysis: HLPTableAnalysis,
|
|
1502
|
+
settings?: TableShadingSettings,
|
|
1503
|
+
discoveredNumId?: number | null,
|
|
1504
|
+
tipsBulletNumId?: number | null,
|
|
1505
|
+
savedNumbering?: Map<Paragraph, { numId: number; level: number; leftIndent?: number }>,
|
|
1506
|
+
hasExplicitNumbering?: boolean,
|
|
1507
|
+
): void {
|
|
1508
|
+
const normalFont = settings?.normalFontFamily ?? 'Verdana';
|
|
1509
|
+
const normalSize = settings?.normalFontSize ?? 12;
|
|
1510
|
+
|
|
1511
|
+
const rows = table.getRows();
|
|
1512
|
+
|
|
1513
|
+
// Use pre-discovered numId if provided, otherwise fall back to internal discovery.
|
|
1514
|
+
// Note: this fallback path doesn't have `doc` so it uses legacy behavior (first numId found).
|
|
1515
|
+
// The primary call path from processHLPTables() always passes the pre-discovered numId.
|
|
1516
|
+
if (discoveredNumId === undefined) {
|
|
1517
|
+
discoveredNumId = this.discoverHLPMainNumId(table, analysis);
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
for (let rowIndex = 1; rowIndex < rows.length; rowIndex++) {
|
|
1521
|
+
const cells = rows[rowIndex].getCells();
|
|
1522
|
+
|
|
1523
|
+
for (let cellIndex = 0; cellIndex < cells.length; cellIndex++) {
|
|
1524
|
+
const cell = cells[cellIndex];
|
|
1525
|
+
const isTipsCell = analysis.hasTipsColumn && cells.length >= 2 && cellIndex === cells.length - 1 && analysis.variant === 'two-column';
|
|
1526
|
+
const paras = cell.getParagraphs();
|
|
1527
|
+
|
|
1528
|
+
for (let pIdx = 0; pIdx < paras.length; pIdx++) {
|
|
1529
|
+
const para = paras[pIdx];
|
|
1530
|
+
|
|
1531
|
+
const numberingBefore = para.getNumbering();
|
|
1532
|
+
const paraStyle = para.getStyle();
|
|
1533
|
+
|
|
1534
|
+
// Save indentation before any modifications — docxmlater setters
|
|
1535
|
+
// can drop <w:ind> when regenerating <w:pPr>.
|
|
1536
|
+
const savedLeftIndent = para.getLeftIndent();
|
|
1537
|
+
|
|
1538
|
+
// Fix ListParagraph paragraphs without numbering: docxmlater's
|
|
1539
|
+
// validateNumberingReferences() strips numId=0 on save, causing
|
|
1540
|
+
// the ListParagraph style's default numbering (numId=33) to show.
|
|
1541
|
+
// Switch these paragraphs to Normal style and preserve indentation.
|
|
1542
|
+
if (paraStyle === 'ListParagraph' && !numberingBefore) {
|
|
1543
|
+
para.setStyle('Normal');
|
|
1544
|
+
if (savedLeftIndent) {
|
|
1545
|
+
para.setLeftIndent(savedLeftIndent);
|
|
1546
|
+
}
|
|
1547
|
+
// Check if this paragraph was originally unnumbered (numId=0).
|
|
1548
|
+
// These paragraphs (e.g. "Note:" lines) never had a visible
|
|
1549
|
+
// prefix and shouldn't get one.
|
|
1550
|
+
const savedEntry = savedNumbering?.get(para);
|
|
1551
|
+
// If savedEntry is undefined (possible identity mismatch), fall back to
|
|
1552
|
+
// text-based detection for Note-like lines that shouldn't be numbered.
|
|
1553
|
+
const isNoteLine = !savedEntry && /^Note:/i.test(para.getText().trim());
|
|
1554
|
+
const wasUnnumbered = (savedEntry !== undefined && savedEntry.numId === 0) || isNoteLine;
|
|
1555
|
+
|
|
1556
|
+
// Only assign numbering if the paragraph originally had a visible prefix.
|
|
1557
|
+
// Skip blank/empty paragraphs to avoid spurious numbered items.
|
|
1558
|
+
const hasText = para.getText().trim().length > 0;
|
|
1559
|
+
if (!wasUnnumbered) {
|
|
1560
|
+
if (!isTipsCell && discoveredNumId !== null && hasText) {
|
|
1561
|
+
para.setNumbering(discoveredNumId, 0);
|
|
1562
|
+
} else if (isTipsCell && tipsBulletNumId && hasText) {
|
|
1563
|
+
para.setNumbering(tipsBulletNumId, 0);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// After numbering fixup, determine if this is a level-0 action item.
|
|
1569
|
+
// Must also match discoveredNumId to distinguish main items from sub-items,
|
|
1570
|
+
// which also use ilvl=0 but in their own separate abstractNums.
|
|
1571
|
+
const numberingAfter = para.getNumbering();
|
|
1572
|
+
const isMainActionItem = !isTipsCell && !!numberingAfter &&
|
|
1573
|
+
numberingAfter.numId === discoveredNumId &&
|
|
1574
|
+
(numberingAfter.level === 0 || numberingAfter.level === undefined);
|
|
1575
|
+
|
|
1576
|
+
// All runs use normal font/size. Level-0 items only get bold (not larger size).
|
|
1577
|
+
// The 14pt numbered prefix (1., 2.) comes from the abstractNum definition, not from run text.
|
|
1578
|
+
// Tips column uses 10pt (matching original document formatting).
|
|
1579
|
+
const font = normalFont;
|
|
1580
|
+
const size = isTipsCell ? 10 : normalSize;
|
|
1581
|
+
|
|
1582
|
+
// Build set of runs that are direct paragraph children (not inside containers).
|
|
1583
|
+
// Runs returned by para.getRuns() that are NOT direct children must be inside
|
|
1584
|
+
// a container element (Hyperlink, Revision, etc.).
|
|
1585
|
+
const directRuns = new Set<Run>();
|
|
1586
|
+
let hasHyperlinkContainer = false;
|
|
1587
|
+
for (const item of para.getContent()) {
|
|
1588
|
+
if (item instanceof Run) {
|
|
1589
|
+
directRuns.add(item);
|
|
1590
|
+
} else if (
|
|
1591
|
+
item instanceof Hyperlink ||
|
|
1592
|
+
(item instanceof PreservedElement && (item as any).getElementType?.() === 'w:hyperlink')
|
|
1593
|
+
) {
|
|
1594
|
+
hasHyperlinkContainer = true;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// Apply font/size to all runs (para.getRuns() includes hyperlink children)
|
|
1599
|
+
for (const run of para.getRuns()) {
|
|
1600
|
+
const runFmt = run.getFormatting();
|
|
1601
|
+
|
|
1602
|
+
// Track whether this run is a real hyperlink that needs blue+underline restored.
|
|
1603
|
+
// Detection uses three methods in priority order:
|
|
1604
|
+
// 1. characterStyle === 'Hyperlink' (direct style check)
|
|
1605
|
+
// 2. Direct blue color (0000FF or 0563C1) from prior pipeline steps
|
|
1606
|
+
// 3. Structural: run is inside a hyperlink container, not a direct paragraph child
|
|
1607
|
+
let restoreHyperlink = false;
|
|
1608
|
+
if (runFmt.characterStyle === 'Hyperlink') {
|
|
1609
|
+
// Clear the style to avoid font/size conflicts, then restore blue+underline after setters.
|
|
1610
|
+
run.setCharacterStyle(undefined as unknown as string);
|
|
1611
|
+
restoreHyperlink = true;
|
|
1612
|
+
} else {
|
|
1613
|
+
// Detect runs already processed by standardizeHyperlinkFormatting() (direct blue color)
|
|
1614
|
+
const color = runFmt.color?.toUpperCase();
|
|
1615
|
+
if (color === '0000FF' || color === '0563C1') {
|
|
1616
|
+
restoreHyperlink = true;
|
|
1617
|
+
}
|
|
1618
|
+
// Structural detection: run is inside a hyperlink container element,
|
|
1619
|
+
// not a direct child of the paragraph. This catches hyperlinks whose
|
|
1620
|
+
// characterStyle was cleared and color was dropped by earlier pipeline steps.
|
|
1621
|
+
else if (hasHyperlinkContainer && !directRuns.has(run)) {
|
|
1622
|
+
restoreHyperlink = true;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
run.setFont(font);
|
|
1627
|
+
run.setSize(size);
|
|
1628
|
+
|
|
1629
|
+
// Bold: all level-0 action items in content cells (including hyperlinks).
|
|
1630
|
+
// Tips column: only P[0] (label) gets bold.
|
|
1631
|
+
if (isMainActionItem) {
|
|
1632
|
+
run.setBold(true);
|
|
1633
|
+
} else if (isTipsCell && pIdx === 0) {
|
|
1634
|
+
run.setBold(true);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
// Restore blue color and underline for real hyperlinks after setFont/setSize
|
|
1638
|
+
// which can drop existing run properties.
|
|
1639
|
+
if (restoreHyperlink) {
|
|
1640
|
+
run.setColor('0000FF');
|
|
1641
|
+
run.setUnderline('single');
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
this.applyNormalSpacing(para, settings);
|
|
1646
|
+
|
|
1647
|
+
// Tips column: ensure left alignment
|
|
1648
|
+
if (isTipsCell) {
|
|
1649
|
+
para.setAlignment('left');
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Apply list indentation from session settings for ALL numbered content items.
|
|
1653
|
+
// Both main items (level 0) and sub-items (level 1+) need explicit paragraph
|
|
1654
|
+
// indentation since applyNormalSpacing can drop <w:ind> from paragraph properties.
|
|
1655
|
+
const indentLevels = settings?.listIndentationLevels;
|
|
1656
|
+
if (indentLevels) {
|
|
1657
|
+
const numbering = para.getNumbering();
|
|
1658
|
+
if (numbering && !isTipsCell) {
|
|
1659
|
+
const level = numbering.level || 0;
|
|
1660
|
+
const indentSetting = indentLevels.find(l => l.level === level);
|
|
1661
|
+
if (indentSetting && indentSetting.symbolIndent < indentSetting.textIndent) {
|
|
1662
|
+
para.setLeftIndent(inchesToTwips(indentSetting.textIndent));
|
|
1663
|
+
para.setFirstLineIndent(-inchesToTwips(indentSetting.textIndent - indentSetting.symbolIndent));
|
|
1664
|
+
}
|
|
1665
|
+
} else if (isTipsCell && numbering) {
|
|
1666
|
+
// Tips cell numbered paragraphs: applyNormalSpacing drops <w:ind>,
|
|
1667
|
+
// and their numbering level may have unusual indentation (e.g. left=-360).
|
|
1668
|
+
// Apply indent settings so bullet markers remain visible in the cell.
|
|
1669
|
+
const level = numbering.level || 0;
|
|
1670
|
+
const indentSetting = indentLevels.find(l => l.level === level);
|
|
1671
|
+
if (indentSetting && indentSetting.symbolIndent < indentSetting.textIndent) {
|
|
1672
|
+
para.setLeftIndent(inchesToTwips(indentSetting.textIndent));
|
|
1673
|
+
para.setFirstLineIndent(-inchesToTwips(indentSetting.textIndent - indentSetting.symbolIndent));
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// Restore indentation for non-numbered paragraphs if it was dropped by setters
|
|
1679
|
+
if (!para.getNumbering() && savedLeftIndent && !para.getLeftIndent()) {
|
|
1680
|
+
para.setLeftIndent(savedLeftIndent);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/**
|
|
1688
|
+
* Convert bullet sub-items in HLP content to lettered sub-items.
|
|
1689
|
+
* Scans ALL content cell paragraphs for unique numIds with bullet format at
|
|
1690
|
+
* level 0, and converts levels 0–2 to ordered formats:
|
|
1691
|
+
* Level 0: bullet → lowerLetter (a., b., c.)
|
|
1692
|
+
* Level 1: bullet → lowerRoman (i., ii., iii.)
|
|
1693
|
+
* Level 2: bullet → upperLetter (A., B., C.)
|
|
1694
|
+
*
|
|
1695
|
+
* Also sets indentation on each converted level using session listIndentationLevels
|
|
1696
|
+
* with a +1 level offset (sub-item numbering level 0 → UI visual level 1, etc.).
|
|
1697
|
+
*
|
|
1698
|
+
* @returns Number of abstractNum definitions converted
|
|
1699
|
+
*/
|
|
1700
|
+
private convertHLPBulletsToLettered(
|
|
1701
|
+
table: Table,
|
|
1702
|
+
doc: Document,
|
|
1703
|
+
analysis: HLPTableAnalysis,
|
|
1704
|
+
settings?: TableShadingSettings,
|
|
1705
|
+
discoveredNumId?: number | null,
|
|
1706
|
+
): number {
|
|
1707
|
+
const manager = doc.getNumberingManager();
|
|
1708
|
+
if (!manager) return 0;
|
|
1709
|
+
|
|
1710
|
+
// Build set of abstractNums used by the main numbered list — don't convert these.
|
|
1711
|
+
// Only protect abstractNums whose level 0 is decimal (the real main list).
|
|
1712
|
+
// Pass-2 fallback numIds (most common, possibly bullet) should still be convertible.
|
|
1713
|
+
const mainAbsIds = new Set<number>();
|
|
1714
|
+
if (discoveredNumId) {
|
|
1715
|
+
const mainInstance = manager.getInstance(discoveredNumId);
|
|
1716
|
+
if (mainInstance) {
|
|
1717
|
+
const mainAbsId = mainInstance.getAbstractNumId();
|
|
1718
|
+
const mainAbstractNum = manager.getAbstractNumbering(mainAbsId);
|
|
1719
|
+
const mainLevel0Format = mainAbstractNum?.getLevel(0)?.getFormat();
|
|
1720
|
+
if (mainLevel0Format === 'decimal') {
|
|
1721
|
+
mainAbsIds.add(mainAbsId);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
const indentLevels = settings?.listIndentationLevels;
|
|
1727
|
+
const convertedAbsIds = new Set<number>();
|
|
1728
|
+
const rows = table.getRows();
|
|
1729
|
+
|
|
1730
|
+
for (let ri = 1; ri < rows.length; ri++) {
|
|
1731
|
+
const cells = rows[ri].getCells();
|
|
1732
|
+
for (let ci = 0; ci < cells.length; ci++) {
|
|
1733
|
+
// Skip tips column
|
|
1734
|
+
const isTips = analysis.hasTipsColumn && cells.length >= 2 && ci === cells.length - 1 && analysis.variant === 'two-column';
|
|
1735
|
+
if (isTips) continue;
|
|
1736
|
+
|
|
1737
|
+
for (const para of cells[ci].getParagraphs()) {
|
|
1738
|
+
const num = para.getNumbering();
|
|
1739
|
+
if (!num || !num.numId) continue;
|
|
1740
|
+
|
|
1741
|
+
const paraLevel = num.level ?? 0;
|
|
1742
|
+
const instance = manager.getInstance(num.numId);
|
|
1743
|
+
if (!instance) continue;
|
|
1744
|
+
const absId = instance.getAbstractNumId();
|
|
1745
|
+
if (convertedAbsIds.has(absId)) continue;
|
|
1746
|
+
|
|
1747
|
+
// Don't convert the main list's abstractNum
|
|
1748
|
+
if (mainAbsIds.has(absId)) continue;
|
|
1749
|
+
|
|
1750
|
+
// Only convert if paragraph is actually at level 0 of this numId.
|
|
1751
|
+
// Paragraphs at ilvl >= 1 are sub-items of a parent numbered list —
|
|
1752
|
+
// their abstractNum should be left alone.
|
|
1753
|
+
if (paraLevel > 0) continue;
|
|
1754
|
+
|
|
1755
|
+
const abstractNum = manager.getAbstractNumbering(absId);
|
|
1756
|
+
if (!abstractNum) continue;
|
|
1757
|
+
|
|
1758
|
+
const level0 = abstractNum.getLevel(0);
|
|
1759
|
+
const level0Format = level0?.getFormat();
|
|
1760
|
+
if (level0 && level0Format === 'bullet') {
|
|
1761
|
+
// Level 0: bullet → lowerLetter (a., b., c.)
|
|
1762
|
+
level0.setFormat('lowerLetter');
|
|
1763
|
+
level0.setText('%1.');
|
|
1764
|
+
level0.setFont('Verdana');
|
|
1765
|
+
level0.setFontSize(24); // 12pt = 24 half-points
|
|
1766
|
+
level0.setColor('000000');
|
|
1767
|
+
level0.setBold(false); // Sub-item prefixes must not be bold
|
|
1768
|
+
this.patchLevelBoldOff(level0);
|
|
1769
|
+
// Sub-item level 0 → UI visual level 1
|
|
1770
|
+
if (indentLevels) {
|
|
1771
|
+
const indent = indentLevels.find(l => l.level === 1);
|
|
1772
|
+
if (indent) {
|
|
1773
|
+
level0.setLeftIndent(inchesToTwips(indent.textIndent));
|
|
1774
|
+
level0.setHangingIndent(inchesToTwips(indent.textIndent - indent.symbolIndent));
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// Level 1: bullet → lowerRoman (i., ii., iii.)
|
|
1779
|
+
const level1 = abstractNum.getLevel(1);
|
|
1780
|
+
if (level1 && level1.getFormat() === 'bullet') {
|
|
1781
|
+
level1.setFormat('lowerRoman');
|
|
1782
|
+
level1.setText('%2.');
|
|
1783
|
+
level1.setFont('Verdana');
|
|
1784
|
+
level1.setFontSize(24);
|
|
1785
|
+
level1.setColor('000000');
|
|
1786
|
+
level1.setBold(false); // Sub-item prefixes must not be bold
|
|
1787
|
+
this.patchLevelBoldOff(level1);
|
|
1788
|
+
// Sub-item level 1 → UI visual level 2
|
|
1789
|
+
if (indentLevels) {
|
|
1790
|
+
const indent = indentLevels.find(l => l.level === 2);
|
|
1791
|
+
if (indent) {
|
|
1792
|
+
level1.setLeftIndent(inchesToTwips(indent.textIndent));
|
|
1793
|
+
level1.setHangingIndent(inchesToTwips(indent.textIndent - indent.symbolIndent));
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
// Level 2: bullet → upperLetter (A., B., C.)
|
|
1799
|
+
const level2 = abstractNum.getLevel(2);
|
|
1800
|
+
if (level2 && level2.getFormat() === 'bullet') {
|
|
1801
|
+
level2.setFormat('upperLetter');
|
|
1802
|
+
level2.setText('%3.');
|
|
1803
|
+
level2.setFont('Verdana');
|
|
1804
|
+
level2.setFontSize(24);
|
|
1805
|
+
level2.setColor('000000');
|
|
1806
|
+
level2.setBold(false); // Sub-item prefixes must not be bold
|
|
1807
|
+
this.patchLevelBoldOff(level2);
|
|
1808
|
+
// Sub-item level 2 → UI visual level 3
|
|
1809
|
+
if (indentLevels) {
|
|
1810
|
+
const indent = indentLevels.find(l => l.level === 3);
|
|
1811
|
+
if (indent) {
|
|
1812
|
+
level2.setLeftIndent(inchesToTwips(indent.textIndent));
|
|
1813
|
+
level2.setHangingIndent(inchesToTwips(indent.textIndent - indent.symbolIndent));
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
convertedAbsIds.add(absId);
|
|
1819
|
+
log.debug(`Converted HLP abstractNum ${absId} levels 0-2: bullet -> lowerLetter/lowerRoman/upperLetter`);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
return convertedAbsIds.size;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* Monkey-patch a NumberingLevel's toXML() to inject <w:b w:val="0"/>
|
|
1830
|
+
* into the level's <w:rPr>. This works around a docxmlater limitation
|
|
1831
|
+
* where setBold(false) produces no XML output instead of the explicit
|
|
1832
|
+
* <w:b w:val="0"/> needed to prevent bold inheritance from context.
|
|
1833
|
+
*/
|
|
1834
|
+
private patchLevelBoldOff(level: unknown): void {
|
|
1835
|
+
const lvl = level as { toXML: () => any };
|
|
1836
|
+
const origToXML = lvl.toXML.bind(lvl);
|
|
1837
|
+
lvl.toXML = function () {
|
|
1838
|
+
const xml = origToXML();
|
|
1839
|
+
if (xml && Array.isArray(xml.children)) {
|
|
1840
|
+
for (const child of xml.children) {
|
|
1841
|
+
if (typeof child === 'object' && child.name === 'w:rPr') {
|
|
1842
|
+
if (!Array.isArray(child.children)) child.children = [];
|
|
1843
|
+
child.children.push({ name: 'w:b', attributes: { 'w:val': '0' } });
|
|
1844
|
+
child.children.push({ name: 'w:bCs', attributes: { 'w:val': '0' } });
|
|
1845
|
+
break;
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return xml;
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
/**
|
|
1854
|
+
* Create a fallback lowerLetter numbered list for HLP tables where all paragraphs
|
|
1855
|
+
* have ListParagraph style with inherited (not explicit) numbering. This ensures
|
|
1856
|
+
* items retain visible markers (a., b., c.) after ListParagraph→Normal conversion.
|
|
1857
|
+
*/
|
|
1858
|
+
private createHLPFallbackList(doc: Document, settings?: TableShadingSettings): number {
|
|
1859
|
+
const manager = doc.getNumberingManager();
|
|
1860
|
+
if (!manager) return 0;
|
|
1861
|
+
const indentLevels = settings?.listIndentationLevels;
|
|
1862
|
+
|
|
1863
|
+
// Match convertHLPBulletsToLettered format: level 0 = lowerLetter (a., b., c.)
|
|
1864
|
+
const indent1 = indentLevels?.find(l => l.level === 1);
|
|
1865
|
+
const leftIndent = indent1 ? inchesToTwips(indent1.textIndent) : 720;
|
|
1866
|
+
const hangingIndent = indent1 ? inchesToTwips(indent1.textIndent - indent1.symbolIndent) : 360;
|
|
1867
|
+
|
|
1868
|
+
const level0 = new NumberingLevel({
|
|
1869
|
+
level: 0,
|
|
1870
|
+
format: 'lowerLetter',
|
|
1871
|
+
text: '%1.',
|
|
1872
|
+
leftIndent,
|
|
1873
|
+
hangingIndent,
|
|
1874
|
+
});
|
|
1875
|
+
|
|
1876
|
+
const numId = manager.createCustomList([level0], "HLP Fallback");
|
|
1877
|
+
if (numId) {
|
|
1878
|
+
// Set font/color/bold on the created level
|
|
1879
|
+
const instance = manager.getInstance(numId);
|
|
1880
|
+
if (instance) {
|
|
1881
|
+
const absId = instance.getAbstractNumId();
|
|
1882
|
+
const abstractNum = manager.getAbstractNumbering(absId);
|
|
1883
|
+
if (abstractNum) {
|
|
1884
|
+
const lvl = abstractNum.getLevel(0);
|
|
1885
|
+
if (lvl) {
|
|
1886
|
+
lvl.setFont('Verdana');
|
|
1887
|
+
lvl.setFontSize(24); // 12pt
|
|
1888
|
+
lvl.setColor('000000');
|
|
1889
|
+
lvl.setBold(false);
|
|
1890
|
+
this.patchLevelBoldOff(lvl);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
return numId ?? 0;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/**
|
|
1899
|
+
* Create a bullet list for HLP tips column paragraphs that have inherited
|
|
1900
|
+
* ListParagraph numbering (no explicit numPr). Tips cells are skipped by
|
|
1901
|
+
* convertHLPBulletsToLettered and excluded from discoveredNumId assignment,
|
|
1902
|
+
* so they need their own bullet list to preserve visible bullet markers.
|
|
1903
|
+
*/
|
|
1904
|
+
private createHLPTipsBulletList(doc: Document): number {
|
|
1905
|
+
const manager = doc.getNumberingManager();
|
|
1906
|
+
if (!manager) return 0;
|
|
1907
|
+
|
|
1908
|
+
const bullet = WORD_NATIVE_BULLETS.FILLED_BULLET;
|
|
1909
|
+
const level0 = new NumberingLevel({
|
|
1910
|
+
level: 0,
|
|
1911
|
+
format: 'bullet',
|
|
1912
|
+
text: bullet.char,
|
|
1913
|
+
font: bullet.font,
|
|
1914
|
+
leftIndent: 360,
|
|
1915
|
+
hangingIndent: 360,
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1918
|
+
const numId = manager.createCustomList([level0], "HLP Tips Bullet");
|
|
1919
|
+
return numId ?? 0;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
/**
|
|
1923
|
+
* Insert empty paragraphs before each level-0 numbered item (except the first)
|
|
1924
|
+
* in HLP content cells. This creates a visual blank line separator between
|
|
1925
|
+
* top-level items (1., 2., 3., etc.). Only applies to the content column.
|
|
1926
|
+
*
|
|
1927
|
+
* @returns Number of blank paragraphs inserted
|
|
1928
|
+
*/
|
|
1929
|
+
private insertHLPBlankLines(table: Table, analysis: HLPTableAnalysis, discoveredNumId: number | null): number {
|
|
1930
|
+
let inserted = 0;
|
|
1931
|
+
const rows = table.getRows();
|
|
1932
|
+
|
|
1933
|
+
for (let rowIndex = 1; rowIndex < rows.length; rowIndex++) {
|
|
1934
|
+
const cells = rows[rowIndex].getCells();
|
|
1935
|
+
|
|
1936
|
+
for (let cellIndex = 0; cellIndex < cells.length; cellIndex++) {
|
|
1937
|
+
const isTipsCell = analysis.hasTipsColumn && cells.length >= 2 && cellIndex === cells.length - 1 && analysis.variant === 'two-column';
|
|
1938
|
+
if (isTipsCell) continue; // Only content column
|
|
1939
|
+
|
|
1940
|
+
const cell = cells[cellIndex];
|
|
1941
|
+
let paras = cell.getParagraphs();
|
|
1942
|
+
let isFirstLevel0 = true;
|
|
1943
|
+
|
|
1944
|
+
for (let pIdx = 0; pIdx < paras.length; pIdx++) {
|
|
1945
|
+
const para = paras[pIdx];
|
|
1946
|
+
const numbering = para.getNumbering();
|
|
1947
|
+
|
|
1948
|
+
// Insert blank lines before:
|
|
1949
|
+
// 1. Main items (discoveredNumId at level 0) — the 1., 2., 3. items
|
|
1950
|
+
// 2. First paragraph of a new numId group at level 0 (content cell only)
|
|
1951
|
+
// — handles cells where sub-item groups also need visual separation
|
|
1952
|
+
const isLevel0 = numbering && (numbering.level === 0 || numbering.level === undefined);
|
|
1953
|
+
const isMainItem = isLevel0 && numbering.numId === discoveredNumId;
|
|
1954
|
+
const isNewGroup = isLevel0 && numbering.numId !== discoveredNumId;
|
|
1955
|
+
|
|
1956
|
+
if (isMainItem || isNewGroup) {
|
|
1957
|
+
if (isFirstLevel0 && isMainItem) {
|
|
1958
|
+
// Don't insert before the very first main item
|
|
1959
|
+
isFirstLevel0 = false;
|
|
1960
|
+
} else {
|
|
1961
|
+
// Check if the previous paragraph is already blank (no text, no numbering).
|
|
1962
|
+
// The original document may already have blank paragraphs between items
|
|
1963
|
+
// (converted from ListParagraph to Normal in step 6), so inserting another
|
|
1964
|
+
// would create a double blank line.
|
|
1965
|
+
const prevPara = paras[pIdx - 1];
|
|
1966
|
+
const prevText = prevPara?.getText()?.trim() || '';
|
|
1967
|
+
const prevNumbering = prevPara?.getNumbering();
|
|
1968
|
+
const prevIsBlank = prevText.length === 0 && !prevNumbering;
|
|
1969
|
+
|
|
1970
|
+
if (pIdx > 0 && !prevIsBlank) {
|
|
1971
|
+
// Insert blank paragraph before this level-0 item
|
|
1972
|
+
const blankPara = Paragraph.create();
|
|
1973
|
+
blankPara.setStyle('Normal');
|
|
1974
|
+
cell.addParagraphAt(pIdx, blankPara);
|
|
1975
|
+
inserted++;
|
|
1976
|
+
pIdx++; // Skip past inserted blank
|
|
1977
|
+
paras = cell.getParagraphs(); // Refresh after insertion
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
if (isMainItem && isFirstLevel0) {
|
|
1981
|
+
isFirstLevel0 = false;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
return inserted;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
/**
|
|
1993
|
+
* Fix paragraphs immediately after an HLP table that would inherit
|
|
1994
|
+
* the table's numbering sequence via ListParagraph's default numPr.
|
|
1995
|
+
*
|
|
1996
|
+
* Without this, a ListParagraph paragraph after a 6-item HLP table
|
|
1997
|
+
* shows "7." because it shares the same numId as the table's items.
|
|
1998
|
+
*/
|
|
1999
|
+
private fixPostHLPTableNumbering(doc: Document, table: Table): void {
|
|
2000
|
+
const bodyElements = doc.getBodyElements();
|
|
2001
|
+
const tableIdx = bodyElements.indexOf(table);
|
|
2002
|
+
if (tableIdx < 0 || tableIdx >= bodyElements.length - 1) return;
|
|
2003
|
+
|
|
2004
|
+
// Check the next few body elements after the table
|
|
2005
|
+
for (let i = tableIdx + 1; i < bodyElements.length && i <= tableIdx + 3; i++) {
|
|
2006
|
+
const element = bodyElements[i];
|
|
2007
|
+
// Stop at next table (duck-type check: tables have getRows)
|
|
2008
|
+
if (typeof (element as any).getRows === 'function') break;
|
|
2009
|
+
// Check if this is a paragraph (duck-type: has getStyle and getRuns)
|
|
2010
|
+
if (typeof (element as any).getStyle === 'function' && typeof (element as any).getRuns === 'function') {
|
|
2011
|
+
const para = element as Paragraph;
|
|
2012
|
+
const style = para.getStyle();
|
|
2013
|
+
const numbering = para.getNumbering();
|
|
2014
|
+
// ListParagraph with no explicit numbering inherits default numPr
|
|
2015
|
+
if (style === 'ListParagraph' && !numbering) {
|
|
2016
|
+
para.setStyle('Normal');
|
|
2017
|
+
log.debug(`Fixed post-HLP ListParagraph → Normal at body index ${i}`);
|
|
2018
|
+
}
|
|
2019
|
+
// If we hit a paragraph with actual content, stop looking
|
|
2020
|
+
const text = para.getRuns().map((r: any) => r.getText()).join('').trim();
|
|
2021
|
+
if (text.length > 0) break;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
/**
|
|
2027
|
+
* Process all HLP (High Level Process) tables in the document.
|
|
2028
|
+
*
|
|
2029
|
+
* HLP tables are detected by FFC000 shading AND "High Level Process" header text.
|
|
2030
|
+
* Special formatting applied:
|
|
2031
|
+
* - Variant-aware borders (table-level for single-column, cell-level for two-column)
|
|
2032
|
+
* - Header row: Heading 2 style with FFC000 shading
|
|
2033
|
+
* - Content formatting: bold list items, blue hyperlinks, session fonts/sizes
|
|
2034
|
+
* - Tips column: FFF2CC shading preserved
|
|
2035
|
+
*
|
|
2036
|
+
* Note: FFC000 and FFF2CC colors are already preserved by existing
|
|
2037
|
+
* preservedColors logic in applyTableUniformity().
|
|
2038
|
+
*
|
|
2039
|
+
* @param doc - Document to process
|
|
2040
|
+
* @param settings - Optional table shading settings for font/size configuration
|
|
2041
|
+
* @returns Processing results
|
|
2042
|
+
*/
|
|
2043
|
+
async processHLPTables(
|
|
2044
|
+
doc: Document,
|
|
2045
|
+
settings?: TableShadingSettings,
|
|
2046
|
+
savedNumbering?: Map<Paragraph, { numId: number; level: number; leftIndent?: number }>,
|
|
2047
|
+
): Promise<HLPTableProcessingResult> {
|
|
2048
|
+
const tables = doc.getTables();
|
|
2049
|
+
let tablesFound = 0;
|
|
2050
|
+
let headersStyled = 0;
|
|
2051
|
+
let singleColumnTables = 0;
|
|
2052
|
+
let twoColumnTables = 0;
|
|
2053
|
+
|
|
2054
|
+
for (const table of tables) {
|
|
2055
|
+
// Don't skip — HLP tables may have nested content or floating positioning
|
|
2056
|
+
// but still need processing. The analyzeHLPTable check is sufficient.
|
|
2057
|
+
const analysis = this.analyzeHLPTable(table);
|
|
2058
|
+
if (!analysis.isHLP) continue;
|
|
2059
|
+
|
|
2060
|
+
tablesFound++;
|
|
2061
|
+
log.debug(`Processing HLP table #${tablesFound} (variant: ${analysis.variant}, ` +
|
|
2062
|
+
`${analysis.columnCount} cols, ${analysis.rowCount} rows, tips: ${analysis.hasTipsColumn})`);
|
|
2063
|
+
|
|
2064
|
+
if (analysis.variant === 'single-column') singleColumnTables++;
|
|
2065
|
+
else twoColumnTables++;
|
|
2066
|
+
|
|
2067
|
+
// 1. Apply table-level borders (variant-aware)
|
|
2068
|
+
this.applyHLPTableBorders(table, analysis);
|
|
2069
|
+
|
|
2070
|
+
// 2. Apply Heading 2 style to header row paragraphs
|
|
2071
|
+
headersStyled += this.applyHLPHeaderStyle(table);
|
|
2072
|
+
|
|
2073
|
+
// 2.5 Apply explicit run formatting to header row (h2Font/h2Size/bold/center)
|
|
2074
|
+
this.applyHLPHeaderRunFormatting(table, settings);
|
|
2075
|
+
|
|
2076
|
+
// 3. Apply header cell formatting (shading, margins, cell borders)
|
|
2077
|
+
this.applyHLPHeaderCellFormatting(table, analysis);
|
|
2078
|
+
|
|
2079
|
+
// 4. Apply cell-level borders for data rows (variant-aware)
|
|
2080
|
+
this.applyHLPCellBorders(table, analysis);
|
|
2081
|
+
|
|
2082
|
+
// 5. Ensure tips column shading is correct
|
|
2083
|
+
this.applyHLPTipsColumnShading(table, analysis);
|
|
2084
|
+
|
|
2085
|
+
// 5.1 Restore original numbering for HLP paragraphs that were corrupted
|
|
2086
|
+
// by applyStyles(), list normalization, or other pipeline steps.
|
|
2087
|
+
if (savedNumbering) {
|
|
2088
|
+
const rows = table.getRows();
|
|
2089
|
+
let restored = 0;
|
|
2090
|
+
for (let ri = 1; ri < rows.length; ri++) {
|
|
2091
|
+
for (const cell of rows[ri].getCells()) {
|
|
2092
|
+
for (const para of cell.getParagraphs()) {
|
|
2093
|
+
const saved = savedNumbering.get(para);
|
|
2094
|
+
if (!saved) continue;
|
|
2095
|
+
|
|
2096
|
+
if (saved.numId === -1) {
|
|
2097
|
+
// Sentinel for ListParagraph with no detectable numbering
|
|
2098
|
+
// (inherits numbering from style). Leave as-is — step 6
|
|
2099
|
+
// (applyHLPContentFormatting) will convert to Normal and
|
|
2100
|
+
// assign discoveredNumId.
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
if (saved.numId === 0) {
|
|
2105
|
+
// numId=0 = explicit numbering suppression (e.g. "Note:" paragraphs).
|
|
2106
|
+
// Remove any numbering, switch to Normal (prevents ListParagraph default
|
|
2107
|
+
// numPr from showing), and restore the pre-processing indentation.
|
|
2108
|
+
if (para.getNumbering()) {
|
|
2109
|
+
para.removeNumbering();
|
|
2110
|
+
}
|
|
2111
|
+
if (para.getStyle() === 'ListParagraph') {
|
|
2112
|
+
para.setStyle('Normal');
|
|
2113
|
+
}
|
|
2114
|
+
if (saved.leftIndent) {
|
|
2115
|
+
para.setLeftIndent(saved.leftIndent);
|
|
2116
|
+
}
|
|
2117
|
+
restored++;
|
|
2118
|
+
continue;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// Existing restoration logic for numId > 0
|
|
2122
|
+
const current = para.getNumbering();
|
|
2123
|
+
const curNumId = current?.numId ?? 0;
|
|
2124
|
+
const curLevel = current?.level ?? 0;
|
|
2125
|
+
if (curNumId !== saved.numId || curLevel !== saved.level) {
|
|
2126
|
+
para.setNumbering(saved.numId, saved.level);
|
|
2127
|
+
restored++;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
if (restored > 0) {
|
|
2133
|
+
log.debug(`Restored numbering for ${restored} HLP paragraphs in table #${tablesFound}`);
|
|
2134
|
+
} else if (savedNumbering.size > 0) {
|
|
2135
|
+
log.warn(`savedNumbering had ${savedNumbering.size} entries but 0 paragraphs were restored in table #${tablesFound} — possible identity mismatch`);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
// 5.7 Discover main numId before bullet conversion and content formatting
|
|
2140
|
+
let discoveredNumId = this.discoverHLPMainNumId(table, analysis, doc);
|
|
2141
|
+
|
|
2142
|
+
// 5.5 Convert bullet sub-items to lettered (a., b., c.) in content column
|
|
2143
|
+
// Runs AFTER discovering main numId so we can skip abstractNums belonging to the main numbered list
|
|
2144
|
+
const bulletsConverted = this.convertHLPBulletsToLettered(table, doc, analysis, settings, discoveredNumId);
|
|
2145
|
+
if (bulletsConverted > 0) {
|
|
2146
|
+
log.debug(`Converted ${bulletsConverted} bullet abstractNums to lowerLetter in HLP table #${tablesFound}`);
|
|
2147
|
+
}
|
|
2148
|
+
const hasExplicitNumbering = discoveredNumId !== null;
|
|
2149
|
+
|
|
2150
|
+
// 5.8 Fallback: if no explicit numbering found but ListParagraph paragraphs
|
|
2151
|
+
// exist (inherited numbering from style default), create a lowerLetter list
|
|
2152
|
+
// so items retain visible markers after ListParagraph→Normal conversion.
|
|
2153
|
+
if (discoveredNumId === null) {
|
|
2154
|
+
const rows = table.getRows();
|
|
2155
|
+
let needsFallback = false;
|
|
2156
|
+
for (let ri = 1; ri < rows.length && !needsFallback; ri++) {
|
|
2157
|
+
const contentCell = rows[ri].getCells()[0];
|
|
2158
|
+
if (!contentCell) continue;
|
|
2159
|
+
for (const p of contentCell.getParagraphs()) {
|
|
2160
|
+
if (p.getStyle() === 'ListParagraph' && p.getText().trim().length > 0) {
|
|
2161
|
+
needsFallback = true;
|
|
2162
|
+
break;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
if (needsFallback) {
|
|
2167
|
+
const fallbackNumId = this.createHLPFallbackList(doc, settings);
|
|
2168
|
+
if (fallbackNumId > 0) {
|
|
2169
|
+
discoveredNumId = fallbackNumId;
|
|
2170
|
+
log.debug(`Created HLP fallback lowerLetter list numId=${discoveredNumId}`);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// 5.9 Create bullet list for tips column paragraphs that have inherited
|
|
2176
|
+
// ListParagraph numbering. Tips cells are skipped by convertHLPBulletsToLettered
|
|
2177
|
+
// and excluded from discoveredNumId assignment, so they need their own bullet list.
|
|
2178
|
+
let tipsBulletNumId: number | null = null;
|
|
2179
|
+
if (analysis.variant === 'two-column' && analysis.hasTipsColumn) {
|
|
2180
|
+
tipsBulletNumId = this.createHLPTipsBulletList(doc);
|
|
2181
|
+
if (tipsBulletNumId > 0) {
|
|
2182
|
+
log.debug(`Created HLP tips bullet list numId=${tipsBulletNumId}`);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// 6. Apply text formatting (fonts, bold, hyperlink colors, tips alignment)
|
|
2187
|
+
this.applyHLPContentFormatting(table, analysis, settings, discoveredNumId, tipsBulletNumId, savedNumbering, hasExplicitNumbering);
|
|
2188
|
+
|
|
2189
|
+
// 6.5 Insert blank paragraphs before level-0 items (2., 3., etc.)
|
|
2190
|
+
const blankLinesInserted = this.insertHLPBlankLines(table, analysis, discoveredNumId);
|
|
2191
|
+
if (blankLinesInserted > 0) {
|
|
2192
|
+
log.debug(`Inserted ${blankLinesInserted} blank lines in HLP table #${tablesFound}`);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// 7. Fix post-table paragraphs: ListParagraph paragraphs immediately
|
|
2196
|
+
// after the HLP table inherit the style's default numId and continue
|
|
2197
|
+
// the table's numbering sequence (producing a phantom "7." etc.).
|
|
2198
|
+
// Convert them to Normal to break the numbering chain.
|
|
2199
|
+
this.fixPostHLPTableNumbering(doc, table);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
if (tablesFound > 0) {
|
|
2203
|
+
log.info(`HLP table processing complete: ${tablesFound} tables ` +
|
|
2204
|
+
`(${singleColumnTables} single-column, ${twoColumnTables} two-column), ` +
|
|
2205
|
+
`${headersStyled} headers styled`);
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
return { tablesFound, headersStyled, singleColumnTables, twoColumnTables };
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// ═══════════════════════════════════════════════════════════
|
|
2212
|
+
// Floating / Nested Table Guard
|
|
2213
|
+
// ═══════════════════════════════════════════════════════════
|
|
2214
|
+
|
|
2215
|
+
/**
|
|
2216
|
+
* Returns true if the table should be skipped by the processing pipeline.
|
|
2217
|
+
* Floating tables (w:tblpPr) and tables containing nested tables are
|
|
2218
|
+
* passed through unmodified to avoid corrupting complex structures.
|
|
2219
|
+
*
|
|
2220
|
+
* @param table - Table to check
|
|
2221
|
+
* @returns true if the table is floating or contains nested tables
|
|
2222
|
+
*/
|
|
2223
|
+
shouldSkipTable(table: Table): boolean {
|
|
2224
|
+
// 1. Floating table check — has positioning properties (w:tblpPr)
|
|
2225
|
+
if (table.isFloating()) return true;
|
|
2226
|
+
|
|
2227
|
+
// 2. Nested table check — any cell contains a child table
|
|
2228
|
+
for (const row of table.getRows()) {
|
|
2229
|
+
for (const cell of row.getCells()) {
|
|
2230
|
+
if (cell.hasNestedTables()) return true;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
export const tableProcessor = new TableProcessor();
|