NanoXLSX.Compatibility 3.2.0
Loading...
Searching...
No Matches
ExternalLink.cs
1/*
2 * NanoXLSX is a small .NET library to generate and read XLSX (Microsoft Excel 2007 or newer) files in an easy and native way
3 * Copyright Raphael Stoeckli © 2026
4 * This library is licensed under the MIT License.
5 * You find a copy of the license in project folder or on: http://opensource.org/licenses/MIT
6 */
7
8using System;
9using System.Collections.Generic;
10using System.Linq;
11
12namespace NanoXLSX
13{
17 public class ExternalLink
18 {
19 private readonly List<ExternalWorksheet> worksheets = new List<ExternalWorksheet>();
20 private readonly List<ExternalDefinedName> definedNames = new List<ExternalDefinedName>();
24 public string AbsoluteUri { get; private set; }
25
29 public string RelativeUri { get; private set; }
30
34 internal string TargetUri { get; private set; }
35
39 internal string AbsoluteAlternateUri { get; private set; }
40
44 internal string RelativeAlternateUri { get; private set; }
45
50 public IReadOnlyList<ExternalWorksheet> Worksheets => worksheets;
51
55 public IReadOnlyList<ExternalDefinedName> DefinedNames => definedNames;
56
60 internal string WorkbookRId { get; set; }
61
65 internal string ReadableReferenceToken
66 {
67 get
68 {
69 string uri = GetReadableUri();
70 if (!string.IsNullOrEmpty(uri))
71 {
72 return "[" + uri + "]";
73 }
74 return null;
75 }
76 }
77
81 internal ExternalLink()
82 {
83 }
84
90 public ExternalLink(string absoluteUri)
91 {
92 SetAuthoringUris(absoluteUri, null);
93 }
94
101 public ExternalLink(string absoluteUri, string relativeUri)
102 {
103 SetAuthoringUris(absoluteUri, relativeUri);
104 }
105
106
111 {
112 return new ExternalLinkBuilder(this);
113 }
114
120 public void AddDefinedName(string name, string refersTo)
121 {
122 AddDefinedName(new ExternalDefinedName(name, refersTo));
123 }
124
129 public void AddDefinedName(ExternalDefinedName definedName)
130 {
131 if (definedName == null)
132 {
133 throw new ArgumentException("An external defined name cannot be null");
134 }
135
136 if (definedNames.Any(
137 item => string.Equals(
138 item.Name,
139 definedName.Name,
140 StringComparison.OrdinalIgnoreCase)))
141 {
142 throw new ArgumentException($"The external defined name '{definedName.Name}' already exists.");
143 }
144 definedNames.Add(definedName);
145 }
146
152 private void SetAuthoringUris(string absoluteUri, string relativeUri)
153 {
154 ValidateWorkbookLocation(absoluteUri, true);
155 if (relativeUri != null)
156 {
157 ValidateWorkbookLocation(relativeUri, false);
158 }
159
160 AbsoluteUri = absoluteUri;
161 RelativeUri = relativeUri;
162 TargetUri = relativeUri ?? absoluteUri;
163 AbsoluteAlternateUri = relativeUri == null ? null : absoluteUri;
164 RelativeAlternateUri = null;
165 }
166
170 internal void SetReadUris(string targetUri, string absoluteAlternateUri, string relativeAlternateUri)
171 {
172 ValidateWorkbookLocation(targetUri, null);
173 if (absoluteAlternateUri != null)
174 {
175 ValidateWorkbookLocation(absoluteAlternateUri, true);
176 }
177 if (relativeAlternateUri != null)
178 {
179 ValidateWorkbookLocation(relativeAlternateUri, false);
180 }
181
182 TargetUri = targetUri;
183 AbsoluteAlternateUri = absoluteAlternateUri;
184 RelativeAlternateUri = relativeAlternateUri;
185
186 bool targetIsAbsolute = IsAbsoluteWorkbookLocation(targetUri);
187 AbsoluteUri = targetIsAbsolute ? targetUri : absoluteAlternateUri;
188 RelativeUri = targetIsAbsolute ? relativeAlternateUri : targetUri;
189 }
190
194 internal IReadOnlyList<string> GetWorkbookLocations()
195 {
196 List<string> locations = new List<string>();
197 AddDistinctLocation(locations, TargetUri);
198 AddDistinctLocation(locations, AbsoluteAlternateUri);
199 AddDistinctLocation(locations, RelativeAlternateUri);
200 return locations.AsReadOnly();
201 }
202
206 internal IReadOnlyList<ExternalLinkUriRelationship> GetUriRelationships()
207 {
208 List<ExternalLinkUriRelationship> relationships = new List<ExternalLinkUriRelationship>();
209 relationships.Add(new ExternalLinkUriRelationship(
210 "rId1",
211 TargetUri,
212 SerializeRelationshipTarget(TargetUri),
213 ExternalLinkUriRole.Target));
214
215 int relationshipIndex = 2;
216 if (AbsoluteAlternateUri != null)
217 {
218 relationships.Add(new ExternalLinkUriRelationship(
219 "rId" + relationshipIndex,
220 AbsoluteAlternateUri,
221 SerializeRelationshipTarget(AbsoluteAlternateUri),
222 ExternalLinkUriRole.AbsoluteAlternate));
223 relationshipIndex++;
224 }
225 if (RelativeAlternateUri != null)
226 {
227 relationships.Add(new ExternalLinkUriRelationship(
228 "rId" + relationshipIndex,
229 RelativeAlternateUri,
230 SerializeRelationshipTarget(RelativeAlternateUri),
231 ExternalLinkUriRole.RelativeAlternate));
232 }
233 return relationships.AsReadOnly();
234 }
235
239 internal static string SerializeRelationshipTarget(string value)
240 {
241 ValidateWorkbookLocation(value, null);
242 string location = value.Trim();
243
244 if (IsWindowsDrivePath(location))
245 {
246 string normalized = location.Replace('\\', '/');
247 string drive = normalized.Substring(0, 2);
248 string path = EscapePath(normalized.Substring(2));
249 return "file:///" + drive + path;
250 }
251
252 if (IsUncPath(location))
253 {
254 string normalized = location.Replace('\\', '/').TrimStart('/');
255 int separator = normalized.IndexOf('/');
256 string host = normalized.Substring(0, separator);
257 string path = normalized.Substring(separator);
258 return "file://" + host + EscapePath(path);
259 }
260
261 if (IsUnixRootedPath(location))
262 {
263 return "file://" + EscapePath(location.Replace('\\', '/'));
264 }
265
266 if (Uri.TryCreate(location.Replace('\\', '/'), UriKind.Absolute, out Uri absoluteUri))
267 {
268 return absoluteUri.AbsoluteUri;
269 }
270
271 string relativeLocation = location.Replace('\\', '/');
272 int query = relativeLocation.IndexOf('?');
273 int fragment = relativeLocation.IndexOf('#');
274 int suffix = query < 0 ? fragment : fragment < 0 ? query : Math.Min(query, fragment);
275 string relativePath = suffix < 0 ? relativeLocation : relativeLocation.Substring(0, suffix);
276 string relativeSuffix = suffix < 0 ? string.Empty : relativeLocation.Substring(suffix);
277 return EscapePath(relativePath) + relativeSuffix;
278 }
279
284 private string GetReadableUri()
285 {
286 if (TargetUri != null && IsAbsoluteWorkbookLocation(TargetUri))
287 {
288 return TargetUri;
289 }
290 if (AbsoluteAlternateUri != null)
291 {
292 return AbsoluteAlternateUri;
293 }
294 if (TargetUri != null)
295 {
296 return TargetUri;
297 }
298 return RelativeAlternateUri;
299 }
300
306 private static void AddDistinctLocation(List<string> locations, string value)
307 {
308 if (value != null && !locations.Contains(value, StringComparer.Ordinal))
309 {
310 locations.Add(value);
311 }
312 }
313
320 private static void ValidateWorkbookLocation(string value, bool? mustBeAbsolute)
321 {
322 if (string.IsNullOrWhiteSpace(value))
323 {
324 throw new ArgumentException("The URI cannot be null or empty.");
325 }
326
327 bool isAbsolute = IsAbsoluteWorkbookLocation(value.Trim());
328 if (mustBeAbsolute == true && !isAbsolute)
329 {
330 throw new ArgumentException($"The URI must be an absolute workbook location: '{value}'.");
331 }
332 if (mustBeAbsolute == false && isAbsolute)
333 {
334 throw new ArgumentException($"The URI must be a relative workbook location: '{value}'.");
335 }
336
337 string path = GetLocationPath(value.Trim(), isAbsolute);
338 int separator = Math.Max(path.LastIndexOf('/'), path.LastIndexOf('\\'));
339 string filename = separator < 0 ? path : path.Substring(separator + 1);
340 int extensionSeparator = filename.LastIndexOf('.');
341 if (filename.Length == 0 || extensionSeparator <= 0 || extensionSeparator == filename.Length - 1)
342 {
343 throw new ArgumentException($"The URI must point to a file with an extension: '{value}'.");
344 }
345 }
346
353 private static string GetLocationPath(string value, bool isAbsolute)
354 {
355 if (isAbsolute && !IsWindowsDrivePath(value) && !IsUncPath(value) && !IsUnixRootedPath(value)
356 && Uri.TryCreate(value.Replace('\\', '/'), UriKind.Absolute, out Uri absoluteUri))
357 {
358 return Uri.UnescapeDataString(absoluteUri.AbsolutePath);
359 }
360
361 int query = value.IndexOf('?');
362 int fragment = value.IndexOf('#');
363 int suffix = fragment < 0 ? query < 0 ? fragment : query : query < 0 ? fragment : Math.Min(query, fragment);
364 string path = suffix < 0 ? value : value.Substring(0, suffix);
365 return Uri.UnescapeDataString(path);
366 }
367
373 private static string EscapePath(string path)
374 {
375 string[] segments = path.Split('/');
376 for (int i = 0; i < segments.Length; i++)
377 {
378 segments[i] = Uri.EscapeDataString(Uri.UnescapeDataString(segments[i]));
379 }
380 return string.Join("/", segments);
381 }
382
383
388 internal void AddWorksheet(ExternalWorksheet worksheet)
389 {
390 if (worksheet == null)
391 {
392 throw new ArgumentException("The worksheet cannot be null");
393 }
394
395 if (worksheets.Any(item => string.Equals(item.Name, worksheet.Name, StringComparison.OrdinalIgnoreCase)))
396 {
397 throw new ArgumentException($"The external worksheet '{worksheet.Name}' already exists.");
398 }
399 worksheets.Add(worksheet);
400 }
401
406 public ExternalWorksheet GetWorksheet(string name)
407 {
408 ExternalWorksheet worksheet = worksheets.FirstOrDefault(item => string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase));
409 if (worksheet == null)
410 {
411 throw new ArgumentException($"The external worksheet '{name}' does not exist.");
412 }
413 return worksheet;
414 }
415
421 private static bool IsAbsoluteWorkbookLocation(string value)
422 {
423 if (IsWindowsDrivePath(value) || IsUncPath(value) || IsUnixRootedPath(value))
424 {
425 return true;
426 }
427 return Uri.TryCreate(value, UriKind.Absolute, out Uri parsedUri) && parsedUri.IsAbsoluteUri;
428 }
429
435 private static bool IsWindowsDrivePath(string value)
436 {
437 return value.Length >= 3
438 && char.IsLetter(value[0])
439 && value[1] == ':'
440 && (value[2] == '\\' || value[2] == '/');
441 }
442
448 private static bool IsUncPath(string value)
449 {
450 return value.StartsWith(@"\\", StringComparison.Ordinal)
451 || value.StartsWith("//", StringComparison.Ordinal);
452 }
453
459 private static bool IsUnixRootedPath(string value)
460 {
461 return value.StartsWith("/", StringComparison.Ordinal) && !value.StartsWith("//", StringComparison.Ordinal);
462 }
463
464 }
465
469 internal enum ExternalLinkUriRole
470 {
471 Target,
472 AbsoluteAlternate,
473 RelativeAlternate
474 }
475
479 internal sealed class ExternalLinkUriRelationship
480 {
481 public string Id { get; private set; }
482 public string RawTarget { get; private set; }
483 public string SerializedTarget { get; private set; }
484 public ExternalLinkUriRole Role { get; private set; }
485
486 public ExternalLinkUriRelationship(string id, string rawTarget, string serializedTarget, ExternalLinkUriRole role)
487 {
488 Id = id;
489 RawTarget = rawTarget;
490 SerializedTarget = serializedTarget;
491 Role = role;
492 }
493 }
494}
Represents a defined name declared by an external workbook.
string Name
Gets the name of the external defined name.
Provides a fluent API for defining worksheets and cached cell values of an external workbook link.
Represents a worksheet declared by an external workbook link.