From c147f0e0d1171a87a741d0ecd981e82ff95c2b18 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:09:37 +0000 Subject: [PATCH 1/8] Bump System.Configuration.ConfigurationManager from 10.0.1 to 10.0.2 --- updated-dependencies: - dependency-name: System.Configuration.ConfigurationManager dependency-version: 10.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../MarkupSimplifierApp/MarkupSimplifierApp.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenXmlPowerToolsExamples/MarkupSimplifierApp/MarkupSimplifierApp.csproj b/OpenXmlPowerToolsExamples/MarkupSimplifierApp/MarkupSimplifierApp.csproj index c9e62b42..df5c1ef1 100644 --- a/OpenXmlPowerToolsExamples/MarkupSimplifierApp/MarkupSimplifierApp.csproj +++ b/OpenXmlPowerToolsExamples/MarkupSimplifierApp/MarkupSimplifierApp.csproj @@ -9,7 +9,7 @@ - + From 8e78ceb4a0be0de4033a3db1e21b0648fb239254 Mon Sep 17 00:00:00 2001 From: Lowell Stewart Date: Wed, 21 Jan 2026 15:42:39 -0700 Subject: [PATCH 2/8] DocumentBuilder fix: avoid spurious error and crash on It seems DocumentBuilder was originally written to handle , which is non-standard AFAIK. I've left that in there, but now handle the proper way to designate numId="0" as well. --- .../DocumentBuilderTests.cs | 50 +++++ .../DocumentBuilder/DocumentBuilder.cs | 192 +++++++++--------- TestFiles/DB012a-No-Numbering0.docx | Bin 0 -> 14569 bytes TestFiles/DB012a-No-Numbering1.docx | Bin 0 -> 12642 bytes 4 files changed, 148 insertions(+), 94 deletions(-) create mode 100644 TestFiles/DB012a-No-Numbering0.docx create mode 100644 TestFiles/DB012a-No-Numbering1.docx diff --git a/OpenXmlPowerTools.Tests/DocumentBuilderTests.cs b/OpenXmlPowerTools.Tests/DocumentBuilderTests.cs index 55b69ca9..13fc63b9 100644 --- a/OpenXmlPowerTools.Tests/DocumentBuilderTests.cs +++ b/OpenXmlPowerTools.Tests/DocumentBuilderTests.cs @@ -472,6 +472,56 @@ public void DB012_NumberingsWithSameAbstractNumbering() Assert.Equal(3, numberingRoot.Elements(W.num).Count()); } + [Fact] + public void DB012a_NumberingWithZeroIdIsValid() + { + // This document has a numbering definition with a zero id (explicitly indicating "no numbering"). + var name = "DB012a-No-Numbering0.docx"; + var sourceDir = new DirectoryInfo("../../../../TestFiles/"); + var sourceDocx = new FileInfo(Path.Combine(sourceDir.FullName, name)); + var sources = new List() + { + new Source(new WmlDocument(sourceDocx.FullName)), + }; + var processedDestDocx = new FileInfo(Path.Combine(TestUtil.TempDir.FullName, + sourceDocx.Name.Replace(".docx", "-processed-by-DocumentBuilder.docx"))); + DocumentBuilder.BuildDocument(sources, processedDestDocx.FullName); + Validate(processedDestDocx); + } + + [Fact] + public void DB012b_NumberingWithZeroIdWorks() + { + var sourceDir = new DirectoryInfo("../../../../TestFiles/"); + var source0 = new FileInfo(Path.Combine(sourceDir.FullName, "DB012a-No-Numbering0.docx")); + var source1 = new FileInfo(Path.Combine(sourceDir.FullName, "DB012a-No-Numbering1.docx")); + var doc1 = new WmlDocument(source0.FullName); + using (var mem = new MemoryStream()) + { + mem.Write(doc1.DocumentByteArray, 0, doc1.DocumentByteArray.Length); + using (var doc = WordprocessingDocument.Open(mem, true)) + { + var xDoc = doc.MainDocumentPart.GetXDocument(); + var frontMatterPara = xDoc.Root.Elements(W.body).Elements(W.p).FirstOrDefault(); + frontMatterPara.ReplaceWith( + new XElement(PtOpenXml.Insert, + new XAttribute("Id", "Front"))); + doc.MainDocumentPart.PutXDocument(); + } + doc1.DocumentByteArray = mem.ToArray(); + } + + var sources = new List() + { + new Source(doc1, true), + new Source(new WmlDocument(source1.FullName), "Front"), + }; + var processedDestDocx = + new FileInfo(Path.Combine(TestUtil.TempDir.FullName, "DB012b-NumberingWithZeroIdWorks.docx")); + DocumentBuilder.BuildDocument(sources, processedDestDocx.FullName); + Validate(processedDestDocx); + } + [Fact] public void DB013a_LocalizedStyleIds_Heading() { diff --git a/OpenXmlPowerTools/DocumentBuilder/DocumentBuilder.cs b/OpenXmlPowerTools/DocumentBuilder/DocumentBuilder.cs index b2bbd452..1260a5df 100644 --- a/OpenXmlPowerTools/DocumentBuilder/DocumentBuilder.cs +++ b/OpenXmlPowerTools/DocumentBuilder/DocumentBuilder.cs @@ -1,4 +1,4 @@ -#define TestForUnsupportedDocuments +#define TestForUnsupportedDocuments #define MergeStylesWithSameNames using Codeuctivity.OpenXmlPowerTools.Exceptions; @@ -1411,9 +1411,11 @@ private static void TestForUnsupportedDocument(WordprocessingDocument doc, int s .Descendants(W.numPr) .Where(n => { - var zeroId = (int?)n.Attribute(W.id) == 0; + var zeroId = (int?)n.Attribute(W.id) == 0; // nonstandard but handle defensively + var numIdElement = n.Element(W.numId); // standard OpenXML has numId as child element + var hasZeroNumId = numIdElement != null && (int?)numIdElement.Attribute(W.val) == 0; var hasChildInsId = n.Elements(W.ins).Any(); - if (zeroId || hasChildInsId) + if (hasZeroNumId || zeroId || hasChildInsId) { return false; } @@ -2747,6 +2749,12 @@ private static void CopyNumbering(WordprocessingDocument sourceDocument, Wordpro var idElement = numReference.Descendants(W.numId).FirstOrDefault(); if (idElement != null) { + var numId = (int)idElement.Attribute(W.val); + if (numId == 0) // indicates "no numbering" + { + continue; // skip processing + } + if (oldNumbering == null) { oldNumbering = sourceDocument.MainDocumentPart.NumberingDefinitionsPart.GetXDocument(); @@ -2784,111 +2792,107 @@ private static void CopyNumbering(WordprocessingDocument sourceDocument, Wordpro newNumbering.Add(new XElement(W.numbering, NamespaceAttributes)); } } - var numId = (int)idElement.Attribute(W.val); - if (numId != 0) + var element = oldNumbering + .Descendants(W.num) + .FirstOrDefault(p => (int)p.Attribute(W.numId) == numId); + if (element == null) { - var element = oldNumbering - .Descendants(W.num) -.FirstOrDefault(p => (int)p.Attribute(W.numId) == numId); - if (element == null) - { - continue; - } + continue; + } - // Copy abstract numbering element, if necessary (use matching NSID) - var abstractNumIdStr = (string)element - .Elements(W.abstractNumId) - .First() + // Copy abstract numbering element, if necessary (use matching NSID) + var abstractNumIdStr = (string)element + .Elements(W.abstractNumId) + .First() + .Attribute(W.val); + if (!int.TryParse(abstractNumIdStr, out var abstractNumId)) + { + throw new DocumentBuilderException("Invalid document - invalid value for abstractNumId"); + } + + var abstractElement = oldNumbering + .Descendants() + .Elements(W.abstractNum) + .First(p => (int)p.Attribute(W.abstractNumId) == abstractNumId); + var nsidElement = abstractElement + .Element(W.nsid); + string? abstractNSID = null; + if (nsidElement != null) + { + abstractNSID = (string)nsidElement .Attribute(W.val); - if (!int.TryParse(abstractNumIdStr, out var abstractNumId)) - { - throw new DocumentBuilderException("Invalid document - invalid value for abstractNumId"); - } + } - var abstractElement = oldNumbering - .Descendants() - .Elements(W.abstractNum) -.First(p => (int)p.Attribute(W.abstractNumId) == abstractNumId); - var nsidElement = abstractElement - .Element(W.nsid); - string? abstractNSID = null; - if (nsidElement != null) + var newAbstractElement = newNumbering + .Descendants() + .Elements(W.abstractNum) + .Where(e => e.Annotation() == null) + .FirstOrDefault(p => { - abstractNSID = (string)nsidElement - .Attribute(W.val); - } - - var newAbstractElement = newNumbering - .Descendants() - .Elements(W.abstractNum) - .Where(e => e.Annotation() == null) -.FirstOrDefault(p => + var thisNsidElement = p.Element(W.nsid); + if (thisNsidElement == null) { - var thisNsidElement = p.Element(W.nsid); - if (thisNsidElement == null) - { - return false; - } - - return (string)thisNsidElement.Attribute(W.val) == abstractNSID; - }); - if (newAbstractElement == null) - { - newAbstractElement = new XElement(abstractElement); - newAbstractElement.Attribute(W.abstractNumId).Value = abstractNumber.ToString(); - abstractNumber++; - if (newNumbering.Root.Elements(W.abstractNum).Any()) - { - newNumbering.Root.Elements(W.abstractNum).Last().AddAfterSelf(newAbstractElement); - } - else - { - newNumbering.Root.Add(newAbstractElement); + return false; } - foreach (var pictId in newAbstractElement.Descendants(W.lvlPicBulletId)) - { - var bulletId = (string)pictId.Attribute(W.val); - var numPicBullet = oldNumbering - .Descendants(W.numPicBullet) - .FirstOrDefault(d => (string)d.Attribute(W.numPicBulletId) == bulletId); - var maxNumPicBulletId = new int[] { -1 }.Concat( - newNumbering.Descendants(W.numPicBullet) - .Attributes(W.numPicBulletId) - .Select(a => (int)a)) - .Max() + 1; - var newNumPicBullet = new XElement(numPicBullet); - newNumPicBullet.Attribute(W.numPicBulletId).Value = maxNumPicBulletId.ToString(); - pictId.Attribute(W.val).Value = maxNumPicBulletId.ToString(); - newNumbering.Root.AddFirst(newNumPicBullet); - } - } - var newAbstractId = newAbstractElement.Attribute(W.abstractNumId).Value; - - // Copy numbering element, if necessary (use matching element with no overrides) - XElement newElement; - if (numIdMap.ContainsKey(numId)) + return (string)thisNsidElement.Attribute(W.val) == abstractNSID; + }); + if (newAbstractElement == null) + { + newAbstractElement = new XElement(abstractElement); + newAbstractElement.Attribute(W.abstractNumId).Value = abstractNumber.ToString(); + abstractNumber++; + if (newNumbering.Root.Elements(W.abstractNum).Any()) { - newElement = newNumbering - .Descendants() - .Elements(W.num) - .Where(e => e.Annotation() == null) -.First(p => (int)p.Attribute(W.numId) == numIdMap[numId]); + newNumbering.Root.Elements(W.abstractNum).Last().AddAfterSelf(newAbstractElement); } else { - newElement = new XElement(element); - newElement - .Elements(W.abstractNumId) - .First() - .Attribute(W.val).Value = newAbstractId; - newElement.Attribute(W.numId).Value = number.ToString(); - numIdMap.Add(numId, number); - number++; - newNumbering.Root.Add(newElement); + newNumbering.Root.Add(newAbstractElement); } - idElement.Attribute(W.val).Value = newElement.Attribute(W.numId).Value; + + foreach (var pictId in newAbstractElement.Descendants(W.lvlPicBulletId)) + { + var bulletId = (string)pictId.Attribute(W.val); + var numPicBullet = oldNumbering + .Descendants(W.numPicBullet) + .FirstOrDefault(d => (string)d.Attribute(W.numPicBulletId) == bulletId); + var maxNumPicBulletId = new int[] { -1 }.Concat( + newNumbering.Descendants(W.numPicBullet) + .Attributes(W.numPicBulletId) + .Select(a => (int)a)) + .Max() + 1; + var newNumPicBullet = new XElement(numPicBullet); + newNumPicBullet.Attribute(W.numPicBulletId).Value = maxNumPicBulletId.ToString(); + pictId.Attribute(W.val).Value = maxNumPicBulletId.ToString(); + newNumbering.Root.AddFirst(newNumPicBullet); + } + } + var newAbstractId = newAbstractElement.Attribute(W.abstractNumId).Value; + + // Copy numbering element, if necessary (use matching element with no overrides) + XElement newElement; + if (numIdMap.ContainsKey(numId)) + { + newElement = newNumbering + .Descendants() + .Elements(W.num) + .Where(e => e.Annotation() == null) + .First(p => (int)p.Attribute(W.numId) == numIdMap[numId]); + } + else + { + newElement = new XElement(element); + newElement + .Elements(W.abstractNumId) + .First() + .Attribute(W.val).Value = newAbstractId; + newElement.Attribute(W.numId).Value = number.ToString(); + numIdMap.Add(numId, number); + number++; + newNumbering.Root.Add(newElement); } + idElement.Attribute(W.val).Value = newElement.Attribute(W.numId).Value; } } if (newNumbering != null) diff --git a/TestFiles/DB012a-No-Numbering0.docx b/TestFiles/DB012a-No-Numbering0.docx new file mode 100644 index 0000000000000000000000000000000000000000..f44d133ae1057ae8456de526b709e638eb026e0e GIT binary patch literal 14569 zcmeIZWmsKF(=NPmHtxYCxJz)i;1Jv)xVt+95AN;`f#AWN;O-hUKyY{Y_RKu@e0%>3|sUtp}f5bdJdTU(ksu-RKE$qwzlljX@4iW9{lMo9~+z?EJtv}6iY|KVV~ zi}R4uetE2crm&!GW=dkH6>7!X+>R^PKxiOrf%oQ%;3aZ~coTrsV4qjL41)ptgA;<| zanK{0<9fr>GePp$YxP6@LB0G9bPR)1X7_IsC)|<%0*!NZjwD?i^lz5rP>PwAR?owQ z4UQ6$8dtqR=V!sV*tX78v>}Kt%^bM0@?2!-sHZB}`M^h;SnLW1}o_(78rnY9Es!p)bLJ= z{BdjPpGzlhw+oB@sCJM%zPvyHS`uY~?D7R#TeULMy4l28P}lYpmy$u@_r$@ElC;l zj^rixJfFAVQs*EsU-DKK)42G*f=h9>}_buXeau28qw?T#qj2@smEsw$!hbn>{aG(QheUz zq%zOWSx9Jk!j$p7Z4V}B9>ej0GZCmxenTG<-Tkdbr&oDDzL6!;g8GUM*NTaeJR{bi z91i!ib#mV$zoYo;#~?ajF7PAcgO`amn&$Iv*ba^W`w59g-TMt&ybK+oi+`055}Wr} zV<0WoiURTq*1?F;$kx!=8l=vC(_@Dk8n(-vC_eA?UIKFZ!6T8h+9wJQXwAb+ zI3%W4liMfItSw1n!6muueV@i+63TncEqE3ozTOLA45mgqIzo1M`evJsg~6pdxEw-@ zw;#x3U62`v8{S-u{#edBka(*u%rdrD-BL8=?R$Rl?&KI67(JZ9@*MyfAJFoe-z8ZJ z6Jk96EN(}X@Vhx(L9kg*&d&NmO_*{8Q(=ZO}S%BH9=p`j^*J^>$}ZHfA4* zT{|QEtQmFDa`&E~Nf+?Q&hK??@u+wKu&f9H)+KY_Jv7$<= z!K-DQ>yPwwOBPuZRI*f@T;En1mU(}wla0V*_iG8Y z^-eYwJKsfx@P)B`Ejn5q4vRcqPBuv5&jCYZloo6sKoI^83dIJ@$)I`5SV8}iF8Gyg z4W9iJ!E{+Sv#wBvli`A1Kklu13bdf&F%;?-8ZJA*Yv?2y<(d`b&*!lrt)j8yvCdQ2 zy?0STIl@*PD6)RWyq^zn45pqUux9-ZS~q$O&T2&Y&|B%)&3+6|jB$`ZAf*@cRatd& zgvWTLGR;dX3X+d+b=~>f3z^_ZK>j!ogyvp?xD6ES6Y7}qY>PTGe7}R!PF5H|KFRli z!mcl#Eegu24h9N!8{dHn_ufy3pBMmrZy?P*${5w06pj+C<@^I323BvWj%;60=4 zUPeM{cP0>Wv!w7H82zmm4WxXjGnm7$zsX{~|H0x_jNrORX2u)IAnq+?pB(@83p>PY zj|cM@YH1Yk&@gMhW}Ob6^Og+}zUJ$Od_ouT{Pz};WRbJvLuV;k({!IZy0K<F zH?`9b&xkrdxim~@T6LisiP?muy|mAo2r5&5B67*>zQ^0hN0tcq#*Yx6zV)@nThqd6 z$V4;tUY6Ihn8tbvY1(m61VAa9`m(T*kwxMxDQR8z-}4}z0OFe47-3X(-q)0(n<@Lwx1|~Tn_0bOL5G^RB(npCx84Vn1lIjA zA8|vs_Xe@<#KGJ^h|*`Uj;ZFjw#QIgw`s|NrHuVR`neR5Pw zn6%ol4!n#t>T0T`NeEzD@yWiuDJvp{n`PUMPTLyaX`Pt@lg*W9=1Z`dMbcuIrfbsE z#-309>dED)(KG@1O|Kg^>==&jlpxw&k@W(_o#M08Bg`l8rh5>2i>+1@mBT=h^uynn?6E+(>^_T{w#a?R&! zv7Xdifee=(v)k4;wpFGUCKX|$({Px+{X1u{pQUIDvQG0@2-(kZ-a_ADrd)+nn|$F4 zl;?DjEOjeff|eK`Ig9SD<-z^(ur)1?E34J!;u*;I*uHvCvSDuN&)p>VDx&0E;lU

;I&B5lj^*Fv{F`)^F+50G)Q_scGu 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 Date: Tue, 25 Apr 2023 18:46:43 -0600 Subject: [PATCH 3/8] emulate Word's whitespace handling and treatment of xml:space="preserve" --- .../DocumentAssemblerTests.cs | 27 ++++++++++++++ OpenXmlPowerTools.Tests/UnicodeMapperTests.cs | 34 ++++++++++++++++++ .../DocumentAssembler/DocumentAssembler.cs | 17 +++++++-- OpenXmlPowerTools/UnicodeMapper.cs | 21 ++++++++++- TestFiles/DA240-Whitespace.docx | Bin 0 -> 16458 bytes TestFiles/DA240-Whitespace.xml | 7 ++++ 6 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 TestFiles/DA240-Whitespace.docx create mode 100644 TestFiles/DA240-Whitespace.xml diff --git a/OpenXmlPowerTools.Tests/DocumentAssemblerTests.cs b/OpenXmlPowerTools.Tests/DocumentAssemblerTests.cs index cdbdb3bb..9d17eb32 100644 --- a/OpenXmlPowerTools.Tests/DocumentAssemblerTests.cs +++ b/OpenXmlPowerTools.Tests/DocumentAssemblerTests.cs @@ -156,6 +156,24 @@ public void DA259(string name, string data, bool err) Assert.Equal(4, brCount); } + [Fact] + public void DA240() + { + string name = "DA240-Whitespace.docx"; + DA101(name, "DA240-Whitespace.xml", false); + var assembledDocx = new FileInfo(Path.Combine(TestUtil.TempDir.FullName, name.Replace(".docx", "-processed-by-DocumentAssembler.docx"))); + WmlDocument afterAssembling = new WmlDocument(assembledDocx.FullName); + + // when elements are inserted that begin or end with white space, make sure white space is preserved + string firstParaTextIncorrect = afterAssembling.MainDocumentPart.Element(W.body).Elements(W.p).First().Value; + Assert.Equal("Content may or may not have spaces: he/she; he, she; he and she.", firstParaTextIncorrect); + // warning: XElement.Value returns the string resulting from direct concatenation of all W.t elements. This is fast but ignores + // proper handling of xml:space="preserve" attributes, which Word honors when rendering content. Below we also check + // the result of UnicodeMapper.RunToString, which has been enhanced to take xml:space="preserve" into account. + string firstParaTextCorrect = InnerText(afterAssembling.MainDocumentPart.Element(W.body).Elements(W.p).First()); + Assert.Equal("Content may or may not have spaces: he/she; he, she; he and she.", firstParaTextCorrect); + } + [Theory] [InlineData("DA024-TrackedRevisions.docx", "DA-Data.xml")] public void DA102_Throws(string name, string data) @@ -487,6 +505,15 @@ private static string GetDocumentText(WmlDocument document) private const string WidePngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAZAAAADICAIAAABJdyC1AAACuUlEQVR4nO3UMQ7CQBAEwT3EvxEvXz/BZKalqniCifrs7gAUvGfmnO/TNwBu7H5edxuAfyFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFFzMzu2y8A5u4PQZkIj89BEMEAAAAASUVORK5CYII="; private const string TallPngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAMgAAAGQCAIAAABkkLjnAAAEF0lEQVR4nO3S0QkCURAEwX1i3mLke0lcI3hVAQzz0Wd3B+72npnzPbfv8mT72devP/CfhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkhEVCWCSERUJYJIRFQlgkzu42y8yTXVnDDBu1Y983AAAAAElFTkSuQmCC"; private const string TruncatedGifBase64 = "R0lGODlhyABQAA=="; + + private static string InnerText(XContainer e) + { + return e.Descendants(W.r) + .Where(r => r.Parent.Name != W.del) + .Select(UnicodeMapper.RunToString) + .StringConcatenate(); + } + private static readonly List s_ExpectedErrors = new List() { "The 'http://schemas.openxmlformats.org/wordprocessingml/2006/main:evenHBand' attribute is not declared.", diff --git a/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs b/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs index 667695d2..9b0154d9 100644 --- a/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs +++ b/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs @@ -153,5 +153,39 @@ public void IgnoresTemporaryLayoutMarkers() // characters) should exactly match the output of UnicodeMapper: Assert.Equal(p.Value, actual); } + + private const string PreserveSpacingXmlString = +@" + + + + The following space is retained: + + + but this one is not: + + + . Similarly these two lines should have only a space between them: + + + + Line 1! +Line 2! + + + + +"; + + [Fact] + public void HonorsXmlSpace() + { + XDocument partDocument = XDocument.Parse(PreserveSpacingXmlString); + XElement p = partDocument.Descendants(W.p).Last(); + string innerText = p.Descendants(W.r) + .Select(UnicodeMapper.RunToString) + .StringConcatenate(); + Assert.Equal(@"The following space is retained: but this one is not:. Similarly these two lines should have only a space between them: Line 1! Line 2!", innerText); + } } } \ No newline at end of file diff --git a/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs b/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs index a639ce5d..8eeaf648 100644 --- a/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs +++ b/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs @@ -654,7 +654,7 @@ private class RunReplacementInfo p.Add(new XElement(W.r, para.Elements(W.r).Elements(W.rPr).FirstOrDefault(), (p.Elements().Count() > 1) ? new XElement(W.br) : null, - new XElement(W.t, line))); + new XElement(W.t, GetXmlSpaceAttribute(line), line))); } return p; } @@ -666,7 +666,7 @@ private class RunReplacementInfo list.Add(new XElement(W.r, run.Elements().Where(e => e.Name != W.t), (list.Count > 0) ? new XElement(W.br) : null, - new XElement(W.t, line))); + new XElement(W.t, GetXmlSpaceAttribute(line), line))); } return list; } @@ -1400,5 +1400,18 @@ private static string EvaluateXPathToString(XElement element, string xPath, bool return xPathSelectResult.ToString(); } + + private static XAttribute GetXmlSpaceAttribute(string textOfTextElement) + { + if (!string.IsNullOrEmpty(textOfTextElement)) + { + if (char.IsWhiteSpace(textOfTextElement[0]) || + char.IsWhiteSpace(textOfTextElement[textOfTextElement.Length - 1])) + { + return new XAttribute(XNamespace.Xml + "space", "preserve"); + } + } + return null; + } } } diff --git a/OpenXmlPowerTools/UnicodeMapper.cs b/OpenXmlPowerTools/UnicodeMapper.cs index ffc42716..2773510b 100644 --- a/OpenXmlPowerTools/UnicodeMapper.cs +++ b/OpenXmlPowerTools/UnicodeMapper.cs @@ -60,7 +60,10 @@ public static string RunToString(XElement element) // For w:t elements, we obviously want the element's value. if (element.Name == W.t) { - return (string)element; + // Emulate Word's handling of the xml:space attribute on text elements + XAttribute spaceAttribute = element.Attribute(XNamespace.Xml + "space"); + string space = spaceAttribute != null ? spaceAttribute.Value : null; + return space == "preserve" ? (string)element : IgnoreTextSpacing((string)element); } // Turn elements representing special characters into their corresponding @@ -141,6 +144,22 @@ public static string RunToString(XElement element) return StartOfHeading.ToString(); } + ///

+ /// Emulate the way Word treats text elements when attribute xml:space="preserve" + /// is NOT present. + /// + /// The entire content of the w:t element. + /// The corresponding text string Word would display, print, and + /// allow to be edited. + private static string IgnoreTextSpacing(string text) + { + // all whitespace at beginning and end of entire string is ignored + // if text contains line breaks, they are ignored/replaced with a single space + return string.Join(" ", + text.Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries) + ).Trim(); + } + /// /// Translate a symbol into a Unicode character, using the specified w:font attribute /// value and unicode value (represented by the w:sym element's w:char attribute), diff --git a/TestFiles/DA240-Whitespace.docx b/TestFiles/DA240-Whitespace.docx new file mode 100644 index 0000000000000000000000000000000000000000..84f2b2286de084082f2acf54c9b0213ee2ae9a0e GIT binary patch literal 16458 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 H*Qfsh8m)u_ literal 0 HcmV?d00001 diff --git a/TestFiles/DA240-Whitespace.xml b/TestFiles/DA240-Whitespace.xml new file mode 100644 index 00000000..7c21bac1 --- /dev/null +++ b/TestFiles/DA240-Whitespace.xml @@ -0,0 +1,7 @@ + + + may or may not + / + , + and + From 11d35b14c0272bde6073a405c35be91a063b5994 Mon Sep 17 00:00:00 2001 From: Lowell Stewart Date: Thu, 8 Jan 2026 06:04:30 -0700 Subject: [PATCH 4/8] more comprehensive fix for whitespace handling in UnicodeMapper --- OpenXmlPowerTools.Tests/UnicodeMapperTests.cs | 86 ++++++++++++++++++ OpenXmlPowerTools/UnicodeMapper.cs | 54 ++++++++--- TestFiles/UM-Whitespace-Word-saved.docx | Bin 0 -> 16270 bytes TestFiles/UM-Whitespace-test.docx | Bin 0 -> 13319 bytes 4 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 TestFiles/UM-Whitespace-Word-saved.docx create mode 100644 TestFiles/UM-Whitespace-test.docx diff --git a/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs b/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs index 9b0154d9..0d2e1e9f 100644 --- a/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs +++ b/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs @@ -180,6 +180,9 @@ Line 2! [Fact] public void HonorsXmlSpace() { + // This somewhat rudimentary test is superceded by TreatsXmlSpaceLikeWord() below, + // but it has been left in to provide a simple/direct illustration of a couple of + // the specific test cases covered by that more extensive suite. XDocument partDocument = XDocument.Parse(PreserveSpacingXmlString); XElement p = partDocument.Descendants(W.p).Last(); string innerText = p.Descendants(W.r) @@ -187,5 +190,88 @@ public void HonorsXmlSpace() .StringConcatenate(); Assert.Equal(@"The following space is retained: but this one is not:. Similarly these two lines should have only a space between them: Line 1! Line 2!", innerText); } + + // Verifies that UnicodeMapper.RunToString interprets whitespace in elements + // exactly the way Microsoft Word does, including honoring xml:space="preserve". + // This is essential because RunToString is used by higher‑level features + // (OpenXmlRegex, DocumentAssembler, etc.) that rely on its output to reflect the + // text an end‑user would actually see and edit in Word. + // + // Word accepts a wide range of “valid” DOCX input, but it normalizes that input + // into a canonical form when displaying or saving the document. These tests + // compare RunToString’s output against Word’s canonicalized output to ensure + // that whitespace is treated as semantic content in the same way Word treats it. + [Fact] + public void TreatsXmlSpaceLikeWord() + { + var sourceDir = new System.IO.DirectoryInfo("../../../../TestFiles/"); + // Test document: crafted to include many whitespace patterns that Word accepts as valid input + var testDoc = new System.IO.FileInfo(System.IO.Path.Combine(sourceDir.FullName, "UM-whitespace-test.docx")); + var testWmlDoc = new WmlDocument(testDoc.FullName); + var testParagraphs = testWmlDoc.MainDocumentPart + .Element(W.body) + .Elements(W.p).ToList(); + // Canonical document: the same test document after being opened and saved by Word, + // representing Word’s own normalized interpretation of that whitespace + var expectedDoc = new System.IO.FileInfo(System.IO.Path.Combine(sourceDir.FullName, "UM-whitespace-Word-saved.docx")); + var expectedWmlDoc = new WmlDocument(expectedDoc.FullName); + var expectedParagraphs = expectedWmlDoc.MainDocumentPart + .Element(W.body) + .Elements(W.p).ToList(); + // Iterate through pairs of paragraphs (test name, test content, expected result) + for (int i = 0; i < testParagraphs.Count - 1; i += 2) + { + var testNameParagraph = testParagraphs[i]; + var testContentParagraph = testParagraphs[i + 1]; + // Get the test name from the first paragraph + var testName = testNameParagraph.Descendants(W.t) + .Select(t => (string)t) + .StringConcatenate(); + // Get the actual result by calling UnicodeMapper.RunToString on the test content runs + var actualResult = testContentParagraph.Descendants(W.r) + .Select(UnicodeMapper.RunToString) + .StringConcatenate(); + // Find corresponding expected result paragraph (same index in expected document) + var expectedResult = ExtractExpectedFromWord(expectedParagraphs[i + 1]); + Assert.True( + expectedResult == actualResult, + $"Test '{testName}' failed. Expected: [{expectedResult}] Actual: [{actualResult}]" + ); + } + } + + // Extracts the expected text from Word’s canonicalized output for the whitespace tests. + // This helper intentionally handles *only* the constructs that Word emits in the saved + // version of UM-whitespace-test.docx: + // • → literal text + // • → '\t' + // • (intentionally ignored) + // If any other run-level element appears, it means Word has emitted something this test + // was not designed to handle, and the test fails loudly. This prevents the helper + // from drifting toward reimplementing UnicodeMapper.RunToString. + private static string ExtractExpectedFromWord(XElement p) + { + var sb = new System.Text.StringBuilder(); + foreach (var run in p.Elements(W.r)) + { + foreach (var child in run.Elements()) + { + if (child.Name == W.t) + { + sb.Append((string)child); + } + else if (child.Name == W.tab) + { + sb.Append('\t'); + } + else if (child.Name != W.lastRenderedPageBreak) + { + throw new System.InvalidOperationException( + $"Unexpected element <{child.Name.LocalName}> encountered in expected Word output."); + } + } + } + return sb.ToString(); + } } } \ No newline at end of file diff --git a/OpenXmlPowerTools/UnicodeMapper.cs b/OpenXmlPowerTools/UnicodeMapper.cs index 2773510b..ee4c46ae 100644 --- a/OpenXmlPowerTools/UnicodeMapper.cs +++ b/OpenXmlPowerTools/UnicodeMapper.cs @@ -61,9 +61,9 @@ public static string RunToString(XElement element) if (element.Name == W.t) { // Emulate Word's handling of the xml:space attribute on text elements - XAttribute spaceAttribute = element.Attribute(XNamespace.Xml + "space"); - string space = spaceAttribute != null ? spaceAttribute.Value : null; - return space == "preserve" ? (string)element : IgnoreTextSpacing((string)element); + XAttribute? spaceAttribute = element.Attribute(XNamespace.Xml + "space"); + string? space = spaceAttribute?.Value; + return NormalizeWhitespace((string) element, space == "preserve"); } // Turn elements representing special characters into their corresponding @@ -145,19 +145,47 @@ public static string RunToString(XElement element) } /// - /// Emulate the way Word treats text elements when attribute xml:space="preserve" - /// is NOT present. + /// Emulate the way Word interprets the content of text elements + /// depending on whether the xml:space="preserve" attribute is present. /// /// The entire content of the w:t element. - /// The corresponding text string Word would display, print, and - /// allow to be edited. - private static string IgnoreTextSpacing(string text) + /// The corresponding text string Word would display, print, save, + /// and allow to be edited. + private static string NormalizeWhitespace(string text, bool preserve) { - // all whitespace at beginning and end of entire string is ignored - // if text contains line breaks, they are ignored/replaced with a single space - return string.Join(" ", - text.Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries) - ).Trim(); + if (string.IsNullOrEmpty(text)) + return string.Empty; + // Trim leading & trailing whitespace when NOT preserving + ReadOnlySpan span = preserve + ? text.AsSpan() + : text.AsSpan().Trim(); + if (span.Length == 0) + return string.Empty; + var sb = new System.Text.StringBuilder(span.Length); + int i = 0; + while (i < span.Length) + { + char c = span[i]; + switch (c) + { + case '\r': // CR or CRLF → space + sb.Append(' '); + if (i + 1 < span.Length && span[i + 1] == '\n') + i++; // skip LF so CRLF becomes one space + break; + case '\n': // LF → space + sb.Append(' '); + break; + case '\t': // TAB preserved or converted to space, depending on mode + sb.Append(preserve ? c : ' '); + break; + default: // SPACE or any other character → preserved exactly + sb.Append(c); + break; + } + i++; + } + return sb.ToString(); } /// diff --git a/TestFiles/UM-Whitespace-Word-saved.docx b/TestFiles/UM-Whitespace-Word-saved.docx new file mode 100644 index 0000000000000000000000000000000000000000..8e590c02c440f3b9ddead3b4563442fcc2ffb0b7 GIT binary patch literal 16270 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 literal 0 HcmV?d00001 diff --git a/TestFiles/UM-Whitespace-test.docx b/TestFiles/UM-Whitespace-test.docx new file mode 100644 index 0000000000000000000000000000000000000000..c72ae3f185495cf3564d4e04f95e72e8ae0dae76 GIT binary patch literal 13319 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 literal 0 HcmV?d00001 From 79806fa54aba8e4d513bf82c3507384bf0d0c933 Mon Sep 17 00:00:00 2001 From: Lowell Stewart Date: Sat, 3 Jan 2026 16:58:13 -0700 Subject: [PATCH 5/8] fix bug where conditionals can leave a table cell in an invalid state, causing Word to report an error. --- .../DocumentAssemblerTests.cs | 1 + .../DocumentAssembler/DocumentAssembler.cs | 8 +++++++- .../DA268-Block-Conditional-In-Table-Cell.docx | Bin 0 -> 19774 bytes TestFiles/DA268-data.xml | 4 ++++ 4 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 TestFiles/DA268-Block-Conditional-In-Table-Cell.docx create mode 100644 TestFiles/DA268-data.xml diff --git a/OpenXmlPowerTools.Tests/DocumentAssemblerTests.cs b/OpenXmlPowerTools.Tests/DocumentAssemblerTests.cs index cdbdb3bb..c775eadc 100644 --- a/OpenXmlPowerTools.Tests/DocumentAssemblerTests.cs +++ b/OpenXmlPowerTools.Tests/DocumentAssemblerTests.cs @@ -107,6 +107,7 @@ public class DaTests [InlineData("DA264-InvalidRunLevelRepeat.docx", "DA-Data.xml", true)] [InlineData("DA265-RunLevelRepeatWithWhiteSpaceBefore.docx", "DA-Data.xml", false)] [InlineData("DA266-RunLevelRepeat-NoData.docx", "DA-Data.xml", true)] + [InlineData("DA268-Block-Conditional-In-Table-Cell.docx", "DA268-data.xml", false)] public void DA101(string name, string data, bool err) { var sourceDir = new DirectoryInfo("../../../../TestFiles/"); diff --git a/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs b/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs index a639ce5d..f3d18a07 100644 --- a/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs +++ b/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs @@ -873,9 +873,15 @@ private class RunReplacementInfo } return null; } + var transformedNodes = element.Nodes().Select(n => ContentReplacementTransform(n, data, templateError, owningPart)); + if (element.Name == W.tc && transformedNodes.All(n => n == null || (n is XElement && (n as XElement).Name == W.tcPr))) + { + // avoid empty table cells, which are invalid -- add an empty paragraph back in + transformedNodes = transformedNodes.Concat(new XNode[] { new XElement(W.p) }); + } return new XElement(element.Name, element.Attributes(), - element.Nodes().Select(n => ContentReplacementTransform(n, data, templateError, owningPart))); + transformedNodes); } return node; } diff --git a/TestFiles/DA268-Block-Conditional-In-Table-Cell.docx b/TestFiles/DA268-Block-Conditional-In-Table-Cell.docx new file mode 100644 index 0000000000000000000000000000000000000000..7796dabba23e5bf0afba1f271a46f5e9a21f5457 GIT binary patch literal 19774 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 UXxl$MI0PUsP#~aEk)KEZ4>ii;=l}o! literal 0 HcmV?d00001 diff --git a/TestFiles/DA268-data.xml b/TestFiles/DA268-data.xml new file mode 100644 index 00000000..10750a67 --- /dev/null +++ b/TestFiles/DA268-data.xml @@ -0,0 +1,4 @@ + + + False + \ No newline at end of file From f3f820f800dbd1b9f31577671233f5d18acdd4f9 Mon Sep 17 00:00:00 2001 From: Lowell Stewart Date: Wed, 21 Jan 2026 17:02:31 -0700 Subject: [PATCH 6/8] more comprehensive fix for DA268 (commit 6811ec2ca4c1878a42fb4feb8f402111a13a1f59) --- .../DocumentAssembler/DocumentAssembler.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs b/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs index f3d18a07..0d47c875 100644 --- a/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs +++ b/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs @@ -873,11 +873,18 @@ private class RunReplacementInfo } return null; } - var transformedNodes = element.Nodes().Select(n => ContentReplacementTransform(n, data, templateError, owningPart)); - if (element.Name == W.tc && transformedNodes.All(n => n == null || (n is XElement && (n as XElement).Name == W.tcPr))) + var transformedNodes = element.Nodes().Select(n => ContentReplacementTransform(n, data, asmResult, owningPart)); + if (element.Name == W.tc) { - // avoid empty table cells, which are invalid -- add an empty paragraph back in - transformedNodes = transformedNodes.Concat(new XNode[] { new XElement(W.p) }); + // Check if the table cell contains any paragraph elements + var nodesList = transformedNodes.ToList(); + var hasParagraph = nodesList.Any(n => n is XElement xe && xe.Name == W.p); + if (!hasParagraph) + { + // avoid empty table cells, which are invalid -- add an empty paragraph back in + nodesList.Add(new XElement(W.p)); + } + transformedNodes = nodesList; } return new XElement(element.Name, element.Attributes(), From db482af095f80713ccac864d756aa3d23e9993d6 Mon Sep 17 00:00:00 2001 From: Lowell Stewart Date: Wed, 21 Jan 2026 17:33:20 -0700 Subject: [PATCH 7/8] only add paragraph when no other block-level elements exist in the table cell --- .../DocumentAssembler/DocumentAssembler.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs b/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs index 0d47c875..478aad91 100644 --- a/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs +++ b/OpenXmlPowerTools/DocumentAssembler/DocumentAssembler.cs @@ -873,15 +873,17 @@ private class RunReplacementInfo } return null; } - var transformedNodes = element.Nodes().Select(n => ContentReplacementTransform(n, data, asmResult, owningPart)); + var transformedNodes = element.Nodes().Select(n => ContentReplacementTransform(n, data, templateError, owningPart)); if (element.Name == W.tc) { - // Check if the table cell contains any paragraph elements + // Check if the table cell contains any block-level elements + // Valid block-level elements in a table cell: p (paragraph), tbl (table), sdt (structured document tag), customXml var nodesList = transformedNodes.ToList(); - var hasParagraph = nodesList.Any(n => n is XElement xe && xe.Name == W.p); - if (!hasParagraph) + var hasBlockLevelContent = nodesList.Any(n => n is XElement xe && + (xe.Name == W.p || xe.Name == W.tbl || xe.Name == W.sdt || xe.Name == W.customXml)); + if (!hasBlockLevelContent) { - // avoid empty table cells, which are invalid -- add an empty paragraph back in + // Table cells must contain at least one block-level element -- add an empty paragraph nodesList.Add(new XElement(W.p)); } transformedNodes = nodesList; From aa10c34fe4bb15de91f9ca402cf70a66e29cf99e Mon Sep 17 00:00:00 2001 From: Lowell Stewart Date: Wed, 21 Jan 2026 20:13:19 -0700 Subject: [PATCH 8/8] fixed case sensitivity issue causing test to fail on Linux (but not Mac or Windows) --- OpenXmlPowerTools.Tests/UnicodeMapperTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs b/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs index 0d2e1e9f..0e129975 100644 --- a/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs +++ b/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs @@ -206,14 +206,14 @@ public void TreatsXmlSpaceLikeWord() { var sourceDir = new System.IO.DirectoryInfo("../../../../TestFiles/"); // Test document: crafted to include many whitespace patterns that Word accepts as valid input - var testDoc = new System.IO.FileInfo(System.IO.Path.Combine(sourceDir.FullName, "UM-whitespace-test.docx")); + var testDoc = new System.IO.FileInfo(System.IO.Path.Combine(sourceDir.FullName, "UM-Whitespace-test.docx")); var testWmlDoc = new WmlDocument(testDoc.FullName); var testParagraphs = testWmlDoc.MainDocumentPart .Element(W.body) .Elements(W.p).ToList(); // Canonical document: the same test document after being opened and saved by Word, - // representing Word’s own normalized interpretation of that whitespace - var expectedDoc = new System.IO.FileInfo(System.IO.Path.Combine(sourceDir.FullName, "UM-whitespace-Word-saved.docx")); + // representing Word's own normalized interpretation of that whitespace + var expectedDoc = new System.IO.FileInfo(System.IO.Path.Combine(sourceDir.FullName, "UM-Whitespace-Word-saved.docx")); var expectedWmlDoc = new WmlDocument(expectedDoc.FullName); var expectedParagraphs = expectedWmlDoc.MainDocumentPart .Element(W.body)