From 0903d3def0bc383e2e0dec6cb0fbd635ad8bd598 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 13:04:52 +0000 Subject: [PATCH 1/9] Bump xunit.analyzers from 2.0.0 to 2.1.0 --- updated-dependencies: - dependency-name: xunit.analyzers dependency-version: 2.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj b/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj index 4ce43ecd..80732392 100644 --- a/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj +++ b/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj @@ -8,7 +8,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 85ef31c1590024659d20151d31f2d90ec33e6dba Mon Sep 17 00:00:00 2001 From: Stefan Seeland <168659+stesee@users.noreply.github.com> Date: Sat, 3 Oct 2026 10:41:44 +0200 Subject: [PATCH 2/9] Group SkiaSharp Dependabot updates into one PR SkiaSharp and SkiaSharp.NativeAssets.Linux.NoDependencies must have the same version. Dependabot opened one PR per package (#241, #242), and each failed the Linux build until the other was merged. Codeuctivity.SkiaSharpCompare joins the group, and a 3 day cooldown on SkiaSharp gives it time to publish a release built against the new SkiaSharp. --- .github/dependabot.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 523c45ce..50eeb036 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -27,6 +27,22 @@ updates: directory: "/" schedule: interval: "daily" + # SkiaSharp and its native assets must always have the same version - separate PRs each fail + # the build until the other one is merged, so update them together in one PR. SkiaSharpCompare + # (test dependency) is built against a specific SkiaSharp version and joins the same PR. + groups: + skiasharp: + patterns: + - "SkiaSharp" + - "SkiaSharp.*" + - "Codeuctivity.SkiaSharpCompare" + # Wait a few days before picking up a new SkiaSharp, so SkiaSharpCompare has time to publish a + # release built against it and both land in the same grouped PR. + cooldown: + default-days: 3 + include: + - "SkiaSharp" + - "SkiaSharp.*" ignore: - dependency-name: "SixLabors.ImageSharp" versions: ["3.x", "4.x"] From 576e01a7e9814a5c9f57c7bd88bf6094edab82cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:52:29 +0000 Subject: [PATCH 3/9] Bump the skiasharp group with 2 updates Bumps SkiaSharp from 4.152.0 to 4.153.1 Bumps SkiaSharp.NativeAssets.Linux.NoDependencies from 4.152.0 to 4.153.1 --- updated-dependencies: - dependency-name: SkiaSharp dependency-version: 4.153.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: skiasharp - dependency-name: SkiaSharp.NativeAssets.Linux.NoDependencies dependency-version: 4.153.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: skiasharp ... Signed-off-by: dependabot[bot] --- OpenXmlPowerTools/OpenXmlPowerTools.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenXmlPowerTools/OpenXmlPowerTools.csproj b/OpenXmlPowerTools/OpenXmlPowerTools.csproj index aaa4cb38..5f8017b1 100644 --- a/OpenXmlPowerTools/OpenXmlPowerTools.csproj +++ b/OpenXmlPowerTools/OpenXmlPowerTools.csproj @@ -43,8 +43,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 4fb02e8f6725c7f7b7c66e33cef0792e7542e8c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:04:38 +0000 Subject: [PATCH 4/9] Bump the skiasharp group with 1 update Bumps Codeuctivity.SkiaSharpCompare from 4.0.331 to 4.0.342 --- updated-dependencies: - dependency-name: Codeuctivity.SkiaSharpCompare dependency-version: 4.0.342 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: skiasharp ... Signed-off-by: dependabot[bot] --- OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj b/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj index 80732392..abcd9b39 100644 --- a/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj +++ b/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj @@ -6,7 +6,7 @@ - + From 0559b5ca72779706f545f5a59d5b11d6c5b7cce3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:45:30 +0000 Subject: [PATCH 5/9] Bump xunit.analyzers from 2.1.0 to 2.2.0 --- updated-dependencies: - dependency-name: xunit.analyzers dependency-version: 2.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj b/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj index abcd9b39..99d3cd76 100644 --- a/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj +++ b/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj @@ -8,7 +8,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 02e727b4037cdb654b82a3cabc0647645f8e3d90 Mon Sep 17 00:00:00 2001 From: Lowell Stewart Date: Wed, 21 Oct 2026 15:47:49 -0600 Subject: [PATCH 6/9] Cover Unicode whitespace using Word-saved fixtures and split-run replacements --- OpenXmlPowerTools.Tests/OpenXmlRegexTests.cs | 21 +++++++ OpenXmlPowerTools.Tests/UnicodeMapperTests.cs | 34 +++++++++-- .../UM-Whitespace-Unicode-Word-saved.docx | Bin 0 -> 14698 bytes TestFiles/UM-Whitespace-Unicode-test.docx | Bin 0 -> 12288 bytes TestFiles/UM-Whitespace-Unicode-test.xml | 4 ++ TestFiles/UM-Whitespace.md | 56 ++++++++++++++++++ 6 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 TestFiles/UM-Whitespace-Unicode-Word-saved.docx create mode 100644 TestFiles/UM-Whitespace-Unicode-test.docx create mode 100644 TestFiles/UM-Whitespace-Unicode-test.xml create mode 100644 TestFiles/UM-Whitespace.md diff --git a/OpenXmlPowerTools.Tests/OpenXmlRegexTests.cs b/OpenXmlPowerTools.Tests/OpenXmlRegexTests.cs index 7c303e10..daaabaa3 100644 --- a/OpenXmlPowerTools.Tests/OpenXmlRegexTests.cs +++ b/OpenXmlPowerTools.Tests/OpenXmlRegexTests.cs @@ -405,5 +405,26 @@ public void CanMatchDespiteInvisibleLayoutMarkers() } } + [Theory] + [InlineData(null)] + [InlineData("default")] + [InlineData("preserve")] + public void CanReplaceSplitMatchesWithNonBreakingSpaces(string space) + { + XElement Text(string value) => new XElement(W.t, + space == null ? null : new XAttribute(XNamespace.Xml + "space", space), value); + + var paragraph = new XElement(W.p, + new XElement(W.r, Text("Prefix\u00A0{[if")), + new XElement(W.r, Text("\u00A0Ready]}\u00A0Middle\u00A0{[if")), + new XElement(W.r, Text("\u00A0Ready]}\u00A0Suffix"))); + var count = OpenXmlRegex.Replace(new[] { paragraph }, + new Regex(Regex.Escape("{[if\u00A0Ready]}")), "Selected", null); + + Assert.Equal(2, count); + Assert.Equal("Prefix\u00A0Selected\u00A0Middle\u00A0Selected\u00A0Suffix", + string.Concat(paragraph.Descendants(W.t).Select(t => t.Value))); + } + } } \ No newline at end of file diff --git a/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs b/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs index 8532dc2d..731e2eed 100644 --- a/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs +++ b/OpenXmlPowerTools.Tests/UnicodeMapperTests.cs @@ -69,6 +69,20 @@ public void CanCreateCoalescedRuns() Assert.Equal("Third", mixedRuns.Elements(W.t).Skip(2).First().Value); } + [Theory] + [InlineData("\u00A0")] + [InlineData("\u00A0text")] + [InlineData("text\u00A0")] + [InlineData("\u00A0text\u00A0")] + public void CanRoundTripNonBreakingSpaces(string text) + { + var runs = UnicodeMapper.StringToCoalescedRunList(text, null); + + // NBSP is content; the mapper's own writer does not require xml:space for it. + Assert.All(runs.Elements(W.t), t => Assert.Null(t.Attribute(XNamespace.Xml + "space"))); + Assert.Equal(text, runs.Select(UnicodeMapper.RunToString).StringConcatenate()); + } + [Fact] public void CanMapSymbols() { @@ -201,25 +215,32 @@ public void HonorsXmlSpace() // 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() + [Theory] + [InlineData("UM-Whitespace", 107)] + [InlineData("UM-Whitespace-Unicode", 43)] + public void TreatsXmlSpaceLikeWord(string fixture, int caseCount) { 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, fixture + "-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 expectedDoc = new System.IO.FileInfo(System.IO.Path.Combine(sourceDir.FullName, fixture + "-Word-saved.docx")); var expectedWmlDoc = new WmlDocument(expectedDoc.FullName); var expectedParagraphs = expectedWmlDoc.MainDocumentPart .Element(W.body) .Elements(W.p).ToList(); + // Each fixture ends with one blank paragraph after its name/content pairs. + Assert.Equal(caseCount * 2 + 1, testParagraphs.Count); + Assert.Equal(testParagraphs.Count, expectedParagraphs.Count); + Assert.Empty(testParagraphs.Last().Descendants(W.t)); + Assert.Empty(expectedParagraphs.Last().Descendants(W.t)); // Iterate through pairs of paragraphs (test name, test content, expected result) - for (int i = 0; i < testParagraphs.Count - 1; i += 2) + for (int i = 0; i < caseCount * 2; i += 2) { var testNameParagraph = testParagraphs[i]; var testContentParagraph = testParagraphs[i + 1]; @@ -227,6 +248,7 @@ public void TreatsXmlSpaceLikeWord() var testName = testNameParagraph.Descendants(W.t) .Select(t => (string)t) .StringConcatenate(); + Assert.Equal(testName, ExtractExpectedFromWord(expectedParagraphs[i])); // Get the actual result by calling UnicodeMapper.RunToString on the test content runs var actualResult = testContentParagraph.Descendants(W.r) .Select(UnicodeMapper.RunToString) @@ -242,7 +264,7 @@ public void TreatsXmlSpaceLikeWord() // 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: + // versions of the UM-Whitespace fixtures: // • → literal text // • → '\t' // • (intentionally ignored) diff --git a/TestFiles/UM-Whitespace-Unicode-Word-saved.docx b/TestFiles/UM-Whitespace-Unicode-Word-saved.docx new file mode 100644 index 0000000000000000000000000000000000000000..a48b5c37c8ac5235d22a59314690e7577b19d7dd GIT binary patch literal 14698 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ð=Rgs)nwbHP*b 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 + + NBSP leading xml:space absent fooNBSP trailing xml:space absentfoo NBSP only xml:space absent NBSP mixed xml:space absent  foo  NBSP split field xml:space absent{[elseif TakingTitle == "Unequal"]}NBSP leading xml:space default fooNBSP trailing xml:space defaultfoo NBSP only xml:space default NBSP mixed xml:space default  foo  NBSP split field xml:space default{[elseif TakingTitle == "Unequal"]}NBSP leading xml:space preserve fooNBSP trailing xml:space preservefoo NBSP only xml:space preserve NBSP mixed xml:space preserve  foo  NBSP split field xml:space preserve{[elseif TakingTitle == "Unequal"]}NBSP trailing keyword split field{[elseif TakingTitle == "Unequal"]}NBSP standalone separator run{[elseif TakingTitle == "Unequal"]}Unicode U+0085 mixed boundaries …foo… Unicode U+1680 mixed boundaries  foo  Unicode U+2000 mixed boundaries  foo  Unicode U+2001 mixed boundaries  foo  Unicode U+2002 mixed boundaries  foo  Unicode U+2003 mixed boundaries  foo  Unicode U+2004 mixed boundaries  foo  Unicode U+2005 mixed boundaries  foo  Unicode U+2006 mixed boundaries  foo  Unicode U+2007 mixed boundaries  foo  Unicode U+2008 mixed boundaries  foo  Unicode U+2009 mixed boundaries  foo  Unicode U+200A mixed boundaries  foo  Unicode U+2028 mixed boundaries 
foo
 Unicode U+2029 mixed boundaries 
foo
 Unicode U+202F mixed boundaries  foo  Unicode U+205F mixed boundaries  foo  Unicode U+3000 mixed boundaries  foo  XML whitespace edges xml:space absent foo referenced CR xml:space absentfoo barreferenced CRLF xml:space absentfoo barinternal tab xml:space absentfoo barXML whitespace edges xml:space preserve foo referenced CR xml:space preservefoo barreferenced CRLF xml:space preservefoo barinternal tab xml:space preservefoo bar + diff --git a/TestFiles/UM-Whitespace.md b/TestFiles/UM-Whitespace.md new file mode 100644 index 00000000..8ad1ce51 --- /dev/null +++ b/TestFiles/UM-Whitespace.md @@ -0,0 +1,56 @@ +# UnicodeMapper whitespace fixtures + +`UnicodeMapperTests.TreatsXmlSpaceLikeWord` compares each crafted input paragraph +with the literal text and tabs in the corresponding paragraph saved by Microsoft +Word. Each case has a name paragraph followed by a content paragraph. Each document +ends with one blank paragraph. The tests check paragraph counts and matching names +so a missing or reordered case cannot silently pass. + +The original `UM-Whitespace-test.docx` and `UM-Whitespace-Word-saved.docx` contain +107 cases from the January 2026 investigation. They are unchanged. + +The supplementary `UM-Whitespace-Unicode-test.docx` and +`UM-Whitespace-Unicode-Word-saved.docx` contain 43 cases. The expected document was +opened and saved without content edits using Microsoft Word for Mac 16.114 on +October 21, 2026. These are synthetic documents; no customer document was used. + +The supplementary cases cover: + +- Leading, trailing, standalone, and mixed-boundary NBSP with `xml:space` absent, + `default`, and `preserve`. +- An NBSP separator at either side of a run boundary and in a standalone run. +- All other XML-valid characters recognized as whitespace by .NET: U+0085, + U+1680, U+2000 through U+200A, U+2028, U+2029, U+202F, U+205F, and U+3000. + Each is surrounded by ordinary XML whitespace to distinguish trimming the XML + whitespace from trimming the Unicode character. +- Ordinary spaces, tabs, CR, and CRLF, with preservation absent and present. + CR and LF use character references so the XML parser does not normalize them + before Word or UnicodeMapper sees them. Literal XML line endings in the original + fixture are normalized by XML parsing and do not make that distinction. + +Word preserves the tested Unicode characters, trims only boundary space/tab/CR/LF +when preservation is absent or `default`, converts internal line endings to spaces, +and retains tabs when preservation is enabled. These observations cover the listed +inputs and Word version, not every possible whitespace or formatting context. + +`UM-Whitespace-Unicode-test.xml` is the exact `word/document.xml` payload in the +supplementary input DOCX, retained for review and reproduction. To regenerate the +input from the repository root using Python's standard library: + +```python +from pathlib import Path +from zipfile import ZipFile + +root = Path("TestFiles") +xml = (root / "UM-Whitespace-Unicode-test.xml").read_bytes() +with ZipFile(root / "UM-Whitespace-test.docx") as source: + with ZipFile(root / "UM-Whitespace-Unicode-test.docx", "w") as target: + for entry in source.infolist(): + data = xml if entry.filename == "word/document.xml" else source.read(entry.filename) + target.writestr(entry, data) +``` + +Keep the crafted input unchanged. Open it in Word and use Save As to create the +separate `UM-Whitespace-Unicode-Word-saved.docx`. Do not manufacture expected output +with UnicodeMapper or another word processor. Review any changes in the Word-saved +text before updating expectations. From f818109ca947bd8bb2e1d082abacaeec6062cb29 Mon Sep 17 00:00:00 2001 From: Lowell Stewart Date: Wed, 21 Oct 2026 15:47:49 -0600 Subject: [PATCH 7/9] Preserve Unicode spaces when trimming Word text boundaries --- OpenXmlPowerTools/UnicodeMapper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenXmlPowerTools/UnicodeMapper.cs b/OpenXmlPowerTools/UnicodeMapper.cs index ee4c46ae..efb8dee3 100644 --- a/OpenXmlPowerTools/UnicodeMapper.cs +++ b/OpenXmlPowerTools/UnicodeMapper.cs @@ -155,10 +155,10 @@ private static string NormalizeWhitespace(string text, bool preserve) { if (string.IsNullOrEmpty(text)) return string.Empty; - // Trim leading & trailing whitespace when NOT preserving + // Word trims XML whitespace at text boundaries, not Unicode spaces such as NBSP. ReadOnlySpan span = preserve ? text.AsSpan() - : text.AsSpan().Trim(); + : text.AsSpan().Trim(" \t\r\n"); if (span.Length == 0) return string.Empty; var sb = new System.Text.StringBuilder(span.Length); From 6ab277382d1180dbb3f482e93fedefdb1eaf529c Mon Sep 17 00:00:00 2001 From: stesee <168659+stesee@users.noreply.github.com> Date: Mon, 26 Oct 2026 08:50:42 +0000 Subject: [PATCH 8/9] Fix xunit.analyzers downgrade and group xunit Dependabot updates Drop the direct xunit.analyzers reference: xunit.v3 already brings it in transitively with a matching minimum version, so the explicit pin only caused NU1605 downgrade errors when the two drifted. Bump xunit.v3 to 4.0.2 and add a Dependabot group so all xunit* packages update together. --- .github/dependabot.yml | 5 +++++ OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 50eeb036..17d37011 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -36,6 +36,11 @@ updates: - "SkiaSharp" - "SkiaSharp.*" - "Codeuctivity.SkiaSharpCompare" + # xunit.v3 pins a minimum xunit.analyzers (and runner) version transitively. Updating them in + # separate PRs causes NU1605 package downgrade errors, so update the whole xunit family together. + xunit: + patterns: + - "xunit*" # Wait a few days before picking up a new SkiaSharp, so SkiaSharpCompare has time to publish a # release built against it and both land in the same grouped PR. cooldown: diff --git a/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj b/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj index 99d3cd76..eb4c5c39 100644 --- a/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj +++ b/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj @@ -8,12 +8,11 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 88b14c4cc3bbe5fbb3f2ab1c6820c8347ee265cd Mon Sep 17 00:00:00 2001 From: Stefan Seeland <168659+stesee@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:02:58 +0100 Subject: [PATCH 9/9] Housekeeping --- .github/dependabot.yml | 5 ----- OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj | 4 ++-- OpenXmlPowerTools/OpenXmlPowerTools.csproj | 2 +- .../MarkupSimplifierApp/MarkupSimplifierApp.csproj | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 17d37011..50eeb036 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -36,11 +36,6 @@ updates: - "SkiaSharp" - "SkiaSharp.*" - "Codeuctivity.SkiaSharpCompare" - # xunit.v3 pins a minimum xunit.analyzers (and runner) version transitively. Updating them in - # separate PRs causes NU1605 package downgrade errors, so update the whole xunit family together. - xunit: - patterns: - - "xunit*" # Wait a few days before picking up a new SkiaSharp, so SkiaSharpCompare has time to publish a # release built against it and both land in the same grouped PR. cooldown: diff --git a/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj b/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj index eb4c5c39..9e367cc1 100644 --- a/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj +++ b/OpenXmlPowerTools.Tests/OpenXmlPowerTools.Tests.csproj @@ -7,8 +7,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OpenXmlPowerTools/OpenXmlPowerTools.csproj b/OpenXmlPowerTools/OpenXmlPowerTools.csproj index 5f8017b1..a3828a71 100644 --- a/OpenXmlPowerTools/OpenXmlPowerTools.csproj +++ b/OpenXmlPowerTools/OpenXmlPowerTools.csproj @@ -45,7 +45,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OpenXmlPowerToolsExamples/MarkupSimplifierApp/MarkupSimplifierApp.csproj b/OpenXmlPowerToolsExamples/MarkupSimplifierApp/MarkupSimplifierApp.csproj index e8ea0dc6..28a26125 100644 --- a/OpenXmlPowerToolsExamples/MarkupSimplifierApp/MarkupSimplifierApp.csproj +++ b/OpenXmlPowerToolsExamples/MarkupSimplifierApp/MarkupSimplifierApp.csproj @@ -7,7 +7,7 @@ - +