17 public class ExternalLink
19 private readonly List<ExternalWorksheet> worksheets =
new List<ExternalWorksheet>();
20 private readonly List<ExternalDefinedName> definedNames =
new List<ExternalDefinedName>();
34 internal string TargetUri {
get;
private set; }
39 internal string AbsoluteAlternateUri {
get;
private set; }
44 internal string RelativeAlternateUri {
get;
private set; }
50 public IReadOnlyList<ExternalWorksheet>
Worksheets => worksheets;
55 public IReadOnlyList<ExternalDefinedName>
DefinedNames => definedNames;
60 internal string WorkbookRId {
get;
set; }
65 internal string ReadableReferenceToken
69 string uri = GetReadableUri();
70 if (!
string.IsNullOrEmpty(uri))
72 return "[" + uri +
"]";
81 internal ExternalLink()
92 SetAuthoringUris(absoluteUri,
null);
103 SetAuthoringUris(absoluteUri, relativeUri);
131 if (definedName ==
null)
133 throw new ArgumentException(
"An external defined name cannot be null");
136 if (definedNames.Any(
137 item =>
string.Equals(
140 StringComparison.OrdinalIgnoreCase)))
142 throw new ArgumentException($
"The external defined name '{definedName.Name}' already exists.");
144 definedNames.Add(definedName);
152 private void SetAuthoringUris(
string absoluteUri,
string relativeUri)
154 ValidateWorkbookLocation(absoluteUri,
true);
155 if (relativeUri !=
null)
157 ValidateWorkbookLocation(relativeUri,
false);
162 TargetUri = relativeUri ?? absoluteUri;
163 AbsoluteAlternateUri = relativeUri ==
null ? null : absoluteUri;
164 RelativeAlternateUri =
null;
170 internal void SetReadUris(
string targetUri,
string absoluteAlternateUri,
string relativeAlternateUri)
172 ValidateWorkbookLocation(targetUri,
null);
173 if (absoluteAlternateUri !=
null)
175 ValidateWorkbookLocation(absoluteAlternateUri,
true);
177 if (relativeAlternateUri !=
null)
179 ValidateWorkbookLocation(relativeAlternateUri,
false);
182 TargetUri = targetUri;
183 AbsoluteAlternateUri = absoluteAlternateUri;
184 RelativeAlternateUri = relativeAlternateUri;
186 bool targetIsAbsolute = IsAbsoluteWorkbookLocation(targetUri);
187 AbsoluteUri = targetIsAbsolute ? targetUri : absoluteAlternateUri;
188 RelativeUri = targetIsAbsolute ? relativeAlternateUri : targetUri;
194 internal IReadOnlyList<string> GetWorkbookLocations()
196 List<string> locations =
new List<string>();
197 AddDistinctLocation(locations, TargetUri);
198 AddDistinctLocation(locations, AbsoluteAlternateUri);
199 AddDistinctLocation(locations, RelativeAlternateUri);
200 return locations.AsReadOnly();
206 internal IReadOnlyList<ExternalLinkUriRelationship> GetUriRelationships()
208 List<ExternalLinkUriRelationship> relationships =
new List<ExternalLinkUriRelationship>();
209 relationships.Add(
new ExternalLinkUriRelationship(
212 SerializeRelationshipTarget(TargetUri),
213 ExternalLinkUriRole.Target));
215 int relationshipIndex = 2;
216 if (AbsoluteAlternateUri !=
null)
218 relationships.Add(
new ExternalLinkUriRelationship(
219 "rId" + relationshipIndex,
220 AbsoluteAlternateUri,
221 SerializeRelationshipTarget(AbsoluteAlternateUri),
222 ExternalLinkUriRole.AbsoluteAlternate));
225 if (RelativeAlternateUri !=
null)
227 relationships.Add(
new ExternalLinkUriRelationship(
228 "rId" + relationshipIndex,
229 RelativeAlternateUri,
230 SerializeRelationshipTarget(RelativeAlternateUri),
231 ExternalLinkUriRole.RelativeAlternate));
233 return relationships.AsReadOnly();
239 internal static string SerializeRelationshipTarget(
string value)
241 ValidateWorkbookLocation(value,
null);
242 string location = value.Trim();
244 if (IsWindowsDrivePath(location))
246 string normalized = location.Replace(
'\\',
'/');
247 string drive = normalized.Substring(0, 2);
248 string path = EscapePath(normalized.Substring(2));
249 return "file:///" + drive + path;
252 if (IsUncPath(location))
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);
261 if (IsUnixRootedPath(location))
263 return "file://" + EscapePath(location.Replace(
'\\',
'/'));
266 if (Uri.TryCreate(location.Replace(
'\\',
'/'), UriKind.Absolute, out Uri absoluteUri))
268 return absoluteUri.AbsoluteUri;
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;
284 private string GetReadableUri()
286 if (TargetUri !=
null && IsAbsoluteWorkbookLocation(TargetUri))
290 if (AbsoluteAlternateUri !=
null)
292 return AbsoluteAlternateUri;
294 if (TargetUri !=
null)
298 return RelativeAlternateUri;
306 private static void AddDistinctLocation(List<string> locations,
string value)
308 if (value !=
null && !locations.Contains(value, StringComparer.Ordinal))
310 locations.Add(value);
320 private static void ValidateWorkbookLocation(
string value,
bool? mustBeAbsolute)
322 if (
string.IsNullOrWhiteSpace(value))
324 throw new ArgumentException(
"The URI cannot be null or empty.");
327 bool isAbsolute = IsAbsoluteWorkbookLocation(value.Trim());
328 if (mustBeAbsolute ==
true && !isAbsolute)
330 throw new ArgumentException($
"The URI must be an absolute workbook location: '{value}'.");
332 if (mustBeAbsolute ==
false && isAbsolute)
334 throw new ArgumentException($
"The URI must be a relative workbook location: '{value}'.");
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)
343 throw new ArgumentException($
"The URI must point to a file with an extension: '{value}'.");
353 private static string GetLocationPath(
string value,
bool isAbsolute)
355 if (isAbsolute && !IsWindowsDrivePath(value) && !IsUncPath(value) && !IsUnixRootedPath(value)
356 && Uri.TryCreate(value.Replace(
'\\',
'/'), UriKind.Absolute, out Uri absoluteUri))
358 return Uri.UnescapeDataString(absoluteUri.AbsolutePath);
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);
373 private static string EscapePath(
string path)
375 string[] segments = path.Split(
'/');
376 for (
int i = 0; i < segments.Length; i++)
378 segments[i] = Uri.EscapeDataString(Uri.UnescapeDataString(segments[i]));
380 return string.Join(
"/", segments);
388 internal void AddWorksheet(ExternalWorksheet worksheet)
390 if (worksheet ==
null)
392 throw new ArgumentException(
"The worksheet cannot be null");
395 if (worksheets.Any(item =>
string.Equals(item.Name, worksheet.Name, StringComparison.OrdinalIgnoreCase)))
397 throw new ArgumentException($
"The external worksheet '{worksheet.Name}' already exists.");
399 worksheets.Add(worksheet);
408 ExternalWorksheet worksheet = worksheets.FirstOrDefault(item =>
string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase));
409 if (worksheet ==
null)
411 throw new ArgumentException($
"The external worksheet '{name}' does not exist.");
421 private static bool IsAbsoluteWorkbookLocation(
string value)
423 if (IsWindowsDrivePath(value) || IsUncPath(value) || IsUnixRootedPath(value))
427 return Uri.TryCreate(value, UriKind.Absolute, out Uri parsedUri) && parsedUri.IsAbsoluteUri;
435 private static bool IsWindowsDrivePath(
string value)
437 return value.Length >= 3
438 &&
char.IsLetter(value[0])
440 && (value[2] ==
'\\' || value[2] ==
'/');
448 private static bool IsUncPath(
string value)
450 return value.StartsWith(
@"\\", StringComparison.Ordinal)
451 || value.StartsWith(
"//", StringComparison.Ordinal);
459 private static bool IsUnixRootedPath(
string value)
461 return value.StartsWith(
"/", StringComparison.Ordinal) && !value.StartsWith(
"//", StringComparison.Ordinal);