8using NanoXLSX.Exceptions;
10using NanoXLSX.Interfaces.Writer;
11using NanoXLSX.Registry;
12using NanoXLSX.Registry.Attributes;
15using System.Collections.Generic;
23 [NanoXlsxQueuePlugIn(PlugInUUID =
"MAIN_COMPATIBILITY_WRITE_INLINE_PREPARATION_PROCESSOR", QueueUUID = PlugInUUID.PreparingInlineProcessor, PlugInOrder = 2000)]
24 internal class CompatibilityPreparingInlineWriteProcessor : IPluginInlineWriteProcessor
27 private Dictionary<int, Dictionary<string, ExternalLinkResolution>> resolvedFormulas;
28 private Dictionary<int, ExternalLinkResolution> resolvedDefinedNames;
34 public IWriteContext WriteContext {
get;
set; }
41 public void Init(IWriteContext context)
43 this.WriteContext = context;
51 resolvedFormulas =
new Dictionary<int, Dictionary<string, ExternalLinkResolution>>();
52 resolvedDefinedNames =
new Dictionary<int, ExternalLinkResolution>();
54 if (!WriteContext.Workbook.Features.ContainsExternalLinks)
58 List<ExternalLink> externalLinks = WriteContext.Workbook.AuxiliaryData
59 .GetDataList<ExternalLink>(PlugInUUID.CompatibilityInlineProcessor, CompatibilityConstants.EXTERNAL_LINK_OBJECT_ENTITY)
60 .OfType<ExternalLink>()
62 List<ExternalLinkCandidate> candidates = CreateExternalLinkCandidates(externalLinks);
63 if (WriteContext.Workbook.Features.ContainsWorksheetFormulas)
65 ResolveExternalLinksFromFormulas(candidates);
67 if (WriteContext.Workbook.Features.ContainsDefinedNameFormulas)
69 ResolveExternalLinksFromDefinedNames(candidates);
84 private void ResolveExternalLinksFromFormulas(List<ExternalLinkCandidate> candidates)
86 for (
int worksheetIndex = 0; worksheetIndex < WriteContext.Workbook.Worksheets.Count; worksheetIndex++)
88 Worksheet worksheet = WriteContext.Workbook.Worksheets[worksheetIndex];
89 if (!worksheet.Features.ContainsWorksheetFormulas || !worksheet.Features.ContainsExternalLinks)
93 foreach (Cell cell
in worksheet.CellValues)
95 if (cell.DataType != Cell.CellType.Formula || cell.Formula ==
null || !cell.Formula.Features.ContainsExternalLinks)
100 string expression = GetFormulaExpression(cell);
101 string cellAddress = Cell.ResolveCellAddress(cell.ColumnNumber, cell.RowNumber);
102 ExternalLinkResolution result = ResolveExpression(
104 new SourceInfo(
"cell formula", worksheet.SheetName +
"!" + cellAddress),
111 if (!resolvedFormulas.TryGetValue(worksheet.SheetID, out Dictionary<string, ExternalLinkResolution> worksheetResolutions))
113 worksheetResolutions =
new Dictionary<string, ExternalLinkResolution>();
114 resolvedFormulas.Add(worksheet.SheetID, worksheetResolutions);
116 worksheetResolutions[cellAddress] = result;
130 private void ResolveExternalLinksFromDefinedNames(List<ExternalLinkCandidate> candidates)
132 IReadOnlyList<DefinedName> definedNames = WriteContext.Workbook.GetDefinedNames();
133 for (
int i = 0; i < definedNames.Count; i++)
135 DefinedName definedName = definedNames[i];
136 if (!definedName.Features.ContainsExternalLinks)
140 ExternalLinkResolution result = ResolveExpression(
141 definedName.TextValue,
142 new SourceInfo(
"defined name", definedName.Name),
146 resolvedDefinedNames[i] = result;
156 private static string GetFormulaExpression(Cell cell)
158 if (cell.Formula.DefinedNameReference !=
null)
160 return cell.Formula.DefinedNameReference.Name;
162 return cell.Formula.Expression;
168 private void StoreResolutions()
170 WriteContext.Workbook.AuxiliaryData.SetData(
171 PlugInUUID.CompatibilityInlineProcessor,
172 CompatibilityConstants.EXTERNAL_LINK_RESOLVED_FORMULAS_ENTITY,
174 WriteContext.Workbook.AuxiliaryData.SetData(
175 PlugInUUID.CompatibilityInlineProcessor,
176 CompatibilityConstants.EXTERNAL_LINK_RESOLVED_DEFINED_NAMES_ENTITY,
177 resolvedDefinedNames);
187 private static ExternalLinkResolution ResolveExpression(
189 SourceInfo sourceInfo,
190 List<ExternalLinkCandidate> candidates)
192 if (ExternalLinkFormulaUtils.DetectExternalLinkId(expression))
194 throw GetUnsupportedNumericReference(sourceInfo);
197 ValidateCaseInsensitiveAmbiguities(expression, sourceInfo, candidates);
199 string resolvedExpression = expression;
200 HashSet<int> matchedIndexes =
new HashSet<int>();
201 foreach (ExternalLinkCandidate candidate
in candidates)
203 if (!TryReplaceOutsideStringConstants(
206 "[" + ParserUtils.ToString(candidate.LinkIndexes[0]) +
"]",
207 out
string replacedExpression))
211 if (candidate.LinkIndexes.Count > 1)
213 throw GetAmbiguousReference(sourceInfo, candidate.Text, candidate.LinkIndexes);
216 int linkIndex = candidate.LinkIndexes[0];
217 resolvedExpression = replacedExpression;
218 matchedIndexes.Add(linkIndex);
221 if (ExternalLinkFormulaUtils.TryFindUnresolvedExternalLink(resolvedExpression, out
string unresolvedToken))
223 throw GetUnregisteredReference(sourceInfo, unresolvedToken);
226 if (matchedIndexes.Count == 0)
230 return new ExternalLinkResolution(resolvedExpression, matchedIndexes.OrderBy(index => index).ToList());
239 private static void ValidateCaseInsensitiveAmbiguities(
241 SourceInfo sourceInfo,
242 List<ExternalLinkCandidate> candidates)
244 foreach (IGrouping<string, ExternalLinkCandidate> group
in candidates.GroupBy(
245 candidate => candidate.Text,
246 StringComparer.OrdinalIgnoreCase))
248 List<int> indexes = group
249 .SelectMany(candidate => candidate.LinkIndexes)
251 .OrderBy(index => index)
253 if (indexes.Count < 2)
258 string representative = group.First().Text;
260 while ((position = IndexOfOutsideStringConstants(
264 StringComparison.OrdinalIgnoreCase)) >= 0)
266 string matchedText = expression.Substring(position, representative.Length);
267 bool hasExactCandidate = group.Any(candidate =>
268 string.Equals(candidate.Text, matchedText, StringComparison.Ordinal));
269 if (!hasExactCandidate)
271 throw GetAmbiguousReference(sourceInfo, matchedText, indexes);
273 position += representative.Length;
285 private static NotSupportedContentException GetAmbiguousReference(SourceInfo sourceInfo,
string matchedText, IEnumerable<int> linkIndexes)
287 string indexes =
string.Join(
", ", linkIndexes.Select(ParserUtils.ToString));
288 return new NotSupportedContentException(
289 "The " + sourceInfo.SourceKind +
" '" + sourceInfo.SourceIdentifier +
"' contains the ambiguous external link '" +
290 matchedText +
"', which matches external link indexes " + indexes +
".");
296 private static NotSupportedContentException GetUnsupportedNumericReference(SourceInfo sourceInfo)
298 return new NotSupportedContentException(
299 "The " + sourceInfo.SourceKind +
" '" + sourceInfo.SourceIdentifier +
300 "' contains a numeric OOXML external-link identifier. Use a human-readable external workbook reference and register the workbook with WorkbookExtensions.AddExternalLink before saving.");
306 private static NotSupportedContentException GetUnregisteredReference(SourceInfo sourceInfo,
string unresolvedToken)
308 return new NotSupportedContentException(
309 "The " + sourceInfo.SourceKind +
" '" + sourceInfo.SourceIdentifier +
"' contains the unregistered external link '" +
310 unresolvedToken +
"'. Register the external workbook with WorkbookExtensions.AddExternalLink before saving.");
316 private static bool TryReplaceOutsideStringConstants(
322 System.Text.StringBuilder builder =
null;
323 bool insideStringConstant =
false;
324 int unchangedSectionStart = 0;
325 for (
int i = 0; i < expression.Length; i++)
327 char current = expression[i];
330 if (insideStringConstant && i + 1 < expression.Length && expression[i + 1] ==
'"')
335 insideStringConstant = !insideStringConstant;
338 if (insideStringConstant || i + oldValue.Length > expression.Length ||
339 string.CompareOrdinal(expression, i, oldValue, 0, oldValue.Length) != 0 ||
340 !HasValidCandidatePrefix(expression, i, oldValue))
347 builder =
new System.Text.StringBuilder(expression.Length);
349 builder.Append(expression, unchangedSectionStart, i - unchangedSectionStart);
350 builder.Append(newValue);
351 i += oldValue.Length - 1;
352 unchangedSectionStart = i + 1;
360 builder.Append(expression, unchangedSectionStart, expression.Length - unchangedSectionStart);
361 result = builder.ToString();
368 private static int IndexOfOutsideStringConstants(
372 StringComparison comparison)
374 bool insideStringConstant =
false;
375 for (
int i = 0; i + value.Length <= expression.Length; i++)
377 char current = expression[i];
380 if (insideStringConstant && i + 1 < expression.Length && expression[i + 1] ==
'"')
385 insideStringConstant = !insideStringConstant;
388 if (i >= startIndex && !insideStringConstant &&
389 string.Compare(expression, i, value, 0, value.Length, comparison) == 0 &&
390 HasValidCandidatePrefix(expression, i, value))
401 private static bool HasValidCandidatePrefix(
string expression,
int matchIndex,
string candidate)
403 if (matchIndex == 0 || candidate.Length == 0 || candidate[0] !=
'[')
408 char previous = expression[matchIndex - 1];
409 return !
char.IsLetterOrDigit(previous) && previous !=
'_' && previous !=
'.' &&
410 previous !=
':' && previous !=
'/' && previous !=
'\\';
418 private static List<ExternalLinkCandidate> CreateExternalLinkCandidates(List<ExternalLink> externalLinks)
420 Dictionary<string, HashSet<int>> candidates =
new Dictionary<string, HashSet<int>>(StringComparer.Ordinal);
422 for (
int i = 0; i < externalLinks.Count; i++)
424 ExternalLink externalLink = externalLinks[i];
425 int linkIndex = i + 1;
426 HashSet<string> linkCandidates =
new HashSet<string>(StringComparer.Ordinal);
427 foreach (
string uri
in externalLink.GetWorkbookLocations())
429 foreach (
string candidate
in CreateUriCandidates(uri))
431 linkCandidates.Add(candidate);
434 string readableToken = externalLink.ReadableReferenceToken;
435 if (!
string.IsNullOrEmpty(readableToken))
437 linkCandidates.Add(readableToken);
438 linkCandidates.Add(readableToken.Replace(
'\\',
'/'));
439 linkCandidates.Add(readableToken.Replace(
'/',
'\\'));
441 foreach (
string candidate
in linkCandidates)
443 if (!candidates.TryGetValue(candidate, out HashSet<int> indexes))
445 indexes =
new HashSet<int>();
446 candidates[candidate] = indexes;
448 indexes.Add(linkIndex);
453 .Select(pair =>
new ExternalLinkCandidate(
455 pair.Value.OrderBy(index => index).ToList()))
456 .OrderByDescending(candidate => candidate.Text.Length)
457 .ThenBy(candidate => candidate.Text, StringComparer.Ordinal)
466 private static HashSet<string> CreateUriCandidates(
string uriText)
468 HashSet<string> candidates =
new HashSet<string>(StringComparer.Ordinal);
469 string value = uriText.Trim();
471 bool isAbsoluteUri = Uri.TryCreate(value, UriKind.Absolute, out Uri uri);
473 if (isAbsoluteUri && uri.IsFile)
475 string filePath = GetFilePathCandidate(uri);
476 AddPathCandidates(candidates, filePath,
true);
482 bool addSeparatorAliases = !isAbsoluteUri;
483 AddPathCandidates(candidates, value, addSeparatorAliases);
494 private static string GetFilePathCandidate(Uri uri)
498 string path = Uri.UnescapeDataString(uri.AbsolutePath).Replace(
'\\',
'/');
500 bool isRemoteFile = !
string.IsNullOrEmpty(uri.Host) && !uri.IsLoopback;
504 string remotePath = path.TrimStart(
'/');
506 return "//" + uri.Host +
"/" + remotePath;
510 if (path.Length >= 3 && path[0] ==
'/' &&
char.IsLetter(path[1]) && path[2] ==
':')
512 path = path.Substring(1);
525 private static void AddPathCandidates(HashSet<string> candidates,
string path,
bool addSeparatorAliases)
527 string formulaPath = ToFormulaPath(path);
529 candidates.Add(formulaPath);
530 if (!addSeparatorAliases)
534 candidates.Add(formulaPath.Replace(
'\\',
'/'));
535 candidates.Add(formulaPath.Replace(
'/',
'\\'));
545 private static string ToFormulaPath(
string path)
547 string value = path.Trim();
549 int separator = Math.Max(value.LastIndexOf(
'/'), value.LastIndexOf(
'\\'));
556 directory = value.Substring(0, separator + 1);
557 filename = value.Substring(separator + 1);
561 directory =
string.Empty;
565 if (filename[0] ==
'[' && filename[filename.Length - 1] ==
']')
570 return directory +
"[" + filename +
"]";
574 #region helperClasses
579 private sealed class ExternalLinkCandidate
584 public string Text {
get; }
586 public List<int> LinkIndexes {
get; }
593 public ExternalLinkCandidate(
string text, List<int> linkIndexes)
596 LinkIndexes = linkIndexes;
603 private sealed class SourceInfo
608 public string SourceKind {
get; }
612 public string SourceIdentifier {
get; }
619 public SourceInfo(
string sourceKind,
string sourceIdentifier)
621 this.SourceKind = sourceKind;
622 this.SourceIdentifier = sourceIdentifier;