NanoXLSX.Compatibility 3.2.0
Loading...
Searching...
No Matches
ExternalLinkWriter.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.Interfaces.Writer;
9using NanoXLSX.Registry;
10using NanoXLSX.Registry.Attributes;
11using NanoXLSX.Utils;
12using NanoXLSX.Utils.Xml;
13using System.Collections.Generic;
14using System.Collections.ObjectModel;
15using System.Linq;
16
18{
19 [NanoXlsxQueuePlugIn(PlugInUUID = "EXTERNAL_LINK_WRITER", QueueUUID = PlugInUUID.WriterAppendingQueue, PlugInOrder = 20001)]
20 internal class ExternalLinkWriter : IPluginIndexedWriter
21 {
22 #region privateFields
23 private string currentUniqueIndex;
24 private List<ExternalLink> externalLinks;
25 private int maxIndex;
26 XmlElement xmlElement;
27 #endregion
28 #region properties
29
33 public int CurrentIndex { get; set; }
37 public string CurrentUniquePackagePartIndex => currentUniqueIndex;
41 public int MaxIndex => maxIndex;
42
46 public Workbook Workbook { get; set; }
50 public XmlElement XmlElement => xmlElement;
51 #endregion
52 #region methods
57 public void Init(IBaseWriter baseWriter)
58 {
59 this.Workbook = baseWriter.Workbook;
60 externalLinks = Workbook.AuxiliaryData
61 .GetDataList<ExternalLink>(PlugInUUID.CompatibilityInlineProcessor, CompatibilityConstants.EXTERNAL_LINK_OBJECT_ENTITY)
62 .OfType<ExternalLink>()
63 .ToList();
64 maxIndex = externalLinks.Count - 1;
65 }
66
70 public void Execute()
71 {
72 currentUniqueIndex = CompatibilityConstants.UNIQUE_PACKAGE_PART_INDEX_PREFIX + ParserUtils.ToString(CurrentIndex);
73 xmlElement = GetElement(externalLinks[CurrentIndex]);
74 }
75
81 internal static XmlElement GetElement(ExternalLink externalLink)
82 {
83 IReadOnlyList<ExternalLinkUriRelationship> relationships = externalLink.GetUriRelationships();
84 XmlElement element = XmlElement.CreateElement("externalLink");
85 element.AddDefaultXmlNameSpace("http://schemas.openxmlformats.org/spreadsheetml/2006/main");
86 element.AddNameSpaceAttribute("mc", "xmlns", "http://schemas.openxmlformats.org/markup-compatibility/2006");
87 element.AddNameSpaceAttribute("x14", "xmlns", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main");
88 element.AddNameSpaceAttribute("xxl21", "xmlns", "http://schemas.microsoft.com/office/spreadsheetml/2021/extlinks2021");
89 element.AddAttribute("mc:Ignorable", "x14 xxl21");
90
91 XmlElement externalBook = XmlElement.CreateElement("externalBook");
92 externalBook.AddNameSpaceAttribute("r", "xmlns", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
93 externalBook.AddAttribute("r:id", relationships[0].Id); // There is one primary external book relationship per external-link part.
94 element.AddChildElement(externalBook);
95
96 if (relationships.Count > 1)
97 {
98 XmlElement alternateUrls = XmlElement.CreateElement("alternateUrls", "xxl21");
99 foreach (ExternalLinkUriRelationship relationship in relationships)
100 {
101 if (relationship.Role == ExternalLinkUriRole.AbsoluteAlternate)
102 {
103 XmlElement absoluteUrl = XmlElement.CreateElement("absoluteUrl", "xxl21");
104 absoluteUrl.AddAttribute("r:id", relationship.Id);
105 alternateUrls.AddChildElement(absoluteUrl);
106 }
107 else if (relationship.Role == ExternalLinkUriRole.RelativeAlternate)
108 {
109 XmlElement relativeUrl = XmlElement.CreateElement("relativeUrl", "xxl21");
110 relativeUrl.AddAttribute("r:id", relationship.Id);
111 alternateUrls.AddChildElement(relativeUrl);
112 }
113 }
114 externalBook.AddChildElement(alternateUrls);
115 }
116 // XSD: sheetNames > definedNames > sheetDataSet
117 if (externalLink.Worksheets.Count > 0)
118 {
119 XmlElement sheetNames = XmlElement.CreateElement("sheetNames");
120 XmlElement sheetDataSet = XmlElement.CreateElement("sheetDataSet");
121 // Despite ISO/IEC 29500 describing sheetId as a 1-based index, Microsoft Excel writes it as a zero-based index into sheetNames.
122 int sheetId = 0;
123 foreach (ExternalWorksheet sheet in externalLink.Worksheets)
124 {
125 // --- Sheet names
126 XmlElement sheetName = XmlElement.CreateElement("sheetName");
127 sheetName.AddAttribute("val", XmlUtils.SanitizeXmlValue(sheet.Name));
128 sheetNames.AddChildElement(sheetName);
129 // --- Sheet data
130 XmlElement sheetData = XmlElement.CreateElement("sheetData");
131 sheetData.AddAttribute("sheetId", ParserUtils.ToString(sheetId));
132 if (sheet.RefreshErros != null)
133 {
134 // currently only for roundtrip
135 sheetData.AddAttribute("refreshErrors", ParserUtils.ToString(sheet.RefreshErros.Value == true ? 1 : 0));
136 }
137 List<XmlElement> row = GetRowData(sheet);
138 foreach (XmlElement rowElement in row)
139 {
140 sheetData.AddChildElement(rowElement);
141 }
142 sheetDataSet.AddChildElement(sheetData);
143 sheetId++;
144 }
145 externalBook.AddChildElement(sheetNames);
146 externalBook.AddChildElement(sheetDataSet);
147 }
148 if (externalLink.DefinedNames.Count > 0)
149 {
150 XmlElement definedNames = XmlElement.CreateElement("definedNames");
151 foreach (ExternalDefinedName externalDefinedName in externalLink.DefinedNames)
152 {
153 XmlElement definedName = XmlElement.CreateElement("definedName");
154 definedName.AddAttribute("name", XmlUtils.SanitizeXmlValue(externalDefinedName.Name));
155 if (externalDefinedName.RefersTo != null)
156 {
157 definedName.AddAttribute("refersTo", XmlUtils.SanitizeXmlValue(externalDefinedName.RefersTo));
158 }
159 if (externalDefinedName.RelationshipId != null) // currently only for roundtrip
160 {
161 definedName.AddAttribute("sheetId", XmlUtils.SanitizeXmlValue(externalDefinedName.RelationshipId)); // sanitized (could be unsafe)
162 }
163 definedNames.AddChildElement(definedName);
164 }
165 if (externalBook.FindChildElementsByName("sheetDataSet").Any())
166 {
167 externalBook.AddChildElementBefore(definedNames, "sheetDataSet");
168 }
169 else
170 {
171 externalBook.AddChildElement(definedNames);
172 }
173 }
174 return element;
175 }
176
182 private static List<XmlElement> GetRowData(ExternalWorksheet sheet)
183 {
184 if (sheet.Cells.Count == 0)
185 {
186 return new List<XmlElement>();
187 }
188 ReadOnlyDictionary<Address, ExternalCellValue> cells = sheet.Cells;
189 SortedDictionary<int, XmlElement> rows = new SortedDictionary<int, XmlElement>();
190
191 foreach (KeyValuePair<Address, ExternalCellValue> cell in cells)
192 {
193 if (!rows.TryGetValue(cell.Key.Row, out XmlElement value))
194 {
195 XmlElement row = XmlElement.CreateElement("row");
196 row.AddAttribute("r", ParserUtils.ToString(cell.Key.Row + 1)); // 1-based
197 value = row;
198 rows.Add(cell.Key.Row, value);
199 }
200 XmlElement element = XmlElement.CreateElement("cell");
201 element.AddAttribute("r", cell.Key.ToString());
202 string type = GetCellType(cell.Value.Type);
203 if (type != null)
204 {
205 element.AddAttribute("t", type);
206 }
207 if (cell.Value.CellMetadata != null) // currently only for roundtrip
208 {
209 element.AddAttribute("vm", ParserUtils.ToString(cell.Value.CellMetadata.Value));
210 }
211 if (cell.Value.Type != ExternalCellValue.DataType.Empty)
212 {
213 XmlElement valueElement = XmlElement.CreateElement("v");
214 valueElement.InnerValue =
215 XmlUtils.SanitizeXmlValue(cell.Value.Value);
216 element.AddChildElement(valueElement);
217 }
218 value.AddChildElement(element);
219 }
220 return rows.Values.ToList();
221 }
222
228 private static string GetCellType(ExternalCellValue.DataType dataType)
229 {
230 switch (dataType)
231 {
232 case ExternalCellValue.DataType.Boolean:
233 return "b";
234 case ExternalCellValue.DataType.Date:
235 return "d";
236 case ExternalCellValue.DataType.Error:
237 return "e";
238 case ExternalCellValue.DataType.String: // s is not used
239 return "str";
240 default:
241 return null; // numeric
242 }
243 }
244 #endregion
245 }
246}