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,1484 @@
|
|
|
1
|
+
using DocumentFormat.OpenXml;
|
|
2
|
+
using DocumentFormat.OpenXml.Packaging;
|
|
3
|
+
using DocumentFormat.OpenXml.Wordprocessing;
|
|
4
|
+
using System;
|
|
5
|
+
using System.Collections.Generic;
|
|
6
|
+
using System.IO;
|
|
7
|
+
using System.Linq;
|
|
8
|
+
using System.Text.RegularExpressions;
|
|
9
|
+
using System.Threading.Tasks;
|
|
10
|
+
using DocxFormatter.Core.Interfaces;
|
|
11
|
+
using DocxFormatter.Core.Models;
|
|
12
|
+
|
|
13
|
+
namespace DocxFormatter.Core.Services
|
|
14
|
+
{
|
|
15
|
+
/// <summary>
|
|
16
|
+
/// Core service for managing hyperlinks in Word documents.
|
|
17
|
+
/// Provides basic hyperlink operations and coordination with specialized services.
|
|
18
|
+
/// </summary>
|
|
19
|
+
public class HyperlinkService : IHyperlinkService
|
|
20
|
+
{
|
|
21
|
+
// Regex patterns for identifying theSource URLs that should have ContentId appended
|
|
22
|
+
private static readonly Regex ContentIdPattern = new(
|
|
23
|
+
@"(TSRC|CMS)-([a-zA-Z0-9]+)-(\d{6})",
|
|
24
|
+
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
|
25
|
+
|
|
26
|
+
private static readonly Regex DocumentIdPattern = new(
|
|
27
|
+
@"docid=([a-zA-Z0-9-]+)(?:[^a-zA-Z0-9-]|$)",
|
|
28
|
+
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
|
29
|
+
|
|
30
|
+
private readonly BackupService _backupService;
|
|
31
|
+
private readonly IHyperlinkApiService _apiService;
|
|
32
|
+
private readonly ChangeHistoryManager _changeHistoryManager;
|
|
33
|
+
private static readonly Serilog.ILogger _logger = Serilog.Log.ForContext<HyperlinkService>();
|
|
34
|
+
|
|
35
|
+
public HyperlinkService(
|
|
36
|
+
IHyperlinkApiService apiService,
|
|
37
|
+
ChangeHistoryManager? changeHistoryManager = null)
|
|
38
|
+
{
|
|
39
|
+
_backupService = new BackupService();
|
|
40
|
+
_apiService = apiService ?? throw new ArgumentNullException(nameof(apiService));
|
|
41
|
+
_changeHistoryManager = changeHistoryManager ?? new ChangeHistoryManager();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Backward compatibility constructor
|
|
45
|
+
public HyperlinkService(ChangeHistoryManager? changeHistoryManager = null)
|
|
46
|
+
{
|
|
47
|
+
_backupService = new BackupService();
|
|
48
|
+
_apiService = new HyperlinkApiService();
|
|
49
|
+
_changeHistoryManager = changeHistoryManager ?? new ChangeHistoryManager();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Nested classes removed - now in separate model files:
|
|
53
|
+
// HyperlinkInfo -> DetailedHyperlinkInfo.cs (internal use)
|
|
54
|
+
// HyperlinkProcessingOptions -> HyperlinkProcessingOptions.cs
|
|
55
|
+
// HyperlinkProcessingResult -> HyperlinkProcessingResult.cs
|
|
56
|
+
|
|
57
|
+
/// <summary>
|
|
58
|
+
/// Scans a document for all hyperlinks and returns detailed information.
|
|
59
|
+
/// </summary>
|
|
60
|
+
/// <param name="document">The document to scan</param>
|
|
61
|
+
/// <param name="validateLinks">Whether to validate each hyperlink</param>
|
|
62
|
+
/// <returns>List of detailed hyperlink information</returns>
|
|
63
|
+
public async Task<List<DetailedHyperlinkInfo>> ScanHyperlinksAsync(WordprocessingDocument document, bool validateLinks = false)
|
|
64
|
+
{
|
|
65
|
+
if (document == null)
|
|
66
|
+
throw new ArgumentNullException(nameof(document));
|
|
67
|
+
|
|
68
|
+
var hyperlinks = new List<DetailedHyperlinkInfo>();
|
|
69
|
+
|
|
70
|
+
await Task.Run(() =>
|
|
71
|
+
{
|
|
72
|
+
// Scan main document
|
|
73
|
+
if (document.MainDocumentPart?.Document?.Body != null)
|
|
74
|
+
{
|
|
75
|
+
ScanHyperlinksInElement(document.MainDocumentPart.Document.Body,
|
|
76
|
+
document.MainDocumentPart, hyperlinks, validateLinks);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Scan headers
|
|
80
|
+
var headerParts = document.MainDocumentPart?.HeaderParts;
|
|
81
|
+
if (headerParts != null)
|
|
82
|
+
{
|
|
83
|
+
foreach (var headerPart in headerParts)
|
|
84
|
+
{
|
|
85
|
+
ScanHyperlinksInElement(headerPart.Header, headerPart, hyperlinks, validateLinks);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Scan footers
|
|
90
|
+
var footerParts = document.MainDocumentPart?.FooterParts;
|
|
91
|
+
if (footerParts != null)
|
|
92
|
+
{
|
|
93
|
+
foreach (var footerPart in footerParts)
|
|
94
|
+
{
|
|
95
|
+
ScanHyperlinksInElement(footerPart.Footer, footerPart, hyperlinks, validateLinks);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return hyperlinks;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// <summary>
|
|
104
|
+
/// Fixes source hyperlinks by appending Content IDs and performing updates.
|
|
105
|
+
/// This is the main functionality requested by the user.
|
|
106
|
+
/// </summary>
|
|
107
|
+
/// <param name="filePath">Path to the document to process</param>
|
|
108
|
+
/// <param name="options">Processing options</param>
|
|
109
|
+
/// <param name="progress">Progress reporting</param>
|
|
110
|
+
/// <returns>Processing result</returns>
|
|
111
|
+
public async Task<HyperlinkProcessingResult> FixSourceHyperlinksAsync(string filePath,
|
|
112
|
+
HyperlinkProcessingOptions? options = null, IProgress<double>? progress = null)
|
|
113
|
+
{
|
|
114
|
+
if (string.IsNullOrWhiteSpace(filePath))
|
|
115
|
+
throw new ArgumentException("File path cannot be null or empty", nameof(filePath));
|
|
116
|
+
|
|
117
|
+
if (!File.Exists(filePath))
|
|
118
|
+
throw new FileNotFoundException($"File not found: {filePath}");
|
|
119
|
+
|
|
120
|
+
options ??= new HyperlinkProcessingOptions();
|
|
121
|
+
var result = new HyperlinkProcessingResult();
|
|
122
|
+
var startTime = DateTime.Now;
|
|
123
|
+
|
|
124
|
+
try
|
|
125
|
+
{
|
|
126
|
+
// Create backup if requested
|
|
127
|
+
if (options.CreateBackup)
|
|
128
|
+
{
|
|
129
|
+
progress?.Report(5);
|
|
130
|
+
result.BackupPath = _backupService.CreateBackupWithCleanup(filePath);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
progress?.Report(10);
|
|
134
|
+
|
|
135
|
+
using var document = WordprocessingDocument.Open(filePath, true);
|
|
136
|
+
|
|
137
|
+
// Use internal advanced fixing functionality
|
|
138
|
+
var fixingOptions = ConvertToFixingOptions(options);
|
|
139
|
+
var fixingResult = await PerformAdvancedHyperlinkFixingAsync(document, fixingOptions);
|
|
140
|
+
|
|
141
|
+
progress?.Report(90);
|
|
142
|
+
|
|
143
|
+
// Convert result back to original format for compatibility
|
|
144
|
+
var convertedResult = ConvertFixingResult(fixingResult, result.BackupPath);
|
|
145
|
+
|
|
146
|
+
// Track the operation for undo/redo if successful
|
|
147
|
+
if (convertedResult.Success && convertedResult.ProcessedHyperlinks > 0)
|
|
148
|
+
{
|
|
149
|
+
var operation = new HyperlinkProcessingOperation
|
|
150
|
+
{
|
|
151
|
+
DocumentPath = filePath,
|
|
152
|
+
Description = $"Fixed {convertedResult.ProcessedHyperlinks} hyperlinks in {Path.GetFileName(filePath)}",
|
|
153
|
+
OperationType = "HyperlinkFix",
|
|
154
|
+
BackupPath = result.BackupPath,
|
|
155
|
+
Options = fixingOptions
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Track changes made
|
|
159
|
+
foreach (var hyperlink in convertedResult.ProcessedLinks)
|
|
160
|
+
{
|
|
161
|
+
operation.Changes.Add(new HyperlinkChange
|
|
162
|
+
{
|
|
163
|
+
HyperlinkId = hyperlink.Id,
|
|
164
|
+
OriginalUrl = hyperlink.Url,
|
|
165
|
+
NewUrl = hyperlink.Url,
|
|
166
|
+
OriginalDisplayText = hyperlink.DisplayText,
|
|
167
|
+
NewDisplayText = hyperlink.DisplayText,
|
|
168
|
+
Type = HyperlinkChangeType.BothChanged
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Add to history
|
|
173
|
+
_changeHistoryManager.AddOperation(operation);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
progress?.Report(100);
|
|
177
|
+
|
|
178
|
+
return convertedResult;
|
|
179
|
+
}
|
|
180
|
+
catch (Exception ex)
|
|
181
|
+
{
|
|
182
|
+
result.Success = false;
|
|
183
|
+
result.ErrorMessages.Add($"Fatal error: {ex.Message}");
|
|
184
|
+
result.Duration = DateTime.Now - startTime;
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// <summary>
|
|
190
|
+
/// Fixes source hyperlinks using the new HyperlinkFixingOptions.
|
|
191
|
+
/// This overload provides access to the enhanced fixing functionality.
|
|
192
|
+
/// </summary>
|
|
193
|
+
/// <param name="filePath">Path to the document to process</param>
|
|
194
|
+
/// <param name="fixingOptions">Advanced fixing options</param>
|
|
195
|
+
/// <param name="progress">Progress reporting</param>
|
|
196
|
+
/// <returns>Processing result</returns>
|
|
197
|
+
public async Task<HyperlinkProcessingResult> FixSourceHyperlinksAsync(string filePath,
|
|
198
|
+
HyperlinkFixingOptions? fixingOptions = null, IProgress<double>? progress = null)
|
|
199
|
+
{
|
|
200
|
+
if (string.IsNullOrWhiteSpace(filePath))
|
|
201
|
+
throw new ArgumentException("File path cannot be null or empty", nameof(filePath));
|
|
202
|
+
|
|
203
|
+
if (!File.Exists(filePath))
|
|
204
|
+
throw new FileNotFoundException($"File not found: {filePath}");
|
|
205
|
+
|
|
206
|
+
fixingOptions ??= new HyperlinkFixingOptions();
|
|
207
|
+
var startTime = DateTime.Now;
|
|
208
|
+
var backupPath = string.Empty;
|
|
209
|
+
|
|
210
|
+
try
|
|
211
|
+
{
|
|
212
|
+
// Create backup if requested
|
|
213
|
+
if (fixingOptions.CreateBackup)
|
|
214
|
+
{
|
|
215
|
+
progress?.Report(5);
|
|
216
|
+
backupPath = _backupService.CreateBackupWithCleanup(filePath);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
progress?.Report(10);
|
|
220
|
+
|
|
221
|
+
using var document = WordprocessingDocument.Open(filePath, true);
|
|
222
|
+
|
|
223
|
+
// Use internal advanced fixing functionality
|
|
224
|
+
var fixingResult = await PerformAdvancedHyperlinkFixingAsync(document, fixingOptions);
|
|
225
|
+
|
|
226
|
+
progress?.Report(90);
|
|
227
|
+
|
|
228
|
+
// Convert result to original format for compatibility
|
|
229
|
+
var result = ConvertFixingResult(fixingResult, backupPath);
|
|
230
|
+
|
|
231
|
+
// Track the operation for undo/redo if successful
|
|
232
|
+
if (result.Success && result.ProcessedHyperlinks > 0)
|
|
233
|
+
{
|
|
234
|
+
var operation = new HyperlinkProcessingOperation
|
|
235
|
+
{
|
|
236
|
+
DocumentPath = filePath,
|
|
237
|
+
Description = $"Fixed {result.ProcessedHyperlinks} hyperlinks in {Path.GetFileName(filePath)}",
|
|
238
|
+
OperationType = "HyperlinkFix",
|
|
239
|
+
BackupPath = backupPath,
|
|
240
|
+
Options = fixingOptions ?? new HyperlinkFixingOptions()
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Track changes made
|
|
244
|
+
foreach (var hyperlink in result.ProcessedLinks)
|
|
245
|
+
{
|
|
246
|
+
operation.Changes.Add(new HyperlinkChange
|
|
247
|
+
{
|
|
248
|
+
HyperlinkId = hyperlink.Id,
|
|
249
|
+
OriginalUrl = hyperlink.Url, // This would need to be tracked better in a real implementation
|
|
250
|
+
NewUrl = hyperlink.Url,
|
|
251
|
+
OriginalDisplayText = hyperlink.DisplayText,
|
|
252
|
+
NewDisplayText = hyperlink.DisplayText,
|
|
253
|
+
Type = HyperlinkChangeType.BothChanged
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Add to history
|
|
258
|
+
_changeHistoryManager.AddOperation(operation);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
progress?.Report(100);
|
|
262
|
+
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
catch (Exception ex)
|
|
266
|
+
{
|
|
267
|
+
var result = new HyperlinkProcessingResult
|
|
268
|
+
{
|
|
269
|
+
Success = false,
|
|
270
|
+
BackupPath = backupPath,
|
|
271
|
+
Duration = DateTime.Now - startTime
|
|
272
|
+
};
|
|
273
|
+
result.ErrorMessages.Add($"Fatal error: {ex.Message}");
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// <summary>
|
|
279
|
+
/// Validates hyperlinks in a document and reports issues.
|
|
280
|
+
/// </summary>
|
|
281
|
+
/// <param name="document">The document to validate</param>
|
|
282
|
+
/// <returns>List of validation issues</returns>
|
|
283
|
+
public async Task<List<DetailedHyperlinkInfo>> ValidateHyperlinksAsync(WordprocessingDocument document)
|
|
284
|
+
{
|
|
285
|
+
var hyperlinks = await ScanHyperlinksAsync(document, true);
|
|
286
|
+
return hyperlinks.Where(h => !h.IsValid).ToList();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// <summary>
|
|
290
|
+
/// Appends Content ID to hyperlinks that match specific criteria.
|
|
291
|
+
/// </summary>
|
|
292
|
+
/// <param name="document">The document to process</param>
|
|
293
|
+
/// <param name="contentId">The Content ID to append (e.g., "#content")</param>
|
|
294
|
+
/// <param name="urlPattern">Pattern to match URLs (optional)</param>
|
|
295
|
+
/// <returns>Number of hyperlinks updated</returns>
|
|
296
|
+
public async Task<int> AppendContentIdToHyperlinksAsync(WordprocessingDocument document,
|
|
297
|
+
string contentId, string? urlPattern = null)
|
|
298
|
+
{
|
|
299
|
+
if (document == null)
|
|
300
|
+
throw new ArgumentNullException(nameof(document));
|
|
301
|
+
|
|
302
|
+
if (string.IsNullOrWhiteSpace(contentId))
|
|
303
|
+
throw new ArgumentException("Content ID cannot be null or empty", nameof(contentId));
|
|
304
|
+
|
|
305
|
+
var updateCount = 0;
|
|
306
|
+
var urlRegex = !string.IsNullOrWhiteSpace(urlPattern)
|
|
307
|
+
? new Regex(urlPattern, RegexOptions.IgnoreCase)
|
|
308
|
+
: null;
|
|
309
|
+
|
|
310
|
+
await Task.Run(() =>
|
|
311
|
+
{
|
|
312
|
+
// Process main document
|
|
313
|
+
if (document.MainDocumentPart?.Document?.Body != null)
|
|
314
|
+
{
|
|
315
|
+
updateCount += AppendContentIdInElement(document.MainDocumentPart.Document.Body,
|
|
316
|
+
document.MainDocumentPart, contentId, urlRegex);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Process headers
|
|
320
|
+
var headerParts = document.MainDocumentPart?.HeaderParts;
|
|
321
|
+
if (headerParts != null)
|
|
322
|
+
{
|
|
323
|
+
foreach (var headerPart in headerParts)
|
|
324
|
+
{
|
|
325
|
+
updateCount += AppendContentIdInElement(headerPart.Header, headerPart, contentId, urlRegex);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Process footers
|
|
330
|
+
var footerParts = document.MainDocumentPart?.FooterParts;
|
|
331
|
+
if (footerParts != null)
|
|
332
|
+
{
|
|
333
|
+
foreach (var footerPart in footerParts)
|
|
334
|
+
{
|
|
335
|
+
updateCount += AppendContentIdInElement(footerPart.Footer, footerPart, contentId, urlRegex);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return updateCount;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/// <summary>
|
|
344
|
+
/// Creates a batch operation for hyperlink processing.
|
|
345
|
+
/// </summary>
|
|
346
|
+
/// <param name="filePaths">Files to process</param>
|
|
347
|
+
/// <param name="options">Processing options</param>
|
|
348
|
+
/// <returns>Batch operation</returns>
|
|
349
|
+
public IBatchOperation CreateBatchOperation(List<string> filePaths, HyperlinkProcessingOptions options)
|
|
350
|
+
{
|
|
351
|
+
return new HyperlinkBatchOperation(this, filePaths, options);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/// <summary>
|
|
355
|
+
/// Creates a batch operation for hyperlink processing with advanced options.
|
|
356
|
+
/// </summary>
|
|
357
|
+
/// <param name="filePaths">Files to process</param>
|
|
358
|
+
/// <param name="fixingOptions">Advanced fixing options</param>
|
|
359
|
+
/// <returns>Batch operation</returns>
|
|
360
|
+
public IBatchOperation CreateBatchOperation(List<string> filePaths, HyperlinkFixingOptions fixingOptions)
|
|
361
|
+
{
|
|
362
|
+
return new HyperlinkBatchOperation(this, filePaths, fixingOptions);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/// <summary>
|
|
366
|
+
/// Processes multiple documents for hyperlink fixes.
|
|
367
|
+
/// </summary>
|
|
368
|
+
/// <param name="filePaths">Files to process</param>
|
|
369
|
+
/// <param name="options">Processing options</param>
|
|
370
|
+
/// <param name="progress">Progress reporting</param>
|
|
371
|
+
/// <param name="cancellationToken">Cancellation token</param>
|
|
372
|
+
/// <returns>List of processing results</returns>
|
|
373
|
+
public async Task<List<HyperlinkProcessingResult>> ProcessDocumentsAsync(
|
|
374
|
+
List<string> filePaths,
|
|
375
|
+
HyperlinkProcessingOptions options,
|
|
376
|
+
IProgress<double>? progress = null,
|
|
377
|
+
CancellationToken cancellationToken = default)
|
|
378
|
+
{
|
|
379
|
+
var results = new List<HyperlinkProcessingResult>();
|
|
380
|
+
var totalFiles = filePaths.Count;
|
|
381
|
+
var processedFiles = 0;
|
|
382
|
+
|
|
383
|
+
foreach (var filePath in filePaths)
|
|
384
|
+
{
|
|
385
|
+
if (cancellationToken.IsCancellationRequested)
|
|
386
|
+
break;
|
|
387
|
+
|
|
388
|
+
try
|
|
389
|
+
{
|
|
390
|
+
var fileProgress = new Progress<double>(p =>
|
|
391
|
+
{
|
|
392
|
+
var overallProgress = (processedFiles * 100.0 + p) / totalFiles;
|
|
393
|
+
progress?.Report(overallProgress);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
var result = await FixSourceHyperlinksAsync(filePath, options, fileProgress);
|
|
397
|
+
results.Add(result);
|
|
398
|
+
}
|
|
399
|
+
catch (Exception ex)
|
|
400
|
+
{
|
|
401
|
+
_logger.Error(ex, "Error processing file {FilePath} in batch operation", filePath);
|
|
402
|
+
results.Add(new HyperlinkProcessingResult
|
|
403
|
+
{
|
|
404
|
+
Success = false,
|
|
405
|
+
ErrorMessages = new List<string> { $"Error processing {filePath}: {ex.Message}" }
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
processedFiles++;
|
|
410
|
+
progress?.Report((processedFiles * 100.0) / totalFiles);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return results;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/// <summary>
|
|
417
|
+
/// Processes multiple documents for hyperlink fixes with advanced options.
|
|
418
|
+
/// </summary>
|
|
419
|
+
/// <param name="filePaths">Files to process</param>
|
|
420
|
+
/// <param name="fixingOptions">Advanced fixing options</param>
|
|
421
|
+
/// <param name="progress">Progress reporting</param>
|
|
422
|
+
/// <param name="cancellationToken">Cancellation token</param>
|
|
423
|
+
/// <returns>List of processing results</returns>
|
|
424
|
+
public async Task<List<HyperlinkProcessingResult>> ProcessDocumentsAsync(
|
|
425
|
+
List<string> filePaths,
|
|
426
|
+
HyperlinkFixingOptions fixingOptions,
|
|
427
|
+
IProgress<double>? progress = null,
|
|
428
|
+
CancellationToken cancellationToken = default)
|
|
429
|
+
{
|
|
430
|
+
var results = new List<HyperlinkProcessingResult>();
|
|
431
|
+
var totalFiles = filePaths.Count;
|
|
432
|
+
var processedFiles = 0;
|
|
433
|
+
|
|
434
|
+
foreach (var filePath in filePaths)
|
|
435
|
+
{
|
|
436
|
+
if (cancellationToken.IsCancellationRequested)
|
|
437
|
+
break;
|
|
438
|
+
|
|
439
|
+
try
|
|
440
|
+
{
|
|
441
|
+
var fileProgress = new Progress<double>(p =>
|
|
442
|
+
{
|
|
443
|
+
var overallProgress = (processedFiles * 100.0 + p) / totalFiles;
|
|
444
|
+
progress?.Report(overallProgress);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
var result = await FixSourceHyperlinksAsync(filePath, fixingOptions, fileProgress);
|
|
448
|
+
results.Add(result);
|
|
449
|
+
}
|
|
450
|
+
catch (Exception ex)
|
|
451
|
+
{
|
|
452
|
+
_logger.Error(ex, "Error processing file {FilePath} in batch operation", filePath);
|
|
453
|
+
results.Add(new HyperlinkProcessingResult
|
|
454
|
+
{
|
|
455
|
+
Success = false,
|
|
456
|
+
ErrorMessages = new List<string> { $"Error processing {filePath}: {ex.Message}" }
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
processedFiles++;
|
|
461
|
+
progress?.Report((processedFiles * 100.0) / totalFiles);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return results;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
#region Helper Methods
|
|
468
|
+
|
|
469
|
+
/// <summary>
|
|
470
|
+
/// Converts HyperlinkFixingResult to HyperlinkProcessingResult for compatibility.
|
|
471
|
+
/// </summary>
|
|
472
|
+
/// <param name="fixingResult">The fixing result to convert</param>
|
|
473
|
+
/// <param name="backupPath">The backup path to include</param>
|
|
474
|
+
/// <returns>Converted processing result</returns>
|
|
475
|
+
private HyperlinkProcessingResult ConvertFixingResult(HyperlinkFixingResult fixingResult, string backupPath = "")
|
|
476
|
+
{
|
|
477
|
+
var result = new HyperlinkProcessingResult
|
|
478
|
+
{
|
|
479
|
+
Success = fixingResult.Success,
|
|
480
|
+
TotalHyperlinks = fixingResult.TotalHyperlinks,
|
|
481
|
+
ProcessedHyperlinks = fixingResult.ProcessedHyperlinks,
|
|
482
|
+
SkippedHyperlinks = Math.Max(0, fixingResult.TotalHyperlinks - fixingResult.ProcessedHyperlinks),
|
|
483
|
+
ErrorCount = fixingResult.ErrorMessages.Count,
|
|
484
|
+
ErrorMessages = new List<string>(fixingResult.ErrorMessages),
|
|
485
|
+
BackupPath = backupPath,
|
|
486
|
+
Duration = fixingResult.Duration
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// For compatibility, treat updated URLs as processed hyperlinks
|
|
490
|
+
if (fixingResult.UpdatedUrls > 0 || fixingResult.UpdatedDisplayTexts > 0)
|
|
491
|
+
{
|
|
492
|
+
result.ProcessedHyperlinks = Math.Max(result.ProcessedHyperlinks,
|
|
493
|
+
fixingResult.UpdatedUrls + fixingResult.UpdatedDisplayTexts);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/// <summary>
|
|
500
|
+
/// Converts HyperlinkProcessingOptions to HyperlinkFixingOptions.
|
|
501
|
+
/// </summary>
|
|
502
|
+
/// <param name="processingOptions">The processing options to convert</param>
|
|
503
|
+
/// <returns>Converted fixing options</returns>
|
|
504
|
+
private HyperlinkFixingOptions ConvertToFixingOptions(HyperlinkProcessingOptions processingOptions)
|
|
505
|
+
{
|
|
506
|
+
var fixingOptions = new HyperlinkFixingOptions
|
|
507
|
+
{
|
|
508
|
+
CreateBackup = processingOptions.CreateBackup,
|
|
509
|
+
UpdateTitles = false, // Default to false for backward compatibility
|
|
510
|
+
AppendContentId = !string.IsNullOrEmpty(processingOptions.ContentIdToAppend),
|
|
511
|
+
PowerAutomateUrl = GetPowerAutomateUrlFromSettings()
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
return fixingOptions;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
#endregion
|
|
518
|
+
|
|
519
|
+
#region Private Methods
|
|
520
|
+
|
|
521
|
+
// Validation methods moved to HyperlinkValidationService
|
|
522
|
+
|
|
523
|
+
private bool ShouldProcessHyperlink(DetailedHyperlinkInfo hyperlink, HyperlinkProcessingOptions options)
|
|
524
|
+
{
|
|
525
|
+
if (!options.ProcessInternalLinks && hyperlink.IsInternal)
|
|
526
|
+
return false;
|
|
527
|
+
|
|
528
|
+
if (!options.ProcessExternalLinks && !hyperlink.IsInternal)
|
|
529
|
+
return false;
|
|
530
|
+
|
|
531
|
+
if (!string.IsNullOrEmpty(options.UrlPattern))
|
|
532
|
+
{
|
|
533
|
+
var urlRegex = new Regex(options.UrlPattern, RegexOptions.IgnoreCase);
|
|
534
|
+
if (!urlRegex.IsMatch(hyperlink.Url))
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (!string.IsNullOrEmpty(options.DisplayTextPattern))
|
|
539
|
+
{
|
|
540
|
+
var displayRegex = new Regex(options.DisplayTextPattern, RegexOptions.IgnoreCase);
|
|
541
|
+
if (!displayRegex.IsMatch(hyperlink.DisplayText))
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private async Task<bool> ProcessSingleHyperlinkAsync(DetailedHyperlinkInfo hyperlink, HyperlinkProcessingOptions options)
|
|
549
|
+
{
|
|
550
|
+
return await Task.Run(() =>
|
|
551
|
+
{
|
|
552
|
+
try
|
|
553
|
+
{
|
|
554
|
+
bool wasModified = false;
|
|
555
|
+
|
|
556
|
+
// Append Content ID only to theSource hyperlinks with matching patterns
|
|
557
|
+
if (!string.IsNullOrEmpty(options.ContentIdToAppend) && !hyperlink.IsInternal)
|
|
558
|
+
{
|
|
559
|
+
// Check if this is a theSource URL with ContentId or DocumentId pattern
|
|
560
|
+
if (hyperlink.Url.Contains("thesource", StringComparison.OrdinalIgnoreCase) &&
|
|
561
|
+
(ContentIdPattern.IsMatch(hyperlink.Url) || DocumentIdPattern.IsMatch(hyperlink.Url)))
|
|
562
|
+
{
|
|
563
|
+
if (!hyperlink.Url.Contains(options.ContentIdToAppend))
|
|
564
|
+
{
|
|
565
|
+
var newUrl = hyperlink.Url + options.ContentIdToAppend;
|
|
566
|
+
if (UpdateHyperlinkUrl(hyperlink, newUrl))
|
|
567
|
+
{
|
|
568
|
+
hyperlink.Url = newUrl;
|
|
569
|
+
wasModified = true;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return wasModified;
|
|
576
|
+
}
|
|
577
|
+
catch
|
|
578
|
+
{
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
private bool UpdateHyperlinkUrl(DetailedHyperlinkInfo hyperlinkInfo, string newUrl)
|
|
585
|
+
{
|
|
586
|
+
try
|
|
587
|
+
{
|
|
588
|
+
var relationshipId = hyperlinkInfo.Id;
|
|
589
|
+
if (string.IsNullOrEmpty(relationshipId))
|
|
590
|
+
return false;
|
|
591
|
+
|
|
592
|
+
var part = hyperlinkInfo.ContainingPart;
|
|
593
|
+
var relationship = part.ExternalRelationships.FirstOrDefault(r => r.Id == relationshipId);
|
|
594
|
+
if (relationship == null)
|
|
595
|
+
return false;
|
|
596
|
+
|
|
597
|
+
// Delete old relationship and create new one
|
|
598
|
+
part.DeleteExternalRelationship(relationship);
|
|
599
|
+
var newRelationship = part.AddExternalRelationship(
|
|
600
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
|
601
|
+
new Uri(newUrl, UriKind.Absolute));
|
|
602
|
+
|
|
603
|
+
hyperlinkInfo.Element.Id = newRelationship.Id;
|
|
604
|
+
hyperlinkInfo.Id = newRelationship.Id;
|
|
605
|
+
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
catch
|
|
609
|
+
{
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private int AppendContentIdInElement(OpenXmlElement element, OpenXmlPart part, string contentId, Regex? urlRegex)
|
|
615
|
+
{
|
|
616
|
+
int updateCount = 0;
|
|
617
|
+
var hyperlinks = element.Descendants<Hyperlink>().ToList();
|
|
618
|
+
|
|
619
|
+
foreach (var hyperlink in hyperlinks)
|
|
620
|
+
{
|
|
621
|
+
try
|
|
622
|
+
{
|
|
623
|
+
var hyperlinkInfo = CreateDetailedHyperlinkInfo(hyperlink, part);
|
|
624
|
+
|
|
625
|
+
if (hyperlinkInfo.IsInternal || string.IsNullOrEmpty(hyperlinkInfo.Url))
|
|
626
|
+
continue;
|
|
627
|
+
|
|
628
|
+
if (urlRegex != null && !urlRegex.IsMatch(hyperlinkInfo.Url))
|
|
629
|
+
continue;
|
|
630
|
+
|
|
631
|
+
// Check if URL is a theSource URL with ContentId or DocumentId pattern
|
|
632
|
+
if (!hyperlinkInfo.Url.Contains("thesource", StringComparison.OrdinalIgnoreCase) ||
|
|
633
|
+
(!ContentIdPattern.IsMatch(hyperlinkInfo.Url) && !DocumentIdPattern.IsMatch(hyperlinkInfo.Url)))
|
|
634
|
+
continue;
|
|
635
|
+
|
|
636
|
+
// Process ALL theSource hyperlinks, even if they already have a content ID
|
|
637
|
+
// (as the existing one might be incorrect)
|
|
638
|
+
var newUrl = hyperlinkInfo.Url + contentId;
|
|
639
|
+
if (UpdateHyperlinkUrl(hyperlinkInfo, newUrl))
|
|
640
|
+
{
|
|
641
|
+
updateCount++;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
catch
|
|
645
|
+
{
|
|
646
|
+
// Skip this hyperlink on error
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return updateCount;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private async Task ValidateProcessedHyperlinksAsync(List<DetailedHyperlinkInfo> hyperlinks)
|
|
655
|
+
{
|
|
656
|
+
await Task.Run(() =>
|
|
657
|
+
{
|
|
658
|
+
foreach (var hyperlink in hyperlinks)
|
|
659
|
+
{
|
|
660
|
+
ValidateHyperlink(hyperlink);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/// <summary>
|
|
666
|
+
/// Helper method to create detailed hyperlink info for internal processing.
|
|
667
|
+
/// </summary>
|
|
668
|
+
private DetailedHyperlinkInfo CreateDetailedHyperlinkInfo(Hyperlink hyperlink, OpenXmlPart part)
|
|
669
|
+
{
|
|
670
|
+
var info = new DetailedHyperlinkInfo
|
|
671
|
+
{
|
|
672
|
+
Element = hyperlink,
|
|
673
|
+
ContainingPart = part,
|
|
674
|
+
DisplayText = string.Join("", hyperlink.Descendants<Text>().Select(t => t.Text))
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
var relationshipId = hyperlink.Id?.Value;
|
|
678
|
+
if (!string.IsNullOrEmpty(relationshipId))
|
|
679
|
+
{
|
|
680
|
+
info.Id = relationshipId;
|
|
681
|
+
|
|
682
|
+
try
|
|
683
|
+
{
|
|
684
|
+
var relationship = part.ExternalRelationships.FirstOrDefault(r => r.Id == relationshipId);
|
|
685
|
+
if (relationship != null)
|
|
686
|
+
{
|
|
687
|
+
info.Url = relationship.Uri?.ToString() ?? string.Empty;
|
|
688
|
+
info.IsInternal = false;
|
|
689
|
+
}
|
|
690
|
+
else
|
|
691
|
+
{
|
|
692
|
+
// Check for internal relationships
|
|
693
|
+
var internalRel = part.HyperlinkRelationships.FirstOrDefault(r => r.Id == relationshipId);
|
|
694
|
+
if (internalRel != null)
|
|
695
|
+
{
|
|
696
|
+
info.Url = internalRel.Uri?.ToString() ?? string.Empty;
|
|
697
|
+
info.IsInternal = true;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch (Exception ex)
|
|
702
|
+
{
|
|
703
|
+
info.IsValid = false;
|
|
704
|
+
info.ValidationMessage = "Unable to resolve hyperlink relationship";
|
|
705
|
+
_logger.Warning(ex, "Error resolving hyperlink relationship {RelationshipId}", relationshipId);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Get context
|
|
710
|
+
var paragraph = hyperlink.Ancestors<Paragraph>().FirstOrDefault();
|
|
711
|
+
if (paragraph != null)
|
|
712
|
+
{
|
|
713
|
+
var paragraphText = string.Join("", paragraph.Descendants<Text>().Select(t => t.Text));
|
|
714
|
+
info.Context = paragraphText.Length > 100 ? paragraphText.Substring(0, 100) + "..." : paragraphText;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return info;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/// <summary>
|
|
721
|
+
/// Retrieves the PowerAutomate URL from user settings with fallback to default.
|
|
722
|
+
/// </summary>
|
|
723
|
+
/// <returns>The configured PowerAutomate URL or default URL if not configured</returns>
|
|
724
|
+
private string GetPowerAutomateUrlFromSettings()
|
|
725
|
+
{
|
|
726
|
+
try
|
|
727
|
+
{
|
|
728
|
+
var settings = UserSettings.Load();
|
|
729
|
+
return !string.IsNullOrEmpty(settings.PowerAutomateUrl)
|
|
730
|
+
? settings.PowerAutomateUrl
|
|
731
|
+
: "https://default-powerautomate-url.com";
|
|
732
|
+
}
|
|
733
|
+
catch (Exception ex)
|
|
734
|
+
{
|
|
735
|
+
// Log warning but don't throw - return default URL
|
|
736
|
+
_logger.Warning(ex, "Could not load PowerAutomate URL from settings");
|
|
737
|
+
return "https://default-powerautomate-url.com";
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/// <summary>
|
|
742
|
+
/// Scans hyperlinks in a specific OpenXML element.
|
|
743
|
+
/// </summary>
|
|
744
|
+
private void ScanHyperlinksInElement(OpenXmlElement element, OpenXmlPart part,
|
|
745
|
+
List<DetailedHyperlinkInfo> hyperlinks, bool validateLinks)
|
|
746
|
+
{
|
|
747
|
+
var hyperlinkElements = element.Descendants<Hyperlink>().ToList();
|
|
748
|
+
|
|
749
|
+
foreach (var hyperlink in hyperlinkElements)
|
|
750
|
+
{
|
|
751
|
+
try
|
|
752
|
+
{
|
|
753
|
+
var hyperlinkInfo = CreateDetailedHyperlinkInfo(hyperlink, part);
|
|
754
|
+
|
|
755
|
+
if (validateLinks)
|
|
756
|
+
{
|
|
757
|
+
ValidateHyperlink(hyperlinkInfo);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
hyperlinks.Add(hyperlinkInfo);
|
|
761
|
+
}
|
|
762
|
+
catch (Exception ex)
|
|
763
|
+
{
|
|
764
|
+
// Create a partial hyperlink info for error cases
|
|
765
|
+
var errorInfo = new DetailedHyperlinkInfo
|
|
766
|
+
{
|
|
767
|
+
Element = hyperlink,
|
|
768
|
+
ContainingPart = part,
|
|
769
|
+
IsValid = false,
|
|
770
|
+
ValidationMessage = $"Error scanning hyperlink: {ex.Message}",
|
|
771
|
+
DisplayText = string.Join("", hyperlink.Descendants<Text>().Select(t => t.Text))
|
|
772
|
+
};
|
|
773
|
+
hyperlinks.Add(errorInfo);
|
|
774
|
+
_logger.Warning(ex, "Error scanning hyperlink in document");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/// <summary>
|
|
780
|
+
/// Advanced hyperlink fixing using API services and intelligent updates.
|
|
781
|
+
/// </summary>
|
|
782
|
+
private async Task<HyperlinkFixingResult> PerformAdvancedHyperlinkFixingAsync(WordprocessingDocument document, HyperlinkFixingOptions options)
|
|
783
|
+
{
|
|
784
|
+
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
|
785
|
+
var result = new HyperlinkFixingResult();
|
|
786
|
+
|
|
787
|
+
try
|
|
788
|
+
{
|
|
789
|
+
// Validate document structure
|
|
790
|
+
if (document.MainDocumentPart?.Document?.Body == null)
|
|
791
|
+
{
|
|
792
|
+
throw new InvalidOperationException("Document structure is invalid or corrupted");
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Phase 1: Extract hyperlink data with relationship IDs
|
|
796
|
+
var hyperlinkData = await ExtractHyperlinkDataAsync(document);
|
|
797
|
+
result.TotalHyperlinks = hyperlinkData.Count;
|
|
798
|
+
|
|
799
|
+
// Filter hyperlinks that have extractable IDs
|
|
800
|
+
var processableHyperlinks = hyperlinkData.Where(h => !string.IsNullOrEmpty(h.Url)).ToList();
|
|
801
|
+
result.ProcessedHyperlinks = processableHyperlinks.Count;
|
|
802
|
+
|
|
803
|
+
if (processableHyperlinks.Count == 0)
|
|
804
|
+
{
|
|
805
|
+
result.Success = true;
|
|
806
|
+
result.ErrorMessages.Add("No processable hyperlinks found in the document");
|
|
807
|
+
return result;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Phase 2: API Communication (if configured)
|
|
811
|
+
if (!string.IsNullOrEmpty(options.PowerAutomateUrl))
|
|
812
|
+
{
|
|
813
|
+
var urls = processableHyperlinks.Select(h => h.Url).ToList();
|
|
814
|
+
var apiSettings = new HyperlinkApiSettings
|
|
815
|
+
{
|
|
816
|
+
ApiUrl = options.PowerAutomateUrl,
|
|
817
|
+
TimeoutSeconds = 30
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
try
|
|
821
|
+
{
|
|
822
|
+
var apiResponse = await _apiService.ProcessHyperlinksAsync(urls, apiSettings);
|
|
823
|
+
if (apiResponse?.Body?.Results != null)
|
|
824
|
+
{
|
|
825
|
+
// Apply API-based fixes
|
|
826
|
+
result.UpdatedUrls += ApplyApiBasedFixes(processableHyperlinks, apiResponse, options);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
catch (Exception ex)
|
|
830
|
+
{
|
|
831
|
+
_logger.Warning(ex, "API communication failed, falling back to basic fixing");
|
|
832
|
+
// Continue with basic fixing even if API fails
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Phase 3: Apply basic content ID appending if configured
|
|
837
|
+
if (options.AppendContentId)
|
|
838
|
+
{
|
|
839
|
+
result.UpdatedUrls += await AppendContentIdToHyperlinksAsync(document, "#content");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Phase 4: Update titles if requested
|
|
843
|
+
if (options.UpdateTitles)
|
|
844
|
+
{
|
|
845
|
+
result.UpdatedDisplayTexts += UpdateHyperlinkTitles(processableHyperlinks);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
result.Success = true;
|
|
849
|
+
}
|
|
850
|
+
catch (Exception ex)
|
|
851
|
+
{
|
|
852
|
+
result.ErrorMessages.Add($"Unexpected error during hyperlink fixing: {ex.Message}");
|
|
853
|
+
_logger.Error(ex, "Error in advanced hyperlink fixing");
|
|
854
|
+
}
|
|
855
|
+
finally
|
|
856
|
+
{
|
|
857
|
+
stopwatch.Stop();
|
|
858
|
+
result.Duration = stopwatch.Elapsed;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return result;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/// <summary>
|
|
865
|
+
/// Extracts hyperlink data from the document for advanced processing.
|
|
866
|
+
/// </summary>
|
|
867
|
+
private async Task<List<DetailedHyperlinkInfo>> ExtractHyperlinkDataAsync(WordprocessingDocument document)
|
|
868
|
+
{
|
|
869
|
+
return await ScanHyperlinksAsync(document, false);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/// <summary>
|
|
873
|
+
/// Applies API-based fixes to hyperlinks.
|
|
874
|
+
/// </summary>
|
|
875
|
+
private int ApplyApiBasedFixes(List<DetailedHyperlinkInfo> hyperlinks, HyperlinkApiResponse apiResponse, HyperlinkFixingOptions options)
|
|
876
|
+
{
|
|
877
|
+
int updatedCount = 0;
|
|
878
|
+
|
|
879
|
+
if (apiResponse?.Body?.Results == null)
|
|
880
|
+
return updatedCount;
|
|
881
|
+
|
|
882
|
+
foreach (var hyperlink in hyperlinks)
|
|
883
|
+
{
|
|
884
|
+
// Find matching API result
|
|
885
|
+
var apiResult = apiResponse.Body.Results.FirstOrDefault(r =>
|
|
886
|
+
hyperlink.Url.Contains(r.Document_ID, StringComparison.OrdinalIgnoreCase) ||
|
|
887
|
+
hyperlink.Url.Contains(r.Content_ID, StringComparison.OrdinalIgnoreCase));
|
|
888
|
+
|
|
889
|
+
if (apiResult != null && !string.IsNullOrEmpty(apiResult.Title))
|
|
890
|
+
{
|
|
891
|
+
// Update hyperlink with API data
|
|
892
|
+
if (UpdateHyperlinkUrl(hyperlink, $"https://thesource.cvshealth.com/nuxeo/thesource/#!/view?docid={apiResult.Document_ID}"))
|
|
893
|
+
{
|
|
894
|
+
updatedCount++;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return updatedCount;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/// <summary>
|
|
903
|
+
/// Updates hyperlink titles based on their URLs.
|
|
904
|
+
/// </summary>
|
|
905
|
+
private int UpdateHyperlinkTitles(List<DetailedHyperlinkInfo> hyperlinks)
|
|
906
|
+
{
|
|
907
|
+
int updatedCount = 0;
|
|
908
|
+
|
|
909
|
+
foreach (var hyperlink in hyperlinks)
|
|
910
|
+
{
|
|
911
|
+
try
|
|
912
|
+
{
|
|
913
|
+
// Extract meaningful title from URL or existing text
|
|
914
|
+
var newTitle = ExtractTitleFromUrl(hyperlink.Url) ?? hyperlink.DisplayText;
|
|
915
|
+
if (!string.IsNullOrEmpty(newTitle) && newTitle != hyperlink.DisplayText)
|
|
916
|
+
{
|
|
917
|
+
// Update display text
|
|
918
|
+
var textElements = hyperlink.Element.Descendants<Text>().ToList();
|
|
919
|
+
if (textElements.Any())
|
|
920
|
+
{
|
|
921
|
+
textElements.First().Text = newTitle;
|
|
922
|
+
foreach (var extraText in textElements.Skip(1))
|
|
923
|
+
{
|
|
924
|
+
extraText.Remove();
|
|
925
|
+
}
|
|
926
|
+
updatedCount++;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
catch (Exception ex)
|
|
931
|
+
{
|
|
932
|
+
_logger.Warning(ex, "Error updating hyperlink title for {Url}", hyperlink.Url);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return updatedCount;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/// <summary>
|
|
940
|
+
/// Finds all hyperlinks in a document that match the specified patterns.
|
|
941
|
+
/// </summary>
|
|
942
|
+
/// <param name="documentPath">Path to the document to search</param>
|
|
943
|
+
/// <param name="urlPattern">Regex pattern to match URLs</param>
|
|
944
|
+
/// <param name="displayPattern">Optional regex pattern to match display text</param>
|
|
945
|
+
/// <returns>List of matching hyperlinks</returns>
|
|
946
|
+
public async Task<List<HyperlinkMatch>> FindHyperlinksAsync(string documentPath, string urlPattern, string? displayPattern = null)
|
|
947
|
+
{
|
|
948
|
+
var matches = new List<HyperlinkMatch>();
|
|
949
|
+
|
|
950
|
+
if (!File.Exists(documentPath))
|
|
951
|
+
{
|
|
952
|
+
_logger.Warning("Document not found: {DocumentPath}", documentPath);
|
|
953
|
+
return matches;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
try
|
|
957
|
+
{
|
|
958
|
+
using var document = WordprocessingDocument.Open(documentPath, false);
|
|
959
|
+
|
|
960
|
+
var urlRegex = new Regex(urlPattern, RegexOptions.IgnoreCase);
|
|
961
|
+
var displayRegex = !string.IsNullOrWhiteSpace(displayPattern)
|
|
962
|
+
? new Regex(displayPattern, RegexOptions.IgnoreCase)
|
|
963
|
+
: null;
|
|
964
|
+
|
|
965
|
+
await Task.Run(() =>
|
|
966
|
+
{
|
|
967
|
+
// Search in main document
|
|
968
|
+
if (document.MainDocumentPart?.Document?.Body != null)
|
|
969
|
+
{
|
|
970
|
+
var mainMatches = FindHyperlinksInPart(document.MainDocumentPart.Document.Body,
|
|
971
|
+
document.MainDocumentPart, urlRegex, displayRegex, documentPath);
|
|
972
|
+
matches.AddRange(mainMatches);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Search in headers
|
|
976
|
+
var headerParts = document.MainDocumentPart?.HeaderParts;
|
|
977
|
+
if (headerParts != null)
|
|
978
|
+
{
|
|
979
|
+
foreach (var headerPart in headerParts)
|
|
980
|
+
{
|
|
981
|
+
var headerMatches = FindHyperlinksInPart(headerPart.Header, headerPart, urlRegex, displayRegex, documentPath);
|
|
982
|
+
matches.AddRange(headerMatches);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// Search in footers
|
|
987
|
+
var footerParts = document.MainDocumentPart?.FooterParts;
|
|
988
|
+
if (footerParts != null)
|
|
989
|
+
{
|
|
990
|
+
foreach (var footerPart in footerParts)
|
|
991
|
+
{
|
|
992
|
+
var footerMatches = FindHyperlinksInPart(footerPart.Footer, footerPart, urlRegex, displayRegex, documentPath);
|
|
993
|
+
matches.AddRange(footerMatches);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
catch (Exception ex)
|
|
999
|
+
{
|
|
1000
|
+
_logger.Error(ex, "Error finding hyperlinks in document: {DocumentPath}", documentPath);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
return matches;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/// <summary>
|
|
1007
|
+
/// Replaces hyperlinks matching specified patterns with new values.
|
|
1008
|
+
/// </summary>
|
|
1009
|
+
/// <param name="documentPath">Path to the document to update</param>
|
|
1010
|
+
/// <param name="urlPattern">Regex pattern to match URLs for replacement</param>
|
|
1011
|
+
/// <param name="newUrl">New URL to replace matches with</param>
|
|
1012
|
+
/// <param name="newDisplayText">Optional new display text</param>
|
|
1013
|
+
/// <param name="displayPattern">Optional regex pattern to match display text</param>
|
|
1014
|
+
/// <returns>Result containing replacement statistics</returns>
|
|
1015
|
+
public async Task<HyperlinkReplacementResult> ReplaceHyperlinksAsync(string documentPath, string urlPattern,
|
|
1016
|
+
string newUrl, string? newDisplayText = null, string? displayPattern = null)
|
|
1017
|
+
{
|
|
1018
|
+
var result = new HyperlinkReplacementResult();
|
|
1019
|
+
|
|
1020
|
+
if (!File.Exists(documentPath))
|
|
1021
|
+
{
|
|
1022
|
+
result.Errors.Add($"Document not found: {documentPath}");
|
|
1023
|
+
return result;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
try
|
|
1027
|
+
{
|
|
1028
|
+
using var document = WordprocessingDocument.Open(documentPath, true);
|
|
1029
|
+
|
|
1030
|
+
var urlRegex = new Regex(urlPattern, RegexOptions.IgnoreCase);
|
|
1031
|
+
var displayRegex = !string.IsNullOrWhiteSpace(displayPattern)
|
|
1032
|
+
? new Regex(displayPattern, RegexOptions.IgnoreCase)
|
|
1033
|
+
: null;
|
|
1034
|
+
|
|
1035
|
+
await Task.Run(() =>
|
|
1036
|
+
{
|
|
1037
|
+
// Replace in main document
|
|
1038
|
+
if (document.MainDocumentPart?.Document?.Body != null)
|
|
1039
|
+
{
|
|
1040
|
+
var mainResult = ReplaceHyperlinksInPart(document.MainDocumentPart.Document.Body,
|
|
1041
|
+
document.MainDocumentPart, urlRegex, newUrl, newDisplayText, displayRegex);
|
|
1042
|
+
result.TotalMatches += mainResult.matches;
|
|
1043
|
+
result.SuccessfulReplacements += mainResult.replacements;
|
|
1044
|
+
result.FailedReplacements += mainResult.failures;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Replace in headers
|
|
1048
|
+
var headerParts = document.MainDocumentPart?.HeaderParts;
|
|
1049
|
+
if (headerParts != null)
|
|
1050
|
+
{
|
|
1051
|
+
foreach (var headerPart in headerParts)
|
|
1052
|
+
{
|
|
1053
|
+
var headerResult = ReplaceHyperlinksInPart(headerPart.Header, headerPart,
|
|
1054
|
+
urlRegex, newUrl, newDisplayText, displayRegex);
|
|
1055
|
+
result.TotalMatches += headerResult.matches;
|
|
1056
|
+
result.SuccessfulReplacements += headerResult.replacements;
|
|
1057
|
+
result.FailedReplacements += headerResult.failures;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Replace in footers
|
|
1062
|
+
var footerParts = document.MainDocumentPart?.FooterParts;
|
|
1063
|
+
if (footerParts != null)
|
|
1064
|
+
{
|
|
1065
|
+
foreach (var footerPart in footerParts)
|
|
1066
|
+
{
|
|
1067
|
+
var footerResult = ReplaceHyperlinksInPart(footerPart.Footer, footerPart,
|
|
1068
|
+
urlRegex, newUrl, newDisplayText, displayRegex);
|
|
1069
|
+
result.TotalMatches += footerResult.matches;
|
|
1070
|
+
result.SuccessfulReplacements += footerResult.replacements;
|
|
1071
|
+
result.FailedReplacements += footerResult.failures;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
catch (Exception ex)
|
|
1077
|
+
{
|
|
1078
|
+
_logger.Error(ex, "Error replacing hyperlinks in document: {DocumentPath}", documentPath);
|
|
1079
|
+
result.Errors.Add($"Error replacing hyperlinks: {ex.Message}");
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
return result;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/// <summary>
|
|
1086
|
+
/// Finds hyperlinks in a specific document part.
|
|
1087
|
+
/// </summary>
|
|
1088
|
+
private List<HyperlinkMatch> FindHyperlinksInPart(OpenXmlElement element, OpenXmlPart part,
|
|
1089
|
+
Regex urlRegex, Regex? displayRegex, string documentPath)
|
|
1090
|
+
{
|
|
1091
|
+
var matches = new List<HyperlinkMatch>();
|
|
1092
|
+
var hyperlinks = element.Descendants<Hyperlink>().ToList();
|
|
1093
|
+
|
|
1094
|
+
foreach (var hyperlink in hyperlinks)
|
|
1095
|
+
{
|
|
1096
|
+
try
|
|
1097
|
+
{
|
|
1098
|
+
var hyperlinkInfo = CreateDetailedHyperlinkInfo(hyperlink, part);
|
|
1099
|
+
|
|
1100
|
+
// Check if URL matches pattern
|
|
1101
|
+
if (!string.IsNullOrEmpty(hyperlinkInfo.Url) && urlRegex.IsMatch(hyperlinkInfo.Url))
|
|
1102
|
+
{
|
|
1103
|
+
// Check display text pattern if specified
|
|
1104
|
+
if (displayRegex == null || displayRegex.IsMatch(hyperlinkInfo.DisplayText))
|
|
1105
|
+
{
|
|
1106
|
+
matches.Add(new HyperlinkMatch
|
|
1107
|
+
{
|
|
1108
|
+
CurrentUrl = hyperlinkInfo.Url,
|
|
1109
|
+
DisplayText = hyperlinkInfo.DisplayText,
|
|
1110
|
+
Context = hyperlinkInfo.Context ?? string.Empty,
|
|
1111
|
+
DocumentPath = documentPath,
|
|
1112
|
+
RelationshipId = hyperlinkInfo.Id,
|
|
1113
|
+
HyperlinkElement = hyperlink,
|
|
1114
|
+
ContainingPart = part
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
catch (Exception ex)
|
|
1120
|
+
{
|
|
1121
|
+
_logger.Warning(ex, "Error processing hyperlink during search");
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
return matches;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/// <summary>
|
|
1129
|
+
/// Replaces hyperlinks in a specific document part.
|
|
1130
|
+
/// </summary>
|
|
1131
|
+
private (int matches, int replacements, int failures) ReplaceHyperlinksInPart(OpenXmlElement element, OpenXmlPart part,
|
|
1132
|
+
Regex urlRegex, string newUrl, string? newDisplayText, Regex? displayRegex)
|
|
1133
|
+
{
|
|
1134
|
+
int matches = 0, replacements = 0, failures = 0;
|
|
1135
|
+
var hyperlinks = element.Descendants<Hyperlink>().ToList();
|
|
1136
|
+
|
|
1137
|
+
foreach (var hyperlink in hyperlinks)
|
|
1138
|
+
{
|
|
1139
|
+
try
|
|
1140
|
+
{
|
|
1141
|
+
var hyperlinkInfo = CreateDetailedHyperlinkInfo(hyperlink, part);
|
|
1142
|
+
|
|
1143
|
+
// Check if URL matches pattern
|
|
1144
|
+
if (!string.IsNullOrEmpty(hyperlinkInfo.Url) && urlRegex.IsMatch(hyperlinkInfo.Url))
|
|
1145
|
+
{
|
|
1146
|
+
// Check display text pattern if specified
|
|
1147
|
+
if (displayRegex == null || displayRegex.IsMatch(hyperlinkInfo.DisplayText))
|
|
1148
|
+
{
|
|
1149
|
+
matches++;
|
|
1150
|
+
|
|
1151
|
+
// Replace URL
|
|
1152
|
+
if (UpdateHyperlinkUrl(hyperlinkInfo, newUrl))
|
|
1153
|
+
{
|
|
1154
|
+
// Replace display text if specified
|
|
1155
|
+
if (!string.IsNullOrEmpty(newDisplayText))
|
|
1156
|
+
{
|
|
1157
|
+
var textElements = hyperlink.Descendants<Text>().ToList();
|
|
1158
|
+
if (textElements.Any())
|
|
1159
|
+
{
|
|
1160
|
+
textElements.First().Text = newDisplayText;
|
|
1161
|
+
foreach (var extraText in textElements.Skip(1))
|
|
1162
|
+
{
|
|
1163
|
+
extraText.Remove();
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
replacements++;
|
|
1169
|
+
}
|
|
1170
|
+
else
|
|
1171
|
+
{
|
|
1172
|
+
failures++;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
catch (Exception ex)
|
|
1178
|
+
{
|
|
1179
|
+
_logger.Warning(ex, "Error replacing hyperlink");
|
|
1180
|
+
failures++;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return (matches, replacements, failures);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/// <summary>
|
|
1188
|
+
/// Extracts a meaningful title from a URL.
|
|
1189
|
+
/// </summary>
|
|
1190
|
+
private string? ExtractTitleFromUrl(string url)
|
|
1191
|
+
{
|
|
1192
|
+
try
|
|
1193
|
+
{
|
|
1194
|
+
if (Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
|
1195
|
+
{
|
|
1196
|
+
// Extract title from path or query parameters
|
|
1197
|
+
var segments = uri.Segments;
|
|
1198
|
+
if (segments.Length > 0)
|
|
1199
|
+
{
|
|
1200
|
+
var lastSegment = segments[segments.Length - 1].Trim('/', '\\');
|
|
1201
|
+
if (!string.IsNullOrEmpty(lastSegment) && lastSegment.Length > 3)
|
|
1202
|
+
{
|
|
1203
|
+
return Uri.UnescapeDataString(lastSegment);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
catch
|
|
1209
|
+
{
|
|
1210
|
+
// Return null if extraction fails
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
#endregion
|
|
1217
|
+
|
|
1218
|
+
#region IHyperlinkService Implementation
|
|
1219
|
+
|
|
1220
|
+
/// <summary>
|
|
1221
|
+
/// Extracts hyperlinks from a document file
|
|
1222
|
+
/// </summary>
|
|
1223
|
+
/// <param name="filePath">Path to the document file</param>
|
|
1224
|
+
/// <returns>List of hyperlink information</returns>
|
|
1225
|
+
public async Task<List<Models.HyperlinkInfo>> ExtractHyperlinksAsync(string filePath)
|
|
1226
|
+
{
|
|
1227
|
+
if (string.IsNullOrWhiteSpace(filePath))
|
|
1228
|
+
throw new ArgumentException("File path cannot be null or empty", nameof(filePath));
|
|
1229
|
+
|
|
1230
|
+
if (!File.Exists(filePath))
|
|
1231
|
+
throw new FileNotFoundException($"File not found: {filePath}");
|
|
1232
|
+
|
|
1233
|
+
return await Task.Run(() =>
|
|
1234
|
+
{
|
|
1235
|
+
using var document = WordprocessingDocument.Open(filePath, false);
|
|
1236
|
+
return ExtractHyperlinks(document);
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/// <summary>
|
|
1241
|
+
/// Updates a specific hyperlink URL in a document
|
|
1242
|
+
/// </summary>
|
|
1243
|
+
/// <param name="filePath">Path to the document file</param>
|
|
1244
|
+
/// <param name="oldUrl">The URL to replace</param>
|
|
1245
|
+
/// <param name="newUrl">The new URL</param>
|
|
1246
|
+
/// <returns>True if successful</returns>
|
|
1247
|
+
public async Task<bool> UpdateHyperlinkAsync(string filePath, string oldUrl, string newUrl)
|
|
1248
|
+
{
|
|
1249
|
+
if (string.IsNullOrWhiteSpace(filePath))
|
|
1250
|
+
throw new ArgumentException("File path cannot be null or empty", nameof(filePath));
|
|
1251
|
+
|
|
1252
|
+
if (!File.Exists(filePath))
|
|
1253
|
+
throw new FileNotFoundException($"File not found: {filePath}");
|
|
1254
|
+
|
|
1255
|
+
return await Task.Run(() =>
|
|
1256
|
+
{
|
|
1257
|
+
try
|
|
1258
|
+
{
|
|
1259
|
+
using var document = WordprocessingDocument.Open(filePath, true);
|
|
1260
|
+
var hyperlinks = ExtractHyperlinks(document);
|
|
1261
|
+
var targetHyperlink = hyperlinks.FirstOrDefault(h => h.Url == oldUrl);
|
|
1262
|
+
|
|
1263
|
+
if (targetHyperlink != null)
|
|
1264
|
+
{
|
|
1265
|
+
// Find and update the hyperlink element directly
|
|
1266
|
+
var hyperlinkElements = document.MainDocumentPart?.Document?.Body?.Descendants<Hyperlink>() ?? Enumerable.Empty<Hyperlink>();
|
|
1267
|
+
foreach (var hyperlink in hyperlinkElements)
|
|
1268
|
+
{
|
|
1269
|
+
if (hyperlink.Id?.Value == targetHyperlink.Id)
|
|
1270
|
+
{
|
|
1271
|
+
// Update the hyperlink URL
|
|
1272
|
+
var relationshipId = hyperlink.Id.Value;
|
|
1273
|
+
var relationship = document.MainDocumentPart?.ExternalRelationships?.FirstOrDefault(r => r.Id == relationshipId);
|
|
1274
|
+
if (relationship != null && document.MainDocumentPart != null)
|
|
1275
|
+
{
|
|
1276
|
+
var mainPart = document.MainDocumentPart; // Store reference to avoid null warnings
|
|
1277
|
+
mainPart.DeleteExternalRelationship(relationship);
|
|
1278
|
+
var newRelationship = mainPart.AddExternalRelationship(
|
|
1279
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
|
1280
|
+
new Uri(newUrl, UriKind.Absolute));
|
|
1281
|
+
hyperlink.Id = newRelationship.Id;
|
|
1282
|
+
return true;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
catch (Exception ex)
|
|
1290
|
+
{
|
|
1291
|
+
_logger.Error(ex, "Error updating hyperlink in document: {FilePath}", filePath);
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/// <summary>
|
|
1298
|
+
/// Updates multiple hyperlinks in a document based on URL mappings
|
|
1299
|
+
/// </summary>
|
|
1300
|
+
/// <param name="filePath">Path to the document file</param>
|
|
1301
|
+
/// <param name="urlMappings">Dictionary mapping old URLs to new URLs</param>
|
|
1302
|
+
/// <returns>Number of hyperlinks updated</returns>
|
|
1303
|
+
public async Task<int> UpdateAllHyperlinksAsync(string filePath, Dictionary<string, string> urlMappings)
|
|
1304
|
+
{
|
|
1305
|
+
if (string.IsNullOrWhiteSpace(filePath))
|
|
1306
|
+
throw new ArgumentException("File path cannot be null or empty", nameof(filePath));
|
|
1307
|
+
|
|
1308
|
+
if (!File.Exists(filePath))
|
|
1309
|
+
throw new FileNotFoundException($"File not found: {filePath}");
|
|
1310
|
+
|
|
1311
|
+
if (urlMappings == null || urlMappings.Count == 0)
|
|
1312
|
+
return 0;
|
|
1313
|
+
|
|
1314
|
+
return await Task.Run(() =>
|
|
1315
|
+
{
|
|
1316
|
+
try
|
|
1317
|
+
{
|
|
1318
|
+
using var document = WordprocessingDocument.Open(filePath, true);
|
|
1319
|
+
var hyperlinks = ExtractHyperlinks(document);
|
|
1320
|
+
int updatedCount = 0;
|
|
1321
|
+
|
|
1322
|
+
foreach (var hyperlink in hyperlinks)
|
|
1323
|
+
{
|
|
1324
|
+
if (urlMappings.TryGetValue(hyperlink.Url, out var newUrl))
|
|
1325
|
+
{
|
|
1326
|
+
// Find and update the hyperlink element directly
|
|
1327
|
+
var hyperlinkElements = document.MainDocumentPart?.Document?.Body?.Descendants<Hyperlink>() ?? Enumerable.Empty<Hyperlink>();
|
|
1328
|
+
foreach (var hyperlinkElement in hyperlinkElements)
|
|
1329
|
+
{
|
|
1330
|
+
if (hyperlinkElement.Id?.Value == hyperlink.Id)
|
|
1331
|
+
{
|
|
1332
|
+
var relationshipId = hyperlinkElement.Id.Value;
|
|
1333
|
+
var relationship = document.MainDocumentPart?.ExternalRelationships?.FirstOrDefault(r => r.Id == relationshipId);
|
|
1334
|
+
if (relationship != null && document.MainDocumentPart != null)
|
|
1335
|
+
{
|
|
1336
|
+
document.MainDocumentPart.DeleteExternalRelationship(relationship);
|
|
1337
|
+
var newRelationship = document.MainDocumentPart.AddExternalRelationship(
|
|
1338
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
|
1339
|
+
new Uri(newUrl, UriKind.Absolute));
|
|
1340
|
+
hyperlinkElement.Id = newRelationship.Id;
|
|
1341
|
+
updatedCount++;
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
return updatedCount;
|
|
1350
|
+
}
|
|
1351
|
+
catch (Exception ex)
|
|
1352
|
+
{
|
|
1353
|
+
_logger.Error(ex, "Error updating hyperlinks in document: {FilePath}", filePath);
|
|
1354
|
+
return 0;
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
/// <summary>
|
|
1360
|
+
/// Validates if a URL is well-formed and uses valid schemes.
|
|
1361
|
+
/// </summary>
|
|
1362
|
+
/// <param name="url">The URL to validate</param>
|
|
1363
|
+
/// <returns>True if valid</returns>
|
|
1364
|
+
public async Task<bool> ValidateHyperlinkAsync(string url)
|
|
1365
|
+
{
|
|
1366
|
+
return await Task.Run(() =>
|
|
1367
|
+
{
|
|
1368
|
+
if (string.IsNullOrWhiteSpace(url))
|
|
1369
|
+
return false;
|
|
1370
|
+
|
|
1371
|
+
// Allow internal links starting with #
|
|
1372
|
+
if (url.StartsWith("#"))
|
|
1373
|
+
return true;
|
|
1374
|
+
|
|
1375
|
+
// Validate as URI
|
|
1376
|
+
if (Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
|
1377
|
+
{
|
|
1378
|
+
// Allow common schemes
|
|
1379
|
+
return uri.Scheme == Uri.UriSchemeHttp ||
|
|
1380
|
+
uri.Scheme == Uri.UriSchemeHttps ||
|
|
1381
|
+
uri.Scheme == Uri.UriSchemeFtp ||
|
|
1382
|
+
uri.Scheme == Uri.UriSchemeMailto ||
|
|
1383
|
+
uri.Scheme == Uri.UriSchemeFile;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
return false;
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/// <summary>
|
|
1391
|
+
/// Performs detailed validation of a single hyperlink.
|
|
1392
|
+
/// </summary>
|
|
1393
|
+
/// <param name="hyperlinkInfo">The hyperlink to validate</param>
|
|
1394
|
+
public void ValidateHyperlink(DetailedHyperlinkInfo hyperlinkInfo)
|
|
1395
|
+
{
|
|
1396
|
+
if (string.IsNullOrEmpty(hyperlinkInfo.Url))
|
|
1397
|
+
{
|
|
1398
|
+
hyperlinkInfo.IsValid = false;
|
|
1399
|
+
hyperlinkInfo.ValidationMessage = "Hyperlink has no URL";
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
if (hyperlinkInfo.IsInternal)
|
|
1404
|
+
{
|
|
1405
|
+
// For internal links, basic validation
|
|
1406
|
+
if (!hyperlinkInfo.Url.StartsWith("#"))
|
|
1407
|
+
{
|
|
1408
|
+
hyperlinkInfo.IsValid = false;
|
|
1409
|
+
hyperlinkInfo.ValidationMessage = "Internal hyperlink should start with #";
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
else
|
|
1413
|
+
{
|
|
1414
|
+
// For external links, basic URL format validation
|
|
1415
|
+
if (!Uri.TryCreate(hyperlinkInfo.Url, UriKind.Absolute, out var uri))
|
|
1416
|
+
{
|
|
1417
|
+
hyperlinkInfo.IsValid = false;
|
|
1418
|
+
hyperlinkInfo.ValidationMessage = "Invalid URL format";
|
|
1419
|
+
}
|
|
1420
|
+
else if (uri.Scheme != "http" && uri.Scheme != "https" && uri.Scheme != "mailto")
|
|
1421
|
+
{
|
|
1422
|
+
hyperlinkInfo.IsValid = false;
|
|
1423
|
+
hyperlinkInfo.ValidationMessage = $"Unsupported URL scheme: {uri.Scheme}";
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/// <summary>
|
|
1429
|
+
/// Extracts hyperlinks from an open WordprocessingDocument
|
|
1430
|
+
/// </summary>
|
|
1431
|
+
/// <param name="document">The document to extract hyperlinks from</param>
|
|
1432
|
+
/// <returns>List of hyperlink information</returns>
|
|
1433
|
+
public List<Models.HyperlinkInfo> ExtractHyperlinks(WordprocessingDocument document)
|
|
1434
|
+
{
|
|
1435
|
+
if (document == null)
|
|
1436
|
+
throw new ArgumentNullException(nameof(document));
|
|
1437
|
+
|
|
1438
|
+
// Use internal scanning method and convert to public model
|
|
1439
|
+
var detailedHyperlinks = ScanHyperlinksAsync(document, false).GetAwaiter().GetResult();
|
|
1440
|
+
|
|
1441
|
+
// Convert DetailedHyperlinkInfo to public Models.HyperlinkInfo for backward compatibility
|
|
1442
|
+
return detailedHyperlinks.Select(h => new Models.HyperlinkInfo
|
|
1443
|
+
{
|
|
1444
|
+
Id = h.Id,
|
|
1445
|
+
Url = h.Url,
|
|
1446
|
+
DisplayText = h.DisplayText,
|
|
1447
|
+
Type = h.IsInternal ? HyperlinkType.Internal : HyperlinkType.External,
|
|
1448
|
+
IsValid = h.IsValid,
|
|
1449
|
+
ValidationMessage = h.ValidationMessage,
|
|
1450
|
+
Tooltip = string.Empty // Not available in DetailedHyperlinkInfo
|
|
1451
|
+
}).ToList();
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
#endregion
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
/// <summary>
|
|
1459
|
+
/// Represents a hyperlink found in the document.
|
|
1460
|
+
/// </summary>
|
|
1461
|
+
public class HyperlinkMatch
|
|
1462
|
+
{
|
|
1463
|
+
public string CurrentUrl { get; set; } = string.Empty;
|
|
1464
|
+
public string DisplayText { get; set; } = string.Empty;
|
|
1465
|
+
public string Context { get; set; } = string.Empty;
|
|
1466
|
+
public string DocumentPath { get; set; } = string.Empty;
|
|
1467
|
+
public string RelationshipId { get; set; } = string.Empty;
|
|
1468
|
+
public Hyperlink HyperlinkElement { get; set; } = null!;
|
|
1469
|
+
public OpenXmlPart ContainingPart { get; set; } = null!;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/// <summary>
|
|
1473
|
+
/// Result of hyperlink replacement operation.
|
|
1474
|
+
/// </summary>
|
|
1475
|
+
public class HyperlinkReplacementResult
|
|
1476
|
+
{
|
|
1477
|
+
public int TotalMatches { get; set; }
|
|
1478
|
+
public int SuccessfulReplacements { get; set; }
|
|
1479
|
+
public int FailedReplacements { get; set; }
|
|
1480
|
+
public List<string> Errors { get; set; } = new();
|
|
1481
|
+
public bool IsSuccess => FailedReplacements == 0;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
}
|