@@ -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