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,1085 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DetailedHyperlinkInfo,
|
|
3
|
+
HyperlinkApiResponse,
|
|
4
|
+
HyperlinkApiResult,
|
|
5
|
+
HyperlinkApiSettings,
|
|
6
|
+
HyperlinkFixingOptions,
|
|
7
|
+
HyperlinkFixingResult,
|
|
8
|
+
HyperlinkProcessingOptions,
|
|
9
|
+
HyperlinkStatistics,
|
|
10
|
+
HyperlinkType,
|
|
11
|
+
HyperlinkValidationIssue,
|
|
12
|
+
PowerAutomateResponse,
|
|
13
|
+
URL_PATTERNS
|
|
14
|
+
} from '@/types/hyperlink';
|
|
15
|
+
import { Document } from '@/types/session';
|
|
16
|
+
import { UserSettings } from '@/types/settings';
|
|
17
|
+
import { logger } from '@/utils/logger';
|
|
18
|
+
import { sanitizeUrl, validatePowerAutomateUrl } from '@/utils/urlHelpers';
|
|
19
|
+
import { extractContentId, extractDocumentId } from '@/utils/urlPatterns';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extended API response with results cache for O(1) lookups
|
|
23
|
+
*/
|
|
24
|
+
interface HyperlinkApiResponseWithCache extends HyperlinkApiResponse {
|
|
25
|
+
resultsCache?: Map<string, HyperlinkApiResult>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* HyperlinkService - Singleton service for managing hyperlink operations
|
|
30
|
+
*
|
|
31
|
+
* Provides comprehensive hyperlink processing capabilities including:
|
|
32
|
+
* - PowerAutomate API integration for Content ID and Document ID lookups
|
|
33
|
+
* - URL pattern matching and validation
|
|
34
|
+
* - Hyperlink extraction and statistics
|
|
35
|
+
* - Retry logic with exponential backoff
|
|
36
|
+
* - Result caching for O(1) lookups
|
|
37
|
+
*
|
|
38
|
+
* @class HyperlinkService
|
|
39
|
+
* @singleton Use `HyperlinkService.getInstance()` to get the instance
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* // Get service instance
|
|
44
|
+
* const service = HyperlinkService.getInstance();
|
|
45
|
+
*
|
|
46
|
+
* // Initialize with user settings
|
|
47
|
+
* service.initialize(userSettings);
|
|
48
|
+
*
|
|
49
|
+
* // Process hyperlinks with API
|
|
50
|
+
* const response = await service.processHyperlinksWithApi(hyperlinks, apiSettings);
|
|
51
|
+
*
|
|
52
|
+
* // Validate hyperlinks
|
|
53
|
+
* const issues = await service.validateHyperlinks(document);
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @see {@link HyperlinkApiSettings} for API configuration options
|
|
57
|
+
* @see {@link DetailedHyperlinkInfo} for hyperlink data structure
|
|
58
|
+
*/
|
|
59
|
+
export class HyperlinkService {
|
|
60
|
+
private static instance: HyperlinkService;
|
|
61
|
+
private apiSettings: HyperlinkApiSettings | null = null;
|
|
62
|
+
private userSettings: UserSettings | null = null;
|
|
63
|
+
private log = logger.namespace('HyperlinkService');
|
|
64
|
+
|
|
65
|
+
private constructor() {}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the singleton instance of HyperlinkService.
|
|
69
|
+
*
|
|
70
|
+
* @returns {HyperlinkService} The singleton instance
|
|
71
|
+
* @static
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const service = HyperlinkService.getInstance();
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
public static getInstance(): HyperlinkService {
|
|
79
|
+
if (!HyperlinkService.instance) {
|
|
80
|
+
HyperlinkService.instance = new HyperlinkService();
|
|
81
|
+
}
|
|
82
|
+
return HyperlinkService.instance;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Initialize the service with user settings.
|
|
87
|
+
*
|
|
88
|
+
* Configures the PowerAutomate API endpoint and validates the URL format.
|
|
89
|
+
* Should be called during application startup with the user's saved settings.
|
|
90
|
+
*
|
|
91
|
+
* @param {UserSettings} settings - User settings containing API configuration
|
|
92
|
+
* @throws {void} Does not throw - logs errors and continues with potentially invalid config
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* const service = HyperlinkService.getInstance();
|
|
97
|
+
* service.initialize({
|
|
98
|
+
* apiConnections: {
|
|
99
|
+
* powerAutomateUrl: 'https://prod-XX.westus.logic.azure.com/...'
|
|
100
|
+
* }
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
public initialize(settings: UserSettings): void {
|
|
105
|
+
// Store full settings for local dictionary access
|
|
106
|
+
this.userSettings = settings;
|
|
107
|
+
|
|
108
|
+
if (settings.apiConnections.powerAutomateUrl) {
|
|
109
|
+
// Sanitize the API URL to fix encoding issues
|
|
110
|
+
const sanitizedUrl = sanitizeUrl(settings.apiConnections.powerAutomateUrl);
|
|
111
|
+
|
|
112
|
+
// Validate URL format
|
|
113
|
+
const validation = validatePowerAutomateUrl(sanitizedUrl);
|
|
114
|
+
if (!validation.valid) {
|
|
115
|
+
this.log.error('Invalid PowerAutomate URL configuration:', validation.issues);
|
|
116
|
+
// Still set it but log the errors - let API call handle the failure
|
|
117
|
+
} else if (validation.warnings.length > 0) {
|
|
118
|
+
this.log.warn('PowerAutomate URL warnings:', validation.warnings);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
this.apiSettings = {
|
|
122
|
+
apiUrl: sanitizedUrl,
|
|
123
|
+
timeout: 30000,
|
|
124
|
+
retryAttempts: 3,
|
|
125
|
+
retryDelay: 1000,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
this.log.debug('Initialized API settings with sanitized URL:', sanitizedUrl);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Log local dictionary status
|
|
132
|
+
if (settings.localDictionary?.enabled) {
|
|
133
|
+
this.log.info('Local dictionary mode enabled - using SQLite database for lookups');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Check if local dictionary mode is enabled
|
|
139
|
+
*/
|
|
140
|
+
public isLocalDictionaryEnabled(): boolean {
|
|
141
|
+
return this.userSettings?.localDictionary?.enabled ?? false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Process hyperlinks using local dictionary (SQLite database)
|
|
146
|
+
* This is used when local dictionary mode is enabled instead of Power Automate API
|
|
147
|
+
*/
|
|
148
|
+
private async processHyperlinksWithLocalDictionary(
|
|
149
|
+
hyperlinks: DetailedHyperlinkInfo[]
|
|
150
|
+
): Promise<HyperlinkApiResponse & { processedHyperlinks?: DetailedHyperlinkInfo[] }> {
|
|
151
|
+
try {
|
|
152
|
+
// Extract all IDs into a single lookup array
|
|
153
|
+
const lookupIds: string[] = [];
|
|
154
|
+
const uniqueIds = new Set<string>();
|
|
155
|
+
|
|
156
|
+
for (const hyperlink of hyperlinks) {
|
|
157
|
+
const contentId = extractContentId(hyperlink.url);
|
|
158
|
+
if (contentId && !uniqueIds.has(contentId)) {
|
|
159
|
+
lookupIds.push(contentId);
|
|
160
|
+
uniqueIds.add(contentId);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const documentId = extractDocumentId(hyperlink.url);
|
|
164
|
+
if (documentId && !uniqueIds.has(documentId)) {
|
|
165
|
+
lookupIds.push(documentId);
|
|
166
|
+
uniqueIds.add(documentId);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (lookupIds.length === 0) {
|
|
171
|
+
// No IDs found is not a failure - it just means no hyperlinks need API processing
|
|
172
|
+
// Return success with empty results so other formatting operations can continue
|
|
173
|
+
this.log.warn('No Content_ID or Document_ID patterns found in hyperlinks - skipping API call');
|
|
174
|
+
return {
|
|
175
|
+
success: true,
|
|
176
|
+
timestamp: new Date(),
|
|
177
|
+
body: {
|
|
178
|
+
results: [],
|
|
179
|
+
errors: [],
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check if electronAPI is available (handles SSR, tests, and browser contexts)
|
|
185
|
+
if (typeof window === 'undefined' || !window.electronAPI) {
|
|
186
|
+
return {
|
|
187
|
+
success: false,
|
|
188
|
+
timestamp: new Date(),
|
|
189
|
+
error: 'Electron API not available - local dictionary requires Electron environment',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.log.info(`Processing ${lookupIds.length} IDs via local dictionary`);
|
|
194
|
+
|
|
195
|
+
// Call local dictionary batch lookup
|
|
196
|
+
const response = await window.electronAPI.dictionary.batchLookup(lookupIds);
|
|
197
|
+
|
|
198
|
+
if (!response.success || !response.results) {
|
|
199
|
+
return {
|
|
200
|
+
success: false,
|
|
201
|
+
timestamp: new Date(),
|
|
202
|
+
error: response.error || 'Local dictionary lookup failed',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.log.info(`Local dictionary returned ${response.results.length} results`);
|
|
207
|
+
|
|
208
|
+
// Build results cache for O(1) lookups
|
|
209
|
+
const resultsMap = new Map<string, HyperlinkApiResult>();
|
|
210
|
+
|
|
211
|
+
const results: HyperlinkApiResult[] = response.results.map((result) => {
|
|
212
|
+
// Map local dictionary result to HyperlinkApiResult format
|
|
213
|
+
const normalizedStatus: HyperlinkApiResult['status'] =
|
|
214
|
+
result.Status?.toLowerCase() === 'deprecated'
|
|
215
|
+
? 'deprecated'
|
|
216
|
+
: result.Status?.toLowerCase() === 'expired'
|
|
217
|
+
? 'expired'
|
|
218
|
+
: result.Status?.toLowerCase() === 'not_found'
|
|
219
|
+
? 'not_found'
|
|
220
|
+
: 'active';
|
|
221
|
+
|
|
222
|
+
const processed: HyperlinkApiResult = {
|
|
223
|
+
url: '',
|
|
224
|
+
documentId: result.Document_ID || '',
|
|
225
|
+
contentId: result.Content_ID || '',
|
|
226
|
+
title: result.Title || '',
|
|
227
|
+
status: normalizedStatus,
|
|
228
|
+
metadata: {},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Cache by both IDs for quick lookup
|
|
232
|
+
if (processed.documentId) resultsMap.set(processed.documentId, processed);
|
|
233
|
+
if (processed.contentId) resultsMap.set(processed.contentId, processed);
|
|
234
|
+
|
|
235
|
+
return processed;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const apiResponse: HyperlinkApiResponse = {
|
|
239
|
+
success: true,
|
|
240
|
+
timestamp: new Date(),
|
|
241
|
+
body: {
|
|
242
|
+
results,
|
|
243
|
+
errors: [],
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Attach cache for O(1) lookups
|
|
248
|
+
(apiResponse as HyperlinkApiResponseWithCache).resultsCache = resultsMap;
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
...apiResponse,
|
|
252
|
+
processedHyperlinks: hyperlinks,
|
|
253
|
+
};
|
|
254
|
+
} catch (error) {
|
|
255
|
+
this.log.error('Local dictionary lookup error:', error);
|
|
256
|
+
return {
|
|
257
|
+
success: false,
|
|
258
|
+
timestamp: new Date(),
|
|
259
|
+
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Extract hyperlink IDs from documents
|
|
266
|
+
* This mimics the C# ExtractHyperlinkDataAsync functionality
|
|
267
|
+
*/
|
|
268
|
+
public async extractHyperlinkData(document: Document): Promise<DetailedHyperlinkInfo[]> {
|
|
269
|
+
const hyperlinks: DetailedHyperlinkInfo[] = [];
|
|
270
|
+
|
|
271
|
+
// In a real implementation, this would parse the document
|
|
272
|
+
// For now, we'll simulate extraction
|
|
273
|
+
this.log.debug(`Extracting hyperlinks from document: ${document.name}`);
|
|
274
|
+
|
|
275
|
+
// Simulate finding hyperlinks
|
|
276
|
+
const mockHyperlinks: DetailedHyperlinkInfo[] = [
|
|
277
|
+
{
|
|
278
|
+
id: 'rId1',
|
|
279
|
+
relationshipId: 'rId1',
|
|
280
|
+
element: {} as any,
|
|
281
|
+
containingPart: 'document.xml',
|
|
282
|
+
url: 'https://thesource.cvshealth.com/docid=TSRC-ABC-123456',
|
|
283
|
+
displayText: 'Click here',
|
|
284
|
+
type: 'external' as HyperlinkType,
|
|
285
|
+
isInternal: false,
|
|
286
|
+
isValid: true,
|
|
287
|
+
context: 'See the document here: Click here for more information.',
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: 'rId2',
|
|
291
|
+
relationshipId: 'rId2',
|
|
292
|
+
element: {} as any,
|
|
293
|
+
containingPart: 'document.xml',
|
|
294
|
+
url: 'https://thesource.cvshealth.com/docid=CMS-XYZ-789012',
|
|
295
|
+
displayText: 'Reference',
|
|
296
|
+
type: 'external' as HyperlinkType,
|
|
297
|
+
isInternal: false,
|
|
298
|
+
isValid: true,
|
|
299
|
+
context: 'For additional details, see the Reference link.',
|
|
300
|
+
},
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
return mockHyperlinks;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Process hyperlinks with PowerAutomate API.
|
|
308
|
+
*
|
|
309
|
+
* Sends collected Content IDs and Document IDs to the configured PowerAutomate endpoint
|
|
310
|
+
* for lookup. Returns updated hyperlink information including new URLs and display texts.
|
|
311
|
+
*
|
|
312
|
+
* **Processing Flow:**
|
|
313
|
+
* 1. Extract unique Content_ID and Document_ID from hyperlinks
|
|
314
|
+
* 2. Send batch request to PowerAutomate API
|
|
315
|
+
* 3. Build O(1) lookup cache from results
|
|
316
|
+
* 4. Match results back to original hyperlinks
|
|
317
|
+
*
|
|
318
|
+
* @param {DetailedHyperlinkInfo[]} hyperlinks - Array of hyperlinks to process
|
|
319
|
+
* @param {HyperlinkApiSettings} [settings] - Optional API settings (uses initialized settings if not provided)
|
|
320
|
+
* @param {Object} [userProfile] - Optional user profile for tracking
|
|
321
|
+
* @param {string} userProfile.firstName - User's first name
|
|
322
|
+
* @param {string} userProfile.lastName - User's last name
|
|
323
|
+
* @param {string} userProfile.email - User's email
|
|
324
|
+
* @returns {Promise<HyperlinkApiResponse & { processedHyperlinks?: DetailedHyperlinkInfo[] }>} API response with processed hyperlinks
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```typescript
|
|
328
|
+
* const response = await service.processHyperlinksWithApi(hyperlinks, {
|
|
329
|
+
* apiUrl: 'https://prod-XX.westus.logic.azure.com/...',
|
|
330
|
+
* timeout: 30000,
|
|
331
|
+
* retryAttempts: 3
|
|
332
|
+
* });
|
|
333
|
+
*
|
|
334
|
+
* if (response.success) {
|
|
335
|
+
* console.log(`Processed ${response.results?.length} IDs`);
|
|
336
|
+
* }
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
public async processHyperlinksWithApi(
|
|
340
|
+
hyperlinks: DetailedHyperlinkInfo[],
|
|
341
|
+
settings?: HyperlinkApiSettings,
|
|
342
|
+
userProfile?: { firstName: string; lastName: string; email: string }
|
|
343
|
+
): Promise<HyperlinkApiResponse & { processedHyperlinks?: DetailedHyperlinkInfo[] }> {
|
|
344
|
+
// Check if local dictionary mode is enabled
|
|
345
|
+
// When enabled, use local SQLite database instead of Power Automate API
|
|
346
|
+
if (this.isLocalDictionaryEnabled()) {
|
|
347
|
+
this.log.info('Using local dictionary for hyperlink processing (API call bypassed)');
|
|
348
|
+
return this.processHyperlinksWithLocalDictionary(hyperlinks);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const apiConfig = settings || this.apiSettings;
|
|
352
|
+
|
|
353
|
+
if (!apiConfig) {
|
|
354
|
+
return {
|
|
355
|
+
success: false,
|
|
356
|
+
timestamp: new Date(),
|
|
357
|
+
error: 'No API settings configured. Please configure PowerAutomate URL in settings.',
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
// Extract all IDs (Content_ID and Document_ID) into a single Lookup_ID array
|
|
363
|
+
const lookupIds: string[] = [];
|
|
364
|
+
// Use Set to avoid duplicates
|
|
365
|
+
const uniqueIds = new Set<string>();
|
|
366
|
+
|
|
367
|
+
for (const hyperlink of hyperlinks) {
|
|
368
|
+
// Extract Content_ID if present
|
|
369
|
+
const contentId = extractContentId(hyperlink.url);
|
|
370
|
+
if (contentId && !uniqueIds.has(contentId)) {
|
|
371
|
+
lookupIds.push(contentId);
|
|
372
|
+
uniqueIds.add(contentId);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Extract Document_ID if present
|
|
376
|
+
const documentId = extractDocumentId(hyperlink.url);
|
|
377
|
+
if (documentId && !uniqueIds.has(documentId)) {
|
|
378
|
+
lookupIds.push(documentId);
|
|
379
|
+
uniqueIds.add(documentId);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// If no IDs found, return early
|
|
384
|
+
if (lookupIds.length === 0) {
|
|
385
|
+
// No IDs found is not a failure - it just means no hyperlinks need API processing
|
|
386
|
+
// Return success with empty results so other formatting operations can continue
|
|
387
|
+
this.log.warn('No Content_ID or Document_ID patterns found in hyperlinks - skipping API call');
|
|
388
|
+
return {
|
|
389
|
+
success: true,
|
|
390
|
+
timestamp: new Date(),
|
|
391
|
+
body: {
|
|
392
|
+
results: [],
|
|
393
|
+
errors: [],
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Calculate hyperlink statistics
|
|
399
|
+
const totalHyperlinks = hyperlinks.length;
|
|
400
|
+
const hyperlinksChecked = hyperlinks.filter((h) =>
|
|
401
|
+
URL_PATTERNS.THE_SOURCE.pattern.test(h.url)
|
|
402
|
+
).length;
|
|
403
|
+
|
|
404
|
+
// Create request payload matching the specification with profile data and statistics
|
|
405
|
+
const request = {
|
|
406
|
+
Lookup_ID: lookupIds,
|
|
407
|
+
Hyperlinks_Checked: hyperlinksChecked,
|
|
408
|
+
Total_Hyperlinks: totalHyperlinks,
|
|
409
|
+
First_Name: userProfile?.firstName || '',
|
|
410
|
+
Last_Name: userProfile?.lastName || '',
|
|
411
|
+
Email: userProfile?.email || '',
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// Make API call
|
|
415
|
+
const response = await this.callPowerAutomateApi(apiConfig, request);
|
|
416
|
+
|
|
417
|
+
// Return response with processed hyperlinks for tracking
|
|
418
|
+
return {
|
|
419
|
+
...response,
|
|
420
|
+
processedHyperlinks: hyperlinks,
|
|
421
|
+
};
|
|
422
|
+
} catch (error) {
|
|
423
|
+
return {
|
|
424
|
+
success: false,
|
|
425
|
+
timestamp: new Date(),
|
|
426
|
+
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Fix source hyperlinks by appending Content IDs.
|
|
433
|
+
*
|
|
434
|
+
* Main hyperlink processing method that extracts, validates, and modifies
|
|
435
|
+
* hyperlinks in a document. Matches the functionality of the C# implementation.
|
|
436
|
+
*
|
|
437
|
+
* **Processing Phases:**
|
|
438
|
+
* 1. Extract hyperlinks from document
|
|
439
|
+
* 2. Call PowerAutomate API (if configured)
|
|
440
|
+
* 3. Append Content IDs to matching URLs
|
|
441
|
+
* 4. Update display texts as needed
|
|
442
|
+
*
|
|
443
|
+
* @param {Document} document - The document to process
|
|
444
|
+
* @param {HyperlinkFixingOptions} [options={}] - Processing options
|
|
445
|
+
* @param {string} [options.powerAutomateUrl] - PowerAutomate API endpoint URL
|
|
446
|
+
* @param {boolean} [options.appendContentId] - Whether to append content IDs
|
|
447
|
+
* @param {string} [options.contentIdToAppend] - Content ID to append (default: '#content')
|
|
448
|
+
* @param {boolean} [options.updateTitles] - Whether to update hyperlink titles
|
|
449
|
+
* @returns {Promise<HyperlinkFixingResult>} Processing results with statistics
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* ```typescript
|
|
453
|
+
* const result = await service.fixSourceHyperlinks(document, {
|
|
454
|
+
* appendContentId: true,
|
|
455
|
+
* contentIdToAppend: '#content',
|
|
456
|
+
* updateTitles: true
|
|
457
|
+
* });
|
|
458
|
+
*
|
|
459
|
+
* console.log(`Modified ${result.modifiedHyperlinks} hyperlinks`);
|
|
460
|
+
* ```
|
|
461
|
+
*/
|
|
462
|
+
public async fixSourceHyperlinks(
|
|
463
|
+
document: Document,
|
|
464
|
+
options: HyperlinkFixingOptions = {}
|
|
465
|
+
): Promise<HyperlinkFixingResult> {
|
|
466
|
+
const startTime = Date.now();
|
|
467
|
+
const result: HyperlinkFixingResult = {
|
|
468
|
+
success: false,
|
|
469
|
+
totalHyperlinks: 0,
|
|
470
|
+
processedHyperlinks: 0,
|
|
471
|
+
modifiedHyperlinks: 0,
|
|
472
|
+
skippedHyperlinks: 0,
|
|
473
|
+
updatedUrls: 0,
|
|
474
|
+
updatedDisplayTexts: 0,
|
|
475
|
+
appendedContentIds: 0,
|
|
476
|
+
errorCount: 0,
|
|
477
|
+
errorMessages: [],
|
|
478
|
+
processedLinks: [],
|
|
479
|
+
validationIssues: [],
|
|
480
|
+
duration: 0,
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
// Phase 1: Extract hyperlinks
|
|
485
|
+
const hyperlinks = await this.extractHyperlinkData(document);
|
|
486
|
+
result.totalHyperlinks = hyperlinks.length;
|
|
487
|
+
|
|
488
|
+
// Filter processable hyperlinks
|
|
489
|
+
const processableHyperlinks = hyperlinks.filter((h) =>
|
|
490
|
+
this.shouldProcessHyperlink(h, options)
|
|
491
|
+
);
|
|
492
|
+
result.processedHyperlinks = processableHyperlinks.length;
|
|
493
|
+
result.skippedHyperlinks = hyperlinks.length - processableHyperlinks.length;
|
|
494
|
+
|
|
495
|
+
if (processableHyperlinks.length === 0) {
|
|
496
|
+
result.success = true;
|
|
497
|
+
result.errorMessages.push('No processable hyperlinks found in the document');
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Phase 2: API Communication (if configured)
|
|
502
|
+
if (options.powerAutomateUrl || this.apiSettings) {
|
|
503
|
+
const apiResponse = await this.processHyperlinksWithApi(processableHyperlinks);
|
|
504
|
+
if (apiResponse.success && apiResponse.body?.results) {
|
|
505
|
+
// Apply API-based fixes
|
|
506
|
+
const apiFixResult = this.applyApiBasedFixes(processableHyperlinks, apiResponse);
|
|
507
|
+
result.updatedUrls += apiFixResult.updatedUrls;
|
|
508
|
+
result.updatedDisplayTexts += apiFixResult.updatedDisplayTexts;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Phase 3: Apply content ID appending if configured
|
|
513
|
+
if (options.appendContentId) {
|
|
514
|
+
const contentId = options.contentIdToAppend || '#content';
|
|
515
|
+
const appendResult = this.appendContentIdToHyperlinks(processableHyperlinks, contentId);
|
|
516
|
+
result.appendedContentIds = appendResult.appendedCount;
|
|
517
|
+
result.updatedUrls += appendResult.appendedCount;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Phase 4: Update titles if requested
|
|
521
|
+
if (options.updateTitles) {
|
|
522
|
+
const titleResult = this.updateHyperlinkTitles(processableHyperlinks);
|
|
523
|
+
result.updatedDisplayTexts += titleResult.updatedCount;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Create summaries for processed links
|
|
527
|
+
result.processedLinks = processableHyperlinks.map((h) => ({
|
|
528
|
+
id: h.id,
|
|
529
|
+
url: h.url,
|
|
530
|
+
displayText: h.displayText,
|
|
531
|
+
type: h.type,
|
|
532
|
+
location: h.containingPart,
|
|
533
|
+
status: 'processed' as const,
|
|
534
|
+
modifications: [],
|
|
535
|
+
}));
|
|
536
|
+
|
|
537
|
+
result.modifiedHyperlinks = result.updatedUrls + result.updatedDisplayTexts;
|
|
538
|
+
result.success = true;
|
|
539
|
+
} catch (error) {
|
|
540
|
+
result.errorMessages.push(error instanceof Error ? error.message : 'Unknown error occurred');
|
|
541
|
+
} finally {
|
|
542
|
+
result.duration = Date.now() - startTime;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return result;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Validate hyperlinks in a document.
|
|
550
|
+
*
|
|
551
|
+
* Checks all hyperlinks for common issues including:
|
|
552
|
+
* - Invalid URL format
|
|
553
|
+
* - Missing Content ID in theSource URLs
|
|
554
|
+
* - Broken internal links
|
|
555
|
+
* - Empty display text
|
|
556
|
+
*
|
|
557
|
+
* @param {Document} document - The document to validate
|
|
558
|
+
* @returns {Promise<HyperlinkValidationIssue[]>} Array of validation issues found
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* ```typescript
|
|
562
|
+
* const issues = await service.validateHyperlinks(document);
|
|
563
|
+
* if (issues.length > 0) {
|
|
564
|
+
* console.log(`Found ${issues.length} issues:`);
|
|
565
|
+
* issues.forEach(issue => console.log(` - ${issue.message}`));
|
|
566
|
+
* }
|
|
567
|
+
* ```
|
|
568
|
+
*/
|
|
569
|
+
public async validateHyperlinks(document: Document): Promise<HyperlinkValidationIssue[]> {
|
|
570
|
+
const issues: HyperlinkValidationIssue[] = [];
|
|
571
|
+
const hyperlinks = await this.extractHyperlinkData(document);
|
|
572
|
+
|
|
573
|
+
for (const hyperlink of hyperlinks) {
|
|
574
|
+
// Check for invalid URL format
|
|
575
|
+
if (!this.isValidUrl(hyperlink.url)) {
|
|
576
|
+
issues.push({
|
|
577
|
+
hyperlinkId: hyperlink.id,
|
|
578
|
+
url: hyperlink.url,
|
|
579
|
+
issueType: 'invalid_url',
|
|
580
|
+
severity: 'error',
|
|
581
|
+
message: 'Invalid URL format',
|
|
582
|
+
suggestion: 'Fix the URL format or remove the hyperlink',
|
|
583
|
+
autoFixable: false,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Check if theSource URLs need content ID
|
|
588
|
+
if (this.isTheSourceUrl(hyperlink.url) && this.needsContentId(hyperlink.url)) {
|
|
589
|
+
issues.push({
|
|
590
|
+
hyperlinkId: hyperlink.id,
|
|
591
|
+
url: hyperlink.url,
|
|
592
|
+
issueType: 'invalid_url',
|
|
593
|
+
severity: 'warning',
|
|
594
|
+
message: 'TheSource URL missing content ID',
|
|
595
|
+
suggestion: 'Append #content to the URL',
|
|
596
|
+
autoFixable: true,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return issues;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Get statistics about hyperlinks in a document.
|
|
606
|
+
*
|
|
607
|
+
* Analyzes all hyperlinks and returns detailed statistics including
|
|
608
|
+
* counts by type, validity, and domain distribution.
|
|
609
|
+
*
|
|
610
|
+
* @param {Document} document - The document to analyze
|
|
611
|
+
* @returns {Promise<HyperlinkStatistics>} Comprehensive hyperlink statistics
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* ```typescript
|
|
615
|
+
* const stats = await service.getHyperlinkStatistics(document);
|
|
616
|
+
* console.log(`Total hyperlinks: ${stats.total}`);
|
|
617
|
+
* console.log(`External: ${stats.byType.external}`);
|
|
618
|
+
* console.log(`Internal: ${stats.byType.internal}`);
|
|
619
|
+
* console.log(`Valid: ${stats.validCount}, Invalid: ${stats.invalidCount}`);
|
|
620
|
+
* ```
|
|
621
|
+
*/
|
|
622
|
+
public async getHyperlinkStatistics(document: Document): Promise<HyperlinkStatistics> {
|
|
623
|
+
const hyperlinks = await this.extractHyperlinkData(document);
|
|
624
|
+
|
|
625
|
+
const stats: HyperlinkStatistics = {
|
|
626
|
+
total: hyperlinks.length,
|
|
627
|
+
byType: {
|
|
628
|
+
external: 0,
|
|
629
|
+
internal: 0,
|
|
630
|
+
bookmark: 0,
|
|
631
|
+
email: 0,
|
|
632
|
+
file: 0,
|
|
633
|
+
},
|
|
634
|
+
byLocation: {
|
|
635
|
+
document: 0,
|
|
636
|
+
headers: 0,
|
|
637
|
+
footers: 0,
|
|
638
|
+
},
|
|
639
|
+
valid: 0,
|
|
640
|
+
invalid: 0,
|
|
641
|
+
withContentId: 0,
|
|
642
|
+
withoutContentId: 0,
|
|
643
|
+
duplicates: 0,
|
|
644
|
+
orphaned: 0,
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const urlSet = new Set<string>();
|
|
648
|
+
|
|
649
|
+
for (const hyperlink of hyperlinks) {
|
|
650
|
+
// Count by type
|
|
651
|
+
stats.byType[hyperlink.type]++;
|
|
652
|
+
|
|
653
|
+
// Count by location
|
|
654
|
+
if (hyperlink.containingPart.includes('header')) {
|
|
655
|
+
stats.byLocation.headers++;
|
|
656
|
+
} else if (hyperlink.containingPart.includes('footer')) {
|
|
657
|
+
stats.byLocation.footers++;
|
|
658
|
+
} else {
|
|
659
|
+
stats.byLocation.document++;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Count valid/invalid
|
|
663
|
+
if (hyperlink.isValid) {
|
|
664
|
+
stats.valid++;
|
|
665
|
+
} else {
|
|
666
|
+
stats.invalid++;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Count content IDs
|
|
670
|
+
if (hyperlink.url.includes('#content')) {
|
|
671
|
+
stats.withContentId++;
|
|
672
|
+
} else {
|
|
673
|
+
stats.withoutContentId++;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Check for duplicates
|
|
677
|
+
if (urlSet.has(hyperlink.url)) {
|
|
678
|
+
stats.duplicates++;
|
|
679
|
+
} else {
|
|
680
|
+
urlSet.add(hyperlink.url);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return stats;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Private helper methods
|
|
688
|
+
|
|
689
|
+
private shouldProcessHyperlink(
|
|
690
|
+
hyperlink: DetailedHyperlinkInfo,
|
|
691
|
+
options: HyperlinkProcessingOptions
|
|
692
|
+
): boolean {
|
|
693
|
+
if (hyperlink.isInternal && !options.processInternalLinks) return false;
|
|
694
|
+
if (!hyperlink.isInternal && !options.processExternalLinks) return false;
|
|
695
|
+
|
|
696
|
+
if (options.urlPattern) {
|
|
697
|
+
const pattern =
|
|
698
|
+
typeof options.urlPattern === 'string'
|
|
699
|
+
? new RegExp(options.urlPattern, 'i')
|
|
700
|
+
: options.urlPattern;
|
|
701
|
+
if (!pattern.test(hyperlink.url)) return false;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (options.displayTextPattern) {
|
|
705
|
+
const pattern =
|
|
706
|
+
typeof options.displayTextPattern === 'string'
|
|
707
|
+
? new RegExp(options.displayTextPattern, 'i')
|
|
708
|
+
: options.displayTextPattern;
|
|
709
|
+
if (!pattern.test(hyperlink.displayText)) return false;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private applyApiBasedFixes(
|
|
716
|
+
hyperlinks: DetailedHyperlinkInfo[],
|
|
717
|
+
apiResponse: HyperlinkApiResponse
|
|
718
|
+
): { updatedUrls: number; updatedDisplayTexts: number } {
|
|
719
|
+
let updatedUrls = 0;
|
|
720
|
+
let updatedDisplayTexts = 0;
|
|
721
|
+
|
|
722
|
+
if (!apiResponse.body?.results) return { updatedUrls, updatedDisplayTexts };
|
|
723
|
+
|
|
724
|
+
// Use cache for O(1) lookups if available
|
|
725
|
+
const apiResponseWithCache = apiResponse as HyperlinkApiResponseWithCache;
|
|
726
|
+
const resultsCache = apiResponseWithCache.resultsCache || new Map<string, HyperlinkApiResult>();
|
|
727
|
+
|
|
728
|
+
for (const hyperlink of hyperlinks) {
|
|
729
|
+
// CRITICAL PRE-FILTER: Extract IDs to determine if this hyperlink is processable
|
|
730
|
+
// Only hyperlinks with Content_ID or Document_ID patterns should be processed
|
|
731
|
+
const urlContentId = extractContentId(hyperlink.url);
|
|
732
|
+
const urlDocumentId = extractDocumentId(hyperlink.url);
|
|
733
|
+
|
|
734
|
+
// SKIP: This hyperlink doesn't contain Content_ID or Document_ID patterns
|
|
735
|
+
// Examples: external URLs, mailto links, internal bookmarks
|
|
736
|
+
if (!urlContentId && !urlDocumentId) {
|
|
737
|
+
this.log.debug(
|
|
738
|
+
`Skipping hyperlink (no Lookup_ID pattern): ${hyperlink.url.substring(0, 80)}`
|
|
739
|
+
);
|
|
740
|
+
continue; // Skip to next hyperlink - no API processing needed
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Try cache first for performance
|
|
744
|
+
let apiResult = null;
|
|
745
|
+
if (urlContentId && resultsCache.has(urlContentId)) {
|
|
746
|
+
apiResult = resultsCache.get(urlContentId);
|
|
747
|
+
} else if (urlDocumentId && resultsCache.has(urlDocumentId)) {
|
|
748
|
+
apiResult = resultsCache.get(urlDocumentId);
|
|
749
|
+
} else {
|
|
750
|
+
// Fallback to array search
|
|
751
|
+
apiResult = apiResponse.body.results.find((r) => {
|
|
752
|
+
if (urlContentId && r.contentId === urlContentId) return true;
|
|
753
|
+
if (urlDocumentId && r.documentId === urlDocumentId) return true;
|
|
754
|
+
return false;
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (apiResult) {
|
|
759
|
+
// Phase 3: URL Reconstruction
|
|
760
|
+
// Always use Document_ID in URL (never Content_ID)
|
|
761
|
+
const newUrl = `https://thesource.cvshealth.com/nuxeo/thesource/#!/view?docid=${apiResult.documentId}`;
|
|
762
|
+
if (newUrl !== hyperlink.url && apiResult.documentId) {
|
|
763
|
+
hyperlink.url = newUrl;
|
|
764
|
+
updatedUrls++;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Phase 4: Display Text Rules
|
|
768
|
+
let newDisplayText = hyperlink.displayText;
|
|
769
|
+
|
|
770
|
+
// Remove existing Content_ID pattern (4-6 digits in parentheses)
|
|
771
|
+
newDisplayText = newDisplayText.replace(/\s*\(\d{4,6}\)\s*$/, '');
|
|
772
|
+
|
|
773
|
+
// Update title if mismatch detected
|
|
774
|
+
if (apiResult.title && newDisplayText.trim() !== apiResult.title.trim()) {
|
|
775
|
+
newDisplayText = apiResult.title;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Append Content_ID (last 6 digits) if present
|
|
779
|
+
if (apiResult.contentId) {
|
|
780
|
+
// Extract last digits and pad with zeros if needed
|
|
781
|
+
const contentIdMatch = apiResult.contentId.match(/(\d+)$/);
|
|
782
|
+
if (contentIdMatch) {
|
|
783
|
+
const digits = contentIdMatch[1].padStart(6, '0').slice(-6);
|
|
784
|
+
newDisplayText = `${newDisplayText.trim()} (${digits})`;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Add status indicators for deprecated or expired documents
|
|
789
|
+
if (apiResult.status === 'deprecated' || apiResult.status === 'expired') {
|
|
790
|
+
newDisplayText += ' - Expired';
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (newDisplayText !== hyperlink.displayText) {
|
|
794
|
+
hyperlink.displayText = newDisplayText;
|
|
795
|
+
updatedDisplayTexts++;
|
|
796
|
+
}
|
|
797
|
+
} else {
|
|
798
|
+
// API result not found - BUT only mark as "Not Found" if we extracted valid IDs
|
|
799
|
+
// This prevents marking external/internal links that were never meant to be processed
|
|
800
|
+
// (Note: We only reach here if urlContentId or urlDocumentId exists, due to the pre-filter above)
|
|
801
|
+
this.log.warn(`No API result for hyperlink with Lookup_ID: ${hyperlink.url}`);
|
|
802
|
+
if (!hyperlink.displayText.includes(' - Not Found')) {
|
|
803
|
+
hyperlink.displayText += ' - Not Found';
|
|
804
|
+
updatedDisplayTexts++;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return { updatedUrls, updatedDisplayTexts };
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
private appendContentIdToHyperlinks(
|
|
813
|
+
hyperlinks: DetailedHyperlinkInfo[],
|
|
814
|
+
contentId: string
|
|
815
|
+
): { appendedCount: number } {
|
|
816
|
+
let appendedCount = 0;
|
|
817
|
+
|
|
818
|
+
for (const hyperlink of hyperlinks) {
|
|
819
|
+
if (
|
|
820
|
+
this.isTheSourceUrl(hyperlink.url) &&
|
|
821
|
+
this.needsContentId(hyperlink.url) &&
|
|
822
|
+
!hyperlink.url.includes(contentId)
|
|
823
|
+
) {
|
|
824
|
+
hyperlink.url += contentId;
|
|
825
|
+
appendedCount++;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return { appendedCount };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
private updateHyperlinkTitles(hyperlinks: DetailedHyperlinkInfo[]): { updatedCount: number } {
|
|
833
|
+
let updatedCount = 0;
|
|
834
|
+
|
|
835
|
+
for (const hyperlink of hyperlinks) {
|
|
836
|
+
const newTitle = this.extractTitleFromUrl(hyperlink.url);
|
|
837
|
+
if (newTitle && newTitle !== hyperlink.displayText) {
|
|
838
|
+
hyperlink.displayText = newTitle;
|
|
839
|
+
updatedCount++;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return { updatedCount };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
private async callPowerAutomateApi(
|
|
847
|
+
settings: HyperlinkApiSettings,
|
|
848
|
+
request: {
|
|
849
|
+
Lookup_ID: string[];
|
|
850
|
+
Hyperlinks_Checked: number;
|
|
851
|
+
Total_Hyperlinks: number;
|
|
852
|
+
First_Name: string;
|
|
853
|
+
Last_Name: string;
|
|
854
|
+
Email: string;
|
|
855
|
+
}
|
|
856
|
+
): Promise<HyperlinkApiResponse> {
|
|
857
|
+
const timeoutMs = settings.timeout || 30000;
|
|
858
|
+
const maxRetries = settings.retryAttempts || 3;
|
|
859
|
+
|
|
860
|
+
// =========================================================================
|
|
861
|
+
// COMPREHENSIVE LOGGING - API CALL START
|
|
862
|
+
// =========================================================================
|
|
863
|
+
this.log.info('═══════════════════════════════════════════════════════════════');
|
|
864
|
+
this.log.info('[HyperlinkService] Starting Power Automate API Call');
|
|
865
|
+
this.log.info('═══════════════════════════════════════════════════════════════');
|
|
866
|
+
this.log.info(`[HyperlinkService] Timestamp: ${new Date().toISOString()}`);
|
|
867
|
+
this.log.info(`[HyperlinkService] Lookup IDs: ${request.Lookup_ID.length} IDs`);
|
|
868
|
+
this.log.info(`[HyperlinkService] IDs: ${request.Lookup_ID.join(', ')}`);
|
|
869
|
+
this.log.info(`[HyperlinkService] Hyperlinks Checked: ${request.Hyperlinks_Checked}`);
|
|
870
|
+
this.log.info(`[HyperlinkService] Total Hyperlinks: ${request.Total_Hyperlinks}`);
|
|
871
|
+
this.log.info(`[HyperlinkService] User: ${request.First_Name} ${request.Last_Name}`);
|
|
872
|
+
this.log.info(`[HyperlinkService] Timeout: ${timeoutMs}ms`);
|
|
873
|
+
this.log.info(`[HyperlinkService] Max Retries: ${maxRetries}`);
|
|
874
|
+
|
|
875
|
+
// Sanitize the API URL to fix any encoding issues
|
|
876
|
+
const sanitizedUrl = sanitizeUrl(settings.apiUrl);
|
|
877
|
+
|
|
878
|
+
if (sanitizedUrl !== settings.apiUrl) {
|
|
879
|
+
this.log.info('[HyperlinkService] URL sanitized - Fixed encoding issues');
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
this.log.info(`[HyperlinkService] API URL: ${sanitizedUrl}`);
|
|
883
|
+
|
|
884
|
+
// Validate the URL before using it
|
|
885
|
+
const validation = validatePowerAutomateUrl(sanitizedUrl);
|
|
886
|
+
if (!validation.valid) {
|
|
887
|
+
const errorMsg = `Invalid PowerAutomate URL: ${validation.issues.join(', ')}`;
|
|
888
|
+
this.log.error('[HyperlinkService] URL Validation FAILED:', errorMsg);
|
|
889
|
+
throw new Error(errorMsg);
|
|
890
|
+
}
|
|
891
|
+
this.log.info('[HyperlinkService] URL Validation: PASSED');
|
|
892
|
+
|
|
893
|
+
// Use main process net.request via IPC (matches C# HttpClient behavior on corporate networks)
|
|
894
|
+
// This uses Chromium's networking stack which respects system proxy and certificates
|
|
895
|
+
if (typeof window !== 'undefined' && window.electronAPI?.callPowerAutomateApi) {
|
|
896
|
+
this.log.info('[HyperlinkService] Using IPC -> Main Process -> net.request');
|
|
897
|
+
this.log.info('───────────────────────────────────────────────────────────────');
|
|
898
|
+
|
|
899
|
+
let lastError: Error | null = null;
|
|
900
|
+
|
|
901
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
902
|
+
try {
|
|
903
|
+
if (attempt > 0) {
|
|
904
|
+
const delay = Math.pow(2, attempt) * 1000;
|
|
905
|
+
this.log.info(`[HyperlinkService] Retry attempt ${attempt + 1} of ${maxRetries} (waiting ${delay}ms)`);
|
|
906
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
this.log.info(`[HyperlinkService] Sending IPC request (attempt ${attempt + 1})...`);
|
|
910
|
+
const startTime = Date.now();
|
|
911
|
+
|
|
912
|
+
const response = await window.electronAPI.callPowerAutomateApi(
|
|
913
|
+
sanitizedUrl,
|
|
914
|
+
request,
|
|
915
|
+
timeoutMs
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
const duration = Date.now() - startTime;
|
|
919
|
+
|
|
920
|
+
if (!response.success) {
|
|
921
|
+
this.log.error('[HyperlinkService] API call FAILED');
|
|
922
|
+
this.log.error(`[HyperlinkService] Error: ${response.error}`);
|
|
923
|
+
this.log.error(`[HyperlinkService] Duration: ${duration}ms`);
|
|
924
|
+
throw new Error(response.error || `API returned status ${response.statusCode}`);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const data = response.data as { Results?: Array<{ Document_ID?: string; Content_ID?: string; Title?: string; Status?: string }> };
|
|
928
|
+
|
|
929
|
+
this.log.info('═══════════════════════════════════════════════════════════════');
|
|
930
|
+
this.log.info('[HyperlinkService] API Call SUCCESS');
|
|
931
|
+
this.log.info('═══════════════════════════════════════════════════════════════');
|
|
932
|
+
this.log.info(`[HyperlinkService] Status Code: ${response.statusCode}`);
|
|
933
|
+
this.log.info(`[HyperlinkService] Duration: ${duration}ms`);
|
|
934
|
+
this.log.info(`[HyperlinkService] Results: ${data?.Results?.length || 0} items`);
|
|
935
|
+
|
|
936
|
+
const apiResponse: HyperlinkApiResponse = {
|
|
937
|
+
success: Array.isArray(data?.Results),
|
|
938
|
+
timestamp: new Date(),
|
|
939
|
+
statusCode: response.statusCode,
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
if (data?.Results) {
|
|
943
|
+
apiResponse.body = this.parseApiResults(data.Results);
|
|
944
|
+
this.log.info(`[HyperlinkService] Parsed ${data.Results.length} results into cache`);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
this.log.info('═══════════════════════════════════════════════════════════════');
|
|
948
|
+
|
|
949
|
+
return apiResponse;
|
|
950
|
+
} catch (error) {
|
|
951
|
+
lastError = error as Error;
|
|
952
|
+
this.log.error(`[HyperlinkService] Attempt ${attempt + 1} failed: ${lastError.message}`);
|
|
953
|
+
// Check if it's a timeout error
|
|
954
|
+
if (error instanceof Error && error.message.includes('timeout')) {
|
|
955
|
+
this.log.error('[HyperlinkService] Timeout detected, not retrying');
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (lastError && lastError.message.includes('timeout')) {
|
|
962
|
+
this.log.error(`[HyperlinkService] Final error: API request timeout after ${timeoutMs}ms`);
|
|
963
|
+
throw new Error(`API request timeout after ${timeoutMs}ms`);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
this.log.error(`[HyperlinkService] All ${maxRetries} attempts failed`);
|
|
967
|
+
throw lastError || new Error('API request failed after retries');
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// No Electron API available - provide detailed diagnostics
|
|
971
|
+
const windowExists = typeof window !== 'undefined';
|
|
972
|
+
const apiExists = windowExists && typeof window.electronAPI !== 'undefined';
|
|
973
|
+
const methodExists = apiExists && typeof window.electronAPI?.callPowerAutomateApi === 'function';
|
|
974
|
+
|
|
975
|
+
this.log.error('[HyperlinkService] ERROR: Electron API not available!');
|
|
976
|
+
this.log.error(`[HyperlinkService] Diagnostics: window=${windowExists}, electronAPI=${apiExists}, callPowerAutomateApi=${methodExists}`);
|
|
977
|
+
|
|
978
|
+
if (!windowExists) {
|
|
979
|
+
this.log.error('[HyperlinkService] Running in non-browser context (SSR/Node.js)');
|
|
980
|
+
} else if (!apiExists) {
|
|
981
|
+
this.log.error('[HyperlinkService] Preload script may not have loaded - check BrowserWindow preload configuration');
|
|
982
|
+
} else if (!methodExists) {
|
|
983
|
+
this.log.error('[HyperlinkService] electronAPI exists but callPowerAutomateApi method is missing');
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
throw new Error('Electron API not available - cannot make API call. Ensure the app is running in Electron.');
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Parse API results into standardized format with caching
|
|
991
|
+
* Accepts results from both IPC (optional fields) and direct API (required fields)
|
|
992
|
+
*/
|
|
993
|
+
private parseApiResults(results: Array<{
|
|
994
|
+
Document_ID?: string;
|
|
995
|
+
Content_ID?: string;
|
|
996
|
+
Title?: string;
|
|
997
|
+
Status?: string;
|
|
998
|
+
}>): HyperlinkApiResponse['body'] {
|
|
999
|
+
const resultsMap = new Map<string, HyperlinkApiResult>();
|
|
1000
|
+
|
|
1001
|
+
const parsedResults = results?.map((result) => {
|
|
1002
|
+
// Trim whitespace from all fields as specified
|
|
1003
|
+
const rawStatus = result.Status?.trim() || 'Active';
|
|
1004
|
+
// Normalize status to match HyperlinkApiResult type
|
|
1005
|
+
const normalizedStatus: HyperlinkApiResult['status'] =
|
|
1006
|
+
rawStatus.toLowerCase() === 'deprecated'
|
|
1007
|
+
? 'deprecated'
|
|
1008
|
+
: rawStatus.toLowerCase() === 'expired'
|
|
1009
|
+
? 'expired'
|
|
1010
|
+
: rawStatus.toLowerCase() === 'moved'
|
|
1011
|
+
? 'moved'
|
|
1012
|
+
: rawStatus.toLowerCase() === 'not_found'
|
|
1013
|
+
? 'not_found'
|
|
1014
|
+
: 'active';
|
|
1015
|
+
|
|
1016
|
+
const processed: HyperlinkApiResult = {
|
|
1017
|
+
url: '', // Will be constructed from Document_ID
|
|
1018
|
+
documentId: result.Document_ID?.trim() || '',
|
|
1019
|
+
contentId: result.Content_ID?.trim() || '',
|
|
1020
|
+
title: result.Title?.trim() || '',
|
|
1021
|
+
status: normalizedStatus,
|
|
1022
|
+
metadata: {},
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
// Cache by both IDs for quick lookup
|
|
1026
|
+
if (processed.documentId) resultsMap.set(processed.documentId, processed);
|
|
1027
|
+
if (processed.contentId) resultsMap.set(processed.contentId, processed);
|
|
1028
|
+
|
|
1029
|
+
return processed;
|
|
1030
|
+
}) || [];
|
|
1031
|
+
|
|
1032
|
+
return {
|
|
1033
|
+
results: parsedResults,
|
|
1034
|
+
errors: [],
|
|
1035
|
+
// Note: resultsCache is added by caller if needed
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Utility methods matching the C# implementation
|
|
1040
|
+
|
|
1041
|
+
private isValidUrl(url: string): boolean {
|
|
1042
|
+
if (!url) return false;
|
|
1043
|
+
if (url.startsWith('#')) return true; // Internal links
|
|
1044
|
+
|
|
1045
|
+
try {
|
|
1046
|
+
new URL(url);
|
|
1047
|
+
return true;
|
|
1048
|
+
} catch {
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
private isTheSourceUrl(url: string): boolean {
|
|
1054
|
+
return URL_PATTERNS.THE_SOURCE.pattern.test(url);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
private needsContentId(url: string): boolean {
|
|
1058
|
+
return (
|
|
1059
|
+
this.isTheSourceUrl(url) &&
|
|
1060
|
+
(URL_PATTERNS.CONTENT_ID.pattern.test(url) || URL_PATTERNS.DOCUMENT_ID.pattern.test(url))
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Extraction methods moved to centralized utility: src/utils/urlPatterns.ts
|
|
1065
|
+
// Use: extractContentId(url), extractDocumentId(url), isTheSourceUrl(url)
|
|
1066
|
+
|
|
1067
|
+
private extractTitleFromUrl(url: string): string | null {
|
|
1068
|
+
try {
|
|
1069
|
+
const urlObj = new URL(url);
|
|
1070
|
+
const segments = urlObj.pathname.split('/').filter(Boolean);
|
|
1071
|
+
if (segments.length > 0) {
|
|
1072
|
+
const lastSegment = segments[segments.length - 1];
|
|
1073
|
+
if (lastSegment && lastSegment.length > 3) {
|
|
1074
|
+
return decodeURIComponent(lastSegment).replace(/[-_]/g, ' ');
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
} catch {
|
|
1078
|
+
// Return null if extraction fails
|
|
1079
|
+
}
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Export singleton instance
|
|
1085
|
+
export const hyperlinkService = HyperlinkService.getInstance();
|