NanoXLSX.Compatibility 3.2.0
Loading...
Searching...
No Matches
ExternalLinkReadProcessor.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 NanoXLSX.Exceptions;
9using NanoXLSX.Interfaces;
10using NanoXLSX.Interfaces.Reader;
11using NanoXLSX.Registry;
12using NanoXLSX.Registry.Attributes;
13using NanoXLSX.Utils;
14using System;
15using System.Collections.Generic;
16using System.Linq;
17
19{
23 [NanoXlsxQueuePlugIn(PlugInUUID = "EXTERNAL_LINK_READ_PROCESSOR", QueueUUID = PlugInUUID.FinalizingInlineProcessor, PlugInOrder = 20000)]
24 internal class ExternalLinkReadProcessor : IPluginInlineReadProcessor
25 {
26 #region properties
30 public Workbook Workbook { get; set; }
31 #endregion
32 #region methods
33
40 public void Init(Workbook workbook, IOptions readerOptions, int? index = null)
41 {
42 this.Workbook = workbook;
43 }
44
49 public void Execute()
50 {
51 List<ExternalLink> externalLinks = Workbook.AuxiliaryData.GetDataList<ExternalLink>(PlugInUUID.CompatibilityInlineProcessor, CompatibilityConstants.EXTERNAL_LINK_OBJECT_ENTITY);
52 List<string> externalReferenceRids = Workbook.AuxiliaryData.GetData<List<string>>(PlugInUUID.CompatibilityInlineProcessor, CompatibilityConstants.EXTERNAL_REFERENCE_WORKBOOK_RID_ENTITY);
53 List<ExternalDefinedNameReference> rawDefinedNames = Workbook.AuxiliaryData.GetData<List<ExternalDefinedNameReference>>(PlugInUUID.CompatibilityInlineProcessor, CompatibilityConstants.EXTERNAL_REFERENCE_DEFINED_NAMES_ENTITY);
54 if (externalReferenceRids == null)
55 {
56 return; // No (valid) external links in workbook
57 }
58 Dictionary<string, ExternalLink> externalReferences = new Dictionary<string, ExternalLink>();
59 Dictionary<string, DefinedName> replacementMap = new Dictionary<string, DefinedName>();
60 Dictionary<string, string> structuralReplacements = new Dictionary<string, string>();
61 PrepareUpdateDefinedNames(
62 externalLinks,
63 externalReferenceRids,
64 externalReferences,
65 replacementMap,
66 structuralReplacements,
67 rawDefinedNames);
68 RebuildDefinedNames(structuralReplacements, replacementMap);
69 if (Workbook.Features.ContainsWorksheetFormulas && externalReferences.Count != 0 && replacementMap.Count > 0)
70 {
71 UpdateCellFormulas(externalReferences, replacementMap);
72 }
73
74 }
75
76 private void PrepareUpdateDefinedNames(
77 List<ExternalLink> externalLinks,
78 List<string> externalReferenceRids,
79 Dictionary<string, ExternalLink> externalReferences,
80 Dictionary<string, DefinedName> replacementMap,
81 Dictionary<string, string> structuralReplacements,
82 List<ExternalDefinedNameReference> rawDefinedNames)
83 {
84 for (int i = 0; i < externalReferenceRids.Count; i++)
85 {
86 ExternalLink link = externalLinks.FirstOrDefault(l => l != null && externalReferenceRids[i].Equals(l.WorkbookRId, StringComparison.OrdinalIgnoreCase));
87 if (link == null)
88 {
89 throw new IOException("Mismatch of read external links and references in workbooks detected. External references cannot be resolved.");
90 }
91 string rId = GetExternalLinkId(i);
92 externalReferences[rId] = link;
93 }
94 IReadOnlyList<DefinedName> definedNames = Workbook.GetDefinedNames();
95 for (int i = 0; i < definedNames.Count; i++)
96 {
97 DefinedName defiedName = definedNames[i];
98 string expression = GetDefinedNameExpression(defiedName, rawDefinedNames);
99 if (ExternalLinkFormulaUtils.DetectExternalLinkId(expression))
100 {
101 string replacedExpression = ExternalLinkFormulaUtils.ReplaceExternalLinkId(expression, externalReferences);
102 if (ExternalLinkFormulaUtils.DetectExternalLinkId(replacedExpression))
103 {
104 throw new IOException("Mismatch of read external links and references in defined names detected. External references cannot be resolved.");
105 }
106 string id = GetDefinedNameId(defiedName);
107 if (defiedName.Type == DefinedName.NameType.Formula)
108 {
109 defiedName.ReplaceExpression(replacedExpression);
110 replacementMap[id] = defiedName;
111 }
112 else
113 {
114 structuralReplacements[id] = replacedExpression;
115 }
116 }
117 }
118 }
119
120 private string GetDefinedNameExpression(DefinedName definedName, List<ExternalDefinedNameReference> rawDefinedNames)
121 {
122 if (rawDefinedNames != null)
123 {
124 foreach (ExternalDefinedNameReference rawDefinedName in rawDefinedNames)
125 {
126 if (string.Equals(rawDefinedName.Name, definedName.Name, StringComparison.OrdinalIgnoreCase) &&
127 IsSameScope(rawDefinedName.LocalSheetIndex, definedName.LocalSheet))
128 {
129 return rawDefinedName.Expression;
130 }
131 }
132 }
133 return definedName.TextValue;
134 }
135
136 private bool IsSameScope(int? localSheetIndex, Worksheet localSheet)
137 {
138 if (!localSheetIndex.HasValue)
139 {
140 return localSheet == null;
141 }
142 int index = localSheetIndex.Value;
143 return index >= 0 && index < Workbook.Worksheets.Count && object.ReferenceEquals(Workbook.Worksheets[index], localSheet);
144 }
145
146 private void UpdateCellFormulas(Dictionary<string, ExternalLink> externalReferences, Dictionary<string, DefinedName> replacementMap)
147 {
148 foreach (Worksheet worksheet in Workbook.Worksheets)
149 {
150 foreach (KeyValuePair<string, Cell> cell in worksheet.Cells)
151 {
152 if (cell.Value.DataType == Cell.CellType.Formula && cell.Value.Formula != null)
153 {
154 string originalValue = cell.Value.Value?.ToString() ?? string.Empty;
155 if (ExternalLinkFormulaUtils.DetectExternalLinkId(originalValue))
156 {
157 string replacement = ExternalLinkFormulaUtils.ReplaceExternalLinkId(originalValue, externalReferences);
158 if (object.Equals(cell.Value.Formula.Expression, cell.Value.Value))
159 {
160 cell.Value.Value = replacement;
161 }
162 cell.Value.Formula.Expression = replacement;
163 cell.Value.Formula.HasExternalReferences = true;
164 }
165 if (replacementMap.Count > 0 && cell.Value.Formula.DefinedNameReference != null)
166 {
167 string id = GetDefinedNameId(cell.Value.Formula.DefinedNameReference);
168 if (replacementMap.TryGetValue(id, out DefinedName newValue))
169 {
170 cell.Value.Formula.DefinedNameReference = newValue;
171 }
172 }
173 }
174 }
175 }
176 }
177
178 private void RebuildDefinedNames(Dictionary<string, string> structuralReplacements, Dictionary<string, DefinedName> replacementMap)
179 {
180 if (structuralReplacements.Count == 0)
181 {
182 return;
183 }
184
185 List<DefinedName> definedNames = Workbook.GetDefinedNames().ToList();
186 for (int i = definedNames.Count - 1; i >= 0; i--)
187 {
188 DefinedName definedName = definedNames[i];
189 Workbook.RemoveDefinedName(definedName.Name, false, definedName.LocalSheet);
190 }
191
192 foreach (DefinedName definedName in definedNames)
193 {
194 string id = GetDefinedNameId(definedName);
195 if (structuralReplacements.TryGetValue(id, out string expression))
196 {
197 DefinedName replacement = new DefinedName(
198 Workbook,
199 DefinedName.NameType.Formula,
200 definedName.Name,
201 expression,
202 null,
203 definedName.LocalSheet,
204 definedName.Comment,
205 true);
206 Workbook.AddDefinedName(replacement);
207 replacementMap[id] = replacement;
208 }
209 else
210 {
211 definedName.Features.Add(Workbook.Features);
212 Workbook.AddDefinedName(definedName);
213 }
214 }
215 }
216
217 private static string GetDefinedNameId(DefinedName defiedName)
218 {
219 string id;
220 if (defiedName.LocalSheet == null)
221 {
222 id = defiedName.Name;
223 }
224 else
225 {
226 id = defiedName.Name + "@" + ParserUtils.ToString(defiedName.LocalSheet.GetHashCode());
227 }
228 return id;
229 }
230
234 private static string GetExternalLinkId(int zeroBasedIndex)
235 {
236 return "[" + ParserUtils.ToString(zeroBasedIndex + 1) + "]";
237 }
238 #endregion
239 }
240}