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,496 @@
|
|
|
1
|
+
# Hyperlink Management in .docx Files: Technical Documentation
|
|
2
|
+
|
|
3
|
+
## Architecture Overview
|
|
4
|
+
|
|
5
|
+
In OpenXML, hyperlinks use a two-part reference system:
|
|
6
|
+
|
|
7
|
+
- `<w:hyperlink>` element in document.xml contains an `r:id` attribute
|
|
8
|
+
- `r:id` references a relationship entry in document.xml.rels with the target URL
|
|
9
|
+
|
|
10
|
+
```xml
|
|
11
|
+
<!-- document.xml -->
|
|
12
|
+
<w:hyperlink r:id="rId4" w:history="1">
|
|
13
|
+
<w:r>
|
|
14
|
+
<w:t>Link text</w:t>
|
|
15
|
+
</w:r>
|
|
16
|
+
</w:hyperlink>
|
|
17
|
+
|
|
18
|
+
<!-- document.xml.rels -->
|
|
19
|
+
<Relationship
|
|
20
|
+
Id="rId4"
|
|
21
|
+
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
|
|
22
|
+
Target="https://example.com"
|
|
23
|
+
TargetMode="External"/>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## TypeScript Implementation
|
|
27
|
+
|
|
28
|
+
### Basic Hyperlink Manager
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { Document, ExternalHyperlink, InternalHyperlink, TextRun, Paragraph } from 'docx';
|
|
32
|
+
import JSZip from 'jszip';
|
|
33
|
+
import { parseStringPromise, Builder } from 'xml2js';
|
|
34
|
+
|
|
35
|
+
interface HyperlinkData {
|
|
36
|
+
id: string;
|
|
37
|
+
type: 'external' | 'internal';
|
|
38
|
+
target: string;
|
|
39
|
+
text: string;
|
|
40
|
+
created: Date;
|
|
41
|
+
modified?: Date;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class HyperlinkManager {
|
|
45
|
+
private relationships = new Map<string, HyperlinkData>();
|
|
46
|
+
private nextId = 1;
|
|
47
|
+
|
|
48
|
+
createExternal(text: string, url: string): ExternalHyperlink {
|
|
49
|
+
const id = `rId${this.nextId++}`;
|
|
50
|
+
|
|
51
|
+
this.relationships.set(id, {
|
|
52
|
+
id,
|
|
53
|
+
type: 'external',
|
|
54
|
+
target: url,
|
|
55
|
+
text,
|
|
56
|
+
created: new Date(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return new ExternalHyperlink({
|
|
60
|
+
link: url,
|
|
61
|
+
children: [
|
|
62
|
+
new TextRun({
|
|
63
|
+
text,
|
|
64
|
+
style: 'Hyperlink',
|
|
65
|
+
}),
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
createInternal(text: string, bookmarkId: string): InternalHyperlink {
|
|
71
|
+
const id = `rId${this.nextId++}`;
|
|
72
|
+
|
|
73
|
+
this.relationships.set(id, {
|
|
74
|
+
id,
|
|
75
|
+
type: 'internal',
|
|
76
|
+
target: `#${bookmarkId}`,
|
|
77
|
+
text,
|
|
78
|
+
created: new Date(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return new InternalHyperlink({
|
|
82
|
+
anchor: bookmarkId,
|
|
83
|
+
children: [
|
|
84
|
+
new TextRun({
|
|
85
|
+
text,
|
|
86
|
+
style: 'Hyperlink',
|
|
87
|
+
}),
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getRelationships(): HyperlinkData[] {
|
|
93
|
+
return Array.from(this.relationships.values());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Modifying Existing Hyperlinks
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
export class HyperlinkModifier {
|
|
102
|
+
async updateUrl(documentPath: string, oldUrl: string, newUrl: string): Promise<UpdateResult> {
|
|
103
|
+
const zip = await JSZip.loadAsync(await fs.readFile(documentPath));
|
|
104
|
+
const changes: Change[] = [];
|
|
105
|
+
|
|
106
|
+
// Parse relationships
|
|
107
|
+
const relsFile = zip.file('word/_rels/document.xml.rels');
|
|
108
|
+
if (!relsFile) throw new Error('Relationships file not found');
|
|
109
|
+
|
|
110
|
+
const relsXml = await relsFile.async('string');
|
|
111
|
+
const parsed = await parseStringPromise(relsXml);
|
|
112
|
+
|
|
113
|
+
// Update matching relationships
|
|
114
|
+
const relationships = parsed.Relationships.Relationship;
|
|
115
|
+
for (const rel of relationships) {
|
|
116
|
+
if (rel.$.Target === oldUrl) {
|
|
117
|
+
changes.push({
|
|
118
|
+
relationshipId: rel.$.Id,
|
|
119
|
+
oldTarget: rel.$.Target,
|
|
120
|
+
newTarget: newUrl,
|
|
121
|
+
});
|
|
122
|
+
rel.$.Target = newUrl;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Save changes
|
|
127
|
+
const builder = new Builder();
|
|
128
|
+
const updatedXml = builder.buildObject(parsed);
|
|
129
|
+
zip.file('word/_rels/document.xml.rels', updatedXml);
|
|
130
|
+
|
|
131
|
+
const buffer = await zip.generateAsync({ type: 'nodebuffer' });
|
|
132
|
+
await fs.writeFile(documentPath, buffer);
|
|
133
|
+
|
|
134
|
+
return { success: true, changes };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async updateText(documentPath: string, relationshipId: string, newText: string): Promise<void> {
|
|
138
|
+
const zip = await JSZip.loadAsync(await fs.readFile(documentPath));
|
|
139
|
+
|
|
140
|
+
const docFile = zip.file('word/document.xml');
|
|
141
|
+
if (!docFile) throw new Error('Document file not found');
|
|
142
|
+
|
|
143
|
+
const docXml = await docFile.async('string');
|
|
144
|
+
const parsed = await parseStringPromise(docXml);
|
|
145
|
+
|
|
146
|
+
// Find and update hyperlink text
|
|
147
|
+
this.findAndUpdateHyperlink(parsed, relationshipId, newText);
|
|
148
|
+
|
|
149
|
+
const builder = new Builder();
|
|
150
|
+
zip.file('word/document.xml', builder.buildObject(parsed));
|
|
151
|
+
|
|
152
|
+
const buffer = await zip.generateAsync({ type: 'nodebuffer' });
|
|
153
|
+
await fs.writeFile(documentPath, buffer);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private findAndUpdateHyperlink(doc: any, relationshipId: string, newText: string): void {
|
|
157
|
+
const traverse = (node: any): void => {
|
|
158
|
+
if (!node || typeof node !== 'object') return;
|
|
159
|
+
|
|
160
|
+
if (node['w:hyperlink']) {
|
|
161
|
+
const hyperlinks = Array.isArray(node['w:hyperlink'])
|
|
162
|
+
? node['w:hyperlink']
|
|
163
|
+
: [node['w:hyperlink']];
|
|
164
|
+
|
|
165
|
+
for (const hyperlink of hyperlinks) {
|
|
166
|
+
if (hyperlink.$?.['r:id'] === relationshipId) {
|
|
167
|
+
// Update text in runs
|
|
168
|
+
const runs = hyperlink['w:r'] || [];
|
|
169
|
+
for (const run of runs) {
|
|
170
|
+
if (run['w:t']) {
|
|
171
|
+
run['w:t'] = [newText];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
Object.values(node).forEach((child) => traverse(child));
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
traverse(doc);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Change Tracking Implementation
|
|
187
|
+
|
|
188
|
+
Since `docx` library doesn't support native Word track changes, implement custom tracking:
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
interface TrackedChange {
|
|
192
|
+
id: string;
|
|
193
|
+
type: 'insert' | 'delete' | 'modify';
|
|
194
|
+
elementType: 'hyperlink';
|
|
195
|
+
author: string;
|
|
196
|
+
timestamp: Date;
|
|
197
|
+
before?: HyperlinkData;
|
|
198
|
+
after?: HyperlinkData;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export class ChangeTracker {
|
|
202
|
+
private changes: TrackedChange[] = [];
|
|
203
|
+
private snapshots = new Map<string, HyperlinkData>();
|
|
204
|
+
|
|
205
|
+
async initialize(documentPath: string): Promise<void> {
|
|
206
|
+
const hyperlinks = await this.extractHyperlinks(documentPath);
|
|
207
|
+
hyperlinks.forEach((h) => this.snapshots.set(h.id, { ...h }));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
track(
|
|
211
|
+
operation: 'insert' | 'delete' | 'modify',
|
|
212
|
+
data: {
|
|
213
|
+
id: string;
|
|
214
|
+
before?: HyperlinkData;
|
|
215
|
+
after?: HyperlinkData;
|
|
216
|
+
}
|
|
217
|
+
): void {
|
|
218
|
+
this.changes.push({
|
|
219
|
+
id: crypto.randomUUID(),
|
|
220
|
+
type: operation,
|
|
221
|
+
elementType: 'hyperlink',
|
|
222
|
+
author: process.env.USER || 'unknown',
|
|
223
|
+
timestamp: new Date(),
|
|
224
|
+
before: data.before,
|
|
225
|
+
after: data.after,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Update snapshot
|
|
229
|
+
if (operation === 'delete') {
|
|
230
|
+
this.snapshots.delete(data.id);
|
|
231
|
+
} else if (data.after) {
|
|
232
|
+
this.snapshots.set(data.id, data.after);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getChanges(): TrackedChange[] {
|
|
237
|
+
return [...this.changes];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async exportReport(outputPath: string): Promise<void> {
|
|
241
|
+
const doc = new Document({
|
|
242
|
+
sections: [
|
|
243
|
+
{
|
|
244
|
+
children: [
|
|
245
|
+
new Paragraph({
|
|
246
|
+
text: 'Change History',
|
|
247
|
+
heading: HeadingLevel.HEADING_1,
|
|
248
|
+
}),
|
|
249
|
+
...this.changes.map(
|
|
250
|
+
(change) =>
|
|
251
|
+
new Paragraph({
|
|
252
|
+
text: `${change.timestamp.toISOString()}: ${change.type} by ${change.author}`,
|
|
253
|
+
bullet: { level: 0 },
|
|
254
|
+
})
|
|
255
|
+
),
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const buffer = await Packer.toBuffer(doc);
|
|
262
|
+
await fs.writeFile(outputPath, buffer);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Helper Utilities
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
export class HyperlinkUtils {
|
|
271
|
+
// Remove orphaned relationships
|
|
272
|
+
static async cleanupOrphaned(documentPath: string): Promise<number> {
|
|
273
|
+
const zip = await JSZip.loadAsync(await fs.readFile(documentPath));
|
|
274
|
+
|
|
275
|
+
// Get used relationship IDs from document
|
|
276
|
+
const docXml = await zip.file('word/document.xml')?.async('string');
|
|
277
|
+
const usedIds = this.extractUsedRelationshipIds(docXml);
|
|
278
|
+
|
|
279
|
+
// Get all relationship IDs
|
|
280
|
+
const relsXml = await zip.file('word/_rels/document.xml.rels')?.async('string');
|
|
281
|
+
const parsed = await parseStringPromise(relsXml);
|
|
282
|
+
|
|
283
|
+
// Remove orphaned
|
|
284
|
+
let removed = 0;
|
|
285
|
+
parsed.Relationships.Relationship = parsed.Relationships.Relationship.filter((rel: any) => {
|
|
286
|
+
if (!usedIds.has(rel.$.Id)) {
|
|
287
|
+
removed++;
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
return true;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (removed > 0) {
|
|
294
|
+
const builder = new Builder();
|
|
295
|
+
zip.file('word/_rels/document.xml.rels', builder.buildObject(parsed));
|
|
296
|
+
const buffer = await zip.generateAsync({ type: 'nodebuffer' });
|
|
297
|
+
await fs.writeFile(documentPath, buffer);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return removed;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Consolidate duplicate URLs
|
|
304
|
+
static async consolidateDuplicates(documentPath: string): Promise<ConsolidationResult> {
|
|
305
|
+
const zip = await JSZip.loadAsync(await fs.readFile(documentPath));
|
|
306
|
+
const relsXml = await zip.file('word/_rels/document.xml.rels')?.async('string');
|
|
307
|
+
const parsed = await parseStringPromise(relsXml);
|
|
308
|
+
|
|
309
|
+
const urlMap = new Map<string, string[]>();
|
|
310
|
+
|
|
311
|
+
// Group by URL
|
|
312
|
+
for (const rel of parsed.Relationships.Relationship) {
|
|
313
|
+
const url = rel.$.Target;
|
|
314
|
+
const ids = urlMap.get(url) || [];
|
|
315
|
+
ids.push(rel.$.Id);
|
|
316
|
+
urlMap.set(url, ids);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const consolidations: Array<{ url: string; kept: string; removed: string[] }> = [];
|
|
320
|
+
|
|
321
|
+
// Process duplicates
|
|
322
|
+
for (const [url, ids] of urlMap.entries()) {
|
|
323
|
+
if (ids.length > 1) {
|
|
324
|
+
const [kept, ...removed] = ids;
|
|
325
|
+
consolidations.push({ url, kept, removed });
|
|
326
|
+
|
|
327
|
+
// Update document references
|
|
328
|
+
await this.updateReferences(zip, removed, kept);
|
|
329
|
+
|
|
330
|
+
// Remove duplicate relationships
|
|
331
|
+
parsed.Relationships.Relationship = parsed.Relationships.Relationship.filter(
|
|
332
|
+
(rel: any) => !removed.includes(rel.$.Id)
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (consolidations.length > 0) {
|
|
338
|
+
const builder = new Builder();
|
|
339
|
+
zip.file('word/_rels/document.xml.rels', builder.buildObject(parsed));
|
|
340
|
+
const buffer = await zip.generateAsync({ type: 'nodebuffer' });
|
|
341
|
+
await fs.writeFile(documentPath, buffer);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { consolidations, saved: consolidations.length };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Validate all hyperlinks
|
|
348
|
+
static async validate(documentPath: string): Promise<ValidationResult> {
|
|
349
|
+
const issues: ValidationIssue[] = [];
|
|
350
|
+
const zip = await JSZip.loadAsync(await fs.readFile(documentPath));
|
|
351
|
+
|
|
352
|
+
const relsXml = await zip.file('word/_rels/document.xml.rels')?.async('string');
|
|
353
|
+
const parsed = await parseStringPromise(relsXml);
|
|
354
|
+
|
|
355
|
+
for (const rel of parsed.Relationships.Relationship) {
|
|
356
|
+
if (rel.$.Type?.includes('hyperlink')) {
|
|
357
|
+
// Check URL validity
|
|
358
|
+
try {
|
|
359
|
+
new URL(rel.$.Target);
|
|
360
|
+
} catch {
|
|
361
|
+
issues.push({
|
|
362
|
+
id: rel.$.Id,
|
|
363
|
+
type: 'invalid_url',
|
|
364
|
+
message: `Invalid URL: ${rel.$.Target}`,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Check for broken internal links
|
|
369
|
+
if (rel.$.Target?.startsWith('#')) {
|
|
370
|
+
const bookmarkId = rel.$.Target.substring(1);
|
|
371
|
+
if (!(await this.bookmarkExists(zip, bookmarkId))) {
|
|
372
|
+
issues.push({
|
|
373
|
+
id: rel.$.Id,
|
|
374
|
+
type: 'broken_bookmark',
|
|
375
|
+
message: `Bookmark not found: ${bookmarkId}`,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
valid: issues.length === 0,
|
|
384
|
+
issues,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Performance Optimization
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
export class HyperlinkProcessor {
|
|
394
|
+
private cache = new Map<string, HyperlinkData>();
|
|
395
|
+
|
|
396
|
+
async processBatch(documentPath: string, operations: Operation[]): Promise<BatchResult> {
|
|
397
|
+
const zip = await JSZip.loadAsync(await fs.readFile(documentPath));
|
|
398
|
+
const results: OperationResult[] = [];
|
|
399
|
+
|
|
400
|
+
// Batch all changes before writing
|
|
401
|
+
for (const op of operations) {
|
|
402
|
+
try {
|
|
403
|
+
await this.applyOperation(zip, op);
|
|
404
|
+
results.push({ success: true, operation: op });
|
|
405
|
+
} catch (error) {
|
|
406
|
+
results.push({
|
|
407
|
+
success: false,
|
|
408
|
+
operation: op,
|
|
409
|
+
error: error.message,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Write once
|
|
415
|
+
const buffer = await zip.generateAsync({ type: 'nodebuffer' });
|
|
416
|
+
await fs.writeFile(documentPath, buffer);
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
total: operations.length,
|
|
420
|
+
successful: results.filter((r) => r.success).length,
|
|
421
|
+
failed: results.filter((r) => !r.success).length,
|
|
422
|
+
results,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Stream processing for large documents
|
|
427
|
+
async *streamHyperlinks(documentPath: string): AsyncGenerator<HyperlinkData> {
|
|
428
|
+
const zip = await JSZip.loadAsync(await fs.readFile(documentPath));
|
|
429
|
+
const docXml = await zip.file('word/document.xml')?.async('string');
|
|
430
|
+
|
|
431
|
+
// Parse in chunks to avoid memory issues
|
|
432
|
+
const chunks = this.splitIntoChunks(docXml, 1000);
|
|
433
|
+
|
|
434
|
+
for (const chunk of chunks) {
|
|
435
|
+
const hyperlinks = this.extractHyperlinksFromChunk(chunk);
|
|
436
|
+
for (const hyperlink of hyperlinks) {
|
|
437
|
+
yield hyperlink;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## Best Practices Summary
|
|
445
|
+
|
|
446
|
+
1. **Always preserve relationship IDs** when modifying hyperlinks
|
|
447
|
+
2. **Track all changes** for audit trails
|
|
448
|
+
3. **Validate URLs** before saving
|
|
449
|
+
4. **Batch operations** to minimize file I/O
|
|
450
|
+
5. **Cache frequently accessed data** for performance
|
|
451
|
+
6. **Use TypeScript strict mode** for type safety
|
|
452
|
+
|
|
453
|
+
## Limitations
|
|
454
|
+
|
|
455
|
+
- `docx` library cannot create native Word tracked changes
|
|
456
|
+
- Real-time collaboration requires server infrastructure
|
|
457
|
+
- Complex field codes require low-level XML manipulation
|
|
458
|
+
- Digital signatures need special handling after modifications
|
|
459
|
+
|
|
460
|
+
## Complete Example
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
async function processDocument(inputPath: string, outputPath: string) {
|
|
464
|
+
const manager = new HyperlinkManager();
|
|
465
|
+
const modifier = new HyperlinkModifier();
|
|
466
|
+
const tracker = new ChangeTracker();
|
|
467
|
+
|
|
468
|
+
// Initialize tracking
|
|
469
|
+
await tracker.initialize(inputPath);
|
|
470
|
+
|
|
471
|
+
// Update URL
|
|
472
|
+
const result = await modifier.updateUrl(inputPath, 'http://old-url.com', 'https://new-url.com');
|
|
473
|
+
|
|
474
|
+
// Track changes
|
|
475
|
+
result.changes.forEach((change) => {
|
|
476
|
+
tracker.track('modify', {
|
|
477
|
+
id: change.relationshipId,
|
|
478
|
+
before: { target: change.oldTarget },
|
|
479
|
+
after: { target: change.newTarget },
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Cleanup orphaned relationships
|
|
484
|
+
const removed = await HyperlinkUtils.cleanupOrphaned(inputPath);
|
|
485
|
+
console.log(`Removed ${removed} orphaned relationships`);
|
|
486
|
+
|
|
487
|
+
// Validate
|
|
488
|
+
const validation = await HyperlinkUtils.validate(inputPath);
|
|
489
|
+
if (!validation.valid) {
|
|
490
|
+
console.error('Validation issues:', validation.issues);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Export change report
|
|
494
|
+
await tracker.exportReport(outputPath);
|
|
495
|
+
}
|
|
496
|
+
```
|