NanoXLSX.Compatibility 3.2.0
Loading...
Searching...
No Matches
ExternalLinkFormulaUtils.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.Utils;
9using System;
10using System.Collections.Generic;
11using System.Text;
12
13namespace NanoXLSX.Internal
14{
18 internal static class ExternalLinkFormulaUtils
19 {
23 internal static bool DetectExternalLinkId(string expression)
24 {
25 return DetectExternalLinkId(expression, null);
26 }
27
32 internal static bool DetectExternalLinkId(string expression, string targetId)
33 {
34 if (targetId != null && !ParserUtils.IsValidExternalLinkId(targetId))
35 {
36 throw new ArgumentException(
37 $"The target ID '{targetId}' is not a valid external link ID.",
38 nameof(targetId));
39 }
40
41 if (string.IsNullOrEmpty(expression))
42 {
43 return false;
44 }
45
46 int firstOpeningBracket = expression.IndexOf('[');
47 if (firstOpeningBracket < 0)
48 {
49 return false;
50 }
51
52 if (targetId != null && expression.IndexOf(targetId, StringComparison.Ordinal) < 0)
53 {
54 return false;
55 }
56
57 int firstQuote = expression.IndexOf('"');
58 int scanStart = firstQuote >= 0 && firstQuote < firstOpeningBracket
59 ? firstQuote
60 : firstOpeningBracket;
61 bool insideStringConstant = false;
62
63 for (int i = scanStart; i < expression.Length; i++)
64 {
65 char current = expression[i];
66 if (current == '"')
67 {
68 if (insideStringConstant && i + 1 < expression.Length && expression[i + 1] == '"')
69 {
70 i++;
71 continue;
72 }
73 insideStringConstant = !insideStringConstant;
74 continue;
75 }
76
77 if (insideStringConstant || current != '[' ||
78 !ParserUtils.TryReadExternalLinkId(expression, i, out int identifierLength))
79 {
80 continue;
81 }
82
83 if (targetId == null ||
84 (identifierLength == targetId.Length &&
85 string.CompareOrdinal(expression, i, targetId, 0, identifierLength) == 0))
86 {
87 return true;
88 }
89
90 i += identifierLength - 1;
91 }
92
93 return false;
94 }
95
100 internal static bool TryFindUnresolvedExternalLink(string expression, out string token)
101 {
102 token = null;
103 if (string.IsNullOrEmpty(expression) || expression.IndexOf('[') < 0)
104 {
105 return false;
106 }
107 bool insideStringConstant = false;
108 for (int i = 0; i < expression.Length; i++)
109 {
110 char current = expression[i];
111 if (current == '"')
112 {
113 if (insideStringConstant && i + 1 < expression.Length && expression[i + 1] == '"')
114 {
115 i++;
116 continue;
117 }
118 insideStringConstant = !insideStringConstant;
119 continue;
120 }
121 if (insideStringConstant || current != '[')
122 {
123 continue;
124 }
125 int closingBracket = expression.IndexOf(']', i + 1);
126 if (closingBracket <= i + 1)
127 {
128 continue;
129 }
130 string bracketToken = expression.Substring(i, closingBracket - i + 1);
131 if (ParserUtils.IsValidExternalLinkId(bracketToken) || HasStructuredReferencePrefix(expression, i))
132 {
133 i = closingBracket;
134 continue;
135 }
136 if (FormsExternalReference(expression, closingBracket))
137 {
138 token = bracketToken;
139 return true;
140 }
141 i = closingBracket;
142 }
143
144 return false;
145 }
146
151 internal static string ReplaceExternalLinkId(string expression, Dictionary<string, ExternalLink> links)
152 {
153 if (expression == null || expression.Length == 0 || links == null || links.Count == 0)
154 {
155 return expression;
156 }
157 int firstOpeningBracket = expression.IndexOf('[');
158 if (firstOpeningBracket < 0)
159 {
160 return expression;
161 }
162 bool insideStringConstant = false;
163 int unchangedSectionStart = 0;
164 StringBuilder builder = null;
165
166 for (int i = firstOpeningBracket; i < expression.Length; i++)
167 {
168 char current = expression[i];
169 if (current == '"')
170 {
171 if (insideStringConstant && i + 1 < expression.Length && expression[i + 1] == '"')
172 {
173 i++;
174 continue;
175 }
176 insideStringConstant = !insideStringConstant;
177 continue;
178 }
179
180 if (insideStringConstant || current != '[' ||
181 !ParserUtils.TryReadExternalLinkId(expression, i, out int identifierLength))
182 {
183 continue;
184 }
185
186 string identifier = expression.Substring(i, identifierLength);
187 if (!links.TryGetValue(identifier, out ExternalLink externalLink))
188 {
189 continue;
190 }
191 if (externalLink == null)
192 {
193 throw new ArgumentException($"The external link '{identifier}' is null.", nameof(links));
194 }
195
196 string replacement = externalLink.ReadableReferenceToken;
197 if (string.IsNullOrEmpty(replacement))
198 {
199 throw new ArgumentException(
200 $"The external link '{identifier}' does not provide a usable URI.",
201 nameof(links));
202 }
203
204 if (builder == null)
205 {
206 builder = new StringBuilder(expression.Length + replacement.Length);
207 }
208 builder.Append(expression, unchangedSectionStart, i - unchangedSectionStart);
209 builder.Append(replacement);
210
211 i += identifierLength - 1;
212 unchangedSectionStart = i + 1;
213 }
214
215 if (builder == null)
216 {
217 return expression;
218 }
219 builder.Append(expression, unchangedSectionStart, expression.Length - unchangedSectionStart);
220 return builder.ToString();
221 }
222
223 private static bool HasStructuredReferencePrefix(string expression, int openingBracket)
224 {
225 if (openingBracket == 0)
226 {
227 return false;
228 }
229 char previous = expression[openingBracket - 1];
230 return char.IsLetterOrDigit(previous) || previous == '_' || previous == '.';
231 }
232
233 private static bool FormsExternalReference(string expression, int closingBracket)
234 {
235 bool hasWorksheetName = false;
236 for (int i = closingBracket + 1; i < expression.Length; i++)
237 {
238 char current = expression[i];
239 if (current == '!')
240 {
241 return hasWorksheetName || i == closingBracket + 1;
242 }
243 if (IsReferenceTerminator(current))
244 {
245 return false;
246 }
247 if (!char.IsWhiteSpace(current) && current != '\'')
248 {
249 hasWorksheetName = true;
250 }
251 }
252 return false;
253 }
254
255 private static bool IsReferenceTerminator(char character)
256 {
257 switch (character)
258 {
259 case '[':
260 case ']':
261 case '"':
262 case '+':
263 case '-':
264 case '*':
265 case '/':
266 case '^':
267 case '&':
268 case '=':
269 case '<':
270 case '>':
271 case ',':
272 case ';':
273 case '(':
274 case ')':
275 case '{':
276 case '}':
277 return true;
278 default:
279 return false;
280 }
281 }
282 }
283}