diff --git a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs index d4c0f917..50bdae52 100644 --- a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs +++ b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs @@ -54,6 +54,7 @@ private async Task GenerateSheetByUpdateModeAsync(ZipArchiveEntry sheetZipEntry, InjectSharedStrings(sharedStrings, rows); GetMergeCells(worksheet); UpdateDimensionAndGetRowsInfo(inputMaps, worksheet, rows, !mergeCells); + PreserveInlineStringSpaces(rows); #if NET var writer = XmlWriter.Create(stream, DocXmlWriterSettings); @@ -90,6 +91,7 @@ private async Task GenerateSheetByCreateModeAsync(ZipArchiveEntry templateSheetZ InjectSharedStrings(sharedStrings, rows); GetMergeCells(worksheet); UpdateDimensionAndGetRowsInfo(inputMaps, worksheet, rows, !mergeCells); + PreserveInlineStringSpaces(rows); #if NET var writer = XmlWriter.Create(outputZipSheetEntryStream, DocXmlWriterSettings); @@ -1068,6 +1070,23 @@ private static void InjectSharedStrings(IDictionary sharedStrings, } } + // Excel drops leading and trailing blanks from a without xml:space="preserve" (https://github.com/mini-software/MiniExcel/issues/295). + // Runs after the single values are filled in; a still holding a placeholder gets its value later, so it is kept as well. + private static void PreserveInlineStringSpaces(IEnumerable rows) + { + foreach (var row in rows) + { + foreach (var cell in row.Elements(SpreadsheetNs + "c")) + { + if (cell.Element(SpreadsheetNs + "is")?.Element(SpreadsheetNs + "t") is not { Value: { Length: > 0 } text } t) + continue; + + if (char.IsWhiteSpace(text[0]) || char.IsWhiteSpace(text[text.Length - 1]) || text.Contains("{{")) + t.SetAttributeValue(XNamespace.Xml + "space", "preserve"); + } + } + } + private static void SetCellType(XElement cell, string type) { // Force inlineStr for strings diff --git a/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGiteeIssuesTests.cs b/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGiteeIssuesTests.cs index 2255c77c..bea5ca66 100644 --- a/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGiteeIssuesTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGiteeIssuesTests.cs @@ -150,7 +150,7 @@ public void TestIssueI4DQUN() var sheetXml = SheetHelper.GetZipFileContent(path.ToString(), "xl/worksheets/sheet1.xml"); // Template now uses inlineStr format (...) instead of SharedStrings (...) Assert.Contains("Hello & World < , > , \" , '", sheetXml); - Assert.Contains("Hello & Value < , > , \" , '", sheetXml); + Assert.Contains("Hello & Value < , > , \" , '", sheetXml); } [Fact] diff --git a/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesTests.cs b/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesTests.cs index 862e821d..d6b3ec1c 100644 --- a/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/Issues/MiniExcelGithubIssuesTests.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using System.Xml.Linq; using MiniExcelLib.Core.Enums; using MiniExcelLib.Core.Exceptions; using MiniExcelLib.Core.Helpers; @@ -1537,6 +1538,68 @@ public void TestIssue294() } } + /// Prefix and suffix blank space are hidden by Excel after FillTemplate - https://github.com/mini-software/MiniExcel/issues/295 + [Fact] + public void TestIssue295() + { + static XElement GetInlineText(string sheetXml, string cellReference) + { + XNamespace ns = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"; + var cell = XDocument.Parse(sheetXml) + .Descendants(ns + "c") + .Single(c => c.Attribute("r")?.Value == cellReference); + + return cell.Element(ns + "is")!.Element(ns + "t")!; + } + + { + using var path = AutoDeletingPath.Create(); + var templatePath = PathHelper.GetFile("xlsx/TestTemplateEasyFill.xlsx"); + var value = new + { + Name = " Jack", + CreateDate = new DateTime(2021, 01, 01), + VIP = true, + Points = 123 + }; + _excelTemplater.FillTemplate(path.ToString(), templatePath, value); + + var sheetXml = SheetHelper.GetZipFileContent(path.ToString(), "xl/worksheets/sheet1.xml"); + var a2 = GetInlineText(sheetXml, "A2"); + Assert.Equal(" Jack", a2.Value); + Assert.Equal("preserve", a2.Attribute(XNamespace.Xml + "space")?.Value); + + var e2 = GetInlineText(sheetXml, "E2"); + Assert.Equal(" Jack has 123 points", e2.Value); + Assert.Equal("preserve", e2.Attribute(XNamespace.Xml + "space")?.Value); + } + { + using var path = AutoDeletingPath.Create(); + var templatePath = PathHelper.GetFile("xlsx/TestTemplateBasicIEmumerableFill.xlsx"); + var value = new + { + employees = new[] + { + new { name = " Jack", department = "HR " }, + new { name = "Lisa", department = " IT " } + } + }; + _excelTemplater.FillTemplate(path.ToString(), templatePath, value); + + var sheetXml = SheetHelper.GetZipFileContent(path.ToString(), "xl/worksheets/sheet1.xml"); + foreach (var (cellReference, expected) in new[] { ("A2", " Jack"), ("B2", "HR "), ("A3", "Lisa"), ("B3", " IT ") }) + { + var t = GetInlineText(sheetXml, cellReference); + Assert.Equal(expected, t.Value); + Assert.Equal("preserve", t.Attribute(XNamespace.Xml + "space")?.Value); + } + + var rows = _excelImporter.Query(path.ToString()).ToList(); + Assert.Equal(" Jack", rows[1].A); + Assert.Equal(" IT ", rows[2].B); + } + } + // SaveAs support Image type - https://github.com/mini-software/MiniExcel/issues/304 [Fact] public void TestIssue304()