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,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for paragraph check helper functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
6
|
+
/* globals jest, describe, it, expect */
|
|
7
|
+
import {
|
|
8
|
+
isParagraphBlank,
|
|
9
|
+
startsWithBoldColon,
|
|
10
|
+
isCenteredBoldText,
|
|
11
|
+
isTextOnlyParagraph,
|
|
12
|
+
isTocParagraph,
|
|
13
|
+
} from "../helpers/paragraphChecks";
|
|
14
|
+
|
|
15
|
+
// Mock docxmlater classes
|
|
16
|
+
jest.mock("docxmlater", () => {
|
|
17
|
+
class MockRun {
|
|
18
|
+
private text: string;
|
|
19
|
+
private formatting: any;
|
|
20
|
+
constructor(text: string = "", formatting: any = {}) {
|
|
21
|
+
this.text = text;
|
|
22
|
+
this.formatting = formatting;
|
|
23
|
+
}
|
|
24
|
+
getText() { return this.text; }
|
|
25
|
+
getFormatting() { return this.formatting; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class MockHyperlink {
|
|
29
|
+
getText() { return "link text"; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class MockImageRun extends MockRun {
|
|
33
|
+
getImageElement() { return {}; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class MockShape {}
|
|
37
|
+
class MockTextBox {}
|
|
38
|
+
class MockField {}
|
|
39
|
+
|
|
40
|
+
class MockRevision {
|
|
41
|
+
private text: string;
|
|
42
|
+
private content: any[];
|
|
43
|
+
constructor(text: string = "", content: any[] = []) {
|
|
44
|
+
this.text = text;
|
|
45
|
+
this.content = content;
|
|
46
|
+
}
|
|
47
|
+
getText() { return this.text; }
|
|
48
|
+
getContent() { return this.content; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class MockParagraph {
|
|
52
|
+
private content: any[];
|
|
53
|
+
private style: string;
|
|
54
|
+
private alignment: string;
|
|
55
|
+
private bookmarksStart: any[];
|
|
56
|
+
private bookmarksEnd: any[];
|
|
57
|
+
|
|
58
|
+
constructor(opts: {
|
|
59
|
+
content?: any[];
|
|
60
|
+
style?: string;
|
|
61
|
+
alignment?: string;
|
|
62
|
+
bookmarksStart?: any[];
|
|
63
|
+
bookmarksEnd?: any[];
|
|
64
|
+
} = {}) {
|
|
65
|
+
this.content = opts.content ?? [];
|
|
66
|
+
this.style = opts.style ?? "";
|
|
67
|
+
this.alignment = opts.alignment ?? "left";
|
|
68
|
+
this.bookmarksStart = opts.bookmarksStart ?? [];
|
|
69
|
+
this.bookmarksEnd = opts.bookmarksEnd ?? [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getContent() { return this.content; }
|
|
73
|
+
getText() {
|
|
74
|
+
return this.content
|
|
75
|
+
.filter((c: any) => c.getText)
|
|
76
|
+
.map((c: any) => c.getText())
|
|
77
|
+
.join("");
|
|
78
|
+
}
|
|
79
|
+
getStyle() { return this.style; }
|
|
80
|
+
getAlignment() { return this.alignment; }
|
|
81
|
+
getBookmarksStart() { return this.bookmarksStart; }
|
|
82
|
+
getBookmarksEnd() { return this.bookmarksEnd; }
|
|
83
|
+
getRuns() { return this.content.filter((c: any) => c instanceof MockRun && !(c instanceof MockImageRun)); }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
Paragraph: MockParagraph,
|
|
88
|
+
Run: MockRun,
|
|
89
|
+
Hyperlink: MockHyperlink,
|
|
90
|
+
ImageRun: MockImageRun,
|
|
91
|
+
Shape: MockShape,
|
|
92
|
+
TextBox: MockTextBox,
|
|
93
|
+
Field: MockField,
|
|
94
|
+
Revision: MockRevision,
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Import mocked module — jest.mock is hoisted above imports by Jest
|
|
99
|
+
import {
|
|
100
|
+
Paragraph, Run, Hyperlink, ImageRun, Shape, TextBox, Field, Revision,
|
|
101
|
+
} from "docxmlater";
|
|
102
|
+
|
|
103
|
+
describe("isParagraphBlank", () => {
|
|
104
|
+
it("should return true for empty paragraph", () => {
|
|
105
|
+
const para = new Paragraph({ content: [] });
|
|
106
|
+
expect(isParagraphBlank(para)).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should return true for paragraph with whitespace-only run", () => {
|
|
110
|
+
const para = new Paragraph({ content: [new Run(" ")] });
|
|
111
|
+
expect(isParagraphBlank(para)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should return false for paragraph with text", () => {
|
|
115
|
+
const para = new Paragraph({ content: [new Run("Hello")] });
|
|
116
|
+
expect(isParagraphBlank(para)).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should return false for paragraph with hyperlink", () => {
|
|
120
|
+
const para = new Paragraph({ content: [new Hyperlink()] });
|
|
121
|
+
expect(isParagraphBlank(para)).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should return false for paragraph with ImageRun", () => {
|
|
125
|
+
const para = new Paragraph({ content: [new ImageRun()] });
|
|
126
|
+
expect(isParagraphBlank(para)).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should return false for paragraph with Shape", () => {
|
|
130
|
+
const para = new Paragraph({ content: [new Shape()] });
|
|
131
|
+
expect(isParagraphBlank(para)).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should return false for paragraph with TextBox", () => {
|
|
135
|
+
const para = new Paragraph({ content: [new TextBox()] });
|
|
136
|
+
expect(isParagraphBlank(para)).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should return false for paragraph with Field", () => {
|
|
140
|
+
const para = new Paragraph({ content: [new Field()] });
|
|
141
|
+
expect(isParagraphBlank(para)).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should return false for paragraph with non-empty Revision", () => {
|
|
145
|
+
const para = new Paragraph({ content: [new Revision("tracked text")] });
|
|
146
|
+
expect(isParagraphBlank(para)).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should return true for paragraph with bookmarks but no content (early return)", () => {
|
|
150
|
+
// Note: The function returns true for empty content before checking bookmarks.
|
|
151
|
+
// Bookmarks are only checked when content items exist but are all blank.
|
|
152
|
+
const para = new Paragraph({
|
|
153
|
+
content: [],
|
|
154
|
+
bookmarksStart: [{ id: 1 }],
|
|
155
|
+
});
|
|
156
|
+
expect(isParagraphBlank(para)).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should return false for paragraph with bookmarks and whitespace run", () => {
|
|
160
|
+
const para = new Paragraph({
|
|
161
|
+
content: [new Run(" ")],
|
|
162
|
+
bookmarksStart: [{ id: 1 }],
|
|
163
|
+
});
|
|
164
|
+
expect(isParagraphBlank(para)).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should return true for Revision with only whitespace", () => {
|
|
168
|
+
const para = new Paragraph({ content: [new Revision(" ")] });
|
|
169
|
+
expect(isParagraphBlank(para)).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("startsWithBoldColon", () => {
|
|
174
|
+
it("should return true for bold text with colon", () => {
|
|
175
|
+
const para = new Paragraph({
|
|
176
|
+
content: [new Run("Note:", { bold: true })],
|
|
177
|
+
});
|
|
178
|
+
expect(startsWithBoldColon(para)).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should return false for non-bold text with colon", () => {
|
|
182
|
+
const para = new Paragraph({
|
|
183
|
+
content: [new Run("Note:", { bold: false })],
|
|
184
|
+
});
|
|
185
|
+
expect(startsWithBoldColon(para)).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should return false for bold text without colon", () => {
|
|
189
|
+
const para = new Paragraph({
|
|
190
|
+
content: [new Run("Note", { bold: true })],
|
|
191
|
+
});
|
|
192
|
+
expect(startsWithBoldColon(para)).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should return false for empty paragraph", () => {
|
|
196
|
+
const para = new Paragraph({ content: [] });
|
|
197
|
+
expect(startsWithBoldColon(para)).toBe(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("isCenteredBoldText", () => {
|
|
202
|
+
it("should return true for centered bold paragraph", () => {
|
|
203
|
+
const para = new Paragraph({
|
|
204
|
+
content: [new Run("TITLE", { bold: true })],
|
|
205
|
+
alignment: "center",
|
|
206
|
+
});
|
|
207
|
+
expect(isCenteredBoldText(para)).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should return false for left-aligned bold paragraph", () => {
|
|
211
|
+
const para = new Paragraph({
|
|
212
|
+
content: [new Run("TITLE", { bold: true })],
|
|
213
|
+
alignment: "left",
|
|
214
|
+
});
|
|
215
|
+
expect(isCenteredBoldText(para)).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should return false for centered non-bold paragraph", () => {
|
|
219
|
+
const para = new Paragraph({
|
|
220
|
+
content: [new Run("TITLE", { bold: false })],
|
|
221
|
+
alignment: "center",
|
|
222
|
+
});
|
|
223
|
+
expect(isCenteredBoldText(para)).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("should return false for empty paragraph", () => {
|
|
227
|
+
const para = new Paragraph({ content: [], alignment: "center" });
|
|
228
|
+
expect(isCenteredBoldText(para)).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe("isTocParagraph", () => {
|
|
233
|
+
it('should return true for "TOC1" style', () => {
|
|
234
|
+
const para = new Paragraph({ style: "TOC1" });
|
|
235
|
+
expect(isTocParagraph(para)).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should return true for "toc2" style', () => {
|
|
239
|
+
const para = new Paragraph({ style: "toc2" });
|
|
240
|
+
expect(isTocParagraph(para)).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should return true for "toc 3" style', () => {
|
|
244
|
+
const para = new Paragraph({ style: "toc 3" });
|
|
245
|
+
expect(isTocParagraph(para)).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("should return false for Normal style", () => {
|
|
249
|
+
const para = new Paragraph({ style: "Normal" });
|
|
250
|
+
expect(isTocParagraph(para)).toBe(false);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should return false for empty style", () => {
|
|
254
|
+
const para = new Paragraph({ style: "" });
|
|
255
|
+
expect(isTocParagraph(para)).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("isTextOnlyParagraph", () => {
|
|
260
|
+
it("should return true for paragraph with only text runs", () => {
|
|
261
|
+
const para = new Paragraph({
|
|
262
|
+
content: [new Run("Hello world")],
|
|
263
|
+
});
|
|
264
|
+
expect(isTextOnlyParagraph(para)).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("should return false for blank paragraph", () => {
|
|
268
|
+
const para = new Paragraph({ content: [] });
|
|
269
|
+
expect(isTextOnlyParagraph(para)).toBe(false);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should return false for paragraph with image", () => {
|
|
273
|
+
const para = new Paragraph({ content: [new ImageRun()] });
|
|
274
|
+
expect(isTextOnlyParagraph(para)).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("should return false for paragraph with shape", () => {
|
|
278
|
+
const para = new Paragraph({ content: [new Shape()] });
|
|
279
|
+
expect(isTextOnlyParagraph(para)).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for creating and inserting blank paragraphs.
|
|
3
|
+
* Encapsulates the repeated insert-or-mark pattern used across all phases.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Document, Paragraph } from "docxmlater";
|
|
7
|
+
import type { BlankLineOptions } from "../types";
|
|
8
|
+
import { isParagraphBlank } from "./paragraphChecks";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a blank paragraph with the specified options.
|
|
12
|
+
*/
|
|
13
|
+
export function createBlankParagraph(options: BlankLineOptions): Paragraph {
|
|
14
|
+
const blankPara = Paragraph.create();
|
|
15
|
+
blankPara.setStyle(options.style);
|
|
16
|
+
if (options.spacingBefore !== undefined) {
|
|
17
|
+
blankPara.setSpaceBefore(options.spacingBefore);
|
|
18
|
+
}
|
|
19
|
+
blankPara.setSpaceAfter(options.spacingAfter);
|
|
20
|
+
if (options.lineSpacing !== undefined) {
|
|
21
|
+
blankPara.setLineSpacing(options.lineSpacing);
|
|
22
|
+
}
|
|
23
|
+
if (options.markAsPreserved) {
|
|
24
|
+
blankPara.setPreserved(true);
|
|
25
|
+
}
|
|
26
|
+
return blankPara;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Inserts a blank paragraph after the element at the given index,
|
|
31
|
+
* or marks the existing blank if one is already there.
|
|
32
|
+
*
|
|
33
|
+
* @returns 'added' if a new blank was inserted, 'marked' if existing was marked, 'skipped' if no action taken
|
|
34
|
+
*/
|
|
35
|
+
export function insertOrMarkBlankAfter(
|
|
36
|
+
doc: Document,
|
|
37
|
+
elementIndex: number,
|
|
38
|
+
options: BlankLineOptions
|
|
39
|
+
): "added" | "marked" | "skipped" {
|
|
40
|
+
const nextElement = doc.getBodyElementAt(elementIndex + 1);
|
|
41
|
+
|
|
42
|
+
if (nextElement instanceof Paragraph && isParagraphBlank(nextElement)) {
|
|
43
|
+
// Mark existing blank as preserved
|
|
44
|
+
nextElement.setStyle(options.style);
|
|
45
|
+
if (options.markAsPreserved && !nextElement.isPreserved()) {
|
|
46
|
+
nextElement.setPreserved(true);
|
|
47
|
+
return "marked";
|
|
48
|
+
}
|
|
49
|
+
return "skipped";
|
|
50
|
+
} else {
|
|
51
|
+
// Insert new blank paragraph
|
|
52
|
+
const blankPara = createBlankParagraph(options);
|
|
53
|
+
doc.insertBodyElementAt(elementIndex + 1, blankPara);
|
|
54
|
+
return "added";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Inserts a blank paragraph before the element at the given index,
|
|
60
|
+
* or marks the existing blank if one is already there.
|
|
61
|
+
*
|
|
62
|
+
* @returns 'added' if a new blank was inserted, 'marked' if existing was marked, 'skipped' if no action taken
|
|
63
|
+
*/
|
|
64
|
+
export function insertOrMarkBlankBefore(
|
|
65
|
+
doc: Document,
|
|
66
|
+
elementIndex: number,
|
|
67
|
+
options: BlankLineOptions
|
|
68
|
+
): "added" | "marked" | "skipped" {
|
|
69
|
+
if (elementIndex <= 0) return "skipped";
|
|
70
|
+
|
|
71
|
+
const prevElement = doc.getBodyElementAt(elementIndex - 1);
|
|
72
|
+
|
|
73
|
+
if (prevElement instanceof Paragraph && isParagraphBlank(prevElement)) {
|
|
74
|
+
// Mark existing blank as preserved
|
|
75
|
+
prevElement.setStyle(options.style);
|
|
76
|
+
if (options.markAsPreserved && !prevElement.isPreserved()) {
|
|
77
|
+
prevElement.setPreserved(true);
|
|
78
|
+
return "marked";
|
|
79
|
+
}
|
|
80
|
+
return "skipped";
|
|
81
|
+
} else {
|
|
82
|
+
// Insert new blank paragraph before the element
|
|
83
|
+
const blankPara = createBlankParagraph(options);
|
|
84
|
+
doc.insertBodyElementAt(elementIndex, blankPara);
|
|
85
|
+
return "added";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blank Line Snapshot - Captures original blank line positions before processing.
|
|
3
|
+
*
|
|
4
|
+
* This module enables the "preserve original if no rule matches" approach.
|
|
5
|
+
* It records blank paragraph positions using content hashes of neighboring
|
|
6
|
+
* elements so positions can be re-located after processing shifts indices.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Document, Paragraph, Table, TableCell, Revision } from "docxmlater";
|
|
10
|
+
import { isParagraphBlank } from "./paragraphChecks";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hash of an element's key characteristics for matching after processing.
|
|
14
|
+
*/
|
|
15
|
+
interface ElementHash {
|
|
16
|
+
type: "paragraph" | "table" | "none";
|
|
17
|
+
textPrefix: string;
|
|
18
|
+
style: string;
|
|
19
|
+
hasNumbering: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface BodyBlankPosition {
|
|
23
|
+
beforeHash: ElementHash;
|
|
24
|
+
afterHash: ElementHash;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface CellBlankPosition {
|
|
28
|
+
/** Identification of the cell by its table's first-cell text + row/col */
|
|
29
|
+
cellId: string;
|
|
30
|
+
paraIndexInCell: number;
|
|
31
|
+
beforeHash: ElementHash;
|
|
32
|
+
afterHash: ElementHash;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BlankLineSnapshot {
|
|
36
|
+
bodyBlanks: BodyBlankPosition[];
|
|
37
|
+
cellBlanks: CellBlankPosition[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Gets text from a paragraph, falling back to Revision content when
|
|
42
|
+
* getText() returns empty (which happens for revision-wrapped paragraphs
|
|
43
|
+
* in preserve mode).
|
|
44
|
+
*/
|
|
45
|
+
function getParaText(para: Paragraph): string {
|
|
46
|
+
const text = para.getText() || "";
|
|
47
|
+
if (text) return text;
|
|
48
|
+
|
|
49
|
+
// Fallback: extract text from Revision elements (tracked insertions)
|
|
50
|
+
for (const item of para.getContent()) {
|
|
51
|
+
if (item instanceof Revision) {
|
|
52
|
+
const revText = item.getText() || "";
|
|
53
|
+
if (revText) return revText;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Computes a hash of an element for neighbor-matching.
|
|
61
|
+
*/
|
|
62
|
+
function hashElement(element: any): ElementHash {
|
|
63
|
+
if (!element) {
|
|
64
|
+
return { type: "none", textPrefix: "", style: "", hasNumbering: false };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (element instanceof Paragraph) {
|
|
68
|
+
const text = getParaText(element);
|
|
69
|
+
return {
|
|
70
|
+
type: "paragraph",
|
|
71
|
+
textPrefix: text.substring(0, 50),
|
|
72
|
+
style: element.getStyle() || "",
|
|
73
|
+
hasNumbering: !!element.getNumbering(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (element instanceof Table) {
|
|
78
|
+
// Use first cell text as identifier
|
|
79
|
+
let firstCellText = "";
|
|
80
|
+
try {
|
|
81
|
+
const cell = element.getCell(0, 0);
|
|
82
|
+
if (cell) {
|
|
83
|
+
firstCellText = cell
|
|
84
|
+
.getParagraphs()
|
|
85
|
+
.map((p) => p.getText())
|
|
86
|
+
.join(" ")
|
|
87
|
+
.substring(0, 50);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// Table may not have cells
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
type: "table",
|
|
94
|
+
textPrefix: firstCellText,
|
|
95
|
+
style: "",
|
|
96
|
+
hasNumbering: false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { type: "none", textPrefix: "", style: "", hasNumbering: false };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Computes a hash for a paragraph within a cell.
|
|
105
|
+
*/
|
|
106
|
+
function hashParagraph(para: Paragraph | undefined): ElementHash {
|
|
107
|
+
if (!para) {
|
|
108
|
+
return { type: "none", textPrefix: "", style: "", hasNumbering: false };
|
|
109
|
+
}
|
|
110
|
+
const text = getParaText(para);
|
|
111
|
+
return {
|
|
112
|
+
type: "paragraph",
|
|
113
|
+
textPrefix: text.substring(0, 50),
|
|
114
|
+
style: para.getStyle() || "",
|
|
115
|
+
hasNumbering: !!para.getNumbering(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Creates a cell identifier string for matching.
|
|
121
|
+
*/
|
|
122
|
+
function makeCellId(
|
|
123
|
+
tableIndex: number,
|
|
124
|
+
rowIndex: number,
|
|
125
|
+
colIndex: number,
|
|
126
|
+
tableFirstCellText: string
|
|
127
|
+
): string {
|
|
128
|
+
return `t${tableIndex}_r${rowIndex}_c${colIndex}_${tableFirstCellText.substring(0, 20)}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Checks if two element hashes match (allowing minor differences).
|
|
133
|
+
*/
|
|
134
|
+
function hashesMatch(a: ElementHash, b: ElementHash): boolean {
|
|
135
|
+
if (a.type !== b.type) return false;
|
|
136
|
+
if (a.type === "none" && b.type === "none") return true;
|
|
137
|
+
// Match on text prefix and numbering state
|
|
138
|
+
return a.textPrefix === b.textPrefix && a.hasNumbering === b.hasNumbering;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Captures positions of all blank paragraphs in the document.
|
|
143
|
+
* Must be called BEFORE any processing modifies the document.
|
|
144
|
+
*/
|
|
145
|
+
export function captureBlankLineSnapshot(doc: Document): BlankLineSnapshot {
|
|
146
|
+
const bodyBlanks: BodyBlankPosition[] = [];
|
|
147
|
+
const cellBlanks: CellBlankPosition[] = [];
|
|
148
|
+
|
|
149
|
+
// Capture body-level blanks
|
|
150
|
+
const bodyCount = doc.getBodyElementCount();
|
|
151
|
+
for (let i = 0; i < bodyCount; i++) {
|
|
152
|
+
const element = doc.getBodyElementAt(i);
|
|
153
|
+
if (!(element instanceof Paragraph)) continue;
|
|
154
|
+
if (!isParagraphBlank(element)) continue;
|
|
155
|
+
|
|
156
|
+
const prev = i > 0 ? doc.getBodyElementAt(i - 1) : undefined;
|
|
157
|
+
const next =
|
|
158
|
+
i < bodyCount - 1 ? doc.getBodyElementAt(i + 1) : undefined;
|
|
159
|
+
|
|
160
|
+
bodyBlanks.push({
|
|
161
|
+
beforeHash: hashElement(prev),
|
|
162
|
+
afterHash: hashElement(next),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Capture cell-level blanks
|
|
167
|
+
const tables = doc.getAllTables();
|
|
168
|
+
for (let ti = 0; ti < tables.length; ti++) {
|
|
169
|
+
const table = tables[ti];
|
|
170
|
+
let tableFirstCellText = "";
|
|
171
|
+
try {
|
|
172
|
+
const firstCell = table.getCell(0, 0);
|
|
173
|
+
if (firstCell) {
|
|
174
|
+
tableFirstCellText = firstCell
|
|
175
|
+
.getParagraphs()
|
|
176
|
+
.map((p) => p.getText())
|
|
177
|
+
.join(" ")
|
|
178
|
+
.substring(0, 20);
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
// Skip
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const rows = table.getRows();
|
|
185
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
186
|
+
const cells = rows[ri].getCells();
|
|
187
|
+
for (let ci = 0; ci < cells.length; ci++) {
|
|
188
|
+
const cell = cells[ci];
|
|
189
|
+
const paras = cell.getParagraphs();
|
|
190
|
+
const cellId = makeCellId(ti, ri, ci, tableFirstCellText);
|
|
191
|
+
|
|
192
|
+
for (let pi = 0; pi < paras.length; pi++) {
|
|
193
|
+
if (!isParagraphBlank(paras[pi])) continue;
|
|
194
|
+
|
|
195
|
+
cellBlanks.push({
|
|
196
|
+
cellId,
|
|
197
|
+
paraIndexInCell: pi,
|
|
198
|
+
beforeHash: hashParagraph(paras[pi - 1]),
|
|
199
|
+
afterHash: hashParagraph(paras[pi + 1]),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { bodyBlanks, cellBlanks };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Checks if a blank line existed at the given body position in the original document.
|
|
211
|
+
* Uses neighbor hashes to match positions after indices have shifted.
|
|
212
|
+
*/
|
|
213
|
+
export function wasOriginallyBlankAtBody(
|
|
214
|
+
snapshot: BlankLineSnapshot,
|
|
215
|
+
doc: Document,
|
|
216
|
+
index: number
|
|
217
|
+
): boolean {
|
|
218
|
+
const prev = index > 0 ? doc.getBodyElementAt(index - 1) : undefined;
|
|
219
|
+
const next =
|
|
220
|
+
index < doc.getBodyElementCount() - 1
|
|
221
|
+
? doc.getBodyElementAt(index + 1)
|
|
222
|
+
: undefined;
|
|
223
|
+
|
|
224
|
+
const prevHash = hashElement(prev);
|
|
225
|
+
const nextHash = hashElement(next);
|
|
226
|
+
|
|
227
|
+
return snapshot.bodyBlanks.some(
|
|
228
|
+
(b) => hashesMatch(b.beforeHash, prevHash) && hashesMatch(b.afterHash, nextHash)
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Checks if a blank line existed at the given cell position in the original document.
|
|
234
|
+
*/
|
|
235
|
+
export function wasOriginallyBlankInCell(
|
|
236
|
+
snapshot: BlankLineSnapshot,
|
|
237
|
+
cell: TableCell,
|
|
238
|
+
paraIndex: number,
|
|
239
|
+
cellId: string
|
|
240
|
+
): boolean {
|
|
241
|
+
const paras = cell.getParagraphs();
|
|
242
|
+
const prevHash = hashParagraph(paras[paraIndex - 1]);
|
|
243
|
+
const nextHash = hashParagraph(paras[paraIndex + 1]);
|
|
244
|
+
|
|
245
|
+
return snapshot.cellBlanks.some(
|
|
246
|
+
(b) =>
|
|
247
|
+
b.cellId === cellId &&
|
|
248
|
+
hashesMatch(b.beforeHash, prevHash) &&
|
|
249
|
+
hashesMatch(b.afterHash, nextHash)
|
|
250
|
+
);
|
|
251
|
+
}
|