From 2123c3ac8920775051a700258b4ff4022fd25cc2 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Fri, 26 Jun 2026 13:09:53 +1000 Subject: [PATCH] Fix OOXML schema validity, correctness, perf, and security issues Correctness: - Tables: derive column count from the rowspan grid so cells in later, wider rows are no longer silently dropped - Tables: thread FootnoteIndex/BookmarkId through per-cell contexts and seed FootnoteIndex from the existing part, so footnote and bookmark IDs no longer collide across cells or across Build calls - Emit w:rPr and w:tcPr children in OOXML schema order; previously the output was schema-invalid and only tolerated by Word - Cell margins emit w:left/w:right instead of the invalid w:start/w:end - ColorParser: accept percentage and decimal rgb()/rgba() components - vertical-align is matched case-insensitively - word-spacing no longer doubles letter-spacing - @page: match the closing brace and skip nested margin-box rules so sibling declarations (margin/size) are not lost Performance: - Parse each element's inline style once instead of twice - Allocate list numbering IDs from the threaded NextNumId counter rather than rescanning the numbering part per list (O(m^2) -> O(m)) - Compute ordered-list item indices in a single pass (O(n^2) -> O(n)) Security: - Image fetch: disable auto-redirect and re-check each redirect hop against the image policy (closes a SafeDomains/SSRF bypass), cap the response size, set a timeout, and parse width/height with InvariantCulture Tests: - Add rowspan, footnote-id, bookmark-id, and OpenXmlValidator round-trip tests, plus rgb() percentage cases - Regenerate affected verified baselines (structural reordering only; rendered text/image output unchanged) --- src/OpenXmlHtml.Tests/ColorParserTests.cs | 4 + ...ployeeOnboardingGuide.DotNet.verified.docx | Bin 3337 -> 3340 bytes ....EmployeeOnboardingGuide.Net.verified.docx | Bin 3303 -> 3308 bytes ...rdSamples.RichDocument.DotNet.verified.xml | 2 +- .../WordSamples.RichDocument.Net.verified.xml | 2 +- ...BookmarksAndLinksDocx.DotNet.verified.docx | Bin 1240 -> 1238 bytes ...ts.BookmarksAndLinksDocx.Net.verified.docx | Bin 1220 -> 1219 bytes ...sts.InternalAnchorLink.DotNet.verified.xml | 2 +- ...rTests.InternalAnchorLink.Net.verified.xml | 2 +- ...AnchorTests.LinkInText.DotNet.verified.xml | 2 +- ...ordAnchorTests.LinkInText.Net.verified.xml | 2 +- ...sts.LinkWithFormatting.DotNet.verified.xml | 2 +- ...rTests.LinkWithFormatting.Net.verified.xml | 2 +- ...orTests.LinkWithNoHref.DotNet.verified.xml | 2 +- ...nchorTests.LinkWithNoHref.Net.verified.xml | 2 +- ...Tests.LinkWithSameText.DotNet.verified.xml | 2 +- ...horTests.LinkWithSameText.Net.verified.xml | 2 +- ...AnchorTests.SimpleLink.DotNet.verified.xml | 2 +- ...ordAnchorTests.SimpleLink.Net.verified.xml | 2 +- src/OpenXmlHtml.Tests/WordBookmarkIdTests.cs | 33 ++++++ ....BorderWithBackground.DotNet.verified.docx | Bin 1097 -> 1097 bytes ...sts.BorderWithBackground.Net.verified.docx | Bin 1072 -> 1072 bytes ...lLinkCreatesHyperlink.DotNet.verified.docx | Bin 1442 -> 1442 bytes ...rnalLinkCreatesHyperlink.Net.verified.docx | Bin 1406 -> 1407 bytes ...allbackWithoutMainPart.DotNet.verified.xml | 2 +- ...nkFallbackWithoutMainPart.Net.verified.xml | 2 +- ...mageAndTextInsideLink.DotNet.verified.docx | Bin 1890 -> 1890 bytes ...s.ImageAndTextInsideLink.Net.verified.docx | Bin 1851 -> 1851 bytes ...ts.ColWidthWithColspan.DotNet.verified.xml | 2 +- ...Tests.ColWidthWithColspan.Net.verified.xml | 2 +- ...s.ConvertFullHtmlPage.DotNet.verified.docx | Bin 1675 -> 1673 bytes ...ests.ConvertFullHtmlPage.Net.verified.docx | Bin 1648 -> 1647 bytes ...ToDocxTests.CssStyles.DotNet.verified.docx | Bin 1090 -> 1088 bytes ...ertToDocxTests.CssStyles.Net.verified.docx | Bin 1068 -> 1068 bytes ...ests.FormattedContent.DotNet.verified.docx | Bin 1183 -> 1181 bytes ...cxTests.FormattedContent.Net.verified.docx | Bin 1163 -> 1162 bytes ...xTests.LinksAndImages.DotNet.verified.docx | Bin 1472 -> 1472 bytes ...DocxTests.LinksAndImages.Net.verified.docx | Bin 1440 -> 1439 bytes ...ocxTests.RichDocument.DotNet.verified.docx | Bin 2220 -> 2220 bytes ...ToDocxTests.RichDocument.Net.verified.docx | Bin 2188 -> 2190 bytes src/OpenXmlHtml.Tests/WordFootnoteIdTests.cs | 70 ++++++++++++ ...ts.NestedCombinations.DotNet.verified.docx | Bin 5098 -> 5104 bytes ...Tests.NestedCombinations.Net.verified.docx | Bin 5148 -> 5150 bytes ...stedTests.DeeplyNested.DotNet.verified.xml | 2 +- ...dNestedTests.DeeplyNested.Net.verified.xml | 2 +- src/OpenXmlHtml.Tests/WordRowspanTests.cs | 30 +++++ ...Tests.FullFeatureDocx.DotNet.verified.docx | Bin 4501 -> 4499 bytes ...mboTests.FullFeatureDocx.Net.verified.docx | Bin 4490 -> 4492 bytes ...tyleTests.CellPadding.DotNet.verified.docx | Bin 1163 -> 1159 bytes ...leStyleTests.CellPadding.Net.verified.docx | Bin 1137 -> 1133 bytes ...CellPaddingIndividual.DotNet.verified.docx | Bin 1158 -> 1153 bytes ...ts.CellPaddingIndividual.Net.verified.docx | Bin 1130 -> 1127 bytes ...sts.CellVerticalAlign.DotNet.verified.docx | Bin 1186 -> 1183 bytes ...eTests.CellVerticalAlign.Net.verified.docx | Bin 1164 -> 1161 bytes ...ts.CombinedCellStyles.DotNet.verified.docx | Bin 1375 -> 1379 bytes ...Tests.CombinedCellStyles.Net.verified.docx | Bin 1354 -> 1358 bytes ...lCellpaddingAttribute.DotNet.verified.docx | Bin 1182 -> 1179 bytes ...HtmlCellpaddingAttribute.Net.verified.docx | Bin 1159 -> 1154 bytes ...bleDefaultCellPadding.DotNet.verified.docx | Bin 1179 -> 1176 bytes ....TableDefaultCellPadding.Net.verified.docx | Bin 1158 -> 1154 bytes ...ts.UnderlineWithColor.DotNet.verified.docx | Bin 1027 -> 1028 bytes ...Tests.UnderlineWithColor.Net.verified.docx | Bin 1006 -> 1006 bytes src/OpenXmlHtml.Tests/WordValidationTests.cs | 43 +++++++ src/OpenXmlHtml/AtPageParser.cs | 60 +++++++++- src/OpenXmlHtml/ColorParser.cs | 34 +++++- src/OpenXmlHtml/HtmlParser.cs | 60 +++++----- src/OpenXmlHtml/ImageResolver.cs | 96 ++++++++++++++-- src/OpenXmlHtml/PaddingHelper.cs | 6 +- src/OpenXmlHtml/WordContentBuilder.Lists.cs | 25 +---- src/OpenXmlHtml/WordContentBuilder.Tables.cs | 81 ++++++++++++-- src/OpenXmlHtml/WordContentBuilder.cs | 34 ++++-- src/OpenXmlHtml/WordHtmlConverter.cs | 105 +++++++++--------- 72 files changed, 563 insertions(+), 158 deletions(-) create mode 100644 src/OpenXmlHtml.Tests/WordBookmarkIdTests.cs create mode 100644 src/OpenXmlHtml.Tests/WordFootnoteIdTests.cs create mode 100644 src/OpenXmlHtml.Tests/WordRowspanTests.cs create mode 100644 src/OpenXmlHtml.Tests/WordValidationTests.cs diff --git a/src/OpenXmlHtml.Tests/ColorParserTests.cs b/src/OpenXmlHtml.Tests/ColorParserTests.cs index a6152ee..2470e14 100644 --- a/src/OpenXmlHtml.Tests/ColorParserTests.cs +++ b/src/OpenXmlHtml.Tests/ColorParserTests.cs @@ -16,6 +16,10 @@ public class ColorParserTests [TestCase("rgba(0, 128, 0, 1)", "008000")] [TestCase("rgba(0,0,255,0)", "0000FF")] [TestCase("rgba(100, 200, 50, 0.8)", "64C832")] + [TestCase("rgb(100%, 0%, 0%)", "FF0000")] + [TestCase("rgb(0%, 100%, 0%)", "00FF00")] + [TestCase("rgba(0%, 0%, 100%, 0.5)", "0000FF")] + [TestCase("rgb(255.0, 0, 0)", "FF0000")] public void ValidColors(string input, string expected) => Assert.That(ColorParser.Parse(input), Is.EqualTo(expected)); diff --git a/src/OpenXmlHtml.Tests/Samples/WordSamples.EmployeeOnboardingGuide.DotNet.verified.docx b/src/OpenXmlHtml.Tests/Samples/WordSamples.EmployeeOnboardingGuide.DotNet.verified.docx index 42410d4141ea157d9b9865f08df57bb74470dc4e..e8309d3dcbe17c966d2773d36a718d1d2b98bde8 100644 GIT binary patch delta 918 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 z$All systems operational. Server: OK Cache: Down -Contact ops team (mailto:ops@example.com) for details. \ No newline at end of file +Contact ops team (mailto:ops@example.com) for details. \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/Samples/WordSamples.RichDocument.Net.verified.xml b/src/OpenXmlHtml.Tests/Samples/WordSamples.RichDocument.Net.verified.xml index cddc8e4..a7b747d 100644 --- a/src/OpenXmlHtml.Tests/Samples/WordSamples.RichDocument.Net.verified.xml +++ b/src/OpenXmlHtml.Tests/Samples/WordSamples.RichDocument.Net.verified.xml @@ -2,4 +2,4 @@ All systems operational. Server: OK Cache: Down -Contact ops team (mailto:ops@example.com) for details. \ No newline at end of file +Contact ops team (mailto:ops@example.com) for details. \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordAnchorTests.BookmarksAndLinksDocx.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordAnchorTests.BookmarksAndLinksDocx.DotNet.verified.docx index 25ee5c64c58b79341ec3dd08415cfeb7fa65610c..a29873d5401140881ba630c519ab91c76821a383 100644 GIT binary patch delta 508 zcmVa^6z;V#eU zpZtDW(ar$tvW+k~{{W%~NjQrZ^XRx~7>4iSuyio@d|7ZpLe8Cs0Dc2}@gNEV3`@2| zOJFJVM`$=W+SXrabxvYgF@CkqFU7|rh(#9{RH_L&w%i8g8AG!g4 z73OmA!;*}>2O@1Nwj$vP)KlIgf93V`>A#{+GxkPpgq5J!13E3kwe({VhzjmKi1j^` z=;1v17q?$iGX>(h5q`bblXaOD4V)#Gb(vND^bZAfNz?Rsn!W{aOz90UZ4bJJw)g^)RW2(VK+-{| zYW0u4+1&1rlL%vJI4Ht2GD( yKIgB}y;&mCnOU{8H{&tn5iS~AY;vNFvlllQPf9*f=4vN2$ZZB1=M z*5VLR;QFxk9z;e|Fa| zpM1aF(XIy8W*brG`~!#%B;hPt%%j7hVHv(xd)MCV`Lf`Igq%AM0sIE|;$Ez3U|6sv zS^`ULgq0QgoXbi$0-x}w(VLO9OR$EXv&|&d=tocda(cBq zWbi!Za)`r{jJ*dUZA*4X!V{>cf4m3E%jwg9MW1HujoJt+L9u&uT82yM$0iUZ+8NS?*P8yKq0VWV{+_090UT3$15De!uAy0dR|ZMvl?@@00000x>x-I diff --git a/src/OpenXmlHtml.Tests/WordAnchorTests.BookmarksAndLinksDocx.Net.verified.docx b/src/OpenXmlHtml.Tests/WordAnchorTests.BookmarksAndLinksDocx.Net.verified.docx index 3f820c2fb31f4c0c29fe03172e2e759e877f3054..1f591dd2186133a2615d7a12e4b369ec8f7c74c8 100644 GIT binary patch delta 344 zcmV-e0jK`N3Bw7n=m7;+)BdyTlkEXDD($mlPm|EZW7(6I@@qWsN&EuNt?e#d=n4s5 zY#+~f=FIWfKfZmR-+ZZ)OaiEXoLyptc4QmAoL((^uDhfeciYk{;1|(c_IX&8u=haZ zPqGyWOQ0U}J+Qo-ef+Q3#|e9*wmv&3c88{vzo`8b`>_c`4!0J><`#TA!Rh6nb-$!% zGQ>?S?55Mx`)X0c$;JAeQu+QL>VJNRU3hgzBJZ?%kn}-izsqzFmBbo<7$KtqSl*Nh zCQtYC|I#!bHI4sT(?2xSIZfl|Y5EqxFja1Wm3q+CHQ8s7bY5BKErU?`>Z{M`|HCMH z9ZuV#3%{Puu4Y5sCjThfS4L>O;rcpkeQcQ*bc7iaVSU6EtWT_kmmmB?eie6cjYuaj qZ>WReSn^0N7{A!y_vNFolkEXDD)H>t(Z9x8a6=CaSjqJ+H% zA|I2jNLT{(kne%zL9shDrTk6p$JmceAac01AU3z)+X+rD|E&8Z zJ(D4BYGF5>p59lB8cr_O@07~-|DeJ1JM6-%I}&-P)xD(mDtju^XQ(89*1!lE6~OYQ zR4{qGpZ}Mp@u+G1*P8yJq0VUNfdD(Y_}_;|Ve4bdyr3h@kO=D|u3&v)Exi2TAM&fXgKI=O rfq6q649Aj3a>4k;2FLFMd+`gC^aF|oDeH5P?3058ItKLv00000QUtYh diff --git a/src/OpenXmlHtml.Tests/WordAnchorTests.InternalAnchorLink.DotNet.verified.xml b/src/OpenXmlHtml.Tests/WordAnchorTests.InternalAnchorLink.DotNet.verified.xml index 48bd986..e47af3a 100644 --- a/src/OpenXmlHtml.Tests/WordAnchorTests.InternalAnchorLink.DotNet.verified.xml +++ b/src/OpenXmlHtml.Tests/WordAnchorTests.InternalAnchorLink.DotNet.verified.xml @@ -1,3 +1,3 @@ -Jump to Section 2 +Jump to Section 2 Section 2 Content here. \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordAnchorTests.InternalAnchorLink.Net.verified.xml b/src/OpenXmlHtml.Tests/WordAnchorTests.InternalAnchorLink.Net.verified.xml index 48bd986..e47af3a 100644 --- a/src/OpenXmlHtml.Tests/WordAnchorTests.InternalAnchorLink.Net.verified.xml +++ b/src/OpenXmlHtml.Tests/WordAnchorTests.InternalAnchorLink.Net.verified.xml @@ -1,3 +1,3 @@ -Jump to Section 2 +Jump to Section 2 Section 2 Content here. \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkInText.DotNet.verified.xml b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkInText.DotNet.verified.xml index 44d9b52..f4ecec4 100644 --- a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkInText.DotNet.verified.xml +++ b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkInText.DotNet.verified.xml @@ -1 +1 @@ -Visit our site (https://example.com) for more info. \ No newline at end of file +Visit our site (https://example.com) for more info. \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkInText.Net.verified.xml b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkInText.Net.verified.xml index 44d9b52..f4ecec4 100644 --- a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkInText.Net.verified.xml +++ b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkInText.Net.verified.xml @@ -1 +1 @@ -Visit our site (https://example.com) for more info. \ No newline at end of file +Visit our site (https://example.com) for more info. \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithFormatting.DotNet.verified.xml b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithFormatting.DotNet.verified.xml index 443cc4d..719bf2b 100644 --- a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithFormatting.DotNet.verified.xml +++ b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithFormatting.DotNet.verified.xml @@ -1 +1 @@ -Bold Link (https://example.com) \ No newline at end of file +Bold Link (https://example.com) \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithFormatting.Net.verified.xml b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithFormatting.Net.verified.xml index 443cc4d..719bf2b 100644 --- a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithFormatting.Net.verified.xml +++ b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithFormatting.Net.verified.xml @@ -1 +1 @@ -Bold Link (https://example.com) \ No newline at end of file +Bold Link (https://example.com) \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithNoHref.DotNet.verified.xml b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithNoHref.DotNet.verified.xml index b61a079..a050219 100644 --- a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithNoHref.DotNet.verified.xml +++ b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithNoHref.DotNet.verified.xml @@ -1 +1 @@ -anchor text \ No newline at end of file +anchor text \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithNoHref.Net.verified.xml b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithNoHref.Net.verified.xml index b61a079..a050219 100644 --- a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithNoHref.Net.verified.xml +++ b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithNoHref.Net.verified.xml @@ -1 +1 @@ -anchor text \ No newline at end of file +anchor text \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithSameText.DotNet.verified.xml b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithSameText.DotNet.verified.xml index c3cdd07..20c3207 100644 --- a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithSameText.DotNet.verified.xml +++ b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithSameText.DotNet.verified.xml @@ -1 +1 @@ -https://example.com \ No newline at end of file +https://example.com \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithSameText.Net.verified.xml b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithSameText.Net.verified.xml index c3cdd07..20c3207 100644 --- a/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithSameText.Net.verified.xml +++ b/src/OpenXmlHtml.Tests/WordAnchorTests.LinkWithSameText.Net.verified.xml @@ -1 +1 @@ -https://example.com \ No newline at end of file +https://example.com \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordAnchorTests.SimpleLink.DotNet.verified.xml b/src/OpenXmlHtml.Tests/WordAnchorTests.SimpleLink.DotNet.verified.xml index a2edd5c..f25c732 100644 --- a/src/OpenXmlHtml.Tests/WordAnchorTests.SimpleLink.DotNet.verified.xml +++ b/src/OpenXmlHtml.Tests/WordAnchorTests.SimpleLink.DotNet.verified.xml @@ -1 +1 @@ -Example (https://example.com) \ No newline at end of file +Example (https://example.com) \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordAnchorTests.SimpleLink.Net.verified.xml b/src/OpenXmlHtml.Tests/WordAnchorTests.SimpleLink.Net.verified.xml index a2edd5c..f25c732 100644 --- a/src/OpenXmlHtml.Tests/WordAnchorTests.SimpleLink.Net.verified.xml +++ b/src/OpenXmlHtml.Tests/WordAnchorTests.SimpleLink.Net.verified.xml @@ -1 +1 @@ -Example (https://example.com) \ No newline at end of file +Example (https://example.com) \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordBookmarkIdTests.cs b/src/OpenXmlHtml.Tests/WordBookmarkIdTests.cs new file mode 100644 index 0000000..367b189 --- /dev/null +++ b/src/OpenXmlHtml.Tests/WordBookmarkIdTests.cs @@ -0,0 +1,33 @@ +[TestFixture] +public class WordBookmarkIdTests +{ + // Bookmarks generated from id/name attributes inside table cells must be document-unique. + // Regression: each cell built a fresh context whose bookmark counter restarted at zero, + // producing duplicate BookmarkStart IDs (invalid OOXML). + [Test] + public void BookmarksInTableCellsAreUnique() + { + using var stream = new MemoryStream(); + WordHtmlConverter.ConvertToDocx( + """ + + + + + +

A

B

+

C

+ """, + stream); + + stream.Position = 0; + using var document = WordprocessingDocument.Open(stream, false); + var bookmarkIds = document.MainDocumentPart!.Document!.Body! + .Descendants() + .Select(_ => _.Id!.Value) + .ToList(); + + Assert.That(bookmarkIds, Is.Unique); + Assert.That(bookmarkIds.Count, Is.EqualTo(3)); + } +} diff --git a/src/OpenXmlHtml.Tests/WordBorderTests.BorderWithBackground.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordBorderTests.BorderWithBackground.DotNet.verified.docx index d7912a17cc4a1f052c8fe2b49b4617f0e52e64a1..21e3f75b545aebe4db99b415a2861a3fa5b82e70 100644 GIT binary patch delta 368 zcmV-$0gwL42+0Vr^#KGuOY$?5`2iq*ZrmUcy$YKDE_NDiwpQ4DB;CJOmD&qn7~818 zAi(QwuGWtnDOadq<8Go{Abia5;LXe%nCHJww|6IJ*%4zh+^Q0zpGHocJKRG*4bTX$6#gQ)*b%>-eCF4hbXZcmr zY`K%hLrK|_{!@G?CRX#Kx}NdsbYx3+lvY5Aw+L+M6j_P_?&~TC=~2M8Zh}-OpoMdE zn)Dj2EXr6YgRxe?;c$5PdQ)lqf6dz1nj=I^7>@}1@077Cw0tJNlllQPvR{))%p@k6 zlGf6kSx+anm^`tbqQ=Oi*Q}RP>h~CF_ImTCA^!3M2~|GV3FFU4wv<0^+b>NOJ_%^I zyU)89;bHtHOXHb>fcE;jDV%7=DPLeuIg`6!i^Hqi38ls;;O45rfhfO% z5CeF<&DHvmBjpMm*tnbI1mQ#Cp$B?Gdj9)#dv|h{9Wf?DUO>BPfT35UjUNTfdHA%0 zyZ!CbX;QO8pV`S-pSnfBlym4fPijIZ(}n{2ld}UwCv)0R9Qi^~he!%jGJbS_mR~i^ zmOE)YlqY-Ae~J&q#A<$2*E3#RnlxLwqqG8)MHzuD-C(R0a5x+uzTUu@*6X;^N*arW zr^r$ia9>wBNRI-xbrYmQ0d2j_|L3fQt-%m6VLT%2zf;Dp(DIr5Ug}5C$bL;GF_V~N zN?J>EW<8zQV)Dd#iW(!6Ub9|Os^4Rz+3U@lg7`}Z5~_U86ULv9Y$-i%+b>O3Gzn<8 zyU)9qdcyckmWnb30qym5Q#jF#Q@+5Sawd1d7Kc~S38ls;;O45rfhfO%;WJ$P6Htjz6#;pvaL;9H{JF@?VH9#FTld=Ph=Sa zY`6AmK6oUrAkePWcmWAv=3~AYz5$ay0yuvmR-x^<575?ZKr4+!0nUYTono^rTd+sah4_ZnwL;UnUy+J~K5o)-V~wr>0dmgqfp7p$QA5p946VFg{z609&>^aa+vPcR#AU=+v3Z^JHM Ta`Od~VFQW;JxlU4lkWpT^ca#B delta 321 zcmV-H0lxmQ2(SpS=m7*Y&de{9>;WJ$Qo|q+z6!&;C9R#dW}5av?VFA>dI18PpOLT( zkf!!(K6oUrAkfCDCy>do`>|ix?*o%Q0yux6R-x^<575?ZKr4+!0nUZ(I>lyLwqPA^ zHH}-ADBofY3gQun5$JOSe diff --git a/src/OpenXmlHtml.Tests/WordClickableImageTests.ExternalLinkCreatesHyperlink.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordClickableImageTests.ExternalLinkCreatesHyperlink.DotNet.verified.docx index 3fd37c5725c12eee815fbaf2acfb82dd17e2585c..a836baaa09532e652345d0f66335c4fb54022ad3 100644 GIT binary patch delta 67 zcmZ3)y@-3mA10QCI&(HoV*Vp3W;-cj*WQm&X7jIKFJCLNDy3|BhTz+UZdWIZvY4`e W^1oajJjHNxAPWy8Sj*%@R!0B@@*L0r delta 67 zcmZ3)y@-3mA10PnkxA;4nEyyxZ!D>PyjACtjmJW-D~sH%QfHli{d)Jdv@4TESxnip WJT8|9n;2{kWZ_{1YnhzL>IeY#s2k$| diff --git a/src/OpenXmlHtml.Tests/WordClickableImageTests.ExternalLinkCreatesHyperlink.Net.verified.docx b/src/OpenXmlHtml.Tests/WordClickableImageTests.ExternalLinkCreatesHyperlink.Net.verified.docx index 2b339a97f87ce8ebd279848fd63925305d05c03e..94b52864e3ffd830cc2996a1a6703cb4ddebc7ae 100644 GIT binary patch delta 391 zcmV;20eJrY3jYeQ`XQuccrlzVShwWM8i6??$KDgoN}(%Wpl2{+PM z>BikCTEVbOzs~(4u>pi{0_l~)V88rHf|NvnXHvPz&nbhu4!A$vZ|mpRKrp=Dbb6m= zB&?gT&X)7#nZKt@%Sa*Ja l>FCTzj*ee~UcOuT36o_6)CHj|oUve&J_R}kW&{8L005Yaz$pL# delta 390 zcmV;10eSxa3jPYP;auLx}fPKp#-!rmDEi^dtcuy z=&D=}d1XZ3f%YWyYnKlNZN0ZSV@|ZtaW^p*bl_@(9rpoZ!v=$uMI*quFs@T$uc3j@+8+5LX)u#xhRNo#g@wY7rdZ9%#8%QtZf*g@!WLv9~=~Kqd8#U@ohIZomU} zQcLHhc&Ji(Qi}gkYU)}F*ql5bbs{M4Ln^M7*3mDG_%tp(8S*_>1&!7eP{QJ=Fv{SW zNp7>*IvpAI_1!NPTR`|`kXk~K>3x#X zuy4aYTg(T;1z`2@1`THjqz8o>HP?k kb24Xf?EECy<++uglV$|e1*%zMCts641v&<01ONa404e&q{r~^~ diff --git a/src/OpenXmlHtml.Tests/WordClickableImageTests.ExternalLinkFallbackWithoutMainPart.DotNet.verified.xml b/src/OpenXmlHtml.Tests/WordClickableImageTests.ExternalLinkFallbackWithoutMainPart.DotNet.verified.xml index 9fbd468..247ebb3 100644 --- a/src/OpenXmlHtml.Tests/WordClickableImageTests.ExternalLinkFallbackWithoutMainPart.DotNet.verified.xml +++ b/src/OpenXmlHtml.Tests/WordClickableImageTests.ExternalLinkFallbackWithoutMainPart.DotNet.verified.xml @@ -1 +1 @@ -Click here (https://example.com) for details. \ No newline at end of file +Click here (https://example.com) for details. \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordClickableImageTests.ExternalLinkFallbackWithoutMainPart.Net.verified.xml b/src/OpenXmlHtml.Tests/WordClickableImageTests.ExternalLinkFallbackWithoutMainPart.Net.verified.xml index 9fbd468..247ebb3 100644 --- a/src/OpenXmlHtml.Tests/WordClickableImageTests.ExternalLinkFallbackWithoutMainPart.Net.verified.xml +++ b/src/OpenXmlHtml.Tests/WordClickableImageTests.ExternalLinkFallbackWithoutMainPart.Net.verified.xml @@ -1 +1 @@ -Click here (https://example.com) for details. \ No newline at end of file +Click here (https://example.com) for details. \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordClickableImageTests.ImageAndTextInsideLink.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordClickableImageTests.ImageAndTextInsideLink.DotNet.verified.docx index 0a5f8d15547c50cc4601cb35893ec5c9d32248fb..eca3dffea4e446742b66421b9d5a008bc91cda44 100644 GIT binary patch delta 619 zcmV-x0+jvY4&n~5k^=-Uj)9DmmIEk%tL(H*Ga*iaE!a??8;W+=yCTyzG2I|3v9n*> z!#-)hFi3VTMS-E{UZis@iaaJM{`vKLc{^xDcHjeAQ_=FGpahJxHJY}f+t|EY(A(9r z=e3nv4JML7D-(EM(Jn^E84I!lEy9I$U$QmC!upo=*4NHk2_c|qwPLe>qL?!+ z(9q;^W9xm|U0S-m1Fz8hAf97g(KSTy8V!bs@~$rF6xhEeux%QYu%0B3VLd1j(VEbq z3pu{e|K&xGrd=(2$I&P>Kxz!}TSW_+(T5|tKcdHsrcAikD4N0?a_~Ge>+vv&avX-3 z(kzFH&KB1<*99eVf5I#q)mA=#c#`r|QeuRLir#6_LOvI~^}=;1Cxzk-0;eJ!U9CkF zWb5$_t{lp^^*~okhmyOaEQj*Hy_wwu$_GCZ`E@rXW%e>D2ZLmuID2t@qtJaqrP5y~ zOgsm@ftS5Fo3b3DhbX%Q8&v8MWXzt=PS|tDi3OJe3O+Pmr`4)D4rR7DtMWNtE!pu|Bz$@;Yc7T5F3kLB2}e0%tsx?PP0VaIzhH3Sb%NT{gu5Ww$%uE=)`7)ihg$v7T@ zva?~4S)LyOWE|bO_UR`~>C=QN%lE~clAd=JE$55N#U#hIUqlibt*dBxbyXxAk8KxE zbYo+*`otA9XQv&A8hk}>W|=nD#xvS(W2RHFPU6^M=%rLn(`oe=vjzqi0R%6OfsB(O F2SM6XDfIvV delta 619 zcmV-x0+jvY4&n~5k^=-}>)TM1mIEk%i|w>cGa*iaE!a??8-jM&yCTyzG2I|3v9n*> z!#-)hFi5tO6a|K&dyx)7iaa`^_~+N})m^V7Ie_zMbV-X@P6=pXD%4F$_o05bq<8C8 z$0{TC3Una7lG?Mbq-_Y6WtkT(DDG#*g6@^9jZ-}MnQ=|l8CO|n1bC0SQ8Jr<=lLR2 z95sz8ccwbV-NvOoT5uBe58@bBCEY*(r%bT7v{)XjR;S%zAo22!C9-%6U(gg%_m!wEemG%8_V!)PosY4BW`&ESlrEQ3Bo znMGgH`SRxWCZ|Lk&p1m#l@VKiN1{#zC7LTJ>Am6&BtK`3; zNyiRcmNfRX9Axj9Fw(fmliw_^J{H3rPUk{w2<9ypP*Ur_gF66Slkey;5RU<3aF~i_ z7sDc1X?}PRVMu42r=O%uixa9S-scNSI@Xr7SS+uWBPH8>;gM1>wxq@Nbsl3JHf=bY zI}?IYXInyj;cbCe;7WQsPiAdQxG@G5j)9DmeFGnVOK#gR5WR}PeM8BK9ao4Fpfy?qNQ)pTx~Hj; zL{tggO-s5`ZX2pWiZqnegbtgdQgJ6wr_f{TfDtn=uD3 zGqdXtNgx47=hMuO8Ch-D*K2ft#7{G(K?$qzXd<5{OiaThWaM7562du4OT%;}_`s08 zf#6W2$<&TH7PUsa!IT!cH-ixat%hnliyz;QXdr!b!u)H{-FAaOiYVVMx$HGW|zhkdP$Iwrw zum;oE1pn#)I$zhuy3e3wqeoXp)P22IW@AkSc2%YYQnS!hs$BJnK7EFf)j=l>kT!bHZ zWj8vdi(wI@Fh48?-kZ}+ub4t!~4aMTL0+s`gqU%)<3#HsbEL#`EcnowTN5`Q@=Ye|r7qjIB5dj1*j)9Dm G`UXKQ3oI-E delta 620 zcmV-y0+ao_4!aJpc>@Gw>)TM1eFGnV+iu%14E+^D@2i}&Ni(Bvfi2ijpc{&I*t;Ur zwlK>QB-u{(YkSxy?H5ML*CN1BbPtvfL5e)Qh<|?lzPUTfDr&)4q3V>ZE@Bcv%~dAq zA|;K>-|fiV?M;_tiZ>E!7omM^lP)Etb2_2a@)9Jo7fM5ox?CB_9Ezr(oidq!Hj0C_ z=uTF29miWLnW%|(c~IHO|Fu8LlLk|X`bT6Ek)>o04vZAFuuky%Y(<8^<~4y;o(m59 zfpQ<#K*by;X-lDPh;Kq(hIL2|p-x1N1R%;V=C_n!B|Q2tp@#`Q1vFGbzlPBeW~{-> z%Ite32_)d?e3|(%CF|YhdV_&~_-V#87-2UaP2}r@g{hf@l-x^JK+vDWtF`+zJt=0^H|KhA1&4D)mf zXK;-}@UID=^L1^k`wU7(9;Cz|iO@IJKG>1YXfyEX8PK5{!D7+nrHpub79P*9g$~cB zKOV!ciMpg^RZ9F!SAE&LgsV!KQ4?>rmmgPs4%5A`Q;-$o*fDLuf@!hA(RX2mi|_+) z?8cyUu`GfV_J_s9dw04`c>am;a6_$D@8c~&Tq54?E_VYZU3_7ll2iD0S65fDw=tq9 z-7FrIb4t!~1^Gf-0?Pnb(e*lrxl(Q+7L5yHJSIAlqvO(}^FqD-3$x_~5dj2b>)TM1 G`UXJ)^DM{! diff --git a/src/OpenXmlHtml.Tests/WordColgroupTests.ColWidthWithColspan.DotNet.verified.xml b/src/OpenXmlHtml.Tests/WordColgroupTests.ColWidthWithColspan.DotNet.verified.xml index e6e5b15..d36b290 100644 --- a/src/OpenXmlHtml.Tests/WordColgroupTests.ColWidthWithColspan.DotNet.verified.xml +++ b/src/OpenXmlHtml.Tests/WordColgroupTests.ColWidthWithColspan.DotNet.verified.xml @@ -1 +1 @@ -MergedCABC \ No newline at end of file +MergedCABC \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordColgroupTests.ColWidthWithColspan.Net.verified.xml b/src/OpenXmlHtml.Tests/WordColgroupTests.ColWidthWithColspan.Net.verified.xml index e6e5b15..d36b290 100644 --- a/src/OpenXmlHtml.Tests/WordColgroupTests.ColWidthWithColspan.Net.verified.xml +++ b/src/OpenXmlHtml.Tests/WordColgroupTests.ColWidthWithColspan.Net.verified.xml @@ -1 +1 @@ -MergedCABC \ No newline at end of file +MergedCABC \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordConvertFileTests.ConvertFullHtmlPage.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordConvertFileTests.ConvertFullHtmlPage.DotNet.verified.docx index 3ff8716935a8eb2f1d7fb902c86346fbd7b0796a..929594aba7370ade51526e4133c87042aae38be6 100644 GIT binary patch delta 629 zcmV-*0*d{M4T%k~`~n3K0$sFIlK}%Bf89>oFciL@w0E$4zd8ZdbrC7zM;g1>G>HkW zo%l4daC|)5A(U6!#U6=Qm>j2+Rx5+j?dI5D>+|{i*q?L%^Vi|gZKKEySSL~EG#w?B zfG$x;UFEd(vv(7EG(GGxfo0o(_T;uv+GSl%Ywu0Qm@8{&xErAXecPxR+J<{Ke?nWa z4y`a2OK?u=s!?n&Nye<0FI&q7 z_GrHKj^@J86_75w;VP%+z(cMnB`hwFi=v71V)$!-8#cZIRbm61x8E}Ar`*2R(h=zh zU$0O-H(-_2*TiO0ACpcxAU|64|Mw9gq(lLv zP*`5SeM5-U0<;IQ%OD(-PJVcOtBbp+m}&Yk8Hf8?=QJHp_9w%AH`Ny&9uJQur|H2# zudKZUEnyV2B@XJiL^W^$Hm4uqwd)Znx zMDNYF&e2|kxdzf^H(clR9C*w%rG)j%`$fI!=f&`8fEzZx0xhtH&D(Fu`YE^XmGVS+ z!q*k5<`$e*=9;)n$()`+09Pxce|%8lvrOqwM)#aD;%ue_oDBz{FQNm@gF@pgWj$S2 ztXZfV>#m{Gw#@`;bc-*^`|kWkWQ zVQwL(ya_m@914AcPJ;6tLc1=^pvuCj&HPbJYn1!vK3k(74f34k}Dy!`` zSH>#|=dbj?+|r+|7J;o6?~_w9PD`icDe8X1M}V!Y3cZ_HJwPw&5?}crTLfE#eO2&F zuA3*K$H8sRV?)qmZ7co~${XUONwRBdClGeQZ0{M&`h)Np!SH_{5kdq? zAcaP8{q_wZ%1AH)bY007(`FVz45 diff --git a/src/OpenXmlHtml.Tests/WordConvertFileTests.ConvertFullHtmlPage.Net.verified.docx b/src/OpenXmlHtml.Tests/WordConvertFileTests.ConvertFullHtmlPage.Net.verified.docx index 1089880f70b282048442659f44c12c5ee90842cc..63391997165b9bb4d236a1a12e379974b000254f 100644 GIT binary patch delta 569 zcmV-90>=ID4DSrE;4MUtQYN8 z3%6W+2TJG^wqJjs)h~&EeM_Vz_~V-qN*;1BN@n-O_%aOWB{(pt%%pWvd=AmuCXHs7 zH0mgl0xsJJ&?M1-;trpTO=NEAX2gV6+H?{2-cL?0n)sG}(fwP!2nBSf&{mFY259Bs z-(oa4W=7mXwNq>GLfxT9SPfV=2+^p3^1|uXUEDv%BEa&hkgEa#g&bovl20){iYp&s3i9eN5h>XXtsyHtihTF)SPH zU>RH0{=ee#Gg3-_3*>vD-T!SOgkZY|vQS#wzkNf9%tEAj2#;xm1>xH68{a%Oj5!>B z^d?okCb*tVPo{0X=kXU_H4kJ<+!&8*X00ixhYg|ixMIg1CD_venc>5zhLOgQ_AoA7 z4Ojfo8OOR$64rn2%zpz=O928D02BZK00;m803lFa5|^TrYX!gs5CUDaP?InQItEq+ H00000=BE(V delta 569 zcmV-90>=IC4DbxFuolgwij6#P!cs0XTA5tv@S&RXYB1GMTla0w z1q3uVV8JYb&dH3SY+j9KzrC!r-VE2ZVt(Tu6p+$iauv`k;8mqRSS z;g*B%Knb0~_TvxqdY9UNw?tZkAK%PS@{og3GP@_nmtjCJ!GTF-Casg=bBNJ4Wi&4- zqmCje;Ih2{O%V+!?(oXkMCO)mW~}J4Th(1bo!inET4~csy!Vrni&fTlr?v`YGeN5e z|CXb{F)QL0s-0Sg7wQfp!fwF6L5NNTlow97A*1{pn*iIZMmpbrOiY`VPO0?mV?f9C zqVdRUEh&iH!O9EmoK6o`H!@BRKU`*3hH&*t{o4ce*?JM!dhtFP8CJ^}=L@Zx1OEVQ zZI^4*#QFu=izUADXDNbpW?yCemaAfib+(G&**LZsJyUtY_i=fPouTI)+qH9W$M9_Q z0?*j0_x}}_pOI33S|HyG?f&m0LI^(hKo&}i`?qfhky(f|58*MLupnIf`o=ep4Py?6 zAH7M{uL-Uv)01i2?|J-%SI-2pB?`vlnptZK>S04@J+9cXM+v^^fXwh=RKrMPN_!X= zu7)dq=$vESFA3{^cjmtVP)h*<6aW+e000O8001FST@sg*X$8RrF3FcLQIjtQItEt- H00000jV};h diff --git a/src/OpenXmlHtml.Tests/WordConvertToDocxTests.CssStyles.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordConvertToDocxTests.CssStyles.DotNet.verified.docx index 187ce0593456a476cf6c3b26ab740c0a896ffed9..1eaedf9402e959d8eca91f530ba9bd1864dec392 100644 GIT binary patch delta 380 zcmV-?0fYX+2*3!i^#KLy?Aj??lllQ3f6Yz;F%W*0rgv!<3F3xA^x#cnV&c7Yhuy}u z)3gH%zM2Og$yYF4L^yEZGLx_K^L^8qpRe!va_1e}Ldq6{;?+oU2EkxsL#z0P);BZ0 zT+I8rL9_7?i0!-!x$YJ3h+-{7HXV4KM;IaOyjx@Pn(_!!D|$>#OlTly8(Qzge^|V zDO0i^aNye<>B=uUxQ_A>sbdWoRZN;JhJ2AWgP}v0*uIsidc~{iUQUPE6|bhV+rx+@ z<5`z&5JttT$wZc%WZRBTaD_zZPuRiwEZTu?ASr$~9^e|0&S1Nt0sim!C{7$NMVzj1 a@e7k}1CRyk?Aj??lL7=f25|!b0001){r z{qnA_8#Egaf!NNwkn3LYjwsebWYdAyd4dtb&bu`xuPIM3wW7z=#DoTNwxRV-e@vyE z3$Jb9rR55neaW$eG=$e7t(Yjupu@!adaIq{QcAgyyi~+Lad1=Ts0}C{6J$tR;L8`i z-LnczIri($KuhSC-%B-(-Hr6Ws4yn;WbJo}0L&&Emml*mZvuYOU zGEbY)Qkk5CKU;Wc!DtDH4OruomCOB{dAt7;3Q};)-ag?|%u3X>oWgM@0FIvTae8~dK zz4zK16U=LHMZ~?6*rAE0Y!qG2Vz*K!&PDitVo;M=EhOYj8?azTVA~fl?xO;`(x=kl zLa_QGoq0k<))5IS+cszKU`n9`b0^J%vgap3m;IVWEz|1+9q&iUO4D_kESdHL4t&E* z=U=gd>nI0al1~2J3+W{GahGj?X`ZpMK%?50hO3iUjKH J+9{Lk13`bsv)ljx delta 363 zcmV-x0hIo%2&@RO=m7*GoHQ?!>;Wc!D0h~2OruomCOB{dAt7;3Q};)-ag?|%XxM&hPinX7|~(Yyj&T)ESQ#5oe$k7LBel-cxy#@ZEMZrUk0L z15M2HwM)m0*F+{2!l@cM=@w`}`&@~(ljNbT#E7;q78N+>%XO_-M$uYyve7($l+3Z% z2XBJ0lX;z7&O~quTeNIU2iayk@+a})d`A2eCpD=vQbERz1qXHjzI~Cyp5@pUfl7xT zCTlLXb6<0lWx@}w?GtKpJ}jMTnvB1LEu@y*N9qUd&Lh_dI+9tuGIX7$ewGv_*TPYj zBinqyRV^QpYF2hTo@Lkuq49~; zDheFiR5hJojzrj%&G_ANf+ZrI!K$YT{_l7eC+3$TPOotL2a{a`iUcB@ JG%u6u13}F4uVMfI diff --git a/src/OpenXmlHtml.Tests/WordConvertToDocxTests.FormattedContent.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordConvertToDocxTests.FormattedContent.DotNet.verified.docx index f344cc2e42be110ec00cc2ba5e9b91b5ac48451c..45aa49e5991e7e828f896fc23c7ac84f11b2c0d4 100644 GIT binary patch delta 473 zcmV;~0Ve*R37rYB^#KJ?&~G5PlllQ4e~!~I6y1vDZ>W=*QRzf=M+K>WAS71cBsYnL z?Q7XLO^2=V!IIblN}4tuC@6#a6YJO~_dG25=l37$r-LasaR+&`3G!+hQK~S z$61#(TNi)douhBj;jnqbPU5#y8R_H97OXPn|9aR4t;|IOBNjckK!f4rWw%R`BpFX8 zi}%Z!7U|hQ$trYo)o6r*jstxCrJvqOo~DnhyQA|(o<`)2y$^(!EP|$B6 PxRYN5ItI=I000005hUZ4 delta 475 zcmV<10VMvN37-kD^#KLdhP~gqlllQ4e{S0_5WEV(@79!(z;y)Efm^_TEeiAm6t$9w zl3Zd*Sx&Fkj~=O4C?r3Vx`hJxhr}#LJF`Q9zkmK(KJA=kTcBvrOPA>v zR@`OTTsUPsA6k}JJr8>r78;kY<5n`UT8a}KHH#RO2FU|K1lj^$eMDOgthDEzn*4+Q zj$I=kB+Mj5law|3R+=5_i>k79( zX>Cp8PeXPAC~7hxO`CN5;0;qUf90$H4SK~<`8Gf0w9#pUK1J3SDr#dS|AZYGAoc}% z&ibNV`}n##Mc<*vv3SEw;*Vn(`QyYCoU(RW4jL`evHxb1Wmz_AT*}_h52e?kt;r~R z5!ZT;r~RhS7AjVwPgjjb&}rWwG~Wg%z2tfRn9WBy$@BT*esQoBn$K#OG$CP-Jb!q| z1`e5~O=nnQO6bnmLVa=Cf~tTd|8O_L8ZlkKrb{E-CnL`0#PN9)=cjk|2b0JHkOkF- Rz2CZ%T?9G?&;tMf004?u<}LsL diff --git a/src/OpenXmlHtml.Tests/WordConvertToDocxTests.FormattedContent.Net.verified.docx b/src/OpenXmlHtml.Tests/WordConvertToDocxTests.FormattedContent.Net.verified.docx index 2b69d5619227c267b14fbea9386f9412b9005621..89a2802b3ffdec15cd37e1c24ab599de45a4757d 100644 GIT binary patch delta 472 zcmV;}0Vn>835p4@=m7;#&~G5JlkEW@f15B6y^7`Uc1?CyX%>a7(n|SjRjDU1W&o?k zW7$LK_Gm;$8U{cd+Sxyvei%LvQ_jR@Esk)|OoNjOGVVMC@O$8!_h^cqMF;LN$$#i? z*d_8Pp=T*-l18g*X|}9O_tIp%Z;k;z%R*}l@J3s>mWx1Me}!4v2kIZW#JGF^Y+*c6 zv^Bc2$0A!751I1X(Y#8B4_;6uf0MR&uFwmP@|XF^=auevXemS$8I;x;|A4L6;MphW zcwDWwHvGLhN54Uf!{QZt624E(SUk+^LL-fNT_3hyOLNwN5sRLy(4e2Z>~=|f#>mOy z-EyWydNNS3GOAxR8bHw@fvdjs+2dWdSUe=l!CQP^uI^T&x~o3Q-WQ>ZF@gL0q~~~D z)pUY65@B<~2FkN&4Kn_*8Gm;>z!H(pVAaw9lc}`HoS3dhak_dpzX6lH1BwMu&~G5J OlS>3T2D<|Q0002DEbLAI delta 473 zcmV;~0Ve*635yA^=m7=QhP~gjlkEW@f7>7sy$a&HyEwb6G;76KrIqs6s!~sY@z_R- z83Z`)_G&MUa)3;8MJy<7E7j$u(Q36_EC3REK z-q$xP`m|XOd4;0yKzkDMwabTsw%(hZF(+E+xVuFII;_=bJMIIt4I9u_#-ae{e?qxV zvAZl=vW`nl$C4#hFXL>+!N%cBoJztrYj%W@Gk1myD5$aE!0v%=KA@_`EPu#77Wqf| zJ8~&`qL4|9(sL!nwK#h+-3apYEuBF0xE1w~W`;huaEjI1mUt1VX zl;0}Z=%dNrghM8*G(OE~{NN2Pe+k#s|BPNxRK875nP%PYP`ZL({KEiCaHj88P^~g?F@8&m?y90^^)rP&_ Pvy(~$ItIK000000qdMsa diff --git a/src/OpenXmlHtml.Tests/WordConvertToDocxTests.LinksAndImages.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordConvertToDocxTests.LinksAndImages.DotNet.verified.docx index 6eef17b55a141a42420f141dc8fee67c260a0fd4..3a3d3ed92e2a72b97370c628439486760395c000 100644 GIT binary patch delta 417 zcmV;S0bc&V3&0Do`~n0u!K#{)10R3EZrd;n{S`s?MRwa|nGvUl76S&Xz<>d}t1@j1 zlSF}{<7B_q!#-)hFvyLUbVG}@7ZOh-#rH_*_rtIC_0Bu71xgk}Ez3nA1q6eQ4P7k< zZr`os^=3U*4VuA2V6pQqq-v~X&m1eIlIg+gw7>{q=Ut2BHKzqqr$(eE5*mL<$%f85 zbx{;c<+TkmUwMzsKKqyrWi#DgrKkPufIXV4*uvD#`^%i{g= z@}`tR<;XCfX3-9&Q}ws*8Hs;5kVux{iAZ&#azgQ}as4}~mvV2=M;-R!3wHR5vZpQ1 zX73|NE)8shi6Z7=9dZ?7saJV}77z`LS3^F;;yo%jN2Fby!gB?sQ%qW2|L) zbyegXvR%(dc#q8JkJv$b674}ZK(+jEF~Js*PhdCj1dDm~GdVTCd36N*Pn$oJpak0l LHNmQylYa$4BjU~w delta 417 zcmV;S0bc&V3&0Do`~n0oxNfMD10R1&Zrd;ry$Zp7O({uTSFoH#f&v8^pg@7{8EPaE zB{{@!WZAu17Cln0P*AaIr$quMn;{6hu-JJQQZ?4HXO5Lp$@JiLT402*^R7kmn$rTQQzKFn2@QXwWJBkj zx+sdJ^4bQOOWtF%&u7ervYDz^DQW|?j06d23v%<_Cd*>2#)>!DPO<$y0y!Ig2&!yT z%UfU|Z-Y&2&F7|+2c~omQ*2vn;P#;Lktc!EY%!$X#xxP0`qJ0w7{pj@wX0>8=0FOS zZw)%6*;BkB$8!#B2t~GoKQS#T>nn$rPv$vQHQgKcrh93tUyKg_mm>+0aek_(!j8$LDa=E%(9hMZkJDr!r7;9Nx zT@^WoY}fM<-Xk;mBX-c9cze(dP%S@POt3}d6W9$r!D3$hOis;jUL6Gg)8-G8pak0l LFSu@~lYa$499GHF diff --git a/src/OpenXmlHtml.Tests/WordConvertToDocxTests.LinksAndImages.Net.verified.docx b/src/OpenXmlHtml.Tests/WordConvertToDocxTests.LinksAndImages.Net.verified.docx index 9d9e568932dc921f4a5c965ce52edfae9d8a459b..ee5e37152840824bcf26a95348e60193444149cb 100644 GIT binary patch delta 424 zcmV;Z0ayN@3!e+Hp_Hg;4@5Y%V z7O}?~JMETN^F3fQ5cT~?sNW$e%Mx*^C+YnqKgjX#X>5tKYjlLSm$PghlVvSTeLIvK(e{!Z1 zCs{TXMrzAb$$P9eX?4j^GDG#kJE@@Lj=%%0fG_@NuLGNl7_dloh;26xsMGd=QK74n zUqJve+G-!Px~X!$WlD!I#k$rCuC^NAcoK9n#IE>OJI~)a(%rBq?2y>HSjn#B+1Ye* z8E+KH(~qY~bO*z!c+dMpBKE|he@OU8q}WqAAa_u^K6dJ>*h@4{+70`K4SpkyedKu< zvuoY-1S~K9zwd|Y#xxHqyfxT0zaQfS;sed|i)_l{zK#3oY%<#p$ThbzjV_>zj?>dD z#gJ~=u!Hv)0-7D}px*PgAS;Lu>-=PZHDcI3!n?IC z(}`)8&4rQL@>KE;t6e%>GL+0vz3@&dD7ho>KpWtzTkUmVOA!NB$qupY&H;7W{$W(; zs^nJ?fQ+`<2d#cqIo~s-bC_aXYXw((jZZuYIvHYDe5;-32afc6IC{1%HnJ;ulGXud zB7IfZA&s8w>-=J!4<2#IdqX1jf5f6lcqUSus2q?xt6eWU^HNb9fpABh&!lHye-HI;$nT94zNZHC$Q}NV5v&ogF#`lRA84zSH3J`i+m6#P5d9U)?`@oJx0I;q3Q-AGfLLw6z)dEJ zg>M==P0O$Gz$ftwDA#sZ-O??*@M3%Hv3<_W`1se4pNqSll4J|$gwZh#$BYutiAjad zV_N!bG^cmVMHQtcDHUi>c1mg&RWU8Rw^0zdqyWX;*jUgzB{PGHdp9Sne{yN(4q&&&?7(o?(B}N+TfipH|jM0m*xxMqC95J>It}vR*SHyPw%8-5* z&OdPEIdWSx0INuss&y4|D+daP#rw3M95OL%NsRWe^Cg#S%M+pV6RCPBDRqYt`TFVq z<*?$OZ@7aiSp2i%UV-OAy6cL24e-3Ui@gPuLVqI|i8Q7w@PJBa;k-zH?$eNV%7>RI zpUECR`(EOQ|PV`*3svrpFX79+0FY_^{S}sXKN9aaM|<1lY&?D9RCy_k{Kk* zAe??^4ieubH(!4zRs9`=;afJNq>74|hO_z2yn}D^FT5t^jg4tIowC{oMPB%WxiQ`w zb+9F5N2>+!6fmZ5CXJaD8RL)4y!4GZ=uQtV=zo~t@Ov$P1Cs#)6tj5+t^x!&A84zS GD+q!1U>|<~ delta 568 zcmV-80>}NV5v&ogF#`m0nzdY$H3J`iU5?W*5Wb4#_clqlTT0Y)g{TB8K&ni z*dA-_G%Z)-fg^DRl%J+$)h)Y)7hdd4JY#>~%=q)q?>`oIJFVCj$VoH_kH#TqU{cJa zDH2}!d^G2G%S9b$m{uB$XFIKoi|d4!-rG0`Tv~z_Zj2VpPOBVA3-88AMNlJuWtK1n z=cFmL3MOGV3$&1iH(PEn+c&dqhX-n>wcjgXb-WcS;qO5-KqAh9?j z;)|UXDJ0wyIG`=?zVY*&cemYe`lZ-+V1R^&jGBS0Fd9X-PoH~< z_2jQvuPdzu?XRQ#Mya!GPZ89Aaf%A*06YxmVHomfVRL)uK|6-FNv=dA)C+Pueql;K zO6MO0iGqYJTYz;vO6&Mz($yhBjZ`Y&=dRc7?B?yDg_pBKX9cf3aQst#SdPRr z5}dzi4pQG2w^)B;b@LoV(d%%=Ssj-Nk7o0mc~8L>pF~5!qfK};oraAM$)fZ}bA#Ta zKH3WMlhqPL29)qulh(`?5&el-RK7I_-8p~@4lhh_e7%;x0Fwa%6tj5+t^x#dnzdY$ GD+qzZ@eqgr diff --git a/src/OpenXmlHtml.Tests/WordConvertToDocxTests.RichDocument.Net.verified.docx b/src/OpenXmlHtml.Tests/WordConvertToDocxTests.RichDocument.Net.verified.docx index 9161e8d31a5f25fd1aea557f300ca1050042d716..cf204546a605f741346713a8ff05c0f8e5643b51 100644 GIT binary patch delta 603 zcmV-h0;K(n5sneC9Rq(jA84x+0ssK>1^@sN0001YZ*pWWWN%}2ZDnqBE_iKh)m2?? z+b|5iilFzza++qD5vLdyU@Nv^MRNkhrfs46F(^4{cC|h1NV~%1pEzleW@)yUzDS}- zQQt>W)bF3aW;c&Yk~N?eM#nT9GD<)vCKWo5Y2~uPl-|r|n<#%ZNu@wL;_bDKHZd)n zE2AK=Ndb!6p(#OoU1m^m=S9qejlr};lYq6}osz+bu}Pq~(6n~BGU>LS%VxO4W?H(f z1hR?NT*mY>@ES-+Nid%Uy-_=~>s6=f#0FnvOP)YXO9TtJ20Fio(x9vDTx;FB>u*1G z_7iA8?jR*blP7;JPM;gG(RjeZL3sW0y{A}@{+ac%Qi|jDGTI-cJj(VE!6r(KG`IuK z*px9w&%$Q6)`7CbmyHjuFFr0`5!>M_L;6)%cgKF+Qz6%~r?6Mvrn}8P z6aA6IXa|q3;&SPs`D7hQ)k{gKCydDVFK?H_f;+zA7A}8a@!yJj0gel4FDvdP!1LlR zw!RXj&<_M7k;c^f2UJ4)sEgz_^*yNVe0WahGg&4QSadzJ?RyBaDRf(i($eRopFX7P z$<@cOOJlX$t(&NX^PU$TZFoJ$zr}}mGDI1K;}6Y3;=1JK%O4(}{|>_NJ)2O!F8q2j zy_$CL%ltPRuZcNRdc}CmY8w=J;r8OnIA_#emXIB23tuwe!~Sm6h?z0&K+G%Gh<)GL pfeUs&OtAmG=6|zJ1-SwRI3H-M6O#-GQ~^1^@sN0001YZ*pWWWN%}2ZDnqBE_iKh)m2@N z+At8lisg5R!?Kh{gjH3fcGXs0l@pj`l34Yh>sUt^lm=eB)NadY6Tieu-87>q_je` zNfi040>%B%SkOUN7^gS}(UsB0xZFCEf%n0klF^8Yv~jsI`L>yDJ3P2{TH;m$ z*(7T&Q~Dlw10<#-n$M!%s2jTVYNy(XExyECo*InGBQ z^hYwI5gxJTav7k7WSvOWb4h6?mypjN{$CCYj(o*ET)}_hpB484kPGRrEAA!0)8ekS zp%SIgUx`aZp3>kSoDw=jU1axp=s{iQbC+~JQxqbD#ja;|eb0ex3*A@3dis=fy$?{+ zDkacU&+B$_^ESThdAXAfujlxu_>e$`DC6MtL-UZ~F1h9MTfi5-gE)T8CN!+8u%1kB zraKC@{KPdI3TSLljK{38aiXm7Ag&CeQ3qKL4r diff --git a/src/OpenXmlHtml.Tests/WordFootnoteIdTests.cs b/src/OpenXmlHtml.Tests/WordFootnoteIdTests.cs new file mode 100644 index 0000000..b4ea062 --- /dev/null +++ b/src/OpenXmlHtml.Tests/WordFootnoteIdTests.cs @@ -0,0 +1,70 @@ +[TestFixture] +public class WordFootnoteIdTests +{ + // Footnotes raised inside table cells must get document-unique IDs. Regression: each cell + // built a fresh context whose footnote counter restarted at zero, producing colliding IDs. + [Test] + public void FootnotesInTableCellsAreUnique() + { + using var stream = new MemoryStream(); + WordHtmlConverter.ConvertToDocx( + """ + + + + + +
AB
+

C

+ """, + stream); + + var (footnoteIds, referenceIds) = ReadFootnoteIds(stream); + + Assert.That(footnoteIds, Is.Unique); + Assert.That(footnoteIds.Count, Is.EqualTo(3)); + Assert.That(referenceIds, Is.EquivalentTo(footnoteIds)); + } + + // A second AppendHtml call against the same document must continue footnote numbering instead + // of restarting at 1 and colliding with footnotes emitted by the first call. + [Test] + public void FootnotesAcrossAppendCallsAreUnique() + { + using var stream = new MemoryStream(); + using (var document = WordprocessingDocument.Create(stream, WordprocessingDocumentType.Document)) + { + var main = document.AddMainDocumentPart(); + var body = new Body(); + WordHtmlConverter.AppendHtml(body, """

A

""", main); + WordHtmlConverter.AppendHtml(body, """

B

""", main); + main.Document = new(body); + } + + var (footnoteIds, _) = ReadFootnoteIds(stream); + + Assert.That(footnoteIds, Is.Unique); + Assert.That(footnoteIds.Count, Is.EqualTo(2)); + } + + static (List FootnoteIds, List ReferenceIds) ReadFootnoteIds(MemoryStream stream) + { + stream.Position = 0; + using var document = WordprocessingDocument.Open(stream, false); + var main = document.MainDocumentPart!; + + // Real footnotes only — skip the reserved separator (-1) and continuation (0) entries. + var footnoteIds = main.FootnotesPart!.Footnotes! + .Elements() + .Select(_ => _.Id!.Value) + .Where(_ => _ > 0) + .ToList(); + + var referenceIds = main.Document!.Body! + .Descendants() + .Select(_ => _.Id!.Value) + .ToList(); + + return (footnoteIds, referenceIds); + } +} diff --git a/src/OpenXmlHtml.Tests/WordNestedComboTests.NestedCombinations.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordNestedComboTests.NestedCombinations.DotNet.verified.docx index e606716bbd81cda426b5b50618ce06c898473c6e..f6cf2bd3d712cc980e149bf87ac9bc0ff5a0f7d7 100644 GIT binary patch delta 1899 zcmV-x2bB2gC-5h*odXU9Mpc4W3IG7mZU6ug0Fyxz6SKYp`~rVfk~*9Z+N+b}VK*7M za5c}^E+K_;PF!2YZWL#)~6X+SP18xBv*~BTAOi%0z^GbCgPud$2IISQv@LC9mR6 zlqYyN@ybz=H57ld{uG2dqh!M7pN0Vl=?D_2a-((g)BuIdjYbDtE6FVB&*f#O(@7zR z{4IU^cs`0HWG28x-IG8e8DLgrkd`&Nvq+IQhd>SmEcDvkn9zuDO}f1k^~l7ddNH!B z)WjucN}^dGONWp_Ysf=EK%i@wX==PPCRg8$7fj2O=5T*^XXxw6yp~e=Xb3bjC;Gh3 z`2V(mZM~XprGK#j0HqbCm&68ATHM0`o|*=fH3xV-zslr(ExMPRa2FxG&S~nq&5=yI zGdC}0n1;TZS2hwgHL-55 z2^(vgl}zQ-Y3BN^EMa9=bK+0rA&DXA_A$t*<8FUU&rj-<=+B8#M2kJ!Rq|Q`$=_M3 z-j1gW47RJ}Z)&ZOz|K8VeAi08p7_~5XzEq->GlZ*t^PIvE-I}k0M2B?RDQ)3hlXao z74nbU9(O}Z=H&sVWY%=jcEM$rwzrn-xV&6jMChGbXM!??vX5dg)@4~(JsyoR^B49R zk}-ehig5*BU3OkwUqkeHby(3yGa?Hj zk4ZRt0#b^}Jlbv)>DF6A9c9as>Vqe{IADL4&KOr7h-&$>+8p0!0f|EZ7yY8cbm{7n zd?W{`+ed-kqxZ)@L@#?QRoGQ19mT~cC|KF}R7_1mz;Ph~dG#M9ERgajm#|LO7RS&~ z16HmRlMh>5AltlRt0YZ?AupCvHQ64o$kq_GP7bmJFrGTao-s#SRjSrjWw&`iD?5Me zddYeBx>FOVynr!bMC*iMgN=RvA8tQ=tmdQ7HiwZu7WZLb18Hfp{qqRj09fM5*N25C zUG7welLa_!i#B+3Q%$vEQ<5Hll4#6D>9~8^(X}$=s?+ zNm&|k$2eo z590H43~A{k&DfyC@7iahLnS zF2mDDln}g=JqslQmcEl@3nPCdoj4_j{IsKgvkb1(h?|=m2Hf01@pzlsw^wA5?>^o~ zKdH?)pRjn6-Avb8Em)WPfS09{f5Dvp{7X^}^R8$w&@)|H`cgc2x+3vP3m)nT@;Mqz z36pOs_RaaCHv8rU;hJY35v|%IuVSX$>CoDO(|&LgrBtJ(t?0NiGy;FhXXb+FbSR6L z_}+K+TUT>*veumZ@g{4lrNmnz0Vh`qc{2^Go}0ySr{He&+y==QgXHY|*rx$Cq>fJa zDs@z!&K2%WZbT&rLsY9eVTKJsyV&ipc+7CAg{f0#@|#bLG6nO98Kj8rs+S4p7PwvsRs#8Bhj)#Rp(&ICo znc3KICW6nDXG4DipF;U6lZA~rMp6{XJmR&ofN#KwJm^_9=lT8HKjOb~G!ZMB&a2TB z!DQ^1)1O8{{=z+mF1Sr?FT!j0HDQFND?Dq4Nng3g{tORT&r6ORUbH#RCZKJo$3sfu z{krfFU}jpM$=!D3y1=4+E&8j?_Go*lNr!rex3}eOowW^!?ey#8+D~6i_wIiHvk49y l1qlU4Rf1Ov007XFE)!4z6_bAxKLJXUzY`?}R}ugK002|cry>9V delta 1889 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 q8nZIgb_-sts6ms#BO{_U(ZosI6!ca^% zC04)NJ+Y<``yDx@P2DH?Gl;)^e`4{EJN`rvm=9-=oOZ4dl!WvfpXl*}3rv`P9SoyaD?3Ni$rLL%bj zQy?aU5vMUN@#p7)Jt4UG6%8_)C94F>tN53Mx^6lXe?enjd9Y16 zr_;*A8J>96Z;t&CyXb1i@n0&Ax7L+ltt;EC4xL+dB#=f zNJjHtAPzCf18X}4fg9*gA~?xKfq8`I`l@uXPmsY0gMpVS*N>*jrI^e}c42q<;kBE+ zxp48t5Qq9Qjzo~$33`QD1 z>sn2oBY{mY_W+vO$aRqmxa4jKUNG1T2CE-~-NI>e0PG7SQOWGp65JUHJXpixtJm$; z62%TEc-+d$DFIlIs$VeDMHf}*5!%yde}bUelmx=4%8N#U3w0y(CB=;0vjWaGa65n-6EF>P%*NuW)s2%LIHUK=~kLfMksd2kTd*%3|uV{M(T0t zt7H=HF&a%gJu32*G((J^5j^o1pGHE!p~G;%^|W=j8~`HUjkSYZBgw2;%H?HOe5K@t zzh!S9_YJ}me-m2jod|-&$9YRWrIOZdx4TM_w}3znL6AwDJjNs@T#{}cL_M)6AaY?~ zC^dE|09Pn(4?_pz)6R%S3=8OVi{_Fj?@XA=#=F|2In>=7`dYfzS}7ll5Xr5HzAPsH z-xib2t}GJ=5XM$vd5KLRrNKQE;HhaqS#yGy>#JPuf48!`-h#U%n{>N_u5M6{!)POB(7?WO=q#I(#%ywcO?MUD5oxfNCm0Y1c~zSa>4ah3AWCm#x|ivTwXy2 z1|anU5wvLlIFf014uBX8B{N*Ts6Z zHV9-s)-)@*#@VZN^EmUel$Cw0i9h1UG>3qDfsxxyB)wWQ$uFr<#G5_s8hNdW5~ZFU%_>V@kPCIr@yoBDMIAl#%q7rCA=VK=)ik+bu3HZGH(A6osNG%q=pulKMYm zf5!voggOs@i-ffllVLvf^)#T2L!S>##Cz}I5ZDWXfHWA0{c!gbJkH-eE~n>*&HE~{ zPt(m5HD@2m$QI69$vi6I90*7$C-bP?EYhnpi<~Fbvb6ia)^OB=XX#9&B~PfkQvIy9 z!1sxvG$07P@vBVJ+TA6^NDhJC5Crm!f8OtZ7r*Qe%iKecQafoz0Rb~H`jmjdB4dHH zs&uVw$}WeI2DhB|Z#y-C$_q3W_9bmXalpoW{;T_sH`QYF+3qlM3fV&xm_S;Re;)rl z0(S@*;>kD93s1V#sSYPga9WE#cz0LLv=UbeJ%j{RLMhsLSN6#aH1qBZhXQhe_zkfx zl6q|r2-gR;GUy59{1PpW=+vg_3Uio(%lT0Dl$>^6$!^r-#pj5uHy2{xjq3m`Y*HvMMNe>aN1vJSkHl@2`JF6?XH70N~K58!Jg zqQDr-UwcOb*_Vxe^ia}%s3=R@%2G*d4B6!{?GQ-ZTIO%%g~Omzi7yX|qZ1#_osV9TX5ZKv9stuio38Rrvf18VEN2Xh< zwdIMl{J)PU$YcfPM6A8OY0tUFctzW}#th}1=Nc2Jn$NYTDDFR3PGQZPInT@;m5+Uh zzfwMCYa1!TG%}NMK0>y%mbq*g~E$3K{044`rKM2$I-OpL3a=b z)a?MFWytE`3OiO$P;SPmfAQ9uEUcyCR|C`%@i`JpG4+~W(`?E@b1~(bp?MV3Dg*K+ zWGY+@ZS6N51}AYvHQw2ZpH>EJMCd$+BWu3a53TlLxx@FNd(ggGV3M^~54LYV$LHl#H?lY>FRSZY66*DiT9d-|PW9Du?UsKU}Vy^W4?d;hiYxDq}~(D z6>4zL-J2uklo=f#{r>HrCiJ0c;0bj&kdP;nZUH?IqqU`~f1Iig#lT&l96rk|zTz{!@%c9VxKtn81+S<636_x`_7O9KRxjuRTQ oM-CDN35wYKt$PXp0M3(Q6Hoysld}^)0aTOo6D0all formats \ No newline at end of file +all formats \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordNestedTests.DeeplyNested.Net.verified.xml b/src/OpenXmlHtml.Tests/WordNestedTests.DeeplyNested.Net.verified.xml index d29c1e6..2c3f086 100644 --- a/src/OpenXmlHtml.Tests/WordNestedTests.DeeplyNested.Net.verified.xml +++ b/src/OpenXmlHtml.Tests/WordNestedTests.DeeplyNested.Net.verified.xml @@ -1 +1 @@ -all formats \ No newline at end of file +all formats \ No newline at end of file diff --git a/src/OpenXmlHtml.Tests/WordRowspanTests.cs b/src/OpenXmlHtml.Tests/WordRowspanTests.cs new file mode 100644 index 0000000..957ce14 --- /dev/null +++ b/src/OpenXmlHtml.Tests/WordRowspanTests.cs @@ -0,0 +1,30 @@ +using WTable = DocumentFormat.OpenXml.Wordprocessing.Table; + +[TestFixture] +public class WordRowspanTests +{ + // A rowspan originating in an earlier row must not cause cells in a later, wider row to be + // dropped. Regression: GetColumnCount ignored carried-over rowspans, so the render loop's + // column bound was too small and trailing cells were silently lost. + [Test] + public void RowspanDoesNotDropTrailingCells() + { + var elements = WordHtmlConverter.ToElements( + """ + + + +
A
BC
+ """); + + var table = elements.OfType().Single(); + var rows = table.Elements().ToList(); + + Assert.That(table.InnerText, Does.Contain("A")); + Assert.That(table.InnerText, Does.Contain("B")); + Assert.That(table.InnerText, Does.Contain("C")); + + // Second row is three columns wide: the vMerge continuation of A, then B, then C. + Assert.That(rows[1].Elements().Count(), Is.EqualTo(3)); + } +} diff --git a/src/OpenXmlHtml.Tests/WordStyleComboTests.FullFeatureDocx.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordStyleComboTests.FullFeatureDocx.DotNet.verified.docx index a822a23878c85f3f68d533efab397c5944ee12ff..bcb00c7f27173760f17ec9be06189df7eac87ac0 100644 GIT binary patch delta 428 zcmV;d0aN~!Ba3oWg5#_~jQ@?GW0Xu759y zw!Zx0suM!H*Dgoh5Zc}2a?}fw`PS^oj{@UKO2=0x}57jRXe-%l+ZV|%YAI;bY^qh{eNrx8iip0LeJ1=8Gu5VOxNUbAKw2~$I8({M5BVIuXv}#t>{#qoLoL` z9r) zzE$RPnL4XKT3+iJ0Bcn6lO_03tNZ=!t*LHh2AHO#BY6uaAzb4WUZcY=zqo3L(0}f9 zeMz+Sq)s*`=q1zNuaAV>_oS8`mFeUZ4^_ zi=YTHue3UKl@IU#TjSR#1p60yhCa&x6vAY>CXf5@{%v0iBcU5I+GDlPeKF0f&=o5h4b^4*&oF0II0cs{jB1 diff --git a/src/OpenXmlHtml.Tests/WordStyleComboTests.FullFeatureDocx.Net.verified.docx b/src/OpenXmlHtml.Tests/WordStyleComboTests.FullFeatureDocx.Net.verified.docx index a0298f88a0e916fa980616ab236c8bfcc6ce9802..adae2f39a798120ec068b1de6c7046b0ecea496d 100644 GIT binary patch delta 1853 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 p3ho636aEvW8iFt_5x$E^3OfA%iC%&a8KEn> zde1c@DT;=~5vb9Rq^=Hcj4blOaRFI3D{0uqtH~RT5zWVj?;^Z#K13w?&WryRCF1w6_ zxa2{P`Oqmtmzg=mv=L)_oQD8@4`ltqr36_B&^q%nyVr1o(p2}CYR_q_^cQN$3|aOY z6+pxx-R93*Ut(3=mCICQ%Sg3c{a6iMaLp6pds{Ry?e8zp!~ zZs!?p5#tGLb}_@_1sap1<3m8FM`ryWlePnp3D1J(3Zel30Gg9e1U3e^0{{R30QOto AD*ylh delta 462 zcmV;<0WtoE35yA^^#KV4AwAlr0RRA*ll}o5e;XkXew7vXnqAj&-Boa;=8#@eMXJ=E zF^qSSfI)!0PF}5tJW^hvg2i6@kVJAW44>fp=8xg^-^&cgsvLe zJ=csRFB%d@phkZr4Q+U4WRW+H3&^@zNy9c?P2OOPXs)Jv)%DjqS2idKKR?O;b0wc% zHi2F?@7^n!ww+h<4)y5p4A6^PaiocHe=Q>7f%~bs10(ar*;|kYUeXAW`}ppSQ5Ww$&tRK0QAS(e{XI^HH8m=f!X@98pD-D(YKrNXP zj0GX3Z1U3e|0{{R3 E0M54C1^@s6 diff --git a/src/OpenXmlHtml.Tests/WordTableStyleTests.CellPadding.Net.verified.docx b/src/OpenXmlHtml.Tests/WordTableStyleTests.CellPadding.Net.verified.docx index de47587639f0cd34ce02b0b7e592f01f3994771f..07fe491d4481f5f41142dc26d7b26bc1523daa5b 100644 GIT binary patch delta 453 zcmV;$0XqKi2<-^4=m81Og69gA0RRA+lkou>e@kw{Fc7^;hJC;#(CKJ5>I z_J{YPGvUp|ozE!ifMe{W28!*!c5o}BH1*t7lsFFTxublIgH#3c`U z%7=Cts=y2>`WMl*M`#k>k>z!$Kli$nL2wqyPq{(P>Cxs3LSSBle!V1H65&**h8*798Lf v(VoGiv;m$Ls7;QxKLXnQWR`!Eq63Nv&w}R)l>q<%nv*pIHU^#p00000c-qtK delta 457 zcmV;)0XF{a2=NH8=m7}?AwAle0RRA*lkou>f6Gn+F%Z>%g|KJd1!K4qY>I52W!1t zeDqz+H`U_9JvNC{gxBnfTrAH1@UIU>~FY0YFx z!YdL2hoeNZC)HECOUWo2nsJD_QAtAPt|l+hIjpXxn=&u1f4o$-6!82{{>PPkdfEhf z+Po)IieTpcd_>ticm(K)muqOe`L-hbe}$Q@xqcFcqoY|68`h>_9W(B%$DvDqyMPjB z?&%>M+6Aa%2V;a`A@ko;$U#!ABQ9hZ;vOplePvr_bDc^;QnIN6e5mj2b)rMIBZXc3SfR38d7pM zj4(rVM=)Qx2oLkK5038t1GM|c%zpuso&$;r1R*`znE?O*nUgUDHU^>t00000A1&6? diff --git a/src/OpenXmlHtml.Tests/WordTableStyleTests.CellPaddingIndividual.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordTableStyleTests.CellPaddingIndividual.DotNet.verified.docx index 88dd6d4bf58a6c5c0838448c617ebf38cfa16ef1..b88811fff46a066d7aee5ec2db2721be284e0586 100644 GIT binary patch delta 452 zcmV;#0XzPN34sZ)^#KWj*aaz^0RR9(ll}o5f8!t!{S}M*1}BZOS*xT{duT6}Dy_78 zCNQxr0fPXCw7<58{iOYc6&S~jlv8@~Fk;@D$2`NoKY#BZPFht*aK1;w$Z}DW3XDM6 zo0gFw9KLPI!|pyd62+jwgzBV~@l9l;3&A#&deMRAet{OuNvi`o&4XW{YiUF$twRCc zfA^-5DL90i!Fvn=_04eCx83c{d*gcvFJHv}x)Dz= zO`wXZHQ#4kOIjI6g;3(meoMpo;q zCDZIvm(7(5de%c{g*GFz#B>RoG7;;*gF6D*JqnzduO?kYG^b7(J-Dn)9~ z7{bWQ4x zTr-lpXvi3W8vPlRy70=#x-1+QPP+zj`sA8v21Y)}$@eiHxtMm+s! z0{v*-53gii_Flx?);UnBPEG2jV;g@JArK$1&u#j?4Ub3bYVRG_I0Op;Av5 zKY)m}NB{g=p5d0ORd!r;ZscYf{nnQaj6bkKG zk>m&1S|Q1C(qHSLpVVKdB+GFEIk^|Fmfe{h?I8bt|E#yirj8C==?X-K7PC2uxS+r) zQDmq!`CWpxn{}5mpl!p2iEO{nX_ukW7@1N9 zf7jG_IiIg^Ln?tB7z72_}e#D7f0<7*S} zwRz8~V(wVjxPy8G341U3ek0{{R309K*fR{#J2 delta 449 zcmV;y0Y3ic2&v2o*?|%TU+MI;eBnq83Uc2V-2j zf4ZX6@pw*aR!ZVq?ob?kw7V*XHyszBN$W>AF%BLTq=YpVNkU`($#r&`148AB_N+>Y zpHhM6aI8@Gq;_deDVc?ZVjM!>(5ZsV-b~)1wOHRw*JWPZKD?Bs6!7v-{>P1cdT9c^ zH1AoL!Y^|dw^$8?K4H5`-hCsFzcM}ef9cGhO#j#~4lF7+mTB8n%obNG&c?Ws>FjC= zND+t3ujJ`j4jlxlU|LY<-#~vLN`nEt2eRHa#*bA@_{S5dp+VP1#b#a5{gH$n0k?5R z{(g$OCXeH}S5|n9i~B_su5za<38&-|vu=0cDh;sP|A751A5a0zxt5ZL?szjq68i_{ rjScYZb1#ne@0AY!g7q(xm;;Il`c1Hrk^uk!L6a*4HU^sm00000zm3=^ diff --git a/src/OpenXmlHtml.Tests/WordTableStyleTests.CellVerticalAlign.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordTableStyleTests.CellVerticalAlign.DotNet.verified.docx index b6c73456f3cb73b66a1f33dd17d710afe8153f4a..6dcf6072590d1544cd137fcfaaaa3ccaf9c13159 100644 GIT binary patch delta 467 zcmV;^0WAKa37-kD^#KV8SZY$b0RRAnll}oJf9>v}y+o?isy!2!U?j{SAaQnot%rV6 zf1v`~>*V0WrlB>1$sq0^wN105Q4U81l@or#T<+IC;qVL~;-Ia*zVxM5)yHz3iftIF)|>a6p&O@m+@~JUF;cDihH{G&B<$;+ zaB^`%4R8sbkq^rZ4~X#ss$i0RRAkll}oJe|39kFOe#>YR?2FHWFqKkT|=))qz%E=lzP!Z$Nd5=m~*EO=sF(!0$oFUbkaH$;Jq?Ur|GgR*R7-NBJ1nu-C)#%?+DhMITd?r0DDLXn_%a3`hz(oIjwG;Cw=8i@rW_%}C6mA#wz2 zbR(&+{Szarw6R=3(oISl4q-NVk0D?;o98wYr)1nV zPRY-xbHZDIUQt5U5c!!EPoK1E%;+?re;u!Z*l{=AfS!@%>e+>nXFde_z+HGT;?m07 zAz~9aLA&|>l>JoI$8sGVwTx8j&HK$@*fu-vqEWPrRIBVdC(WV> z=J%d(9Fr4jflF|Vd{`!UKn%}da|{Vy2E}NOjsu@gKa=f0lgR^-35YK)yS)Jb0D_Za M1U3fK0{{R30Hj^w+W-In diff --git a/src/OpenXmlHtml.Tests/WordTableStyleTests.CellVerticalAlign.Net.verified.docx b/src/OpenXmlHtml.Tests/WordTableStyleTests.CellVerticalAlign.Net.verified.docx index 7d669540ab0840e952634e83f4b88c430a09b020..89164c705b686fab684e66c662c70ce01e694f36 100644 GIT binary patch delta 480 zcmV<60U!R135f}?=m7}`SZY$S0RRAnlkou?e{bU;5dA9__bo{pWwTM8YP*NsOQn@s z?VbrtY$VJeAaUEj)Z&9=zx}o(*XyfxC6MnL(7p)q+O67>)ZUvFrHZZT8Tl_EDctj8r;G=<(u$@Oz&~E=GFbB;E zp$S2Irb-e|Q4n)DDztr4yK+w@nWu$e91?G0Qn2-BCvVVuY|c)%vZ~KNTxur;{P|%1 zzccgcX%XmY@qSw=;<5_!FI4%!F+fjJe|+ZPdF&YZEwe+V!)Gy@AAK0&ft^-!Jm65V z@sPeMV-fU7&;F;9mF_JJ>V}A{+$6>%JwBV>Lu0{#-2++Qqe<~8%cjwE44>8q9-0K$ zcUy5Qg;4P2+{onbcH<_cjk`hfQ=y0E>45u=ySjVIQm4}&lSLF;hxKB4x$JIZF{&W7 zNX~_gdA?A(;sCSm0Y?`HR09hyza-yh32qVn5v+DT!9%a;#nHa!)8T)zegl)e1BwX< WSZY$S0RRAnlTZXU2D$?P0002%knBzX delta 483 zcmV<90UZ8`35*G_=m80cFD|>Z0RRAklkou?e^28u5dABW@8Gsul~R!+;IMl^LRti8 z*PbSp9FJwY4e;0Yus>=40{%!EdMHTQ8<*Nop7-W;#-9B9^Y`ldrBTHmtdpoqGCiM= z0yIYZT9 ze`Aq@b3v|AbT*kRXv3r?QF4RgHLiZ6y5+%ko9Cj9DT0{*Uk#Lm?L3l#cIUT2Ie1P8 zO$gdERg#!RLF8~$XnRt-a&IM>r;TA8l5SE`u=S(K8}uHV(R3@TdVF%JofPovKlx81 z`SiF6^tgFHtrT%vh4l_qPIwB?BZ|)&e?0$Y#r=(vmF}H}cK6qS-!nVhfN))A^A8tB zys$3Nd1n2m5roki7nE&^Ho9u-f?q4}+pNNBeF1xe=004rMO$0Uuy#oLM006gJ=sW-b diff --git a/src/OpenXmlHtml.Tests/WordTableStyleTests.CombinedCellStyles.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordTableStyleTests.CombinedCellStyles.DotNet.verified.docx index 7fa9508c8d0c2ce6aa7f80508e351ab3d4de395d..b071c17158bfcdc0ffc6069813a24f8f9f89153b 100644 GIT binary patch delta 686 zcmV;f0#W_n3gZf}^#KXu#&Mf|0ssJull}o3e_wCgAQ1m57TW6qalJIL>Qrf#Yi+uzuF%5k@giTupKvA-TbYzDa`|L(cN)(zvGASpI^VPu2%_nmQ*Sx z(g1Wj2skvwg0M6WK(3>YeQ-UzDtsbvp3qb~tAwY@F9NX8I`bh^ctMj;bwozfRl*}7 zf0IzFj*u}dgd~|1m@386IN{Jms0WjfrNFqH3-VxQJ?GVQC#%K1Qn7re*ib}`yv@J%UCip7QEcBO@JFFy7Ym|vGRzk zhPY_y2elw|9+=Zm8l8x79sqaZp--1}+u++!HXUO+toNe7sw>(R*2uT3)abTpz;!&Z z$uzT;b}0Xtm(DCEb&6B_+b4Cn_Gb@p4P_>&qH;;W@Jn+a delta 686 zcmV;f0#W_r3f~H_^#KV`1))fK0ssJlll}o3f6Z(yFgI05D1Z4B^+!I%>Rr(Yxwi)_u0iFQ_)OVCyh=} zvq4ZKG&5Z4G(iPSK6cQ>`Psb34J$ICAzEZgyWTuOQ-HjOv13z_QP(iJ(2Gn>jLj&x ze}=Iso*T<^YnX6O>NHb0CZvrsDmC&d$A;he9mcBKZfte9jpp7&lMq5sH1EM(E)v8S z6h*jVxiXdu>jGu+Xx^Jql^}0O2`|EoGB!c90OLt_pP*JaA}H9f3DQpwz+2wbIC{hB&F| z2RG%*IG7UDdY*|?Dnap}MLwM_+lF0JyY3j@VeO~&0kyD|V^#Wk7NPl;-|@;Fybd%E zZ4s98i*czclw=vHmL7KS{E`REuj9Fpf?$UZK7FCa3L7vRX@ z@!}_Nq#T=qMZus-emc6Z3fAnBwl_WX^GD~TQ>7wLuc=QGm^?wvRx6Y>45DkYY>YMS zXXG_C6_g8`pwqG?CdR-cGA$sGt0{5m99|Cmc>QO>`2zp|0RR6308mQ-la~aC2~Y*0 UNO}SQ0D_Yz1vUnd1ONa40Pv(m^#A|> diff --git a/src/OpenXmlHtml.Tests/WordTableStyleTests.CombinedCellStyles.Net.verified.docx b/src/OpenXmlHtml.Tests/WordTableStyleTests.CombinedCellStyles.Net.verified.docx index 0c857c85009160e3dc4da0577f8fcafb7f10c742..b49d7f7cc50c5dc57a014e21af3a42d79eea595a 100644 GIT binary patch delta 686 zcmV;f0#W_S3eF0!=m81h#&Mf=0ssJulkou=e^}ja+AtJ;mB`xda7Z%>sG*_^wp~oC zgtT58$NABcII{~lrNja5 zBLW62Sni~9>9(e5DtXo@a)2@v5ha3YM41CnCa5C$vjc3sNfDO1%tBK{JkeQrMTMYo zXK@z#L3iU}!b6vQK2iRAM|rpl0^G&#e-jH`we{tFMx)$d8v%DjM6LtpYoQSr1##6< z4?J+oIB-M|Q#@W6Nz$6z=7;ODZ17ja>WWbnHfYpWR7E>Og?u|piEbMQto(s>q^XwF zOa8+uSEey3LtN4c$Lc5Z)>oqN$djS&b4U*S_ebCmdF@sc}O6l~!wCTALYm(%Q_joz^CfJ!x%;{~>gJB8=%VGvL1u z`kgiEjYj9_vbi;1e4Ms-*}sq(>G?DY{iF&(e$WwCGqp6}K|$~BFJmXm`&-;Jhx*mB zr^SzP;Y(sH1;syJ@Z>dOy~Htce|K49#6H&;SikCDZ~JY73BK0MfhAu7nz_vBwx&~@8 zubP@xWmZ!GLGtkDfa|1RTd)p4SpTaq`2$c(0Rj{NlYs<;3F5|a Un|1;K0E?3j1vUnM1ONa40EMhZzyJUM delta 686 zcmV;f0#W_W3d#zw=m7~(1))fC0ssJllkou=e;C_t+A#E2B5SY1AVrxfLyea;dbb&4cZO&;6Dhe*gS+b$uU4#+r#NxS!Hr_ZW07%CeXEb0j$$C6xiM;c;hzUqg zfB1OAg_{aaS(Zt5afB_?9N?IQ2~sRq+qAPY7e*4a=C=9avPv8Jm53%|RE7=O`2i8I6Qd4&HwsJK9rw6a2RDYMIxX~4 z{IDum>5x$i$zDpM&~H8xiw#WoR+Gtr%~u9HKzPzVxqhH{#Boe18m+~T*j7@_t&>+4)t4QPkkSv z!k5HY0;fNAXz~)azQP`I^QwA?e|@eVvY5^7`RsM|kS8{uy@xCo=5u<;Au4>!!QkEr zedKt1+xkc`Hff2BLg{>e`B(<5KQaeObf7-Jh8M%q6{#-+X6W>h7q+d#st3^qEE1!E z)r{Og0V8tebI@gx5+3L91oU&zU_B*P%He9@$IYJ!lRr>P0Rj{N6aWAKlZga?2~Y*0 UNOA%I0D_Ye1vUnI1ONa40OkfwRsaA1 diff --git a/src/OpenXmlHtml.Tests/WordTableStyleTests.HtmlCellpaddingAttribute.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordTableStyleTests.HtmlCellpaddingAttribute.DotNet.verified.docx index dd4688f0cd5b394cfabeb01305173717ccbf05ff..545fe7623c3460e5392bf092309a7e37995e8d59 100644 GIT binary patch delta 479 zcmV<50U-XK37ZM9^#KVFOumP<0RRB3ll}o3e?e~BFcAC-!Fx?9bzD~n9ke+lhZ-o* zqB&FKS|UJc36ip$e65E*sb45aNtTVkhqxCl2ilok&XWB5`_J9|p)q6+&TDKr%@-LZ z&`MOgtvKz%?!%hiZ|?d+q3jxH135IN^+nHV9fBrj)YpWAOlf-zR8c%PDL(jFSJLml}J z@h{dI4T|iwAR+{(%PzoW-58FWkFp+Q7)-F$kJr(@{ByR72G*SBtM$kA=&-G}Fv3M& z(Q=wEm&xR0`wWLc%{WeWMl}cpj?+&!3GNWX39P!1;PFUJ&9UieVDq!H`IE>4kqHk> VzK6B}0069$Vgxn@%mV-b003P%=Q{uZ delta 482 zcmV<80UiFE37!eC^#KXOGET&~0RRB2ll}o3f5DF1AQ1f(#J$1ZrfF&|S+#rU9vZ1q ztL_=Y#74q^U>s-n*LvtD^%p9z6DO*6rKT4TPt1EWegpje`Rn%X&>6Ca5VfoG$ai3kg{_D;e0~)#TbCy6$Rv&`o=Nac!ek@bI7f zA6N3(Uqh(PJs7hUVIIlAY5DGKg6bfqf8r5Ez*jR)-oBgyR=u*%E_-5^$Ln*@DlMC! z)x9&L;2B^SSvb;U`uaS&jMGgx+TsI52z#KLRFVuDTk`OI`-Mm_#tW6MQ&!E|V?%zd zBmY7CqxVLGB6}@}Kn%L>5nk3!;q>#_c#t@n;G1vHlYQlfVwDVRIW1S~x9iDYGv91Q zNWMDHX}Mfxlk=gS;W(xlr~5Rc7KDPp>HC`uch2D%YE3sP6 zjy+8*Ik9C=8sOJB@Jag%*iO>4LOoC}_B?sso6#HF|KC5Zu3sumHsHK6bw;PB6H1^K zMyk5VXoKsM1-)KfwM%J4Q$dX+? zf57`7S7~-Onao+mRZXL0Zscp+G1vX}$MwN8X=91dOn|QzGRhk?l(7E%HYkUl6EZd= zZMe>8tfC=ugwe(gq;~1wGCE6v!58bmgWCXI#ge2|SdxdokH3is&~h2yQ&LUZT|>63BfcSiW35&o z$wqO)5uM5#gu}Y5>z!@im+PFfPUg|TGQ{)6`J#8&7Qs1^rWp=qDzjvA zvVDZzphg@gJE9VJxF8w*b(Y}T7~FwHg9&c0)X*Foz6LgaJF8EVxC4p_4@|y?ssR81 PtdmOwHU_f;00000UzzL4 delta 478 zcmV<40U`c^35N-==m81BGET&=0RRB2lkou?e{S0_4E+^B_f<)orCAWC*m_tGEij-( zdzK}}79dN4M8|D^ZHIl*exZ`=#0iELSbDMeK=P4zB>ngIpVifKqsbP0P}UT5dN!d1 zjIdIfs-P`yzAWg~detqZ6>S3sNsKpP*%h=#bW6s9sG;HE%sMd9x3RwA7@@CNXT5aZ ze+md8(lwf$PbPELaAjzsEUkP=FP3_Ee)++8B3&vGn~CtnK|y(omJ&Aje?;ckb4J$1 zpe@%0O;t3c9AUNfyQFRzUJE+QhT{S|}8s0{AV^bt*|Zg*CZ(xVt4HLdRt~r_7qU`-WU!NBjr* zcjvSMNw$g;j_6g{BD~Mb`rg_8!~zKHMR3*q>mc9s+hm>utfM_&Tr38CT@{rhFL{{J zF`H$P^U{v6AJd4_J&mXa9`~c5m**L7ti=OZwV2_4qjt@)-JgMtf1ULkld}Vg3Boc? U#IFGW0IQQm1U3e^0{{R306#46egFUf diff --git a/src/OpenXmlHtml.Tests/WordTableStyleTests.TableDefaultCellPadding.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordTableStyleTests.TableDefaultCellPadding.DotNet.verified.docx index c5adc2e240fe80c70f667ea53587ffa4b5521280..49f3abf4b83d298e68bbc60dd02623d90b9d858f 100644 GIT binary patch delta 449 zcmV;y0Y3hl37846^#KV-(Y)ug0RRB2ll}oVe_-Shk{oCE*LvtD^%sg{J5DG$G`)EA z@VqypHs7`XbQLMxH3jcO{cgt}_V+`ssOk-LfgKHYJ|7C+ zhG26ceAR-Henl3#qrsY-k-@LXHDVyAtfLCN*Ii>!+-2ET7^yoRB}-DTF@_SS@YPL= ze;;g!B{I&2JiJ;cxaf z49rRuFA=L985U%TkW`cQ*pMCTRR19UXf0|`ED)23tO7)TT{ec(-lM9ADvTc3=G)uk z-uh#HwaE%@HU`N900000Cp6Z2 delta 452 zcmV;#0XzPf37ZM9^#KVoMXJ%Z0RRB1ll}oVe?Za*X%c7m*LvtD^%sg{Cr&84(DdTb zL-XE@-$?rX^VjX&!5X%Q5H-4rFBb)8;2Kogbrs)H`)#r}aUDOJ`{3rkWm3;Bn5Nh)X#yrU|k7TI$a(y;Ib&yl>NHWly z87FUFPXVXi*cX>QiOb{lIcSxZUC`0b}CpvMHSYJ^Ky~jUM>! z+sowM@?)`1{`JYp&HK${ukW5@NVcM{_;R(%9_KSV!*NP8PSJgd u40b!p@N}S#&57f~Am&GB`v;T51CR+aMXJ%Z0RRB1lUxKg2FwEh0001-A>TLv diff --git a/src/OpenXmlHtml.Tests/WordTableStyleTests.TableDefaultCellPadding.Net.verified.docx b/src/OpenXmlHtml.Tests/WordTableStyleTests.TableDefaultCellPadding.Net.verified.docx index 65d39ff9f9ddadee0097ca0a6c1506ab574738ba..ceb97b713cab7ca2131004cc8a6a20e592bb7979 100644 GIT binary patch delta 474 zcmV<00VV#134#f*=m7~w(Y)uX0RRB2lkou>e@$;2ArSp5h;zPws+ z2CYI}w`ImVDn4!a-R`#EYE)eVEwK>a`fZ=_ipXw-@T!7F`YW{1hP?t^BPl>viXNS| ze-0IRALtq*t{027Xk^{;NSUL4iZkVAoPN1+@t5_nL}(_!Ckq*u9U*68-2WPwL(d7J z4MBTpG9IfKh&c)kx+$sO`sa+V(!xpwi8nDBDCpwk9Eq^GINjA{b$Ri+^|gi{AI$%A zVZL}{LT(mvvCqdb-Ue|*3}+VdfkeCHy>X;b3#etmbfj>|I44`>F; zBOp$qXh>6Ww5fQ)@hMEYSO*^59{BDX6tXkKtkm%lvBHIukjxDv*2FzDWQRKH9pv}c zni@1CWYQR>!twlhS~0XP4o}R3qCo>&{(c_STYg!rqkdIXa&xm81-4}njwLh0E5>S- zG)}6|aPVoy(bySPAjAF0_~+{c3q(4CWk(4fZq(GAn0^K^|2n(>leYtk2}jYq=c)k! Q0IQQq1U3e<0{{R30M5AOy8r+H delta 478 zcmV<40U`c^35E%<=m7~bMXJ%R0RRB1lkou>f6Z>&FbsYbLHAWj8)R8fr|3Fhy9^jG zU^~ka{R7CNAklH#SKDEav{#rUJ8^=d1(sbbeIWUf{z&T2uiv+KkDXzA@S#Op@a5Hl zGq4JEYwLpdRK45qyWQ=u)u{RoEV1Zs!*(cmL*%wX1l2$%!xcKP(N>}FBt__JF`(DZ zf1`pBB3)<1^nlqKq?bW&-i3sO9PoTbB_hNF^~EO z@_Xk@3z`u!X$({G_rmM6Vk})8H!OgnQ3F?hf1cD^epsxNeoazxbF-P`b#)YuFXdiF z!)lc^&NDm1u}d>f>olVVWE_uzzrW6~LZlN|_mtsrpbo`}!^0rvM`!mJld}Vg2{J{h U(XIgi0IHKl1U3e@0{{R30L1g-2><{9 diff --git a/src/OpenXmlHtml.Tests/WordUnderlineTests.UnderlineWithColor.DotNet.verified.docx b/src/OpenXmlHtml.Tests/WordUnderlineTests.UnderlineWithColor.DotNet.verified.docx index 5828b9711ee6cb2dc409abb65cb891bfce8f34a2..55f271d35328d968ac3306b9ebbefb0ff1046150 100644 GIT binary patch delta 294 zcmV+>0one82!sf*^#KJ)mu^=dlllQhe{L^hjm_9&^Zog?KVGaMGlV#z6SP@zO28@9 zqw58oQunZ<$HP9i8r5XMC33OG#Wo8%q~u%1q8h-;xIz!^Vois^$`n@^dX_P0A5cMz zqwB3D9YAUbfdC s%ZD6(00030{{sL}O928D02BZKlRX201xS}}S09tZ13CsZ0{{R306VXVA^-pY delta 294 zcmV+>0one92!jZ)^#KJZ`E8CKlllQhf5&C4u^D@8e!qXV`;#?fh7d<|f;J0I2{?s% zbiJTc>Yg@qzuV?kqna$ZL{8SY*k(b8lzhurR0CKU7wEyAtm!aVnc@ON&oTz>11gAd zbiFlf$@!XDIXYTe9#Nl5^aVnN_ZlXW+gTbxvz&nvR&ojrhHL(C;GFYXVK-H^f7V7X z6$t7BLKR+d+>;WHtOK!s;5WNaw-$2qRO|973Wml+ Gtph>s%#~&U delta 308 zcmV-40n7gG2kr;3=m7*L`E8Dq>;WHt&2GaW41N{FeI@--)uu7c9d@R~`H=!f z;3Vr_ZHGP5USU9&wj7K<+n@aze!qVX$4@=537qfIlzhF)I0K`w?oC_rk(#GHKURmi zs8Nj?46#sed@+~2BeI1MUUi_QU!esPat%5yDL~hX8C`81D)2tE(}UP#SuV7H>8F6DJX@$2^LCLKH4=%8)4>?_! zgPM(|1~>EuB8RTbWYc^PimrWZ`~DRMg$>v1mn`SO?}DH2xBGuBwtbaR3!x2(-7bqY zu5UZK!!r`0zT*L!hinHjh$;CN^CrOtkse?>Qi8V)7ja^FG;#ls>JO721BwJE`E8Dq Gtph=9?vLvL diff --git a/src/OpenXmlHtml.Tests/WordValidationTests.cs b/src/OpenXmlHtml.Tests/WordValidationTests.cs new file mode 100644 index 0000000..c992afe --- /dev/null +++ b/src/OpenXmlHtml.Tests/WordValidationTests.cs @@ -0,0 +1,43 @@ +using DocumentFormat.OpenXml.Validation; + +[TestFixture] +public class WordValidationTests +{ + // The generated docx must satisfy the OOXML schema. This primarily guards the run-property + // and cell-property ordering: out-of-sequence children are the most common validity failure + // and are silently repaired by Word but rejected by stricter consumers. + [Test] + public void GeneratedDocumentIsSchemaValid() + { + using var stream = new MemoryStream(); + WordHtmlConverter.ConvertToDocx( + """ +

Report

+

+ styled then super then + spaced then + rtl +

+

term

+ + + + + + +
header
mx
yz
+
  1. one
  2. two
+ """, + stream); + + stream.Position = 0; + using var document = WordprocessingDocument.Open(stream, false); + var validator = new OpenXmlValidator(); + var errors = validator.Validate(document).ToList(); + + Assert.That( + errors, + Is.Empty, + () => string.Join("\n", errors.Select(_ => $"{_.Description} ({_.Path?.XPath})"))); + } +} diff --git a/src/OpenXmlHtml/AtPageParser.cs b/src/OpenXmlHtml/AtPageParser.cs index 5da03bc..edbe27f 100644 --- a/src/OpenXmlHtml/AtPageParser.cs +++ b/src/OpenXmlHtml/AtPageParser.cs @@ -14,13 +14,12 @@ static class AtPageParser return null; } - var braceEnd = html.IndexOf('}', braceStart); - if (braceEnd < 0) + var body = ExtractBlockBody(html, braceStart); + if (body == null) { return null; } - var body = html.Substring(braceStart + 1, braceEnd - braceStart - 1); var declarations = StyleParser.Parse(body); var layout = new AtPageLayout(); @@ -209,4 +208,59 @@ static bool TryParseNamedSize(ReadOnlySpan name, out int width, out int he height = 0; return false; } + + // Extracts the top-level declarations of the @page block starting at the given '{'. + // Nested margin-box rules (e.g. @top-center { ... }) and their selectors are skipped + // so sibling declarations such as margin/size are not lost. Returns null if unbalanced. + static string? ExtractBlockBody(string html, int braceStart) + { + var builder = new StringBuilder(); + var depth = 0; + for (var i = braceStart; i < html.Length; i++) + { + var current = html[i]; + if (current == '{') + { + if (depth == 1) + { + // Drop the buffered selector that precedes this nested block. + builder.Length = LastSeparatorIndex(builder) + 1; + } + + depth++; + continue; + } + + if (current == '}') + { + depth--; + if (depth == 0) + { + return builder.ToString(); + } + + continue; + } + + if (depth == 1) + { + builder.Append(current); + } + } + + return null; + } + + static int LastSeparatorIndex(StringBuilder builder) + { + for (var i = builder.Length - 1; i >= 0; i--) + { + if (builder[i] == ';') + { + return i; + } + } + + return -1; + } } diff --git a/src/OpenXmlHtml/ColorParser.cs b/src/OpenXmlHtml/ColorParser.cs index 550d25e..396f05f 100644 --- a/src/OpenXmlHtml/ColorParser.cs +++ b/src/OpenXmlHtml/ColorParser.cs @@ -233,14 +233,40 @@ static class ColorParser bSpan = bSpan[..thirdComma]; } - if (!int.TryParse(inner[..firstComma].Trim(), out var r) || - !int.TryParse(rest[..secondComma].Trim(), out var g) || - !int.TryParse(bSpan.Trim(), out var b)) + if (!TryParseComponent(inner[..firstComma], out var r) || + !TryParseComponent(rest[..secondComma], out var g) || + !TryParseComponent(bSpan, out var b)) { return null; } - return $"{Clamp(r):X2}{Clamp(g):X2}{Clamp(b):X2}"; + return $"{r:X2}{g:X2}{b:X2}"; + } + + // Parses a single rgb() component: integer, decimal, or percentage (e.g. "255", "49.5", "50%"). + static bool TryParseComponent(ReadOnlySpan value, out int component) + { + value = value.Trim(); + if (value.EndsWith("%".AsSpan(), StringComparison.Ordinal)) + { + if (double.TryParse(value[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out var percent)) + { + component = Clamp((int)Math.Round(percent / 100 * 255)); + return true; + } + + component = 0; + return false; + } + + if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var number)) + { + component = Clamp((int)Math.Round(number)); + return true; + } + + component = 0; + return false; } static int Clamp(int value) => diff --git a/src/OpenXmlHtml/HtmlParser.cs b/src/OpenXmlHtml/HtmlParser.cs index 0011c3b..5d0dc1e 100644 --- a/src/OpenXmlHtml/HtmlParser.cs +++ b/src/OpenXmlHtml/HtmlParser.cs @@ -18,6 +18,7 @@ internal static List Parse(string html, HtmlConvertSettings? settin static void ProcessNode(INode node, FormatState format, List segments, bool inPre, HtmlConvertSettings? settings) { + var orderedIndex = 0; foreach (var child in node.ChildNodes) { switch (child) @@ -35,22 +36,25 @@ static void ProcessNode(INode node, FormatState format, List segmen break; } case IElement element: - ProcessElement(element, format, segments, inPre, settings); + { + var listIndex = element.LocalName == "li" ? ++orderedIndex : 0; + ProcessElement(element, format, segments, inPre, settings, listIndex); break; + } } } } - static void ProcessElement(IElement element, FormatState format, List segments, bool inPre, HtmlConvertSettings? settings) + static void ProcessElement(IElement element, FormatState format, List segments, bool inPre, HtmlConvertSettings? settings, int listIndex = 0) { var tag = element.LocalName; - if (IsHiddenElement(element, tag)) + var newFormat = format; + ApplyElementFormatting(element, tag, ref newFormat, out var styleDeclarations); + if (IsHiddenElement(element, tag, styleDeclarations)) { return; } - var newFormat = format; - ApplyElementFormatting(element, tag, ref newFormat, out var styleDeclarations); inPre = ApplyWhiteSpace(styleDeclarations, ref newFormat, inPre); switch (tag) @@ -124,21 +128,7 @@ static void ProcessElement(IElement element, FormatState format, List? declarations) { // Always skip non-rendered metadata/script/form-control tags. // (input is intentionally NOT here so checkboxes can be rendered.) @@ -574,13 +566,11 @@ internal static bool IsHiddenElement(IElement element, string tag) return true; } - var style = element.GetAttribute("style"); - if (style == null) + if (declarations == null) { return false; } - var declarations = StyleParser.Parse(style); if (declarations.TryGetValue("display", out var display) && display.Equals("none", StringComparison.OrdinalIgnoreCase)) { diff --git a/src/OpenXmlHtml/ImageResolver.cs b/src/OpenXmlHtml/ImageResolver.cs index 390576a..6bb21b5 100644 --- a/src/OpenXmlHtml/ImageResolver.cs +++ b/src/OpenXmlHtml/ImageResolver.cs @@ -1,6 +1,15 @@ static class ImageResolver { - static readonly HttpClient sharedClient = new(); + // The default client does NOT auto-redirect: redirects are followed manually so each hop is + // re-checked against the image policy. Otherwise a redirect from an allowed host could point + // at an internal address and bypass SafeDomains (an SSRF vector). + static readonly HttpClient sharedClient = new(new HttpClientHandler { AllowAutoRedirect = false }) + { + Timeout = TimeSpan.FromSeconds(30) + }; + + const int maxRedirects = 5; + const long maxImageBytes = 50L * 1024 * 1024; static readonly Dictionary extensionToContentType = new(StringComparer.OrdinalIgnoreCase) { @@ -48,14 +57,14 @@ static class ImageResolver var client = settings.HttpClient ?? sharedClient; try { - using var response = client.GetAsync(src).GetAwaiter().GetResult(); - if (!response.IsSuccessStatusCode) + var downloaded = Download(client, src, settings.WebImages); + if (downloaded == null) { return null; } - bytes = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult(); - contentType = response.Content.Headers.ContentType?.MediaType; + bytes = downloaded.Value.Bytes; + contentType = downloaded.Value.ContentType; } catch { @@ -156,7 +165,7 @@ internal static (int? Width, int? Height) ParseImageDimensions(IElement element) if (width == null) { var widthAttr = element.GetAttribute("width"); - if (widthAttr != null && int.TryParse(widthAttr, out var w)) + if (widthAttr != null && int.TryParse(widthAttr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var w)) { width = w; } @@ -165,7 +174,7 @@ internal static (int? Width, int? Height) ParseImageDimensions(IElement element) if (height == null) { var heightAttr = element.GetAttribute("height"); - if (heightAttr != null && int.TryParse(heightAttr, out var h)) + if (heightAttr != null && int.TryParse(heightAttr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var h)) { height = h; } @@ -174,6 +183,79 @@ internal static (int? Width, int? Height) ParseImageDimensions(IElement element) return (width, height); } + // Downloads an image, following redirects manually so each hop is policy-checked, and + // capping the response size to guard against unbounded memory use. + static (byte[] Bytes, string? ContentType)? Download(HttpClient client, string url, ImagePolicy policy) + { + for (var redirect = 0; redirect <= maxRedirects; redirect++) + { + using var request = new HttpRequestMessage(HttpMethod.Get, url); + using var response = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).GetAwaiter().GetResult(); + + if (IsRedirect(response)) + { + var location = response.Headers.Location; + if (location == null) + { + return null; + } + + url = (location.IsAbsoluteUri ? location : new Uri(new Uri(url), location)).ToString(); + if (!policy.IsAllowed(url)) + { + return null; + } + + continue; + } + + if (!response.IsSuccessStatusCode) + { + return null; + } + + if (response.Content.Headers.ContentLength > maxImageBytes) + { + return null; + } + + var bytes = ReadCapped(response); + if (bytes == null) + { + return null; + } + + return (bytes, response.Content.Headers.ContentType?.MediaType); + } + + return null; + } + + static byte[]? ReadCapped(HttpResponseMessage response) + { + using var stream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); + using var memory = new MemoryStream(); + var buffer = new byte[81920]; + int read; + while ((read = stream.Read(buffer, 0, buffer.Length)) > 0) + { + if (memory.Length + read > maxImageBytes) + { + return null; + } + + memory.Write(buffer, 0, read); + } + + return memory.ToArray(); + } + + static bool IsRedirect(HttpResponseMessage response) + { + var code = (int)response.StatusCode; + return code is >= 300 and <= 399 && code != 304; + } + static string GuessContentType(string path) => extensionToContentType.GetValueOrDefault(Path.GetExtension(path), "image/png"); } diff --git a/src/OpenXmlHtml/PaddingHelper.cs b/src/OpenXmlHtml/PaddingHelper.cs index 193164c..9f19813 100644 --- a/src/OpenXmlHtml/PaddingHelper.cs +++ b/src/OpenXmlHtml/PaddingHelper.cs @@ -47,6 +47,8 @@ internal static T BuildMargin(int? top, int? right, int? bottom, int? left) where T : OpenXmlCompositeElement, new() { var margin = new T(); + // Both CT_TcMar (w:tcMar) and CT_TblCellMar (w:tblCellMar) use w:left/w:right; the w:start/w:end + // variants are not schema-valid here and are silently dropped by stricter consumers. if (top != null) { margin.Append( @@ -60,7 +62,7 @@ internal static T BuildMargin(int? top, int? right, int? bottom, int? left) if (left != null) { margin.Append( - new StartMargin + new LeftMargin { Width = left.Value.ToString(), Type = TableWidthUnitValues.Dxa @@ -80,7 +82,7 @@ internal static T BuildMargin(int? top, int? right, int? bottom, int? left) if (right != null) { margin.Append( - new EndMargin + new RightMargin { Width = right.Value.ToString(), Type = TableWidthUnitValues.Dxa diff --git a/src/OpenXmlHtml/WordContentBuilder.Lists.cs b/src/OpenXmlHtml/WordContentBuilder.Lists.cs index d4dfea2..237db08 100644 --- a/src/OpenXmlHtml/WordContentBuilder.Lists.cs +++ b/src/OpenXmlHtml/WordContentBuilder.Lists.cs @@ -65,7 +65,7 @@ static void BuildRealNumberedList(IElement element, string tag, FormatState newF { if (context.BulletAbstractNumId == null) { - var id = WordNumberingBuilder.GetNextId(numbering); + var id = context.NextNumId++; context.BulletAbstractNumId = WordNumberingBuilder.CreateBulletAbstractNum(numbering, id); } @@ -73,8 +73,8 @@ static void BuildRealNumberedList(IElement element, string tag, FormatState newF } else { - // Each ordered format type gets its own abstract num - var id = WordNumberingBuilder.GetNextId(numbering); + // Each ordered list gets its own abstract num (for independent restart/start) + var id = context.NextNumId++; abstractNumId = WordNumberingBuilder.CreateOrderedAbstractNum(numbering, id, format); } @@ -87,7 +87,7 @@ static void BuildRealNumberedList(IElement element, string tag, FormatState newF startOverride = startVal; } - var numId = WordNumberingBuilder.GetNextId(numbering); + var numId = context.NextNumId++; WordNumberingBuilder.AddNumberingInstance(numbering, numId, abstractNumId, startOverride); var ilvl = context.ListStack.Count > 0 ? Math.Min(context.ListStack.Peek().Ilvl + 1, 8) : 0; var parentInside = context.ListStack.Count > 0 && context.ListStack.Peek().Inside; @@ -96,7 +96,7 @@ static void BuildRealNumberedList(IElement element, string tag, FormatState newF context.ListStack.Pop(); } - static void BuildListItem(IElement element, FormatState newFormat, List elements, WordBuildContext context, bool inPre) + static void BuildListItem(IElement element, FormatState newFormat, List elements, WordBuildContext context, bool inPre, int listIndex) { FlushParagraph(elements, context); if (context.ListStack.Count > 0) @@ -123,20 +123,7 @@ static void BuildListItem(IElement element, FormatState newFormat, List 1) { contTcPr.Append( new GridSpan - { - Val = spanInfo.Colspan - }); + { + Val = spanInfo.Colspan + }); } + contTcPr.Append(new VerticalMerge()); tableRow.Append(new TableCell(contTcPr, new Paragraph())); if (spanInfo.Remaining <= 1) @@ -309,6 +311,7 @@ static TableCell BuildTableCell(IElement cellElement, FormatState format, WordBu if (cellProperties != null) { + ReorderCellProperties(cellProperties); tc.Append(cellProperties); } @@ -324,6 +327,8 @@ static TableCell BuildTableCell(IElement cellElement, FormatState format, WordBu StyleMap = parentCtx.StyleMap, BulletAbstractNumId = parentCtx.BulletAbstractNumId, NextNumId = parentCtx.NextNumId, + FootnoteIndex = parentCtx.FootnoteIndex, + BookmarkId = parentCtx.BookmarkId, ParagraphRightToLeft = cellFormat.RightToLeft }; ProcessChildren(cellElement, cellFormat, cellElements, cellCtx, false); @@ -331,6 +336,8 @@ static TableCell BuildTableCell(IElement cellElement, FormatState format, WordBu parentCtx.ImageIndex = cellCtx.ImageIndex; parentCtx.NextNumId = cellCtx.NextNumId; parentCtx.BulletAbstractNumId = cellCtx.BulletAbstractNumId; + parentCtx.FootnoteIndex = cellCtx.FootnoteIndex; + parentCtx.BookmarkId = cellCtx.BookmarkId; if (cellElements.Count == 0) { @@ -577,18 +584,44 @@ static List GetCells(IElement row) static int GetColumnCount(List rows) { var maxCols = 0; + // Mirror the render loop's rowspan tracking so rowspans originating in earlier rows + // are counted; otherwise later, wider rows would be truncated and cells lost. + var rowspanTracker = new Dictionary(); foreach (var row in rows) { - var cols = 0; - foreach (var cell in row.Children) + var cells = GetCells(row); + var cellIndex = 0; + var colIndex = 0; + while (cellIndex < cells.Count || rowspanTracker.ContainsKey(colIndex)) { - if (cell.LocalName is "td" or "th") + if (rowspanTracker.TryGetValue(colIndex, out var spanInfo)) + { + if (spanInfo.Remaining <= 1) + { + rowspanTracker.Remove(colIndex); + } + else + { + rowspanTracker[colIndex] = (spanInfo.Remaining - 1, spanInfo.Colspan); + } + + colIndex += spanInfo.Colspan; + continue; + } + + var cell = cells[cellIndex]; + cellIndex++; + var colspan = ParseIntAttribute(cell, "colspan", 1); + var rowspan = ParseIntAttribute(cell, "rowspan", 1); + if (rowspan > 1) { - cols += ParseIntAttribute(cell, "colspan", 1); + rowspanTracker[colIndex] = (rowspan - 1, colspan); } + + colIndex += colspan; } - maxCols = Math.Max(maxCols, cols); + maxCols = Math.Max(maxCols, colIndex); } return Math.Max(1, maxCols); @@ -604,4 +637,34 @@ static int ParseIntAttribute(IElement element, string attribute, int defaultValu return defaultValue; } + + static readonly Type[] cellPropertyOrder = + [ + typeof(TableCellWidth), + typeof(GridSpan), + typeof(VerticalMerge), + typeof(TableCellBorders), + typeof(Shading), + typeof(TableCellMargin), + typeof(TextDirection), + typeof(TableCellVerticalAlignment) + ]; + + // The various cell-style sources append in a convenient order; CT_TcPr requires a fixed + // schema sequence, so normalise the children before emitting the cell. + static void ReorderCellProperties(TableCellProperties tcPr) + { + var sorted = tcPr.ChildElements + .OrderBy(_ => + { + var index = Array.IndexOf(cellPropertyOrder, _.GetType()); + return index < 0 ? int.MaxValue : index; + }) + .ToList(); + tcPr.RemoveAllChildren(); + foreach (var child in sorted) + { + tcPr.Append(child); + } + } } diff --git a/src/OpenXmlHtml/WordContentBuilder.cs b/src/OpenXmlHtml/WordContentBuilder.cs index 823981c..cc98bcd 100644 --- a/src/OpenXmlHtml/WordContentBuilder.cs +++ b/src/OpenXmlHtml/WordContentBuilder.cs @@ -22,6 +22,17 @@ internal static List Build(string html, MainDocumentPart? main, context.NextNumId = 1; } + // Seed the footnote counter from any existing footnotes so a later Build call against + // the same document continues numbering instead of emitting colliding footnote IDs. + if (main?.FootnotesPart?.Footnotes is { } existingFootnotes) + { + context.FootnoteIndex = (int)existingFootnotes + .Elements() + .Select(_ => _.Id?.Value ?? 0) + .DefaultIfEmpty(0) + .Max(); + } + if (settings?.NumberingSession is { } session) { context.BulletAbstractNumId = session.BulletAbstractNumId; @@ -58,6 +69,7 @@ internal static List Build(string html, MainDocumentPart? main, static void ProcessChildren(INode node, FormatState format, List elements, WordBuildContext ctx, bool inPre) { + var orderedIndex = 0; foreach (var child in node.ChildNodes) { switch (child) @@ -75,22 +87,25 @@ static void ProcessChildren(INode node, FormatState format, List break; } case IElement element: - ProcessElement(element, format, elements, ctx, inPre); + { + var listIndex = element.LocalName == "li" ? ++orderedIndex : 0; + ProcessElement(element, format, elements, ctx, inPre, listIndex); break; + } } } } - static void ProcessElement(IElement element, FormatState format, List elements, WordBuildContext context, bool inPre) + static void ProcessElement(IElement element, FormatState format, List elements, WordBuildContext context, bool inPre, int listIndex = 0) { var tag = element.LocalName; - if (HtmlSegmentParser.IsHiddenElement(element, tag)) + var newFormat = format; + HtmlSegmentParser.ApplyElementFormatting(element, tag, ref newFormat, out var styleDeclarations); + if (HtmlSegmentParser.IsHiddenElement(element, tag, styleDeclarations)) { return; } - var newFormat = format; - HtmlSegmentParser.ApplyElementFormatting(element, tag, ref newFormat, out var styleDeclarations); inPre = HtmlSegmentParser.ApplyWhiteSpace(styleDeclarations, ref newFormat, inPre); switch (tag) @@ -161,7 +176,7 @@ static void ProcessElement(IElement element, FormatState format, List elements, WordBuildContext conte context.ParagraphRightToLeft = false; context.ListNumId = null; context.ListIlvl = null; + context.ListInside = false; + context.ListDepth = 0; return; } @@ -541,8 +558,9 @@ static void ApplyParagraphFormat(ParagraphProperties props, ParagraphFormatState pf.MarginRightTwips != null || pf.TextIndentTwips != null) { - var indent = props.GetFirstChild() ?? new Indentation(); - if (props.GetFirstChild() == null) + var existingIndent = props.GetFirstChild(); + var indent = existingIndent ?? new Indentation(); + if (existingIndent == null) { props.Append(indent); } diff --git a/src/OpenXmlHtml/WordHtmlConverter.cs b/src/OpenXmlHtml/WordHtmlConverter.cs index a26c6d1..2846643 100644 --- a/src/OpenXmlHtml/WordHtmlConverter.cs +++ b/src/OpenXmlHtml/WordHtmlConverter.cs @@ -386,6 +386,19 @@ internal static RunProperties BuildWordRunProperties(FormatState format) }); } + // Children are appended in OOXML CT_RPr schema order (rFonts, b, i, smallCaps, + // strike, shadow, color, spacing, w, sz, u, bdr, shd, vertAlign, rtl). Out-of-order + // children produce schema-invalid runs that strict consumers reject. + if (format.FontFamily != null) + { + props.Append( + new RunFonts + { + Ascii = format.FontFamily, + HighAnsi = format.FontFamily + }); + } + if (format.Bold) { props.Append(new Bold()); @@ -396,18 +409,9 @@ internal static RunProperties BuildWordRunProperties(FormatState format) props.Append(new Italic()); } - if (format.UnderlineStyle != null) + if (format.SmallCaps) { - var underline = new Underline - { - Val = format.UnderlineStyle - }; - if (format.UnderlineColor != null) - { - underline.Color = format.UnderlineColor; - } - - props.Append(underline); + props.Append(new SmallCaps()); } if (format.Strikethrough) @@ -415,14 +419,18 @@ internal static RunProperties BuildWordRunProperties(FormatState format) props.Append(new Strike()); } - if (format.SmallCaps) + if (format.Shadow) { - props.Append(new SmallCaps()); + props.Append(new Shadow()); } - if (format.Shadow) + if (format.Color != null) { - props.Append(new Shadow()); + props.Append( + new Color + { + Val = format.Color + }); } if (format.CharacterSpacingTwips != null) @@ -443,37 +451,50 @@ internal static RunProperties BuildWordRunProperties(FormatState format) }); } - if (format.RightToLeft) - { - props.Append(new RightToLeftText()); - } - - if (format.Color != null) + if (format.FontSizePt != null) { + var halfPoints = (int)(format.FontSizePt.Value * 2); props.Append( - new Color + new FontSize { - Val = format.Color + Val = halfPoints.ToString() }); } - if (format.FontSizePt != null) + if (format.UnderlineStyle != null) + { + var underline = new Underline + { + Val = format.UnderlineStyle + }; + if (format.UnderlineColor != null) + { + underline.Color = format.UnderlineColor; + } + + props.Append(underline); + } + + if (format.Border != null && + format.Border.Style != BorderValues.None) { - var halfPoints = (int)(format.FontSizePt.Value * 2); props.Append( - new FontSize + new Border { - Val = halfPoints.ToString() + Val = format.Border.Style, + Size = (uint)format.Border.SizeEighths, + Space = 1, + Color = format.Border.Color ?? "auto" }); } - if (format.FontFamily != null) + if (format.BackgroundColor != null) { props.Append( - new RunFonts + new Shading { - Ascii = format.FontFamily, - HighAnsi = format.FontFamily + Val = ShadingPatternValues.Clear, + Fill = format.BackgroundColor }); } @@ -494,27 +515,9 @@ internal static RunProperties BuildWordRunProperties(FormatState format) }); } - if (format.BackgroundColor != null) - { - props.Append( - new Shading - { - Val = ShadingPatternValues.Clear, - Fill = format.BackgroundColor - }); - } - - if (format.Border != null && - format.Border.Style != BorderValues.None) + if (format.RightToLeft) { - props.Append( - new Border - { - Val = format.Border.Style, - Size = (uint)format.Border.SizeEighths, - Space = 1, - Color = format.Border.Color ?? "auto" - }); + props.Append(new RightToLeftText()); } return props;