Skip to content

Commit 7eb1196

Browse files
committed
fix: fix template export generating corrupted Excel files
- Change SharedString conversion to use inlineStr format with <is><t> structure - Add SetCellType method to properly handle cell types (inlineStr for strings, remove t attr for numbers) - Fix XML element order per ECMA-376 spec (autoFilter before mergeCells/phoneticPr) - Clean duplicate xmlns declarations from phoneticPr, conditionalFormatting, autoFilter - Remove empty <v></v> tags that cause invalid XML - Support <is><t> structure in value node lookup Fixes template SaveAs generating files that Excel cannot open
1 parent 5cfa15b commit 7eb1196

1 file changed

Lines changed: 117 additions & 21 deletions

File tree

src/MiniExcel.Core/OpenXml/Templates/OpenXmlTemplate.Impl.cs

Lines changed: 117 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,15 @@ private async Task WriteSheetXmlAsync(Stream outputFileStream, XmlDocument doc,
299299
phoneticPr.ParentNode.RemoveChild(phoneticPr);
300300
}
301301

302+
// Extract autoFilter - must be written before mergeCells and phoneticPr per ECMA-376
303+
var autoFilter = doc.SelectSingleNode("/x:worksheet/x:autoFilter", Ns);
304+
var autoFilterXml = string.Empty;
305+
if (autoFilter is not null)
306+
{
307+
autoFilterXml = autoFilter.OuterXml;
308+
autoFilter.ParentNode.RemoveChild(autoFilter);
309+
}
310+
302311
var contents = doc.InnerXml.Split(new[] { $"<{prefix}sheetData>{{{{{{{{{{{{split}}}}}}}}}}}}</{prefix}sheetData>" }, StringSplitOptions.None);
303312

304313
using var writer = new StreamWriter(outputFileStream, Encoding.UTF8);
@@ -524,6 +533,19 @@ await writer.WriteAsync($"</{prefix}sheetData>"
524533
#endif
525534
).ConfigureAwait(false);
526535

536+
// ECMA-376 element order: sheetData → autoFilter → mergeCells → phoneticPr → conditionalFormatting
537+
538+
// 1. autoFilter (must come before mergeCells)
539+
if (!string.IsNullOrEmpty(autoFilterXml))
540+
{
541+
await writer.WriteAsync(CleanXml(autoFilterXml, endPrefix)
542+
#if NET7_0_OR_GREATER
543+
.AsMemory(), cancellationToken
544+
#endif
545+
).ConfigureAwait(false);
546+
}
547+
548+
// 2. mergeCells
527549
if (_newXMergeCellInfos.Count != 0)
528550
{
529551
await writer.WriteAsync($"<{prefix}mergeCells count=\"{_newXMergeCellInfos.Count}\">"
@@ -546,18 +568,20 @@ await writer.WriteLineAsync($"</{prefix}mergeCells>"
546568
).ConfigureAwait(false);
547569
}
548570

571+
// 3. phoneticPr
549572
if (!string.IsNullOrEmpty(phoneticPrXml))
550573
{
551-
await writer.WriteAsync(phoneticPrXml
574+
await writer.WriteAsync(CleanXml(phoneticPrXml, endPrefix)
552575
#if NET7_0_OR_GREATER
553576
.AsMemory(), cancellationToken
554577
#endif
555578
).ConfigureAwait(false);
556579
}
557580

581+
// 4. conditionalFormatting
558582
if (newConditionalFormatRanges.Count != 0)
559583
{
560-
await writer.WriteAsync(string.Join(string.Empty, newConditionalFormatRanges.Select(cf => cf.Node.OuterXml))
584+
await writer.WriteAsync(CleanXml(string.Join(string.Empty, newConditionalFormatRanges.Select(cf => cf.Node.OuterXml)), endPrefix)
561585
#if NET7_0_OR_GREATER
562586
.AsMemory(), cancellationToken
563587
#endif
@@ -762,6 +786,10 @@ private async Task<GenerateCellValuesContext> GenerateCellValuesAsync(GenerateCe
762786

763787
substXmlRow = rowXml.ToString();
764788
substXmlRow = TemplateRegex.Replace(substXmlRow, MatchDelegate);
789+
790+
// Cleanup empty <v> tags which defaults to invalid XML
791+
substXmlRow = Regex.Replace(substXmlRow, @"<v>\s*</v>", "");
792+
substXmlRow = Regex.Replace(substXmlRow, @"<x:v>\s*</x:v>", "");
765793
}
766794

767795
rowXml.Clear();
@@ -794,9 +822,13 @@ private async Task<GenerateCellValuesContext> GenerateCellValuesAsync(GenerateCe
794822
var mergeBaseRowIndex = newRowIndex;
795823
newRowIndex += rowInfo.IEnumerableMercell?.Height ?? 1;
796824

825+
// Replace {{$rowindex}} in the already-built substXmlRow
826+
rowXml.Replace("{{$rowindex}}", mergeBaseRowIndex.ToString());
827+
797828
// replace formulas
798829
ProcessFormulas(rowXml, newRowIndex);
799-
await writer.WriteAsync(CleanXml(rowXml, endPrefix).ToString()
830+
var finalXml = CleanXml(rowXml, endPrefix).ToString();
831+
await writer.WriteAsync(finalXml
800832
#if NET7_0_OR_GREATER
801833
.AsMemory(), cancellationToken
802834
#endif
@@ -1040,7 +1072,8 @@ private void ProcessFormulas(StringBuilder rowXml, int rowIndex)
10401072
private static string CleanXml(string xml, string endPrefix) => CleanXml(new StringBuilder(xml), endPrefix).ToString();
10411073
private static StringBuilder CleanXml(StringBuilder xml, string endPrefix) => xml
10421074
.Replace("xmlns:x14ac=\"http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac\"", "")
1043-
.Replace($"xmlns{endPrefix}=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"", "");
1075+
.Replace($"xmlns{endPrefix}=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"", "")
1076+
.Replace("xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\"", "");
10441077

10451078
private static void ReplaceSharedStringsToStr(IDictionary<int, string> sharedStrings, XmlNodeList rows)
10461079
{
@@ -1061,10 +1094,67 @@ private static void ReplaceSharedStringsToStr(IDictionary<int, string> sharedStr
10611094
if (sharedStrings is null || !sharedStrings.TryGetValue(int.Parse(v.InnerText), out var shared))
10621095
continue;
10631096

1064-
// change type = str and replace its value
1065-
//TODO: remove sharedstring?
1066-
v.InnerText = shared;
1067-
c.SetAttribute("t", "str");
1097+
// change type = inlineStr and replace its value
1098+
c.RemoveChild(v);
1099+
var isNode = c.OwnerDocument.CreateElement("is", Schemas.SpreadsheetmlXmlns);
1100+
var tNode = c.OwnerDocument.CreateElement("t", Schemas.SpreadsheetmlXmlns);
1101+
tNode.InnerText = shared;
1102+
isNode.AppendChild(tNode);
1103+
c.AppendChild(isNode);
1104+
1105+
c.RemoveAttribute("t");
1106+
c.SetAttribute("t", "inlineStr");
1107+
}
1108+
}
1109+
}
1110+
1111+
private static void SetCellType(XmlElement c, string type)
1112+
{
1113+
if (type == "str") type = "inlineStr"; // Force inlineStr for strings
1114+
1115+
if (type == "inlineStr")
1116+
{
1117+
// Ensure <is><t>...</t></is>
1118+
c.SetAttribute("t", "inlineStr");
1119+
var v = c.SelectSingleNode("x:v", Ns);
1120+
if (v != null)
1121+
{
1122+
var text = v.InnerText;
1123+
c.RemoveChild(v);
1124+
var isNode = c.OwnerDocument.CreateElement("is", Schemas.SpreadsheetmlXmlns);
1125+
var tNode = c.OwnerDocument.CreateElement("t", Schemas.SpreadsheetmlXmlns);
1126+
tNode.InnerText = text;
1127+
isNode.AppendChild(tNode);
1128+
c.AppendChild(isNode);
1129+
}
1130+
else if (c.SelectSingleNode("x:is", Ns) == null)
1131+
{
1132+
// Create empty <is><t></t></is> if neither <v> nor <is> exists
1133+
var isNode = c.OwnerDocument.CreateElement("is", Schemas.SpreadsheetmlXmlns);
1134+
var tNode = c.OwnerDocument.CreateElement("t", Schemas.SpreadsheetmlXmlns);
1135+
isNode.AppendChild(tNode);
1136+
c.AppendChild(isNode);
1137+
}
1138+
}
1139+
else
1140+
{
1141+
// Ensure <v>...</v>
1142+
// For numbers/booleans, we remove 't' attribute to let it be default (number)
1143+
// or we could set it to 'n' explicitly, but removing is safer for general number types
1144+
if (type == "b")
1145+
c.SetAttribute("t", "b");
1146+
else
1147+
c.RemoveAttribute("t");
1148+
1149+
var isNode = c.SelectSingleNode("x:is", Ns);
1150+
if (isNode != null)
1151+
{
1152+
var tNode = isNode.SelectSingleNode("x:t", Ns);
1153+
var text = tNode?.InnerText;
1154+
c.RemoveChild(isNode);
1155+
var v = c.OwnerDocument.CreateElement("v", Schemas.SpreadsheetmlXmlns);
1156+
v.InnerText = text;
1157+
c.AppendChild(v);
10681158
}
10691159
}
10701160
}
@@ -1117,7 +1207,7 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary<string, object?> inputMap
11171207
c.SetAttribute("r", $"{StringHelper.GetLetters(r)}{{{{$rowindex}}}}");
11181208
}
11191209

1120-
var v = c.SelectSingleNode("x:v", Ns);
1210+
var v = c.SelectSingleNode("x:v", Ns) ?? c.SelectSingleNode("x:is/x:t", Ns);
11211211
if (v?.InnerText is null)
11221212
continue;
11231213

@@ -1240,19 +1330,19 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary<string, object?> inputMap
12401330

12411331
if (isMultiMatch)
12421332
{
1243-
c.SetAttribute("t", "str");
1333+
SetCellType(c, "str");
12441334
}
12451335
else if (TypeHelper.IsNumericType(type) && !type.IsEnum)
12461336
{
1247-
c.SetAttribute("t", "n");
1337+
SetCellType(c, "n");
12481338
}
12491339
else if (Type.GetTypeCode(type) == TypeCode.Boolean)
12501340
{
1251-
c.SetAttribute("t", "b");
1341+
SetCellType(c, "b");
12521342
}
12531343
else if (Type.GetTypeCode(type) == TypeCode.DateTime)
12541344
{
1255-
c.SetAttribute("t", "str");
1345+
SetCellType(c, "str");
12561346
}
12571347

12581348
break;
@@ -1292,36 +1382,36 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary<string, object?> inputMap
12921382

12931383
if (isMultiMatch)
12941384
{
1295-
c.SetAttribute("t", "str");
1385+
SetCellType(c, "str");
12961386
}
12971387
else if (TypeHelper.IsNumericType(type) && !type.IsEnum)
12981388
{
1299-
c.SetAttribute("t", "n");
1389+
SetCellType(c, "n");
13001390
}
13011391
else if (Type.GetTypeCode(type) == TypeCode.Boolean)
13021392
{
1303-
c.SetAttribute("t", "b");
1393+
SetCellType(c, "b");
13041394
}
13051395
else if (Type.GetTypeCode(type) == TypeCode.DateTime)
13061396
{
1307-
c.SetAttribute("t", "str");
1397+
SetCellType(c, "str");
13081398
}
13091399
}
13101400
else
13111401
{
13121402
var cellValueStr = cellValue?.ToString(); // value did encodexml, so don't duplicate encode value (https://gitee.com/dotnetchina/MiniExcel/issues/I4DQUN)
13131403
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)
13141404
{
1315-
c.SetAttribute("t", "str");
1405+
SetCellType(c, "str");
13161406
}
13171407
else if (decimal.TryParse(cellValueStr, out var outV))
13181408
{
1319-
c.SetAttribute("t", "n");
1409+
SetCellType(c, "n");
13201410
cellValueStr = outV.ToString(CultureInfo.InvariantCulture);
13211411
}
13221412
else if (cellValue is bool b)
13231413
{
1324-
c.SetAttribute("t", "b");
1414+
SetCellType(c, "b");
13251415
cellValueStr = b ? "1" : "0";
13261416
}
13271417
else if (cellValue is DateTime timestamp)
@@ -1330,6 +1420,12 @@ private void UpdateDimensionAndGetRowsInfo(IDictionary<string, object?> inputMap
13301420
cellValueStr = timestamp.ToString("yyyy-MM-dd HH:mm:ss");
13311421
}
13321422

1423+
if (string.IsNullOrEmpty(cellValueStr) && string.IsNullOrEmpty(c.GetAttribute("t")))
1424+
{
1425+
SetCellType(c, "str");
1426+
v = c.SelectSingleNode("x:v", Ns) ?? c.SelectSingleNode("x:is/x:t", Ns);
1427+
}
1428+
13331429
v.InnerText = v.InnerText.Replace($"{{{{{propNames[0]}}}}}", cellValueStr); //TODO: auto check type and set value
13341430
}
13351431
}
@@ -1396,4 +1492,4 @@ private static bool EvaluateStatement(object tagValue, string comparisonOperator
13961492
_ => false
13971493
};
13981494
}
1399-
}
1495+
}

0 commit comments

Comments
 (0)