Skip to content

Commit a5d84ca

Browse files
Add conditional formatting support to Excel template (#745)
- Introduced `ConditionalFormats` property in `XRowInfo` to store XML elements for conditional formatting. - Added `_cellRegex` for matching cell references and implemented `ParseConditionalFormatRanges` to extract conditional formatting ranges. - Defined `Range` struct and `ConditionalFormatRange` class for managing cell ranges and their associated nodes. - Updated `WriteSheetXml` to handle conditional formats, ensuring correct parsing, storage, and writing of these formats in the XML document.
1 parent 0f8be6e commit a5d84ca

File tree

1 file changed

+123
-1
lines changed

1 file changed

+123
-1
lines changed

src/MiniExcel/SaveByTemplate/ExcelOpenXmlTemplate.Impl.cs

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class XRowInfo
3030
public IEnumerable CellIEnumerableValues { get; set; }
3131
public XMergeCell IEnumerableMercell { get; set; }
3232
public List<XMergeCell> RowMercells { get; set; }
33+
public List<XmlElement> ConditionalFormats { get; set; }
3334
}
3435

3536
public class PropInfo
@@ -153,6 +154,64 @@ private void GetMercells(XmlDocument doc, XmlNode worksheet)
153154
}
154155
}
155156

157+
private static readonly Regex _cellRegex = new Regex("([A-Z]+)([0-9]+)");
158+
private static IEnumerable<ConditionalFormatRange> ParseConditionalFormatRanges(XmlDocument doc)
159+
{
160+
var conditionalFormatting = doc.SelectNodes("/x:worksheet/x:conditionalFormatting", _ns);
161+
162+
for(var i = 0; i < conditionalFormatting.Count; ++i)
163+
{
164+
var conditionalFormat = conditionalFormatting[i];
165+
var rangeValue = conditionalFormat.Attributes["sqref"]?.Value;
166+
var rangeValues = rangeValue?.Split(' ');
167+
var rangeList = new List<Range>();
168+
foreach (var rangeVal in rangeValues)
169+
{
170+
var rangeValSplit = rangeVal.Split(':');
171+
if(rangeValSplit.Length != 0)
172+
{
173+
if(rangeValSplit.Length == 1)
174+
{
175+
var match = _cellRegex.Match(rangeValSplit[0]);
176+
if(match.Success)
177+
{
178+
var row = int.Parse(match.Groups[2].Value);
179+
var column = ColumnHelper.GetColumnIndex(match.Groups[1].Value);
180+
rangeList.Add(new Range
181+
{
182+
StartColumn = column,
183+
StartRow = row,
184+
EndColumn = column,
185+
EndRow = row
186+
});
187+
}
188+
}
189+
else
190+
{
191+
var match1 = _cellRegex.Match(rangeValSplit[0]);
192+
var match2 = _cellRegex.Match(rangeValSplit[1]);
193+
if (match1.Success && match2.Success)
194+
{
195+
rangeList.Add(new Range
196+
{
197+
StartColumn = ColumnHelper.GetColumnIndex(match1.Groups[1].Value),
198+
StartRow = int.Parse(match1.Groups[2].Value),
199+
EndColumn = ColumnHelper.GetColumnIndex(match2.Groups[1].Value),
200+
EndRow = int.Parse(match2.Groups[2].Value)
201+
});
202+
}
203+
}
204+
}
205+
}
206+
207+
yield return new ConditionalFormatRange
208+
{
209+
Node = conditionalFormat,
210+
Ranges = rangeList
211+
};
212+
}
213+
}
214+
156215
private class MergeCellIndex
157216
{
158217
public int RowStart { get; set; }
@@ -172,14 +231,44 @@ private class XChildNode
172231
public int RowIndex { get; set; }
173232
}
174233

234+
private struct Range
235+
{
236+
public int StartColumn { get; set; }
237+
public int StartRow { get; set; }
238+
public int EndColumn { get; set; }
239+
public int EndRow { get; set; }
240+
241+
public bool ContainsRow(int row)
242+
{
243+
return row >= StartRow && row <= EndRow;
244+
}
245+
}
246+
247+
private class ConditionalFormatRange
248+
{
249+
public XmlNode Node { get; set; }
250+
public List<Range> Ranges { get; set; }
251+
}
252+
175253
private void WriteSheetXml(Stream stream, XmlDocument doc, XmlNode sheetData, bool mergeCells = false)
176254
{
255+
var conditionalFormatRanges = ParseConditionalFormatRanges(doc).ToList();
256+
var newConditionalFormatRanges = new List<ConditionalFormatRange>();
257+
newConditionalFormatRanges.AddRange(conditionalFormatRanges);
258+
177259
//Q.Why so complex?
178260
//A.Because try to use string stream avoid OOM when rendering rows
179261
sheetData.RemoveAll();
180262
sheetData.InnerText = "{{{{{{split}}}}}}"; //TODO: bad code smell
181263
var prefix = string.IsNullOrEmpty(sheetData.Prefix) ? "" : $"{sheetData.Prefix}:";
182264
var endPrefix = string.IsNullOrEmpty(sheetData.Prefix) ? "" : $":{sheetData.Prefix}"; //![image](https://user-images.githubusercontent.com/12729184/115000066-fd02b300-9ed4-11eb-8e65-bf0014015134.png)
265+
266+
var conditionalFormatNodes = doc.SelectNodes("/x:worksheet/x:conditionalFormatting", _ns);
267+
for (var i = 0; i < conditionalFormatNodes.Count; ++i)
268+
{
269+
var node = conditionalFormatNodes.Item(i);
270+
node.ParentNode.RemoveChild(node);
271+
}
183272
var contents = doc.InnerXml.Split(new string[] { $"<{prefix}sheetData>{{{{{{{{{{{{split}}}}}}}}}}}}</{prefix}sheetData>" }, StringSplitOptions.None);
184273
using (var writer = new StreamWriter(stream, Encoding.UTF8))
185274
{
@@ -743,6 +832,35 @@ private void WriteSheetXml(Stream stream, XmlDocument doc, XmlNode sheetData, bo
743832
}
744833

745834
enumrowend = newRowIndex-1;
835+
836+
var conditionalFormats = conditionalFormatRanges.Where(cfr => cfr.Ranges.Any(r => r.ContainsRow(originRowIndex)));
837+
foreach (var conditionalFormat in conditionalFormats) {
838+
var newConditionalFormat = conditionalFormat.Node.Clone();
839+
var sqref = newConditionalFormat.Attributes["sqref"];
840+
var ranges = conditionalFormat.Ranges.Select(r =>
841+
{
842+
if (r.ContainsRow(originRowIndex))
843+
{
844+
return new Range()
845+
{
846+
StartColumn = r.StartColumn,
847+
StartRow = enumrowstart + 1,
848+
EndColumn = r.EndColumn,
849+
EndRow = enumrowend + 1
850+
};
851+
}
852+
else
853+
{
854+
return r;
855+
}
856+
}).ToList();
857+
sqref.Value = string.Join(" ", ranges.Select(r => $"{ColumnHelper.GetAlphabetColumnName(r.StartColumn)}{r.StartRow}:{ColumnHelper.GetAlphabetColumnName(r.EndColumn)}{r.EndRow}"));
858+
newConditionalFormatRanges.Remove(conditionalFormat);
859+
newConditionalFormatRanges.Add(new ConditionalFormatRange {
860+
Node = newConditionalFormat,
861+
Ranges = ranges
862+
});
863+
}
746864
}
747865
else
748866
{
@@ -790,6 +908,11 @@ private void WriteSheetXml(Stream stream, XmlDocument doc, XmlNode sheetData, bo
790908
writer.Write($"</{prefix}mergeCells>");
791909
}
792910

911+
if(newConditionalFormatRanges.Count != 0)
912+
{
913+
writer.Write(string.Join(string.Empty, newConditionalFormatRanges.Select(cf => cf.Node.OuterXml)));
914+
}
915+
793916
writer.Write(contents[1]);
794917
}
795918
}
@@ -913,7 +1036,6 @@ private void ReplaceSharedStringsToStr(IDictionary<int, string> sharedStrings, r
9131036
private void UpdateDimensionAndGetRowsInfo(IDictionary<string, object> inputMaps, ref XmlDocument doc, ref XmlNodeList rows, bool changeRowIndex = true)
9141037
{
9151038
// note : dimension need to put on the top ![image](https://user-images.githubusercontent.com/12729184/114507911-5dd88400-9c66-11eb-94c6-82ed7bdb5aab.png)
916-
9171039
var dimension = doc.SelectSingleNode("/x:worksheet/x:dimension", _ns) as XmlElement;
9181040
if (dimension == null)
9191041
throw new NotImplementedException("Excel Dimension Xml is null, please issue file for me. https://github.com/shps951023/MiniExcel/issues");

0 commit comments

Comments
 (0)