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