Skip to content

Commit d583683

Browse files
Mirrors PRs 917, 923 and 924 from the master branch (#926)
* Mirrors PR 917 from the master branch * Mirrors PR 923 from the master branch Fixes issue 914 * Mirrors PR 924 from the master branch Fixes issue 915
1 parent 300cae6 commit d583683

4 files changed

Lines changed: 225 additions & 34 deletions

File tree

samples/xlsx/TestIssue915.xlsx

6.06 KB
Binary file not shown.

src/MiniExcel/Csv/CsvReader.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ public IEnumerable<IDictionary<string, object>> Query(bool useHeaderRow, string
3737
string row;
3838
for (var rowIndex = 1; (row = reader.ReadLine()) != null; rowIndex++)
3939
{
40+
if (string.IsNullOrWhiteSpace(row))
41+
continue;
42+
4043
string finalRow = row;
4144
if (_config.ReadLineBreaksWithinQuotes)
4245
{

src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs

Lines changed: 166 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,13 @@ internal partial class ExcelOpenXmlTemplate
140140
private static readonly Regex _templateRegex = TemplateRegex();
141141
[GeneratedRegex(@".*?\{\{.*?\}\}.*?")] private static partial Regex NonTemplateRegex();
142142
private static readonly Regex _nonTemplateRegex = TemplateRegex();
143+
[GeneratedRegex(@"<(?:x:)?v>\s*</(?:x:)?v>")] private static partial Regex EmptyVTagRegexImpl();
144+
private static readonly Regex _emptyVTagRegex = EmptyVTagRegexImpl();
143145
#else
144146
private static readonly Regex _cellRegex = new Regex("([A-Z]+)([0-9]+)", RegexOptions.Compiled);
145147
private static readonly Regex _templateRegex = new Regex(@"\{\{(.*?)\}\}", RegexOptions.Compiled);
146148
private static readonly Regex _nonTemplateRegex = new Regex(@".*?\{\{.*?\}\}.*?", RegexOptions.Compiled);
149+
private static readonly Regex _emptyVTagRegex = new Regex(@"<(?:x:)?v>\s*</(?:x:)?v>", RegexOptions.Compiled);
147150
#endif
148151

149152
private void GenerateSheetXmlImplByUpdateMode(ZipArchiveEntry sheetZipEntry, Stream stream, Stream sheetStream, IDictionary<string, object> inputMaps, IDictionary<int, string> sharedStrings, bool mergeCells = false)
@@ -324,6 +327,15 @@ private void WriteSheetXml(Stream outputFileStream, XmlDocument doc, XmlNode she
324327
phoneticPr.ParentNode.RemoveChild(phoneticPr);
325328
}
326329

330+
// Extract autoFilter - must be written before mergeCells and phoneticPr per ECMA-376
331+
var autoFilter = doc.SelectSingleNode("/x:worksheet/x:autoFilter", _ns);
332+
var autoFilterXml = string.Empty;
333+
if (autoFilter != null)
334+
{
335+
autoFilterXml = autoFilter.OuterXml;
336+
autoFilter.ParentNode.RemoveChild(autoFilter);
337+
}
338+
327339
var contents = doc.InnerXml.Split(new[] { $"<{prefix}sheetData>{{{{{{{{{{{{split}}}}}}}}}}}}</{prefix}sheetData>" }, StringSplitOptions.None);
328340

329341
using (var writer = new StreamWriter(outputFileStream, Encoding.UTF8))
@@ -514,6 +526,15 @@ private void WriteSheetXml(Stream outputFileStream, XmlDocument doc, XmlNode she
514526

515527
writer.Write($"</{prefix}sheetData>");
516528

529+
// ECMA-376 element order: sheetData → autoFilter → mergeCells → phoneticPr → conditionalFormatting
530+
531+
// 1. autoFilter (must come before mergeCells)
532+
if (!string.IsNullOrEmpty(autoFilterXml))
533+
{
534+
writer.Write(CleanXml(autoFilterXml, endPrefix));
535+
}
536+
537+
// 2. mergeCells
517538
if (_newXMergeCellInfos.Count != 0)
518539
{
519540
writer.Write($"<{prefix}mergeCells count=\"{_newXMergeCellInfos.Count}\">");
@@ -524,14 +545,16 @@ private void WriteSheetXml(Stream outputFileStream, XmlDocument doc, XmlNode she
524545
writer.Write($"</{prefix}mergeCells>");
525546
}
526547

548+
// 3. PhoneticPr
527549
if (!string.IsNullOrEmpty(phoneticPrXml))
528550
{
529-
writer.Write(phoneticPrXml);
551+
writer.Write(CleanXml(phoneticPrXml, endPrefix));
530552
}
531553

554+
// 4. conditionalFormatting
532555
if (newConditionalFormatRanges.Count != 0)
533556
{
534-
writer.Write(string.Join(string.Empty, newConditionalFormatRanges.Select(cf => cf.Node.OuterXml)));
557+
writer.Write(CleanXml(string.Join(string.Empty, newConditionalFormatRanges.Select(cf => cf.Node.OuterXml)), endPrefix));
535558
}
536559

537560
writer.Write(contents[1]);
@@ -548,12 +571,23 @@ private void GenerateCellValues(string endPrefix, StreamWriter writer, ref int r
548571
var cleanOuterXmlOpen = CleanXml(outerXmlOpen, endPrefix);
549572

550573
// https://github.com/mini-software/MiniExcel/issues/771 Saving by template introduces unintended value replication in each row #771
551-
var notFirstRowElement = rowElement.Clone();
574+
var notFirstRowElement = rowElement.Clone();
552575
foreach (XmlElement c in notFirstRowElement.SelectNodes("x:c", _ns))
553576
{
554-
var v = c.SelectSingleNode("x:v", _ns);
555-
if (v != null && !_nonTemplateRegex.IsMatch(v.InnerText))
556-
v.InnerText = string.Empty;
577+
// Try <v> first (for t="n"/t="b" cells), then <is><t> (for t="inlineStr" cells)
578+
var vTag = c.SelectSingleNode("x:v", _ns);
579+
if (vTag != null)
580+
{
581+
if (!_nonTemplateRegex.IsMatch(vTag.InnerText))
582+
vTag.InnerText = string.Empty;
583+
}
584+
else
585+
{
586+
// Handle inline string cells
587+
var t = c.SelectSingleNode("x:is/x:t", _ns);
588+
if (t != null && !_nonTemplateRegex.IsMatch(t.InnerText))
589+
t.InnerText = string.Empty;
590+
}
557591
}
558592

559593
foreach (var item in rowInfo.CellIEnumerableValues)
@@ -694,7 +728,7 @@ private void GenerateCellValues(string endPrefix, StreamWriter writer, ref int r
694728
else
695729
{
696730
cellValueStr = ExcelOpenXmlUtils.EncodeXML(cellValue?.ToString());
697-
if (!isDictOrTable && TypeHelper.IsNumericType(type))
731+
if (TypeHelper.IsNumericType(type))
698732
{
699733
if (decimal.TryParse(cellValueStr, out var decimalValue))
700734
cellValueStr = decimalValue.ToString(CultureInfo.InvariantCulture);
@@ -712,6 +746,9 @@ private void GenerateCellValues(string endPrefix, StreamWriter writer, ref int r
712746

713747
substXmlRow = rowXml.ToString();
714748
substXmlRow = _templateRegex.Replace(substXmlRow, MatchDelegate);
749+
750+
// Cleanup empty <v> tags which defaults to invalid XML
751+
substXmlRow = _emptyVTagRegex.Replace(substXmlRow, "");
715752
}
716753

717754
rowXml.Clear();
@@ -744,9 +781,14 @@ private void GenerateCellValues(string endPrefix, StreamWriter writer, ref int r
744781
var mergeBaseRowIndex = newRowIndex;
745782
newRowIndex += rowInfo.IEnumerableMercell?.Height ?? 1;
746783

784+
// Replace {{$rowindex}} in the already-built substXmlRow
785+
rowXml.Replace("{{$rowindex}}", mergeBaseRowIndex.ToString());
786+
747787
// replace formulas
748788
ProcessFormulas(rowXml, newRowIndex);
749-
writer.Write(CleanXml(rowXml, endPrefix));
789+
790+
var finalXml = CleanXml(rowXml, endPrefix).ToString();
791+
writer.Write(finalXml);
750792

751793
//mergecells
752794
if (rowInfo.RowMercells == null)
@@ -936,11 +978,11 @@ private void ProcessFormulas(StringBuilder rowXml, int rowIndex)
936978
continue;
937979

938980
/* Target:
939-
<c r="C8" s="3">
940-
<f>SUM(C2:C7)</f>
941-
</c>
981+
<c r="C8" s="3">
982+
<is><t>SUM(C2:C7)</t></is>
983+
</c>
942984
*/
943-
var vs = c.SelectNodes("x:v", _ns);
985+
var vs = c.SelectNodes("x:is", _ns);
944986
foreach (XmlElement v in vs)
945987
{
946988
if (!v.InnerText.StartsWith("$="))
@@ -975,7 +1017,8 @@ private static string ConvertToDateTimeString(PropertyInfo propInfo, object cell
9751017
private static string CleanXml(string xml, string endPrefix) => CleanXml(new StringBuilder(xml), endPrefix).ToString();
9761018
private static StringBuilder CleanXml(StringBuilder xml, string endPrefix) => xml
9771019
.Replace("xmlns:x14ac=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac\"", "")
978-
.Replace($"xmlns{endPrefix}=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"", "");
1020+
.Replace($"xmlns{endPrefix}=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"", "")
1021+
.Replace("xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"", "");
9791022

9801023
private static void ReplaceSharedStringsToStr(IDictionary<int, string> sharedStrings, XmlNodeList rows)
9811024
{
@@ -996,14 +1039,100 @@ private static void ReplaceSharedStringsToStr(IDictionary<int, string> sharedStr
9961039
if (sharedStrings == null || !sharedStrings.TryGetValue(int.Parse(v.InnerText), out var shared))
9971040
continue;
9981041

999-
// change type = str and replace its value
1000-
//TODO: remove sharedstring?
1001-
v.InnerText = shared;
1002-
c.SetAttribute("t", "str");
1003-
}
1042+
// change type = inlineStr and replace its value
1043+
// Use the same prefix as the source element to handle namespaced documents (e.g., x:v -> x:is, x:t)
1044+
var prefix = v.Prefix;
1045+
c.RemoveChild(v);
1046+
1047+
var isNode = string.IsNullOrEmpty(prefix)
1048+
? c.OwnerDocument.CreateElement("is", Config.SpreadsheetmlXmlns)
1049+
: c.OwnerDocument.CreateElement(prefix, "is", Config.SpreadsheetmlXmlns);
1050+
1051+
var tNode = string.IsNullOrEmpty(prefix)
1052+
? c.OwnerDocument.CreateElement("t", Config.SpreadsheetmlXmlns)
1053+
: c.OwnerDocument.CreateElement(prefix, "t", Config.SpreadsheetmlXmlns);
1054+
1055+
tNode.InnerText = shared;
1056+
isNode.AppendChild(tNode);
1057+
c.AppendChild(isNode);
1058+
1059+
c.RemoveAttribute("t");
1060+
c.SetAttribute("t", "inlineStr"); }
10041061
}
10051062
}
10061063

1064+
private static void SetCellType(XmlElement c, string type)
1065+
{
1066+
if (type == "str") type = "inlineStr"; // Force inlineStr for strings
1067+
1068+
// Determine the prefix used in this document (e.g., "x" for x:c, x:v, etc.)
1069+
var prefix = c.Prefix;
1070+
1071+
if (type == "inlineStr")
1072+
{
1073+
// Ensure <is><t>...</t></is>
1074+
c.SetAttribute("t", "inlineStr");
1075+
var v = c.SelectSingleNode("x:v", _ns);
1076+
1077+
if (v != null)
1078+
{
1079+
var text = v.InnerText;
1080+
c.RemoveChild(v);
1081+
1082+
var isNode = string.IsNullOrEmpty(prefix)
1083+
? c.OwnerDocument.CreateElement("is", Config.SpreadsheetmlXmlns)
1084+
: c.OwnerDocument.CreateElement(prefix, "is", Config.SpreadsheetmlXmlns);
1085+
1086+
var tNode = string.IsNullOrEmpty(prefix)
1087+
? c.OwnerDocument.CreateElement("t", Config.SpreadsheetmlXmlns)
1088+
: c.OwnerDocument.CreateElement(prefix, "t", Config.SpreadsheetmlXmlns);
1089+
1090+
tNode.InnerText = text;
1091+
isNode.AppendChild(tNode);
1092+
c.AppendChild(isNode);
1093+
}
1094+
else if (c.SelectSingleNode("x:is", _ns) == null)
1095+
{
1096+
// Create empty <is><t></t></is> if neither <v> nor <is> exists
1097+
var isNode = string.IsNullOrEmpty(prefix)
1098+
? c.OwnerDocument.CreateElement("is", Config.SpreadsheetmlXmlns)
1099+
: c.OwnerDocument.CreateElement(prefix, "is", Config.SpreadsheetmlXmlns);
1100+
1101+
var tNode = string.IsNullOrEmpty(prefix)
1102+
? c.OwnerDocument.CreateElement("t", Config.SpreadsheetmlXmlns)
1103+
: c.OwnerDocument.CreateElement(prefix, "t", Config.SpreadsheetmlXmlns);
1104+
1105+
isNode.AppendChild(tNode);
1106+
c.AppendChild(isNode);
1107+
}
1108+
}
1109+
else
1110+
{
1111+
// Ensure <v>...</v>
1112+
// For numbers/booleans, we remove 't' attribute to let it be default (number)
1113+
// or we could set it to 'n' explicitly, but removing is safer for general number types
1114+
if (type == "b")
1115+
c.SetAttribute("t", "b");
1116+
else
1117+
c.RemoveAttribute("t");
1118+
1119+
var isNode = c.SelectSingleNode("x:is", _ns);
1120+
if (isNode != null)
1121+
{
1122+
var tNode = isNode.SelectSingleNode("x:t", _ns);
1123+
var text = tNode?.InnerText ?? string.Empty;
1124+
c.RemoveChild(isNode);
1125+
1126+
var v = string.IsNullOrEmpty(prefix)
1127+
? c.OwnerDocument.CreateElement("v", Config.SpreadsheetmlXmlns)
1128+
: c.OwnerDocument.CreateElement(prefix, "v", Config.SpreadsheetmlXmlns);
1129+
1130+
v.InnerText = text;
1131+
c.AppendChild(v);
1132+
}
1133+
}
1134+
}
1135+
10071136
private void UpdateDimensionAndGetRowsInfo(IDictionary<string, object> inputMaps, XmlDocument doc, XmlNodeList rows, bool changeRowIndex = true)
10081137
{
10091138
string[] refs;
@@ -1053,7 +1182,7 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary<string, object> inputMaps
10531182
c.SetAttribute("r", $"{StringHelper.GetLetters(r)}{{{{$rowindex}}}}");
10541183
}
10551184

1056-
var v = c.SelectSingleNode("x:v", _ns);
1185+
var v = c.SelectSingleNode("x:v", _ns) ?? c.SelectSingleNode("x:is/x:t", _ns);
10571186
if (v?.InnerText == null)
10581187
continue;
10591188

@@ -1176,19 +1305,19 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary<string, object> inputMaps
11761305

11771306
if (isMultiMatch)
11781307
{
1179-
c.SetAttribute("t", "str");
1308+
SetCellType(c, "str");
11801309
}
11811310
else if (TypeHelper.IsNumericType(type) && !type.IsEnum)
11821311
{
1183-
c.SetAttribute("t", "n");
1312+
SetCellType(c, "n");
11841313
}
11851314
else if (Type.GetTypeCode(type) == TypeCode.Boolean)
11861315
{
1187-
c.SetAttribute("t", "b");
1316+
SetCellType(c, "b");
11881317
}
11891318
else if (Type.GetTypeCode(type) == TypeCode.DateTime)
11901319
{
1191-
c.SetAttribute("t", "str");
1320+
SetCellType(c, "str");
11921321
}
11931322

11941323
break;
@@ -1228,36 +1357,36 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary<string, object> inputMaps
12281357

12291358
if (isMultiMatch)
12301359
{
1231-
c.SetAttribute("t", "str");
1360+
SetCellType(c, "str");
12321361
}
12331362
else if (TypeHelper.IsNumericType(type) && !type.IsEnum)
12341363
{
1235-
c.SetAttribute("t", "n");
1364+
SetCellType(c, "n");
12361365
}
12371366
else if (Type.GetTypeCode(type) == TypeCode.Boolean)
12381367
{
1239-
c.SetAttribute("t", "b");
1368+
SetCellType(c, "b");
12401369
}
12411370
else if (Type.GetTypeCode(type) == TypeCode.DateTime)
12421371
{
1243-
c.SetAttribute("t", "str");
1372+
SetCellType(c, "str");
12441373
}
12451374
}
12461375
else
12471376
{
12481377
var cellValueStr = cellValue?.ToString(); // value did encodexml, so don't duplicate encode value (https://gitee.com/dotnetchina/MiniExcel/issues/I4DQUN)
12491378
if (isMultiMatch || cellValue is string) // if matchs count over 1 need to set type=str (https://user-images.githubusercontent.com/12729184/114530109-39d46d00-9c7d-11eb-8f6b-52ad8600aca3.png)
12501379
{
1251-
c.SetAttribute("t", "str");
1380+
SetCellType(c, "str");
12521381
}
12531382
else if (decimal.TryParse(cellValueStr, out var outV))
12541383
{
1255-
c.SetAttribute("t", "n");
1384+
SetCellType(c, "n");
12561385
cellValueStr = outV.ToString(CultureInfo.InvariantCulture);
12571386
}
12581387
else if (cellValue is bool b)
12591388
{
1260-
c.SetAttribute("t", "b");
1389+
SetCellType(c, "b");
12611390
cellValueStr = b ? "1" : "0";
12621391
}
12631392
else if (cellValue is DateTime timestamp)
@@ -1266,6 +1395,13 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary<string, object> inputMaps
12661395
cellValueStr = timestamp.ToString("yyyy-MM-dd HH:mm:ss");
12671396
}
12681397

1398+
if (string.IsNullOrEmpty(cellValueStr) && string.IsNullOrEmpty(c.GetAttribute("t")))
1399+
{
1400+
SetCellType(c, "str");
1401+
}
1402+
1403+
// Re-acquire v after SetCellType may have changed DOM structure
1404+
v = c.SelectSingleNode("x:v", _ns) ?? c.SelectSingleNode("x:is/x:t", _ns);
12691405
v.InnerText = v.InnerText.Replace($"{{{{{propNames[0]}}}}}", cellValueStr); //TODO: auto check type and set value
12701406
}
12711407
}

0 commit comments

Comments
 (0)