diff --git a/src/MiniExcel.Core/Helpers/NetSatandardExtensions.cs b/src/MiniExcel.Core/Helpers/NetStandardHelper.cs
similarity index 57%
rename from src/MiniExcel.Core/Helpers/NetSatandardExtensions.cs
rename to src/MiniExcel.Core/Helpers/NetStandardHelper.cs
index 50d412a9..16372635 100644
--- a/src/MiniExcel.Core/Helpers/NetSatandardExtensions.cs
+++ b/src/MiniExcel.Core/Helpers/NetStandardHelper.cs
@@ -1,11 +1,16 @@
namespace MiniExcelLib.Core.Helpers;
+#if NETSTANDARD2_0
+
+///
+/// Provides .NET Standard 2.0 polyfills for utility methods found in later framework versions.
+/// This enables a unified API surface across the codebase without the need for conditional compilation directives.
+///
public static class NetStandardExtensions
{
-#if NETSTANDARD2_0
public static TValue? GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key, TValue? defaultValue = default)
{
return dictionary.TryGetValue(key, out var value) ? value : defaultValue;
}
-#endif
}
+#endif
diff --git a/src/MiniExcel.Core/Helpers/SynchronousHelper.cs b/src/MiniExcel.Core/Helpers/SynchronousHelper.cs
new file mode 100644
index 00000000..9277af6c
--- /dev/null
+++ b/src/MiniExcel.Core/Helpers/SynchronousHelper.cs
@@ -0,0 +1,16 @@
+using System.IO.Compression;
+
+namespace MiniExcelLib.Core.Helpers;
+
+///
+/// Supplements base classes with synchronous method counterparts, ensuring compatibility with the SyncMethodGenerator
+/// by providing missing entry points without requiring manual preprocessor directives (#if SYNC_ONLY)
+///
+public static class SynchronousHelper
+{
+ extension(ZipArchive)
+ {
+ public static ZipArchive Create(Stream stream, ZipArchiveMode mode, bool leaveOpen, Encoding? encoding = null)
+ => new(stream, mode, leaveOpen, encoding);
+ }
+}
diff --git a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs
index 535602a4..24e78a8b 100644
--- a/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs
+++ b/src/MiniExcel.OpenXml/Api/OpenXmlImporter.cs
@@ -223,11 +223,11 @@ public async Task> GetSheetNamesAsync(Stream stream, OpenXmlConfigu
{
config ??= OpenXmlConfiguration.Default;
- using var archive = new OpenXmlZip(stream, leaveOpen: true);
-
+ var archive = await OpenXmlZip.CreateAsync(stream, leaveOpen: true, cancellationToken: cancellationToken).ConfigureAwait(false);
+ await using var disposableArchive = archive.ConfigureAwait(false);
using var reader = await OpenXmlReader.CreateAsync(stream, config, cancellationToken: cancellationToken).ConfigureAwait(false);
- var rels = await reader.GetWorkbookRelsAsync(archive.EntryCollection, cancellationToken).ConfigureAwait(false);
+ var rels = await reader.GetWorkbookRelsAsync(archive.EntryCollection, cancellationToken).ConfigureAwait(false);
return rels?.Select(s => s.Name).ToList() ?? [];
}
@@ -248,10 +248,11 @@ public async Task> GetSheetInformationsAsync(Stream stream, Open
{
config ??= OpenXmlConfiguration.Default;
- using var archive = new OpenXmlZip(stream);
+ var archive = await OpenXmlZip.CreateAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
+ await using var disposableArchve = archive.ConfigureAwait(false);
using var reader = await OpenXmlReader.CreateAsync(stream, config, cancellationToken: cancellationToken).ConfigureAwait(false);
- var rels = await reader.GetWorkbookRelsAsync(archive.EntryCollection, cancellationToken).ConfigureAwait(false);
+ var rels = await reader.GetWorkbookRelsAsync(archive.EntryCollection, cancellationToken).ConfigureAwait(false);
return rels?.Select((s, i) => s.ToSheetInfo((uint)i)).ToList() ?? [];
}
diff --git a/src/MiniExcel.OpenXml/Constants/Schemas.cs b/src/MiniExcel.OpenXml/Constants/Schemas.cs
index 35f3f079..88d436a4 100644
--- a/src/MiniExcel.OpenXml/Constants/Schemas.cs
+++ b/src/MiniExcel.OpenXml/Constants/Schemas.cs
@@ -2,7 +2,7 @@
internal static class Schemas
{
- public const string SpreadsheetmlXmlNs = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
+ public const string SpreadsheetmlXmlMain = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
public const string SpreadsheetmlXmlStrictNs = "http://purl.oclc.org/ooxml/spreadsheetml/main";
public const string OpenXmlPackageRelationships = "http://schemas.openxmlformats.org/package/2006/relationships";
diff --git a/src/MiniExcel.OpenXml/OpenXmlReader.cs b/src/MiniExcel.OpenXml/OpenXmlReader.cs
index 2313cc6a..a6924561 100644
--- a/src/MiniExcel.OpenXml/OpenXmlReader.cs
+++ b/src/MiniExcel.OpenXml/OpenXmlReader.cs
@@ -10,7 +10,7 @@ namespace MiniExcelLib.OpenXml;
internal partial class OpenXmlReader : IMiniExcelReader
{
- private static readonly string[] Ns = [Schemas.SpreadsheetmlXmlNs, Schemas.SpreadsheetmlXmlStrictNs];
+ private static readonly string[] Ns = [Schemas.SpreadsheetmlXmlMain, Schemas.SpreadsheetmlXmlStrictNs];
private static readonly string[] RelationshiopNs = [Schemas.SpreadsheetmlXmlRelationships, Schemas.SpreadsheetmlXmlStrictRelationships];
private readonly OpenXmlConfiguration _config;
@@ -21,9 +21,9 @@ internal partial class OpenXmlReader : IMiniExcelReader
internal readonly OpenXmlZip Archive;
internal IDictionary SharedStrings = new Dictionary();
- private OpenXmlReader(Stream stream, IMiniExcelConfiguration? configuration)
+ private OpenXmlReader(OpenXmlZip openXmlZip, IMiniExcelConfiguration? configuration)
{
- Archive = new OpenXmlZip(stream);
+ Archive = openXmlZip;
_config = (OpenXmlConfiguration?)configuration ?? OpenXmlConfiguration.Default;
}
@@ -31,8 +31,10 @@ private OpenXmlReader(Stream stream, IMiniExcelConfiguration? configuration)
internal static async Task CreateAsync(Stream stream, IMiniExcelConfiguration? configuration, CancellationToken cancellationToken = default)
{
ThrowHelper.ThrowIfInvalidOpenXml(stream);
-
- var reader = new OpenXmlReader(stream, configuration);
+
+ var archive = await OpenXmlZip.CreateAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
+ var reader = new OpenXmlReader(archive, configuration);
+
await reader.SetSharedStringsAsync(cancellationToken).ConfigureAwait(false);
return reader;
}
@@ -1149,7 +1151,7 @@ internal async Task ReadCommentsAsync(string? sheetName, Cance
XNamespace nsRel = Schemas.OpenXmlPackageRelationships;
XNamespace ns18Tc = Schemas.SpreadsheetmlXmlX18Tc;
- XNamespace nsMain = Schemas.SpreadsheetmlXmlNs;
+ XNamespace nsMain = Schemas.SpreadsheetmlXmlMain;
XNamespace ns14R = Schemas.SpreadsheetmlXmlX14R;
SetWorkbookRels(Archive.EntryCollection);
diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs
index d2390c7c..feecc08a 100644
--- a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs
+++ b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs
@@ -4,10 +4,10 @@
namespace MiniExcelLib.OpenXml;
-internal partial class OpenXmlWriter : IMiniExcelWriter
+internal partial class OpenXmlWriter
{
private readonly Dictionary _zipDictionary = [];
- private Dictionary _cellXfIdMap;
+ private Dictionary _cellXfIdMap = [];
private IEnumerable> GetSheets()
{
diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.cs
index 9d9e47ad..78a705e1 100644
--- a/src/MiniExcel.OpenXml/OpenXmlWriter.cs
+++ b/src/MiniExcel.OpenXml/OpenXmlWriter.cs
@@ -23,18 +23,12 @@ internal partial class OpenXmlWriter : IMiniExcelWriter
private int _currentSheetIndex = 0;
- private OpenXmlWriter(Stream stream, object? value, string? sheetName, IMiniExcelConfiguration? configuration, bool printHeader)
+ private OpenXmlWriter(Stream stream, ZipArchive archive, object? value, string? sheetName, OpenXmlConfiguration configuration, bool printHeader)
{
_stream = stream;
- // A. Why ZipArchiveMode.Update and not ZipArchiveMode.Create?
- // R. ZipArchiveEntry does not support seeking when Mode is Create.
- _configuration = configuration as OpenXmlConfiguration ?? OpenXmlConfiguration.Default;
- if (_configuration is { EnableAutoWidth: true, FastMode: false })
- throw new InvalidOperationException("Auto width requires fast mode to be enabled");
-
- var archiveMode = _configuration.FastMode ? ZipArchiveMode.Update : ZipArchiveMode.Create;
- _archive = new ZipArchive(_stream, archiveMode, true, Utf8WithBom);
+ _configuration = configuration;
+ _archive = archive;
_value = value;
_printHeader = printHeader;
@@ -42,12 +36,24 @@ private OpenXmlWriter(Stream stream, object? value, string? sheetName, IMiniExce
}
[CreateSyncVersion]
- internal static Task CreateAsync(Stream stream, object? value, string? sheetName, bool printHeader, IMiniExcelConfiguration? configuration, CancellationToken cancellationToken = default)
+ internal static async ValueTask CreateAsync(Stream stream, object? value, string? sheetName, bool printHeader, IMiniExcelConfiguration? configuration, CancellationToken cancellationToken = default)
{
ThrowHelper.ThrowIfInvalidSheetName(sheetName);
- var writer = new OpenXmlWriter(stream, value, sheetName, configuration, printHeader);
- return Task.FromResult(writer);
+ var conf = configuration as OpenXmlConfiguration ?? OpenXmlConfiguration.Default;
+ if (conf is { EnableAutoWidth: true, FastMode: false })
+ throw new InvalidOperationException("Auto width requires fast mode to be enabled");
+
+ // A. Why ZipArchiveMode.Update and not ZipArchiveMode.Create?
+ // R. ZipArchiveEntry does not support seeking when Mode is Create.
+ var archiveMode = conf.FastMode ? ZipArchiveMode.Update : ZipArchiveMode.Create;
+
+#if NET10_0_OR_GREATER
+ var archive = await ZipArchive.CreateAsync(stream, archiveMode, true, Utf8WithBom, cancellationToken).ConfigureAwait(false);
+#else
+ var archive = new ZipArchive(stream, archiveMode, true, Utf8WithBom);
+#endif
+ return new OpenXmlWriter(stream, archive, value, sheetName, conf, printHeader);
}
[CreateSyncVersion]
diff --git a/src/MiniExcel.OpenXml/Picture/OpenXmlPictureImplement.cs b/src/MiniExcel.OpenXml/Picture/OpenXmlPictureImplement.cs
index 817a7d86..920330cd 100644
--- a/src/MiniExcel.OpenXml/Picture/OpenXmlPictureImplement.cs
+++ b/src/MiniExcel.OpenXml/Picture/OpenXmlPictureImplement.cs
@@ -17,16 +17,18 @@ private static XmlNamespaceManager GetRNamespaceManager(XmlDocument doc)
public static async Task AddPictureAsync(Stream excelStream, CancellationToken cancellationToken = default, params MiniExcelPicture[] images)
{
// get sheets
- using var excelArchive = new OpenXmlZip(excelStream);
+ var excelArchive = await OpenXmlZip.CreateAsync(excelStream, cancellationToken: cancellationToken).ConfigureAwait(false);
+ await using var disposableExcelArchive = excelArchive.ConfigureAwait(false);
+
using var reader = await OpenXmlReader.CreateAsync(excelStream, null, cancellationToken).ConfigureAwait(false);
#if NET10_0_OR_GREATER
- var archive = new ZipArchive(excelStream, ZipArchiveMode.Update, true);
- await using var disposableArchive = archive.ConfigureAwait(false);
+ var archive = await ZipArchive.CreateAsync(excelStream, ZipArchiveMode.Update, true, null, cancellationToken).ConfigureAwait(false);
+ await using var disposableArchive = archive.ConfigureAwait(false);
#else
using var archive = new ZipArchive(excelStream, ZipArchiveMode.Update, true);
#endif
- var rels = await reader.GetWorkbookRelsAsync(excelArchive.EntryCollection, cancellationToken).ConfigureAwait(false);
+ var rels = await reader.GetWorkbookRelsAsync(excelArchive.EntryCollection, cancellationToken).ConfigureAwait(false);
var sheetEntries = rels?.ToList() ?? [];
// Group images by sheet
diff --git a/src/MiniExcel.OpenXml/Styles/OpenXmlStyles.cs b/src/MiniExcel.OpenXml/Styles/OpenXmlStyles.cs
index fa11822e..dd55dc1b 100644
--- a/src/MiniExcel.OpenXml/Styles/OpenXmlStyles.cs
+++ b/src/MiniExcel.OpenXml/Styles/OpenXmlStyles.cs
@@ -5,7 +5,7 @@ namespace MiniExcelLib.OpenXml.Styles;
internal class OpenXmlStyles
{
- private static readonly string[] Ns = [Schemas.SpreadsheetmlXmlNs, Schemas.SpreadsheetmlXmlStrictNs];
+ private static readonly string[] Ns = [Schemas.SpreadsheetmlXmlMain, Schemas.SpreadsheetmlXmlStrictNs];
private readonly Dictionary _cellXfs = new();
private readonly Dictionary _cellStyleXfs = new();
diff --git a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs
index 2c77b1cc..714c6394 100644
--- a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs
+++ b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs
@@ -1,18 +1,33 @@
using MiniExcelLib.Core.Attributes;
-using MiniExcelLib.OpenXml.Constants;
using System.ComponentModel;
+using System.Xml.Linq;
+using MiniExcelLib.OpenXml.Constants;
namespace MiniExcelLib.OpenXml.Templates;
internal partial class OpenXmlTemplate
{
- private readonly List _calcChainCellRefs = [];
+ private static readonly XNamespace SpreadsheetNs = Schemas.SpreadsheetmlXmlMain;
+
+ private static readonly XmlWriterSettings DocXmlWriterSettings = new()
+ {
+#if !SYNC_ONLY
+ Async = true
+#endif
+ };
- private List _xRowInfos;
- private Dictionary _xMergeCellInfos;
- private List _newXMergeCellInfos;
+ private static readonly XmlWriterSettings FragXmlWriterSettings = new()
+ {
+ OmitXmlDeclaration = true,
+ ConformanceLevel = ConformanceLevel.Fragment,
+#if !SYNC_ONLY
+ Async = true
+#endif
+ };
-#if NET7_0_OR_GREATER
+#if NET8_0_OR_GREATER
+ [GeneratedRegex("(?<={{).*?(?=}})")] private static partial Regex ExpressionRegex();
+ private static readonly Regex IsExpressionRegex = ExpressionRegex();
[GeneratedRegex("([A-Z]+)([0-9]+)")] private static partial Regex CellRegexImpl();
private static readonly Regex CellRegex = CellRegexImpl();
[GeneratedRegex(@"\{\{(.*?)\}\}")] private static partial Regex TemplateRegexImpl();
@@ -22,113 +37,135 @@ internal partial class OpenXmlTemplate
[GeneratedRegex(@"<(?:x:)?v>\s*(?:x:)?v>")] private static partial Regex EmptyVTagRegexImpl();
private static readonly Regex EmptyVTagRegex = EmptyVTagRegexImpl();
#else
+ private static readonly Regex IsExpressionRegex = new("(?<={{).*?(?=}})");
private static readonly Regex CellRegex = new("([A-Z]+)([0-9]+)", RegexOptions.Compiled);
private static readonly Regex TemplateRegex = new(@"\{\{(.*?)\}\}", RegexOptions.Compiled);
private static readonly Regex NonTemplateRegex = new(@".*?\{\{.*?\}\}.*?", RegexOptions.Compiled);
private static readonly Regex EmptyVTagRegex = new(@"<(?:x:)?v>\s*(?:x:)?v>", RegexOptions.Compiled);
#endif
+ private readonly List _xRowInfos = [];
+ private readonly Dictionary _xMergeCellInfos = [];
+ private readonly List _newXMergeCellInfos = [];
+ private readonly List _calcChainCellRefs = [];
+
+
[CreateSyncVersion]
- private async Task GenerateSheetXmlImplByUpdateModeAsync(ZipArchiveEntry sheetZipEntry, Stream stream, Stream sheetStream, IDictionary inputMaps, IDictionary sharedStrings, bool mergeCells = false, CancellationToken cancellationToken = default)
+ private async Task GenerateSheetByUpdateModeAsync(ZipArchiveEntry sheetZipEntry, Stream stream, Stream sheetStream, IDictionary inputMaps, IDictionary sharedStrings, bool mergeCells = false, CancellationToken cancellationToken = default)
{
- var doc = new XmlDocument();
- doc.Load(sheetStream);
-
-#if NET5_0_OR_GREATER
+#if NET8_0_OR_GREATER
+ var doc = await XDocument.LoadAsync(sheetStream, LoadOptions.None, cancellationToken).ConfigureAwait(false);
await sheetStream.DisposeAsync().ConfigureAwait(false);
#else
+ var doc = XDocument.Load(sheetStream);
sheetStream.Dispose();
#endif
- sheetZipEntry.Delete(); // ZipArchiveEntry can't update directly, so need to delete then create logic
+ // we can't update ZipArchiveEntry directly, so we delete the original entry and recreate it
+ sheetZipEntry.Delete();
- var worksheet = doc.SelectSingleNode("/x:worksheet", Ns);
- var sheetData = doc.SelectSingleNode("/x:worksheet/x:sheetData", Ns);
- var newSheetData = sheetData?.Clone(); //avoid delete lost data
- var rows = newSheetData?.SelectNodes("x:row", Ns);
+ var worksheet = doc.Element(SpreadsheetNs + "worksheet");
+ var sheetData = worksheet?.Element(SpreadsheetNs + "sheetData");
+ var newSheetData = new XElement(sheetData);
+ var rows = newSheetData.Elements(SpreadsheetNs + "row");
- ReplaceSharedStringsToStr(sharedStrings, rows);
- GetMergeCells(doc, worksheet);
- UpdateDimensionAndGetRowsInfo(inputMaps, doc, rows, !mergeCells);
+ InjectSharedStrings(sharedStrings, rows);
+ GetMergeCells(worksheet);
+ UpdateDimensionAndGetRowsInfo(inputMaps, worksheet, rows, !mergeCells);
- await WriteSheetXmlAsync(stream, doc, sheetData, mergeCells, cancellationToken).ConfigureAwait(false);
+#if NET8_0_OR_GREATER
+ var writer = XmlWriter.Create(stream, DocXmlWriterSettings);
+ await using var disposableWriter = writer.ConfigureAwait(false);
+#else
+ using var writer = XmlWriter.Create(stream, DocXmlWriterSettings);
+#endif
+
+ await WriteSheetXmlAsync(writer, worksheet, sheetData, mergeCells, cancellationToken).ConfigureAwait(false);
}
-
+
[CreateSyncVersion]
- private async Task GenerateSheetXmlImplByCreateModeAsync(ZipArchiveEntry templateSheetZipEntry, Stream outputZipSheetEntryStream, IDictionary inputMaps, IDictionary sharedStrings, bool mergeCells = false)
+ private async Task GenerateSheetByCreateModeAsync(ZipArchiveEntry templateSheetZipEntry, Stream outputZipSheetEntryStream, IDictionary inputMaps, IDictionary sharedStrings, bool mergeCells = false, CancellationToken cancellationToken = default)
{
- var doc = new XmlDocument
- {
- XmlResolver = null
- };
-
-#if NET5_0_OR_GREATER
+#if NET8_0_OR_GREATER
#if NET10_0_OR_GREATER
- var newTemplateStream = await templateSheetZipEntry.OpenAsync().ConfigureAwait(false);
+ var newTemplateStream = await templateSheetZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false);
#else
var newTemplateStream = templateSheetZipEntry.Open();
#endif
- await using var disposableStream = newTemplateStream.ConfigureAwait(false);
+ await using var disposableNewTemplateStream = newTemplateStream.ConfigureAwait(false);
+ var doc = await XDocument.LoadAsync(newTemplateStream, LoadOptions.None, cancellationToken).ConfigureAwait(false);
#else
using var newTemplateStream = templateSheetZipEntry.Open();
+ var doc = XDocument.Load(newTemplateStream);
#endif
- doc.Load(newTemplateStream);
+ var worksheet = doc.Element(SpreadsheetNs + "worksheet");
+ var prefix = worksheet?.GetPrefixOfNamespace(SpreadsheetNs);
+ if (!string.IsNullOrEmpty(prefix))
+ {
+ // we remove the main namespace's prefix declaration so that we don't have to worry about inconstencies when we serialize single elements
+ worksheet?.Attribute(XNamespace.Xmlns + prefix)?.Remove();
+ }
- var worksheet = doc.SelectSingleNode("/x:worksheet", Ns);
- var sheetData = doc.SelectSingleNode("/x:worksheet/x:sheetData", Ns);
- var newSheetData = sheetData?.Clone(); //avoid delete lost data
- var rows = newSheetData?.SelectNodes("x:row", Ns);
+ var sheetData = worksheet?.Element(SpreadsheetNs + "sheetData");
+ var newSheetData = new XElement(sheetData);
+
+ var rows = newSheetData.Elements(SpreadsheetNs + "row");
- ReplaceSharedStringsToStr(sharedStrings, rows);
- GetMergeCells(doc, worksheet);
- UpdateDimensionAndGetRowsInfo(inputMaps, doc, rows, !mergeCells);
+ InjectSharedStrings(sharedStrings, rows);
+ GetMergeCells(worksheet);
+ UpdateDimensionAndGetRowsInfo(inputMaps, worksheet, rows, !mergeCells);
- await WriteSheetXmlAsync(outputZipSheetEntryStream, doc, sheetData, mergeCells).ConfigureAwait(false);
+#if NET8_0_OR_GREATER
+ var writer = XmlWriter.Create(outputZipSheetEntryStream, DocXmlWriterSettings);
+ await using var disposableWriter = writer.ConfigureAwait(false);
+#else
+ using var writer = XmlWriter.Create(outputZipSheetEntryStream, DocXmlWriterSettings);
+#endif
+ await WriteSheetXmlAsync(writer, worksheet, sheetData, mergeCells, cancellationToken).ConfigureAwait(false);
}
- private void GetMergeCells(XmlDocument doc, XmlNode worksheet)
+ private void GetMergeCells(XElement worksheet)
{
- var mergeCells = doc.SelectSingleNode("/x:worksheet/x:mergeCells", Ns);
- if (mergeCells is null)
+ if (worksheet.Element(SpreadsheetNs + "mergeCells") is not { } mergeCells)
return;
- var newMergeCells = mergeCells.Clone();
- worksheet.RemoveChild(mergeCells);
+ var newMergeCells = new XElement(mergeCells);
+ mergeCells.Remove();
- foreach (XmlElement cell in newMergeCells)
+ foreach (var cell in newMergeCells.Elements())
{
- var mergerCell = new XMergeCell(cell);
- _xMergeCellInfos[mergerCell.XY1] = mergerCell;
+ var mergeCell = new XMergeCell(cell);
+ _xMergeCellInfos[mergeCell.XY1] = mergeCell;
}
}
- private static IEnumerable ParseConditionalFormatRanges(XmlDocument doc)
+ private static IEnumerable NewParseConditionalFormatRanges(XElement worksheet)
{
- var conditionalFormatting = doc.SelectNodes("/x:worksheet/x:conditionalFormatting", Ns);
+ var conditionalFormatting = worksheet.Element(SpreadsheetNs + "conditionalFormatting");
if (conditionalFormatting is null)
yield break;
- foreach (XmlNode conditionalFormat in conditionalFormatting)
+ foreach (var format in conditionalFormatting.Elements())
{
- var rangeValues = conditionalFormat.Attributes?["sqref"]?.Value.Split(' ');
- if (rangeValues is null)
+ var ranges = format.Attribute("sqref")?.Value.Split(' ');
+ if (ranges is null)
continue;
- var rangeList = new List();
- foreach (var rangeVal in rangeValues)
+ List rangeList = [];
+ foreach (var range in ranges)
{
- var rangeValSplit = rangeVal.Split(':');
- if (rangeValSplit.Length == 0)
+ var rangeValue = range.Split(':');
+ if (rangeValue.Length == 0)
continue;
- if (rangeValSplit.Length == 1)
+ if (rangeValue.Length == 1)
{
- var match = CellRegex.Match(rangeValSplit[0]);
- if (!match.Success)
+ if (CellRegex.Match(rangeValue[0]) is not { Success: true } match)
continue;
var row = int.Parse(match.Groups[2].Value);
var column = CellReferenceConverter.GetNumericalIndex(match.Groups[1].Value);
+
rangeList.Add(new Range
{
StartColumn = column,
@@ -137,88 +174,80 @@ private static IEnumerable ParseConditionalFormatRanges(
EndRow = row
});
}
- else
+ else if (CellRegex.Match(rangeValue[0]) is { Success: true } match1 &&
+ CellRegex.Match(rangeValue[1]) is { Success: true } match2)
{
- var match1 = CellRegex.Match(rangeValSplit[0]);
- var match2 = CellRegex.Match(rangeValSplit[1]);
- if (match1.Success && match2.Success)
+ rangeList.Add(new Range
{
- rangeList.Add(new Range
- {
- StartColumn = CellReferenceConverter.GetNumericalIndex(match1.Groups[1].Value),
- StartRow = int.Parse(match1.Groups[2].Value),
- EndColumn = CellReferenceConverter.GetNumericalIndex(match2.Groups[1].Value),
- EndRow = int.Parse(match2.Groups[2].Value)
- });
- }
+ StartColumn = CellReferenceConverter.GetNumericalIndex(match1.Groups[1].Value),
+ StartRow = int.Parse(match1.Groups[2].Value),
+ EndColumn = CellReferenceConverter.GetNumericalIndex(match2.Groups[1].Value),
+ EndRow = int.Parse(match2.Groups[2].Value)
+ });
}
}
yield return new ConditionalFormatRange
{
- Node = conditionalFormat,
+ Node = format,
Ranges = rangeList
};
}
}
[CreateSyncVersion]
- private async Task WriteSheetXmlAsync(Stream outputFileStream, XmlDocument doc, XmlNode sheetData, bool mergeCells = false, CancellationToken cancellationToken = default)
+ private async Task WriteSheetXmlAsync(XmlWriter writer, XElement worksheet, XElement sheetData, bool mergeCells = false, CancellationToken cancellationToken = default)
{
- //Q.Why so complex?
- //A.Because try to use string stream avoid OOM when rendering rows
+ // TODO: Can we make this less complex?
- var conditionalFormatRanges = ParseConditionalFormatRanges(doc).ToList();
+ var conditionalFormatRanges = NewParseConditionalFormatRanges(worksheet).ToList();
var newConditionalFormatRanges = new List();
newConditionalFormatRanges.AddRange(conditionalFormatRanges);
sheetData.RemoveAll();
- sheetData.InnerText = "{{{{{{split}}}}}}"; //TODO: bad code smell
-
- var prefix = string.IsNullOrEmpty(sheetData.Prefix) ? "" : $"{sheetData.Prefix}:";
- var endPrefix = string.IsNullOrEmpty(sheetData.Prefix) ? "" : $":{sheetData.Prefix}"; // https://user-images.githubusercontent.com/12729184/115000066-fd02b300-9ed4-11eb-8e65-bf0014015134.png
-
- var conditionalFormatNodes = doc.SelectNodes("/x:worksheet/x:conditionalFormatting", Ns);
- for (var i = 0; i < conditionalFormatNodes?.Count; ++i)
- {
- var node = conditionalFormatNodes.Item(i);
- node.ParentNode.RemoveChild(node);
- }
+ worksheet.Elements(SpreadsheetNs + "conditionalFormatting").Remove();
+
+ var prefix = worksheet.GetPrefixOfNamespace(SpreadsheetNs);
+ var fullPrefix = !string.IsNullOrEmpty(prefix) ? $"{prefix}:" : "";
- var phoneticPr = doc.SelectSingleNode("/x:worksheet/x:phoneticPr", Ns);
var phoneticPrXml = string.Empty;
- if (phoneticPr is not null)
+ if (worksheet.Element(SpreadsheetNs + "phoneticPr") is { } phoneticPr)
{
- phoneticPrXml = phoneticPr.OuterXml;
- phoneticPr.ParentNode.RemoveChild(phoneticPr);
+ phoneticPrXml = phoneticPr.ToString(SaveOptions.DisableFormatting);
+ phoneticPr.Remove();
}
// Extract autoFilter - must be written before mergeCells and phoneticPr per ECMA-376
- var autoFilter = doc.SelectSingleNode("/x:worksheet/x:autoFilter", Ns);
var autoFilterXml = string.Empty;
- if (autoFilter is not null)
+ if (worksheet.Element(SpreadsheetNs + "autoFilter") is { } autoFilter)
{
- autoFilterXml = autoFilter.OuterXml;
- autoFilter.ParentNode.RemoveChild(autoFilter);
+ autoFilterXml = autoFilter.ToString(SaveOptions.DisableFormatting);
+ autoFilter.Remove();
}
- var contents = doc.InnerXml.Split(new[] { $"<{prefix}sheetData>{{{{{{{{{{{{split}}}}}}}}}}}}{prefix}sheetData>" }, StringSplitOptions.None);
-#if NETCOREAPP3_0_OR_GREATER
- var writer = new StreamWriter(outputFileStream, Encoding.UTF8);
- await using var disposableWriter = writer.ConfigureAwait(false);
+ var beforeSheetData = worksheet.Element(SpreadsheetNs + "sheetData")?.ElementsBeforeSelf() ?? [];
+ var afterSheetData = worksheet.Element(SpreadsheetNs + "sheetData")?.ElementsAfterSelf() ?? [];
+
+ await writer.WriteStartElementAsync(null, "worksheet", Schemas.SpreadsheetmlXmlMain).ConfigureAwait(false);
+ foreach (var attr in worksheet.Attributes())
+ {
+ var (nsPrefix, ns) = attr is { IsNamespaceDeclaration: true, Name.LocalName: not "xmlns" }
+ ? ("xmlns", null as string)
+ : (null, attr.Name.NamespaceName);
+
+ await writer.WriteAttributeStringAsync(nsPrefix, attr.Name.LocalName, ns, attr.Value).ConfigureAwait(false);
+ }
+
+ foreach (var beforeElement in beforeSheetData)
+ {
+#if NET8_0_OR_GREATER
+ await beforeElement.WriteToAsync(writer, cancellationToken).ConfigureAwait(false);
#else
- using var writer = new StreamWriter(outputFileStream, Encoding.UTF8);
-#endif
- await writer.WriteAsync(contents[0]
-#if NET7_0_OR_GREATER
- .AsMemory(), cancellationToken
+ beforeElement.WriteTo(writer);
#endif
- ).ConfigureAwait(false);
- await writer.WriteAsync($"<{prefix}sheetData>"
-#if NET7_0_OR_GREATER
- .AsMemory(), cancellationToken
-#endif
- ).ConfigureAwait(false); // prefix problem
+ }
+
+ await writer.WriteStartElementAsync(null,"sheetData", Schemas.SpreadsheetmlXmlMain).ConfigureAwait(false);
if (mergeCells)
{
@@ -228,7 +257,6 @@ await writer.WriteAsync($"<{prefix}sheetData>"
#region Generate rows and cells
int rowIndexDiff = 0;
- var rowXml = new StringBuilder();
// for formula cells
int enumrowstart = -1;
@@ -238,7 +266,7 @@ await writer.WriteAsync($"<{prefix}sheetData>"
bool groupingStarted = false;
bool hasEverGroupStarted = false;
int groupStartRowIndex = 0;
- IList