From 7a0c5e1449253b9bec90c47783d067a40fd582b1 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 29 Mar 2023 00:29:49 +0200 Subject: [PATCH] feat: changed CompareStrategy to return CompareResult * Ensure that the correct diff is returned. * Use records. * Move kind to beginning of text. * Fix compilation error and test for custom diff. * Update src/AngleSharp.Diffing/Core/CompareResult.cs Co-authored-by: Egil Hansen * Update src/AngleSharp.Diffing/Core/Comparison.cs Co-authored-by: Egil Hansen * Update src/AngleSharp.Diffing/Core/AttributeComparison.cs Co-authored-by: Egil Hansen * 1. Rename CompareResultDecision 2. Move method inside type. * Rename method. * Get rid of StylesOrder * More records. * refactor: move IsExternalInit to folder matching namespace * add comment to explain use of ReferenceEquals * Move types into two files * simplify attrdiff object hierarchi * remove diff type check * clean up using statements * refactor: IsSameOrSkip as property * refactor: normalize tests * feat: add CommentComparer tests * nodes do not have different closing tags, elements do, so switch to using ElementDiff when comparing elements * fix: override ComparisonSource.ToString for better output * fix: use unspecified when no diff is returned * override ToString in AttributeComparisonSource * bump version number --------- Co-authored-by: Egil Hansen --- CHANGELOG.md | 5 + .../Core/HtmlDifferenceEngineTest.cs | 109 +++++++++++++++--- .../DiffingTestBase.cs | 6 + .../AttributeComparerTest.cs | 29 +++-- .../BooleanAttributeComparerTest.cs | 14 ++- .../ClassAttributeComparerTest.cs | 28 ++++- .../IgnoreAttributeComparerTest.cs | 14 +++ .../OrderingStyleAttributeComparerTest.cs | 21 +++- .../StyleAttributeComparerTest.cs | 16 ++- .../CommentStrategies/CommentComparerTest.cs | 53 +++++++++ .../Strategies/DiffingStrategyPipelineTest.cs | 58 +++++----- .../ElementStrategies/ElementComparerTest.cs | 40 +++++-- .../StyleSheetTextNodeComparerTest.cs | 5 +- .../TextNodeComparerTest.cs | 34 +++--- .../Core/AttributeComparison.cs | 38 +----- .../Core/AttributeComparisonSource.cs | 15 ++- .../Core/CompareDecision.cs | 34 ++++++ src/AngleSharp.Diffing/Core/CompareResult.cs | 47 ++++---- src/AngleSharp.Diffing/Core/Comparison.cs | 39 +------ .../Core/ComparisonSource.cs | 11 +- src/AngleSharp.Diffing/Core/Diffs/AttrDiff.cs | 13 ++- .../Core/Diffs/AttrDiffKind.cs | 20 ++++ .../Core/Diffs/CommentDiff.cs | 14 +++ src/AngleSharp.Diffing/Core/Diffs/DiffBase.cs | 31 +---- .../Core/Diffs/ElementDiff.cs | 21 ++++ .../Core/Diffs/ElementDiffKind.cs | 20 ++++ .../Core/Diffs/MissingAttrDiff.cs | 3 +- .../Core/Diffs/MissingDiffBase.cs | 25 +--- .../Core/Diffs/MissingNodeDiff.cs | 3 +- src/AngleSharp.Diffing/Core/Diffs/NodeDiff.cs | 5 +- .../Core/Diffs/StylesheetDiff.cs | 14 +++ src/AngleSharp.Diffing/Core/Diffs/TextDiff.cs | 14 +++ .../Core/Diffs/UnexpectedAttrDiff.cs | 3 +- .../Core/Diffs/UnexpectedDiffBase.cs | 24 +--- .../Core/Diffs/UnexpectedNodeDiff.cs | 3 +- .../Core/HtmlDifferenceEngine.cs | 22 ++-- .../UnexpectedDOMTreeStructureException.cs | 4 +- .../AttributeStrategies/AttributeComparer.cs | 6 +- .../BooleanAttributeComparer.cs | 8 +- .../ClassAttributeComparer.cs | 8 +- .../IgnoreAttributeComparer.cs | 2 +- .../OrderingStyleAttributeComparer.cs | 16 +-- .../StyleAttributeComparer.cs | 16 +-- .../CommentStrategies/CommentComparer.cs | 12 +- .../ElementStrategies/ElementComparer.cs | 16 ++- .../StyleSheetTextNodeComparer.cs | 8 +- .../TextNodeStrategies/TextNodeComparer.cs | 18 +-- .../IsExternalInit.cs | 66 +++++++++++ src/Directory.Build.props | 2 +- 49 files changed, 688 insertions(+), 345 deletions(-) create mode 100644 src/AngleSharp.Diffing.Tests/Strategies/CommentStrategies/CommentComparerTest.cs create mode 100644 src/AngleSharp.Diffing/Core/CompareDecision.cs create mode 100644 src/AngleSharp.Diffing/Core/Diffs/AttrDiffKind.cs create mode 100644 src/AngleSharp.Diffing/Core/Diffs/CommentDiff.cs create mode 100644 src/AngleSharp.Diffing/Core/Diffs/ElementDiff.cs create mode 100644 src/AngleSharp.Diffing/Core/Diffs/ElementDiffKind.cs create mode 100644 src/AngleSharp.Diffing/Core/Diffs/StylesheetDiff.cs create mode 100644 src/AngleSharp.Diffing/Core/Diffs/TextDiff.cs create mode 100644 src/AngleSharp.Diffing/System.Runtime.CompilerServices/IsExternalInit.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9120658..c352737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 0.18.2 + +- Changed `CompareStrategy` such that it now can control the `IDiff` type that should be returned in case a difference is found in a comparison. This allows a comparer to embed additional context in the `IDiff` object. By [@SebastianStehle](https://github.com/SebastianStehle). +- Changed `ElementComparer` to skip comparing two nodes of different types. By [@SebastianStehle](https://github.com/SebastianStehle). + # 0.18.1 - Fixed element comparer such that it can strictly check if the closing tags in the source markup is the same. diff --git a/src/AngleSharp.Diffing.Tests/Core/HtmlDifferenceEngineTest.cs b/src/AngleSharp.Diffing.Tests/Core/HtmlDifferenceEngineTest.cs index 6fc8cc6..7f8ae45 100644 --- a/src/AngleSharp.Diffing.Tests/Core/HtmlDifferenceEngineTest.cs +++ b/src/AngleSharp.Diffing.Tests/Core/HtmlDifferenceEngineTest.cs @@ -103,17 +103,46 @@ public void WhenNodesAreDifferentADiffIsReturned() var results = sut.Compare(nodes, nodes).ToList(); results.Count.ShouldBe(3); - results[0].ShouldBeOfType().ShouldSatisfyAllConditions( + results[0].ShouldBeAssignableTo().ShouldSatisfyAllConditions( diff => diff.Control.Node.NodeName.ShouldBe("P"), diff => diff.Result.ShouldBe(DiffResult.Different), diff => diff.Target.ShouldBe(DiffTarget.Element) ); - results[1].ShouldBeOfType().ShouldSatisfyAllConditions( + results[1].ShouldBeAssignableTo().ShouldSatisfyAllConditions( diff => diff.Control.Node.NodeName.ShouldBe("#comment"), diff => diff.Result.ShouldBe(DiffResult.Different), diff => diff.Target.ShouldBe(DiffTarget.Comment) ); - results[2].ShouldBeOfType().ShouldSatisfyAllConditions( + results[2].ShouldBeAssignableTo().ShouldSatisfyAllConditions( + diff => diff.Control.Node.NodeName.ShouldBe("#text"), + diff => diff.Result.ShouldBe(DiffResult.Different), + diff => diff.Target.ShouldBe(DiffTarget.Text) + ); + } + + [Fact(DisplayName = "When matched control/test nodes are different, a custom diff is returned")] + public void WhenNodesAreDifferentADiffIsReturnedWithCustomDiff() + { + var nodes = ToNodeList("

textnode"); + var sut = CreateHtmlDiffer( + nodeMatcher: OneToOneNodeListMatcher, + nodeFilter: NoneNodeFilter, + nodeComparer: DiffResultCustomNodeComparer); + + var results = sut.Compare(nodes, nodes).ToList(); + + results.Count.ShouldBe(3); + results[0].ShouldBeOfType().ShouldSatisfyAllConditions( + diff => diff.Control.Node.NodeName.ShouldBe("P"), + diff => diff.Result.ShouldBe(DiffResult.Different), + diff => diff.Target.ShouldBe(DiffTarget.Element) + ); + results[1].ShouldBeOfType().ShouldSatisfyAllConditions( + diff => diff.Control.Node.NodeName.ShouldBe("#comment"), + diff => diff.Result.ShouldBe(DiffResult.Different), + diff => diff.Target.ShouldBe(DiffTarget.Comment) + ); + results[2].ShouldBeOfType().ShouldSatisfyAllConditions( diff => diff.Control.Node.NodeName.ShouldBe("#text"), diff => diff.Result.ShouldBe(DiffResult.Different), diff => diff.Target.ShouldBe(DiffTarget.Text) @@ -237,6 +266,30 @@ public void WhenMatchedAttrsAreDiffAttrDiffIsReturned() ); } + [Fact(DisplayName = "When matched control/test attributes are different, a diff is returned with custom diff")] + public void WhenMatchedAttrsAreDiffAttrDiffIsReturnedWithCustomDiff() + { + var nodes = ToNodeList(@"

"); + + var sut = CreateHtmlDiffer( + nodeMatcher: OneToOneNodeListMatcher, + nodeFilter: NoneNodeFilter, + nodeComparer: SameResultNodeComparer, + attrMatcher: AttributeNameMatcher, + attrFilter: NoneAttrFilter, + attrComparer: DiffResultCustomAttrComparer); + + var results = sut.Compare(nodes, nodes).ToList(); + + results.Count.ShouldBe(1); + results[0].ShouldBeOfType().ShouldSatisfyAllConditions( + diff => diff.Control.Attribute.Name.ShouldBe("id"), + diff => diff.Test.Attribute.Name.ShouldBe("id"), + diff => diff.Result.ShouldBe(DiffResult.Different), + diff => diff.Target.ShouldBe(DiffTarget.Attribute) + ); + } + [Fact(DisplayName = "When matched control/test attributes are the same, no diffs are returned")] public void WhenMatchedAttrsAreSameNoDiffIsReturned() { @@ -268,11 +321,11 @@ public void WhenBothTestAndControlHaveChildNodesTheseAreCompared() var results = sut.Compare(nodes, nodes).ToList(); results.Count.ShouldBe(5); - results[0].ShouldBeOfType().Control.Node.NodeName.ShouldBe("MAIN"); - results[1].ShouldBeOfType().Control.Node.NodeName.ShouldBe("H1"); - results[2].ShouldBeOfType().Control.Node.NodeValue.ShouldBe("foobar"); - results[3].ShouldBeOfType().Control.Node.NodeName.ShouldBe("P"); - results[4].ShouldBeOfType().Control.Node.NodeName.ShouldBe("#text"); + results[0].ShouldBeAssignableTo().Control.Node.NodeName.ShouldBe("MAIN"); + results[1].ShouldBeAssignableTo().Control.Node.NodeName.ShouldBe("H1"); + results[2].ShouldBeAssignableTo().Control.Node.NodeValue.ShouldBe("foobar"); + results[3].ShouldBeAssignableTo().Control.Node.NodeName.ShouldBe("P"); + results[4].ShouldBeAssignableTo().Control.Node.NodeName.ShouldBe("#text"); } [Theory(DisplayName = "When only one of the control or test node in a comparison has child nodes, a missing/unexpected diff is returned")] @@ -288,7 +341,7 @@ public void OnlyOnePartHasChildNodes(string control, string test, Type expectedD var results = sut.Compare(ToNodeList(control), ToNodeList(test)).ToList(); results.Count.ShouldBe(2); - results[0].ShouldBeOfType(); + results[0].ShouldBeAssignableTo(); results[1].ShouldBeOfType(expectedDiffType); } @@ -309,8 +362,8 @@ public void ComparisonSourcesHaveCorrectType() results.Count.ShouldBe(2); - results[0].ShouldBeOfType().Control.SourceType.ShouldBe(ComparisonSourceType.Control); - results[0].ShouldBeOfType().Test.SourceType.ShouldBe(ComparisonSourceType.Test); + results[0].ShouldBeAssignableTo().Control.SourceType.ShouldBe(ComparisonSourceType.Control); + results[0].ShouldBeAssignableTo().Test.SourceType.ShouldBe(ComparisonSourceType.Test); results[1].ShouldBeOfType().Control.SourceType.ShouldBe(ComparisonSourceType.Control); results[1].ShouldBeOfType().Test.SourceType.ShouldBe(ComparisonSourceType.Test); } @@ -349,14 +402,14 @@ public void Test2() } [Theory(DisplayName = "When comparer returns SkipChildren flag from an element comparison, child nodes are not compared")] - [InlineData(CompareResult.Same | CompareResult.SkipChildren)] - [InlineData(CompareResult.Skip | CompareResult.SkipChildren)] - public void Test3(CompareResult compareResult) + [InlineData(CompareDecision.Same | CompareDecision.SkipChildren)] + [InlineData(CompareDecision.Skip | CompareDecision.SkipChildren)] + public void Test3(CompareDecision decision) { var sut = CreateHtmlDiffer( nodeMatcher: OneToOneNodeListMatcher, nodeFilter: NoneNodeFilter, - nodeComparer: c => c.Control.Node.NodeName == "P" ? compareResult : throw new Exception("NODE COMPARER SHOULD NOT BE CALLED ON CHILD NODES"), + nodeComparer: c => c.Control.Node.NodeName == "P" ? new CompareResult(decision) : throw new Exception("NODE COMPARER SHOULD NOT BE CALLED ON CHILD NODES"), attrMatcher: AttributeNameMatcher, attrFilter: NoneAttrFilter, attrComparer: SameResultAttrComparer @@ -368,14 +421,14 @@ public void Test3(CompareResult compareResult) } [Theory(DisplayName = "When comparer returns SkipAttributes flag from an element comparison, attributes are not compared")] - [InlineData(CompareResult.Same | CompareResult.SkipAttributes)] - [InlineData(CompareResult.Skip | CompareResult.SkipAttributes)] - public void Test4(CompareResult compareResult) + [InlineData(CompareDecision.Same | CompareDecision.SkipAttributes)] + [InlineData(CompareDecision.Skip | CompareDecision.SkipAttributes)] + public void Test4(CompareDecision decision) { var sut = CreateHtmlDiffer( nodeMatcher: OneToOneNodeListMatcher, nodeFilter: NoneNodeFilter, - nodeComparer: c => compareResult, + nodeComparer: c => new CompareResult(decision), attrMatcher: AttributeNameMatcher, attrFilter: NoneAttrFilter, attrComparer: SameResultAttrComparer @@ -411,6 +464,7 @@ private static IEnumerable OneToOneNodeListMatcher( #region NodeComparers private static CompareResult SameResultNodeComparer(Comparison comparison) => CompareResult.Same; private static CompareResult DiffResultNodeComparer(Comparison comparison) => CompareResult.Different; + private static CompareResult DiffResultCustomNodeComparer(Comparison comparison) => CompareResult.FromDiff(new CustomNodeDiff(comparison)); #endregion #region AttributeMatchers @@ -452,5 +506,22 @@ private static Func SpecificAttrFilte #region AttributeComparers public static CompareResult SameResultAttrComparer(AttributeComparison comparison) => CompareResult.Same; public static CompareResult DiffResultAttrComparer(AttributeComparison comparison) => CompareResult.Different; + public static CompareResult DiffResultCustomAttrComparer(AttributeComparison comparison) => CompareResult.FromDiff(new CustomAttrDiff(comparison)); + #endregion + + #region CustomDiff + public record CustomNodeDiff : NodeDiff + { + public CustomNodeDiff(in Comparison comparison) : base(comparison) + { + } + } + + public record CustomAttrDiff : AttrDiff + { + public CustomAttrDiff(in AttributeComparison comparison) : base(comparison, AttrDiffKind.Unspecified) + { + } + } #endregion } \ No newline at end of file diff --git a/src/AngleSharp.Diffing.Tests/DiffingTestBase.cs b/src/AngleSharp.Diffing.Tests/DiffingTestBase.cs index e463258..af96fe0 100644 --- a/src/AngleSharp.Diffing.Tests/DiffingTestBase.cs +++ b/src/AngleSharp.Diffing.Tests/DiffingTestBase.cs @@ -68,4 +68,10 @@ protected SourceMap ToSourceMap(string html, ComparisonSourceType sourceType = C var source = ToComparisonSource(html, sourceType); return new SourceMap(source); } + + public static TheoryData SameAndSkipCompareResult = new TheoryData + { + CompareResult.Same, + CompareResult.Skip, + }; } \ No newline at end of file diff --git a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/AttributeComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/AttributeComparerTest.cs index e281fb8..d3fcafd 100644 --- a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/AttributeComparerTest.cs +++ b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/AttributeComparerTest.cs @@ -1,20 +1,21 @@ namespace AngleSharp.Diffing.Strategies.AttributeStrategies; - public class AttributeComparerTest : DiffingTestBase { public AttributeComparerTest(DiffingTestFixture fixture) : base(fixture) { } - [Fact(DisplayName = "When compare is called with a current decision of Same or Skip, the current decision is returned")] - public void Test001() + [Theory(DisplayName = "When current result is same or skip, the current decision is returned")] + [MemberData(nameof(SameAndSkipCompareResult))] + public void Test001(CompareResult currentResult) { var comparison = ToAttributeComparison(@"", "foo", - "", "bar"); + "", "bar"); - AttributeComparer.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same); - AttributeComparer.Compare(comparison, CompareResult.Skip).ShouldBe(CompareResult.Skip); + new BooleanAttributeComparer(BooleanAttributeComparision.Strict) + .Compare(comparison, currentResult) + .ShouldBe(currentResult); } [Fact(DisplayName = "When two attributes has the same name and no value, the compare result is Same")] @@ -23,7 +24,9 @@ public void Test002() var comparison = ToAttributeComparison(@"", "foo", "", "foo"); - AttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Same); + AttributeComparer + .Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.Same); } [Fact(DisplayName = "When two attributes does not have the same name, the compare result is Different")] @@ -32,7 +35,9 @@ public void Test003() var comparison = ToAttributeComparison(@"", "foo", "", "bar"); - AttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Different); + AttributeComparer + .Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Name))); } [Fact(DisplayName = "When two attribute values are the same, the compare result is Same")] @@ -41,7 +46,9 @@ public void Test004() var comparison = ToAttributeComparison(@"", "foo", @"", "foo"); - AttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Same); + AttributeComparer + .Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.Same); } [Fact(DisplayName = "When two attribute values are different, the compare result is Different")] @@ -50,7 +57,9 @@ public void Test005() var comparison = ToAttributeComparison(@"", "foo", @"", "foo"); - AttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Different); + AttributeComparer + .Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value))); } [Fact(DisplayName = "When the control attribute is postfixed with :ignoreCase, " + diff --git a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/BooleanAttributeComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/BooleanAttributeComparerTest.cs index 05d3d98..db9dcbe 100644 --- a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/BooleanAttributeComparerTest.cs +++ b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/BooleanAttributeComparerTest.cs @@ -8,13 +8,25 @@ public BooleanAttributeComparerTest(DiffingTestFixture fixture) : base(fixture) { } + [Theory(DisplayName = "When current result is same or skip, the current decision is returned")] + [MemberData(nameof(SameAndSkipCompareResult))] + public void Test000(CompareResult currentResult) + { + var comparison = ToAttributeComparison(@"", "allowfullscreen", @"", "allowfullscreen"); + + new BooleanAttributeComparer(BooleanAttributeComparision.Strict) + .Compare(comparison, currentResult) + .ShouldBe(currentResult); + } + [Fact(DisplayName = "When attribute names are not the same comparer returns different")] public void Test001() { var sut = new BooleanAttributeComparer(BooleanAttributeComparision.Strict); var comparison = ToAttributeComparison("", "foo", "", "bar"); - sut.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Different); + sut.Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Name))); } [Fact(DisplayName = "When attribute name is not an boolean attribute, its current result is returned")] diff --git a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/ClassAttributeComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/ClassAttributeComparerTest.cs index a9eb44f..5987846 100644 --- a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/ClassAttributeComparerTest.cs +++ b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/ClassAttributeComparerTest.cs @@ -6,6 +6,18 @@ public ClassAttributeComparerTest(DiffingTestFixture fixture) : base(fixture) { } + [Theory(DisplayName = "When current result is same or skip, the current decision is returned")] + [MemberData(nameof(SameAndSkipCompareResult))] + public void Test000(CompareResult currentResult) + { + var comparison = ToAttributeComparison($@"

", "class", + $@"

", "class"); + + ClassAttributeComparer + .Compare(comparison, currentResult) + .ShouldBe(currentResult); + } + [Theory(DisplayName = "When a class attribute is compared, the order of individual " + "classes and multiple whitespace is ignored")] [InlineData("", "")] @@ -18,7 +30,9 @@ public void Test009(string controlClasses, string testClasses) var comparison = ToAttributeComparison($@"

", "class", $@"

", "class"); - ClassAttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Same); + ClassAttributeComparer + .Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.Same); } [Fact(DisplayName = "When a class attribute is matched up with another attribute, the result is different")] @@ -27,7 +41,9 @@ public void Test010() var comparison = ToAttributeComparison(@"

", "class", @"

", "bar"); - ClassAttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Different); + ClassAttributeComparer + .Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Name))); } [Theory(DisplayName = "When there are different number of classes in the class attributes the result is different")] @@ -38,7 +54,9 @@ public void Test011(string controlClasses, string testClasses) var comparison = ToAttributeComparison($@"

", "class", $@"

", "class"); - ClassAttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Different); + ClassAttributeComparer + .Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value))); } [Theory(DisplayName = "When the classes in the class attributes are different the result is different")] @@ -51,6 +69,8 @@ public void Test012(string controlClasses, string testClasses) var comparison = ToAttributeComparison($@"

", "class", $@"

", "class"); - ClassAttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Different); + ClassAttributeComparer + .Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value))); } } diff --git a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/IgnoreAttributeComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/IgnoreAttributeComparerTest.cs index 06cd19d..a59efc3 100644 --- a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/IgnoreAttributeComparerTest.cs +++ b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/IgnoreAttributeComparerTest.cs @@ -8,6 +8,20 @@ public IgnoreAttributeComparerTest(DiffingTestFixture fixture) : base(fixture) { } + [Theory(DisplayName = "When current result is same or skip, the current decision is returned")] + [MemberData(nameof(SameAndSkipCompareResult))] + public void Test000(CompareResult currentResult) + { + var comparison = ToAttributeComparison( + @"

", "foo", + @"

", "foo" + ); + + IgnoreAttributeComparer + .Compare(comparison, currentResult) + .ShouldBe(currentResult); + } + [Fact(DisplayName = "When a attribute does not contain have the ':ignore' postfix, the current decision is returned")] public void Test003() { diff --git a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/OrderingStyleAttributeComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/OrderingStyleAttributeComparerTest.cs index 2b06a0f..6e54c4b 100644 --- a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/OrderingStyleAttributeComparerTest.cs +++ b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/OrderingStyleAttributeComparerTest.cs @@ -6,13 +6,23 @@ public OrderingStyleAttributeComparerTest(DiffingTestFixture fixture) : base(fix { } + [Theory(DisplayName = "When current result is same or skip, the current decision is returned")] + [MemberData(nameof(SameAndSkipCompareResult))] + public void Test000(CompareResult currentResult) + { + var comparison = ToAttributeComparison(@"

", "style", @"

", "style"); + OrderingStyleAttributeComparer + .Compare(comparison, currentResult) + .ShouldBe(currentResult); + } + [Fact(DisplayName = "When attribute is not style the current decision is used")] public void Test001() { var comparison = ToAttributeComparison(@"

", "foo", @"

", "foo"); - StyleAttributeComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different); - StyleAttributeComparer.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same); - StyleAttributeComparer.Compare(comparison, CompareResult.Skip).ShouldBe(CompareResult.Skip); + OrderingStyleAttributeComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different); + OrderingStyleAttributeComparer.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same); + OrderingStyleAttributeComparer.Compare(comparison, CompareResult.Skip).ShouldBe(CompareResult.Skip); } [Theory(DisplayName = "When style attributes has different values then Different is returned")] @@ -23,7 +33,10 @@ public void Test001() public void Test002(string control, string test) { var comparison = ToAttributeComparison(control, "style", test, "style"); - OrderingStyleAttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Different); + + OrderingStyleAttributeComparer + .Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value))); } [Fact(DisplayName = "Comparer should correctly ignore insignificant whitespace")] diff --git a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/StyleAttributeComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/StyleAttributeComparerTest.cs index b2e617c..1595920 100644 --- a/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/StyleAttributeComparerTest.cs +++ b/src/AngleSharp.Diffing.Tests/Strategies/AttributeStrategies/StyleAttributeComparerTest.cs @@ -6,6 +6,16 @@ public StyleAttributeComparerTest(DiffingTestFixture fixture) : base(fixture) { } + [Theory(DisplayName = "When current result is same or skip, the current decision is returned")] + [MemberData(nameof(SameAndSkipCompareResult))] + public void Test000(CompareResult currentResult) + { + var comparison = ToAttributeComparison(@"

", "style", @"

", "style"); + StyleAttributeComparer + .Compare(comparison, currentResult) + .ShouldBe(currentResult); + } + [Fact(DisplayName = "When attribute is not style the current decision is used")] public void Test001() { @@ -15,6 +25,7 @@ public void Test001() StyleAttributeComparer.Compare(comparison, CompareResult.Skip).ShouldBe(CompareResult.Skip); } + [Theory(DisplayName = "When style attributes has different values then Different is returned")] [InlineData(@"

", @"

")] [InlineData(@"

", @"

")] @@ -23,7 +34,10 @@ public void Test001() public void Test002(string control, string test) { var comparison = ToAttributeComparison(control, "style", test, "style"); - StyleAttributeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Different); + + StyleAttributeComparer + .Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value))); } [Fact(DisplayName = "Comparer should correctly ignore insignificant whitespace")] diff --git a/src/AngleSharp.Diffing.Tests/Strategies/CommentStrategies/CommentComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/CommentStrategies/CommentComparerTest.cs new file mode 100644 index 0000000..b525104 --- /dev/null +++ b/src/AngleSharp.Diffing.Tests/Strategies/CommentStrategies/CommentComparerTest.cs @@ -0,0 +1,53 @@ +namespace AngleSharp.Diffing.Strategies.CommentStrategies; + +public class CommentComparerTest : DiffingTestBase +{ + public CommentComparerTest(DiffingTestFixture fixture) : base(fixture) + { + } + + [Theory(DisplayName = "When current result is same or skip, current result is returned")] + [MemberData(nameof(SameAndSkipCompareResult))] + public void Test000(CompareResult currentResult) + { + var comparison = ToComparison("", ""); + CommentComparer + .Compare(comparison, currentResult) + .ShouldBe(currentResult); + } + + [Theory(DisplayName = "When control and test are comment with equal content, the result is Same")] + [InlineData("", "")] + [InlineData("", "")] + public void Test001(string controlHtml, string testHtml) + { + var comparison = ToComparison(controlHtml, testHtml); + CommentComparer + .Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.Same); + } + + [Theory(DisplayName = "When control and test are comment with unequal content, the result is Different")] + [InlineData("", "")] + [InlineData("", "")] + [InlineData("", "")] + public void Test002(string controlHtml, string testHtml) + { + var comparison = ToComparison(controlHtml, testHtml); + CommentComparer + .Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.FromDiff(new CommentDiff(comparison))); + } + + [Theory(DisplayName = "When input node is not a IComment node, comparer does not run nor change the current decision")] + [InlineData("foo", "bar")] + [InlineData("

", "

")] + public void Test003(string controlHtml, string testHtml) + { + var comparison = ToComparison(controlHtml, testHtml); + + CommentComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different); + CommentComparer.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same); + CommentComparer.Compare(comparison, CompareResult.Skip).ShouldBe(CompareResult.Skip); + } +} diff --git a/src/AngleSharp.Diffing.Tests/Strategies/DiffingStrategyPipelineTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/DiffingStrategyPipelineTest.cs index 00e1e40..c071b28 100644 --- a/src/AngleSharp.Diffing.Tests/Strategies/DiffingStrategyPipelineTest.cs +++ b/src/AngleSharp.Diffing.Tests/Strategies/DiffingStrategyPipelineTest.cs @@ -71,8 +71,6 @@ public void Test003(FilterDecision expected) sut.Filter(new AttributeComparisonSource()).ShouldBe(expected); } -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. -#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. [Fact(DisplayName = "When no matcher strategy have been added, no comparisons are returned")] public void Test2() { @@ -81,8 +79,6 @@ public void Test2() sut.Match(null, null, (SourceCollection)null).ShouldBeEmpty(); sut.Match(null, null, (SourceMap)null).ShouldBeEmpty(); } -#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. [Fact(DisplayName = "Specialized node matchers are executed in the reverse order they are added in")] public void Test61() @@ -179,51 +175,51 @@ public void Test3() } [Theory(DisplayName = "Specialized comparers are executed in the order they are added in")] - [InlineData(CompareResult.Different, CompareResult.Same)] - [InlineData(CompareResult.Same, CompareResult.Different)] - public void Test8(CompareResult first, CompareResult final) + [InlineData(CompareDecision.Different, CompareDecision.Same)] + [InlineData(CompareDecision.Same, CompareDecision.Different)] + public void Test8(CompareDecision first, CompareDecision final) { var sut = new DiffingStrategyPipeline(); - sut.AddComparer((in Comparison c, CompareResult current) => first, StrategyType.Specialized); - sut.AddComparer((in Comparison c, CompareResult current) => final, StrategyType.Specialized); - sut.AddComparer((in AttributeComparison c, CompareResult current) => first, StrategyType.Specialized); - sut.AddComparer((in AttributeComparison c, CompareResult current) => final, StrategyType.Specialized); + sut.AddComparer((in Comparison c, CompareResult current) => new CompareResult(first), StrategyType.Specialized); + sut.AddComparer((in Comparison c, CompareResult current) => new CompareResult(final), StrategyType.Specialized); + sut.AddComparer((in AttributeComparison c, CompareResult current) => new CompareResult(first), StrategyType.Specialized); + sut.AddComparer((in AttributeComparison c, CompareResult current) => new CompareResult(final), StrategyType.Specialized); - sut.Compare(new Comparison()).ShouldBe(final); - sut.Compare(new AttributeComparison()).ShouldBe(final); + sut.Compare(new Comparison()).ShouldBe(new CompareResult(final)); + sut.Compare(new AttributeComparison()).ShouldBe(new CompareResult(final)); } [Theory(DisplayName = "Generalized comparers are executed in the reverse order they are added in")] - [InlineData(CompareResult.Different, CompareResult.Same)] - [InlineData(CompareResult.Same, CompareResult.Different)] - public void Test12321(CompareResult first, CompareResult final) + [InlineData(CompareDecision.Different, CompareDecision.Same)] + [InlineData(CompareDecision.Same, CompareDecision.Different)] + public void Test12321(CompareDecision first, CompareDecision final) { var sut = new DiffingStrategyPipeline(); - sut.AddComparer((in Comparison c, CompareResult current) => final, StrategyType.Generalized); - sut.AddComparer((in Comparison c, CompareResult current) => first, StrategyType.Generalized); - sut.AddComparer((in AttributeComparison c, CompareResult current) => final, StrategyType.Generalized); - sut.AddComparer((in AttributeComparison c, CompareResult current) => first, StrategyType.Generalized); + sut.AddComparer((in Comparison c, CompareResult current) => new CompareResult(final), StrategyType.Generalized); + sut.AddComparer((in Comparison c, CompareResult current) => new CompareResult(first), StrategyType.Generalized); + sut.AddComparer((in AttributeComparison c, CompareResult current) => new CompareResult(final), StrategyType.Generalized); + sut.AddComparer((in AttributeComparison c, CompareResult current) => new CompareResult(first), StrategyType.Generalized); - sut.Compare(new Comparison()).ShouldBe(final); - sut.Compare(new AttributeComparison()).ShouldBe(final); + sut.Compare(new Comparison()).ShouldBe(new CompareResult(final)); + sut.Compare(new AttributeComparison()).ShouldBe(new CompareResult(final)); } [Theory(DisplayName = "Generalized comparers are always executed before specialized comparers")] - [InlineData(CompareResult.Different, CompareResult.Same)] - [InlineData(CompareResult.Same, CompareResult.Different)] - public void Test8314(CompareResult first, CompareResult final) + [InlineData(CompareDecision.Different, CompareDecision.Same)] + [InlineData(CompareDecision.Same, CompareDecision.Different)] + public void Test8314(CompareDecision first, CompareDecision final) { var sut = new DiffingStrategyPipeline(); - sut.AddComparer((in Comparison c, CompareResult current) => first, StrategyType.Generalized); - sut.AddComparer((in Comparison c, CompareResult current) => final, StrategyType.Specialized); - sut.AddComparer((in AttributeComparison c, CompareResult current) => first, StrategyType.Generalized); - sut.AddComparer((in AttributeComparison c, CompareResult current) => final, StrategyType.Specialized); + sut.AddComparer((in Comparison c, CompareResult current) => new CompareResult(first), StrategyType.Generalized); + sut.AddComparer((in Comparison c, CompareResult current) => new CompareResult(final), StrategyType.Specialized); + sut.AddComparer((in AttributeComparison c, CompareResult current) => new CompareResult(first), StrategyType.Generalized); + sut.AddComparer((in AttributeComparison c, CompareResult current) => new CompareResult(final), StrategyType.Specialized); - sut.Compare(new Comparison()).ShouldBe(final); - sut.Compare(new AttributeComparison()).ShouldBe(final); + sut.Compare(new Comparison()).ShouldBe(new CompareResult(final)); + sut.Compare(new AttributeComparison()).ShouldBe(new CompareResult(final)); } [Fact(DisplayName = "After two nodes has been matched, they are marked as matched in the source collection")] diff --git a/src/AngleSharp.Diffing.Tests/Strategies/ElementStrategies/ElementComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/ElementStrategies/ElementComparerTest.cs index f0877ce..1061a98 100644 --- a/src/AngleSharp.Diffing.Tests/Strategies/ElementStrategies/ElementComparerTest.cs +++ b/src/AngleSharp.Diffing.Tests/Strategies/ElementStrategies/ElementComparerTest.cs @@ -1,4 +1,6 @@ -namespace AngleSharp.Diffing.Strategies.ElementStrategies; +using AngleSharp.Diffing.Core.Diffs; + +namespace AngleSharp.Diffing.Strategies.ElementStrategies; public class ElementComparerTest : DiffingTestBase { @@ -6,6 +8,17 @@ public ElementComparerTest(DiffingTestFixture fixture) : base(fixture) { } + [Theory(DisplayName = "When current result is same or skip, the current decision is returned")] + [MemberData(nameof(SameAndSkipCompareResult))] + public void Test000(CompareResult currentResult) + { + var comparison = ToComparison("

", "

"); + + new ElementComparer(enforceTagClosing: false) + .Compare(comparison, currentResult) + .ShouldBe(currentResult); + } + [Theory(DisplayName = "When control and test nodes have the same type and name and enforceTagClosing is false, the result is Same")] [InlineData("

", "

")] [InlineData("
", "
")] @@ -19,23 +32,28 @@ public void Test001(string controlHtml, string testHtml) .ShouldBe(CompareResult.Same); } - [Theory(DisplayName = "When control and test nodes have the a different type and name, the result is Different")] + [Theory(DisplayName = "When control and test nodes have the a name, the result is Different")] [InlineData("

", "

", false)] - [InlineData("

", "textnode", false)] - [InlineData("
", "", false)] - [InlineData("", "textnode", false)] - [InlineData("
", "
", true)] - [InlineData("", "", true)] [InlineData("
", "

", true)] - [InlineData("

", "textnode", true)] - [InlineData("
", "", true)] - [InlineData("", "textnode", true)] public void Test002(string controlHtml, string testHtml, bool enforceTagClosing) { var comparison = ToComparison(controlHtml, testHtml); + + new ElementComparer(enforceTagClosing) + .Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.FromDiff(new ElementDiff(comparison, ElementDiffKind.Name))); + } + + [Theory(DisplayName = "When control and test nodes have the a different closing style, the result is Different")] + [InlineData("
", "
", true)] + [InlineData("", "", true)] + public void Test003(string controlHtml, string testHtml, bool enforceTagClosing) + { + var comparison = ToComparison(controlHtml, testHtml); + new ElementComparer(enforceTagClosing) .Compare(comparison, CompareResult.Unknown) - .ShouldBe(CompareResult.Different); + .ShouldBe(CompareResult.FromDiff(new ElementDiff(comparison, ElementDiffKind.ClosingStyle))); } [Theory(DisplayName = "When unknown node is used in comparison, but node name is equal, the result is Same")] diff --git a/src/AngleSharp.Diffing.Tests/Strategies/TextNodeStrategies/StyleSheetTextNodeComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/TextNodeStrategies/StyleSheetTextNodeComparerTest.cs index e37e669..a7db2a8 100644 --- a/src/AngleSharp.Diffing.Tests/Strategies/TextNodeStrategies/StyleSheetTextNodeComparerTest.cs +++ b/src/AngleSharp.Diffing.Tests/Strategies/TextNodeStrategies/StyleSheetTextNodeComparerTest.cs @@ -32,7 +32,10 @@ public void Test001() { var comparison = ToStyleComparison(@"h1{background:#000;}", @"h1{color:#000;}"); - StyleSheetTextNodeComparer.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Different); + var result = StyleSheetTextNodeComparer.Compare(comparison, CompareResult.Unknown); + + result.Decision.ShouldBe(CompareDecision.Different); + result.Diff.ShouldBeEquivalentTo(new StylesheetDiff(comparison)); } [Theory(DisplayName = "The comparer ignores insignificant whitespace")] diff --git a/src/AngleSharp.Diffing.Tests/Strategies/TextNodeStrategies/TextNodeComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/TextNodeStrategies/TextNodeComparerTest.cs index 85d6e2b..7d18c0e 100644 --- a/src/AngleSharp.Diffing.Tests/Strategies/TextNodeStrategies/TextNodeComparerTest.cs +++ b/src/AngleSharp.Diffing.Tests/Strategies/TextNodeStrategies/TextNodeComparerTest.cs @@ -6,24 +6,22 @@ public TextNodeComparerTest(DiffingTestFixture fixture) : base(fixture) { } - [Fact(DisplayName = "When input node is not a IText node, comparer does not run nor change the current decision")] - public void Test2() + [Theory(DisplayName = "When current result is same or skip, the current decision is returned")] + [MemberData(nameof(SameAndSkipCompareResult))] + public void Test000(CompareResult currentResult) { - var comparison = ToComparison("

", "

"); - var sut = new TextNodeComparer(); + var comparison = ToComparison("hello world", " hello world "); - sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different); - sut.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same); - sut.Compare(comparison, CompareResult.Skip).ShouldBe(CompareResult.Skip); + new TextNodeComparer(WhitespaceOption.Preserve) + .Compare(comparison, currentResult) + .ShouldBe(currentResult); } - [Theory(DisplayName = "When option is Preserve or RemoveWhitespaceNodes, comparer does not run nor change the current decision")] - [InlineData(WhitespaceOption.Preserve)] - [InlineData(WhitespaceOption.RemoveWhitespaceNodes)] - public void Test5(WhitespaceOption whitespaceOption) + [Fact(DisplayName = "When input node is not a IText node, comparer does not run nor change the current decision")] + public void Test2() { - var comparison = ToComparison("hello world", " hello world "); - var sut = new TextNodeComparer(whitespaceOption); + var comparison = ToComparison("

", "

"); + var sut = new TextNodeComparer(); sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different); sut.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same); @@ -122,7 +120,8 @@ public void Test005(string tag) var testSource = ToComparisonSource("foo bar", ComparisonSourceType.Test); var comparison = new Comparison(controlSource, testSource); - sut.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Different); + sut.Compare(comparison, CompareResult.Unknown) + .ShouldBe(CompareResult.FromDiff(new TextDiff(comparison))); } [Theory(DisplayName = "When the parent element is
 and the whitespace option is set " +
@@ -187,7 +186,8 @@ public void Test009(string controlHtml)
         var testSource = ToComparisonSource("hello world", ComparisonSourceType.Test);
         var comparison = new Comparison(controlSource, testSource);
 
-        sut.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Different);
+        sut.Compare(comparison, CompareResult.Unknown)
+            .ShouldBe(CompareResult.FromDiff(new TextDiff(comparison)));
     }
 
     [Theory(DisplayName = "When diff:regex attribute is found on the immediate parent element, the control text is expected to a regex and that used when comparing to the test text node.")]
@@ -228,6 +228,4 @@ public void Test012(string controlHtml)
 
         sut.Compare(comparison, CompareResult.Unknown).ShouldBe(CompareResult.Same);
     }
-}
-
-
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Diffing/Core/AttributeComparison.cs b/src/AngleSharp.Diffing/Core/AttributeComparison.cs
index 9d9d5c9..18a0da7 100644
--- a/src/AngleSharp.Diffing/Core/AttributeComparison.cs
+++ b/src/AngleSharp.Diffing/Core/AttributeComparison.cs
@@ -3,45 +3,13 @@ namespace AngleSharp.Diffing.Core;
 /// 
 /// A match between two attributes that should be compared.
 /// 
-public readonly struct AttributeComparison : IEquatable
+/// Gets the control attribute which the  attribute is supposed to match.
+/// Gets the test attribute which should be compared to the  attribute.
+public readonly record struct AttributeComparison(in AttributeComparisonSource Control, in AttributeComparisonSource Test)
 {
-    /// 
-    /// Gets the control attribute which the  attribute is supposed to match.
-    /// 
-    public AttributeComparisonSource Control { get; }
-
-    /// 
-    /// Gets the test attribute which should be compared to the  attribute.
-    /// 
-    public AttributeComparisonSource Test { get; }
-
-    /// 
-    /// Create a attribute comparison match.
-    /// 
-    /// The attribute control source
-    /// The attribute test source
-    public AttributeComparison(in AttributeComparisonSource control, in AttributeComparisonSource test)
-    {
-        Control = control;
-        Test = test;
-    }
-
     /// 
     /// Returns the control and test elements which the control and test attributes belongs to.
     /// 
     public (IElement ControlElement, IElement TestElement) GetAttributeElements()
         => ((IElement)Control.ElementSource.Node, (IElement)Test.ElementSource.Node);
-
-    #region Equals and HashCode
-    /// 
-    public bool Equals(AttributeComparison other) => Control.Equals(other.Control) && Test.Equals(other.Test);
-    /// 
-    public override bool Equals(object? obj) => obj is AttributeComparison other && Equals(other);
-    /// 
-    public override int GetHashCode() => (Control, Test).GetHashCode();
-    /// 
-    public static bool operator ==(AttributeComparison left, AttributeComparison right) => left.Equals(right);
-    /// 
-    public static bool operator !=(AttributeComparison left, AttributeComparison right) => !left.Equals(right);
-    #endregion
 }
diff --git a/src/AngleSharp.Diffing/Core/AttributeComparisonSource.cs b/src/AngleSharp.Diffing/Core/AttributeComparisonSource.cs
index 0c35d15..4f9939f 100644
--- a/src/AngleSharp.Diffing/Core/AttributeComparisonSource.cs
+++ b/src/AngleSharp.Diffing/Core/AttributeComparisonSource.cs
@@ -30,7 +30,6 @@ namespace AngleSharp.Diffing.Core;
     /// 
     /// Name of the attribute.
     /// The source of the element the attribute belongs to.
-    [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Path should be in lower case")]
     public AttributeComparisonSource(string attributeName, in ComparisonSource elementSource)
     {
         if (string.IsNullOrEmpty(attributeName))
@@ -44,16 +43,24 @@ public AttributeComparisonSource(string attributeName, in ComparisonSource eleme
         Path = $"{elementSource.Path}[{attribute.Name.ToLowerInvariant()}]";
     }
 
-    #region Equals and HashCode
     /// 
-    public bool Equals(AttributeComparisonSource other) => Object.ReferenceEquals(Attribute, other.Attribute) && Path.Equals(other.Path, StringComparison.Ordinal) && ElementSource.Equals(other.ElementSource);
+    public override string ToString() => $"{{ Type = Attribute, Path = {Path} }}";
+
+    /// 
+    public bool Equals(AttributeComparisonSource other)
+        => ReferenceEquals(Attribute, other.Attribute) // AngleSharp overrides Equals and == for it's types, so we're using ReferenceEquals to check if the instances are the same.
+        && Path.Equals(other.Path, StringComparison.Ordinal)
+        && ElementSource.Equals(other.ElementSource);
+
     /// 
     public override int GetHashCode() => (Attribute, ElementSource).GetHashCode();
+
     /// 
     public override bool Equals(object? obj) => obj is AttributeComparisonSource other && Equals(other);
+
     /// 
     public static bool operator ==(AttributeComparisonSource left, AttributeComparisonSource right) => left.Equals(right);
+
     /// 
     public static bool operator !=(AttributeComparisonSource left, AttributeComparisonSource right) => !left.Equals(right);
-    #endregion
 }
diff --git a/src/AngleSharp.Diffing/Core/CompareDecision.cs b/src/AngleSharp.Diffing/Core/CompareDecision.cs
new file mode 100644
index 0000000..cc0942e
--- /dev/null
+++ b/src/AngleSharp.Diffing/Core/CompareDecision.cs
@@ -0,0 +1,34 @@
+namespace AngleSharp.Diffing.Core;
+
+/// 
+/// Represents the decision of a comparison.
+/// 
+[Flags]
+public enum CompareDecision
+{
+    /// 
+    /// Use when the compare result is unknown.
+    /// 
+    Unknown = 0,
+    /// 
+    /// Use when the two compared nodes or attributes are the same.
+    /// 
+    Same = 1,
+    /// 
+    /// Use when the two compared nodes or attributes are the different.
+    /// 
+    Different = 2,
+    /// 
+    /// Use when the comparison should be skipped and any child-nodes or attributes skipped as well.
+    /// 
+    Skip = 4,
+    /// 
+    /// Use when the comparison should skip any child-nodes.
+    /// 
+    SkipChildren = 8,
+    /// 
+    /// Use when the comparison should skip any attributes.
+    /// 
+    SkipAttributes = 16,
+}
+
diff --git a/src/AngleSharp.Diffing/Core/CompareResult.cs b/src/AngleSharp.Diffing/Core/CompareResult.cs
index 6ca7564..0d4ea28 100644
--- a/src/AngleSharp.Diffing/Core/CompareResult.cs
+++ b/src/AngleSharp.Diffing/Core/CompareResult.cs
@@ -3,44 +3,49 @@
 /// 
 /// Represents a result of a comparison.
 /// 
-[Flags]
-public enum CompareResult
+/// Gets the latest  of the comparison.
+/// Gets the optional  related to the current .
+public readonly record struct CompareResult(CompareDecision Decision, IDiff? Diff = null)
 {
     /// 
     /// Use when the compare result is unknown.
     /// 
-    Unknown = 0,
+    public static readonly CompareResult Unknown = default;
+
     /// 
     /// Use when the two compared nodes or attributes are the same.
     /// 
-    Same = 1,
-    /// 
-    /// Use when the two compared nodes or attributes are the different.
-    /// 
-    Different = 2,
+    public static readonly CompareResult Same = new CompareResult(CompareDecision.Same);
+
     /// 
     /// Use when the comparison should be skipped and any child-nodes or attributes skipped as well.
     /// 
-    Skip = 4,
+    public static readonly CompareResult Skip = new CompareResult(CompareDecision.Skip);
+
     /// 
     /// Use when the comparison should skip any child-nodes.
     /// 
-    SkipChildren = 8,
+    public static readonly CompareResult SkipChildren = new CompareResult(CompareDecision.SkipChildren);
+
     /// 
     /// Use when the comparison should skip any attributes.
     /// 
-    SkipAttributes = 16,
-}
+    public static readonly CompareResult SkipAttributes = new CompareResult(CompareDecision.SkipAttributes);
 
-/// 
-/// Helper methods for 
-/// 
-public static class CompareResultExtensions
-{
     /// 
-    /// Checks if a  is either a  or .
+    /// Use when the two compared nodes or attributes are the different.
     /// 
-    /// The compare result
-    public static bool IsSameOrSkip(this CompareResult compareResult) => compareResult == CompareResult.Same || compareResult == CompareResult.Skip;
-}
+    public static CompareResult Different => new CompareResult(CompareDecision.Different);
 
+    /// 
+    /// Use when the two compared nodes or attributes are the different.
+    /// 
+    /// The associated  describing the difference.
+    /// Returns a  with  set to .
+    public static CompareResult FromDiff(IDiff diff) => new CompareResult(CompareDecision.Different, diff);
+
+    /// 
+    /// Checks if a  is either a  or .
+    /// 
+    public bool IsSameOrSkip => this == Same || this == Skip;
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Diffing/Core/Comparison.cs b/src/AngleSharp.Diffing/Core/Comparison.cs
index 3050f18..cdab391 100644
--- a/src/AngleSharp.Diffing/Core/Comparison.cs
+++ b/src/AngleSharp.Diffing/Core/Comparison.cs
@@ -3,19 +3,10 @@ namespace AngleSharp.Diffing.Core;
 /// 
 /// Represent a comparison between two nodes.
 /// 
-[DebuggerDisplay("Control: {Control.Path} | Test: {Test.Path}")]
-public readonly struct Comparison : IEquatable
+/// Gets the control source in the comparison.
+/// Gets the test source in the comparison.
+public readonly record struct Comparison(in ComparisonSource Control, in ComparisonSource Test)
 {
-    /// 
-    /// Gets the control source in the comparison.
-    /// 
-    public ComparisonSource Control { get; }
-
-    /// 
-    /// Gets the test source in the comparison
-    /// 
-    public ComparisonSource Test { get; }
-
     /// 
     /// Gets whether the control and test nodes are of the same type and has the same name.
     /// 
@@ -23,17 +14,6 @@ public bool AreNodeTypesEqual
         => Control.Node.NodeType == Test.Node.NodeType
         && Control.Node.NodeName.Equals(Test.Node.NodeName, StringComparison.OrdinalIgnoreCase);
 
-    /// 
-    /// Creates a new comparison.
-    /// 
-    /// The control source in the comparison.
-    /// The tes tsource in the comparison.
-    public Comparison(in ComparisonSource control, in ComparisonSource test)
-    {
-        Control = control;
-        Test = test;
-    }
-
     /// 
     /// Try to get the control and test node as the  type.
     /// Returns true if both test and control node is of type , false otherwise.
@@ -55,17 +35,4 @@ public bool TryGetNodesAsType([NotNullWhen(true)]out TNode? controlNode,
             return false;
         }
     }
-
-    #region Equals and HashCode
-    /// 
-    public bool Equals(Comparison other) => Control.Equals(other.Control) && Test.Equals(other.Test);
-    /// 
-    public override bool Equals(object? obj) => obj is Comparison other && Equals(other);
-    /// 
-    public override int GetHashCode() => (Control, Test).GetHashCode();
-    /// 
-    public static bool operator ==(Comparison left, Comparison right) => left.Equals(right);
-    /// 
-    public static bool operator !=(Comparison left, Comparison right) => !left.Equals(right);
-    #endregion
 }
diff --git a/src/AngleSharp.Diffing/Core/ComparisonSource.cs b/src/AngleSharp.Diffing/Core/ComparisonSource.cs
index ddf45ba..13025e2 100644
--- a/src/AngleSharp.Diffing/Core/ComparisonSource.cs
+++ b/src/AngleSharp.Diffing/Core/ComparisonSource.cs
@@ -3,7 +3,6 @@ namespace AngleSharp.Diffing.Core;
 /// 
 /// Represents a node source in a comparison.
 ///     
-[DebuggerDisplay("{Index} : {Path}")]
 public readonly struct ComparisonSource : IEquatable, IComparisonSource
 {
     private readonly int _hashCode;
@@ -65,7 +64,6 @@ public ComparisonSource(INode node, int index, string parentsPath, ComparisonSou
     /// 
     /// 
     /// 
-    [SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "Path should be in lower case")]
     public static string GetNodePathSegment(INode node)
     {
         var index = GetPathIndex(node);
@@ -123,16 +121,21 @@ private static int GetPathIndex(INode node)
         throw new InvalidOperationException("Unexpected node tree state. The node was not found in its parents child nodes collection.");
     }
 
-    #region Equals and HashCode
+    /// 
+    public override string ToString() => $"{{ Type = {Node.NodeType}, Index = {Index}, Path = {Path} }}";
+
     /// 
     public bool Equals(ComparisonSource other) => Object.ReferenceEquals(Node, other.Node) && Index == other.Index && Path.Equals(other.Path, StringComparison.Ordinal) && SourceType == other.SourceType;
+
     /// 
     public override int GetHashCode() => _hashCode;
+
     /// 
     public override bool Equals(object? obj) => obj is ComparisonSource other && Equals(other);
+
     /// 
     public static bool operator ==(ComparisonSource left, ComparisonSource right) => left.Equals(right);
+
     /// 
     public static bool operator !=(ComparisonSource left, ComparisonSource right) => !left.Equals(right);
-    #endregion
 }
diff --git a/src/AngleSharp.Diffing/Core/Diffs/AttrDiff.cs b/src/AngleSharp.Diffing/Core/Diffs/AttrDiff.cs
index 9924b9c..e667a0d 100644
--- a/src/AngleSharp.Diffing/Core/Diffs/AttrDiff.cs
+++ b/src/AngleSharp.Diffing/Core/Diffs/AttrDiff.cs
@@ -1,15 +1,20 @@
 namespace AngleSharp.Diffing.Core;
 
 /// 
-/// Represents an attribute difference
+/// Represents an attribute difference.
 /// 
-[DebuggerDisplay("Attribute diff: Control = {Control.Path}, Test = {Test.Path}")]
-public class AttrDiff : DiffBase
+public record class AttrDiff : DiffBase
 {
+    /// 
+    /// Gets the kind of the diff.
+    /// 
+    public AttrDiffKind Kind { get; }
+
     /// 
     /// Creates an .
     /// 
-    public AttrDiff(in AttributeComparison comparison) : base(comparison.Control, comparison.Test, DiffTarget.Attribute)
+    public AttrDiff(in AttributeComparison comparison, AttrDiffKind kind) : base(comparison.Control, comparison.Test, DiffTarget.Attribute)
     {
+        Kind = kind;
     }
 }
diff --git a/src/AngleSharp.Diffing/Core/Diffs/AttrDiffKind.cs b/src/AngleSharp.Diffing/Core/Diffs/AttrDiffKind.cs
new file mode 100644
index 0000000..18f105a
--- /dev/null
+++ b/src/AngleSharp.Diffing/Core/Diffs/AttrDiffKind.cs
@@ -0,0 +1,20 @@
+namespace AngleSharp.Diffing.Core;
+
+/// 
+/// Defines the reason for two attributes to be the different.
+/// 
+public enum AttrDiffKind
+{
+    /// 
+    /// The attribute difference is unspecified.
+    /// 
+    Unspecified = 0,
+    /// 
+    /// The name of the attribute is different.
+    /// 
+    Name,
+    /// 
+    /// The value of the attribute is different.
+    /// 
+    Value,
+}
diff --git a/src/AngleSharp.Diffing/Core/Diffs/CommentDiff.cs b/src/AngleSharp.Diffing/Core/Diffs/CommentDiff.cs
new file mode 100644
index 0000000..e450eb2
--- /dev/null
+++ b/src/AngleSharp.Diffing/Core/Diffs/CommentDiff.cs
@@ -0,0 +1,14 @@
+namespace AngleSharp.Diffing.Core;
+
+/// 
+/// Represents a difference between two nodes.
+/// 
+public record class CommentDiff : NodeDiff
+{
+    /// 
+    /// Creates a .
+    /// 
+    public CommentDiff(in Comparison comparison) : base(comparison)
+    {
+    }
+}
diff --git a/src/AngleSharp.Diffing/Core/Diffs/DiffBase.cs b/src/AngleSharp.Diffing/Core/Diffs/DiffBase.cs
index 40273ce..2657337 100644
--- a/src/AngleSharp.Diffing/Core/Diffs/DiffBase.cs
+++ b/src/AngleSharp.Diffing/Core/Diffs/DiffBase.cs
@@ -4,32 +4,11 @@ namespace AngleSharp.Diffing.Core;
 /// Represents a difference found during comparison.
 /// 
 /// 
-public abstract class DiffBase : IDiff where T : struct
+/// Gets the control source in the comparison.
+/// Gets the test source in the comparison.
+///  Gets the target type in that failed the comparison.
+public abstract record class DiffBase(T Control, T Test, DiffTarget Target) : IDiff where T : struct
 {
-    /// 
-    /// Gets the control source in the comparison.
-    /// 
-    public T Control { get; }
-
-    /// 
-    /// Gets the test source in the comparison.
-    /// 
-    public T Test { get; }
-
     /// 
-    public DiffResult Result { get; }
-
-    /// 
-    public DiffTarget Target { get; }
-
-    /// 
-    /// Instantiate the 
-    /// 
-    protected DiffBase(in T control, in T test, DiffTarget target)
-    {
-        Control = control;
-        Test = test;
-        Result = DiffResult.Different;
-        Target = target;
-    }
+    public DiffResult Result => DiffResult.Different;
 }
diff --git a/src/AngleSharp.Diffing/Core/Diffs/ElementDiff.cs b/src/AngleSharp.Diffing/Core/Diffs/ElementDiff.cs
new file mode 100644
index 0000000..bebd620
--- /dev/null
+++ b/src/AngleSharp.Diffing/Core/Diffs/ElementDiff.cs
@@ -0,0 +1,21 @@
+namespace AngleSharp.Diffing.Core.Diffs;
+
+/// 
+/// Represents an element difference.
+/// 
+public record class ElementDiff : NodeDiff
+{
+    /// 
+    /// Gets the kind of the diff.
+    /// 
+    public ElementDiffKind Kind { get; }
+
+    /// 
+    /// Creates an .
+    /// 
+    public ElementDiff(in Comparison comparison, ElementDiffKind kind) : base(comparison)
+    {
+        Kind = kind;
+    }
+
+}
diff --git a/src/AngleSharp.Diffing/Core/Diffs/ElementDiffKind.cs b/src/AngleSharp.Diffing/Core/Diffs/ElementDiffKind.cs
new file mode 100644
index 0000000..4404e03
--- /dev/null
+++ b/src/AngleSharp.Diffing/Core/Diffs/ElementDiffKind.cs
@@ -0,0 +1,20 @@
+namespace AngleSharp.Diffing.Core.Diffs;
+
+/// 
+/// Defines the reason of two elements to be different.
+/// 
+public enum ElementDiffKind
+{
+    /// 
+    /// The attribute difference is unspecified.
+    /// 
+    Unspecified = 0,
+    /// 
+    /// The type/name of the elements are different.
+    /// 
+    Name,
+    /// 
+    /// The two elements have difference tag closing styles.
+    /// 
+    ClosingStyle,
+}
diff --git a/src/AngleSharp.Diffing/Core/Diffs/MissingAttrDiff.cs b/src/AngleSharp.Diffing/Core/Diffs/MissingAttrDiff.cs
index 2df6334..17d7c54 100644
--- a/src/AngleSharp.Diffing/Core/Diffs/MissingAttrDiff.cs
+++ b/src/AngleSharp.Diffing/Core/Diffs/MissingAttrDiff.cs
@@ -3,8 +3,7 @@ namespace AngleSharp.Diffing.Core;
 /// 
 /// Represents a missing attribute in a test node.
 /// 
-[DebuggerDisplay("Missing Attribute: Control = {Control.Path}")]
-public class MissingAttrDiff : MissingDiffBase
+public record class MissingAttrDiff : MissingDiffBase
 {
     /// 
     /// Creates a .
diff --git a/src/AngleSharp.Diffing/Core/Diffs/MissingDiffBase.cs b/src/AngleSharp.Diffing/Core/Diffs/MissingDiffBase.cs
index d172c55..ae076c6 100644
--- a/src/AngleSharp.Diffing/Core/Diffs/MissingDiffBase.cs
+++ b/src/AngleSharp.Diffing/Core/Diffs/MissingDiffBase.cs
@@ -3,27 +3,10 @@
 /// 
 /// Represents a missing node or attribute in the test DOM tree.
 /// 
-/// 
-public abstract class MissingDiffBase : IDiff where T : struct
+/// Gets the control source that has the missing item.
+/// Gets the target type in that failed the comparison.
+public abstract record class MissingDiffBase(T Control, DiffTarget Target) : IDiff where T : struct
 {
-    /// 
-    /// Gets the control source that has the missing item.
-    /// 
-    public T Control { get; }
-
-    /// 
-    public DiffResult Result { get; }
-
     /// 
-    public DiffTarget Target { get; }
-
-    /// 
-    /// Create a 
-    /// 
-    protected MissingDiffBase(in T control, DiffTarget target)
-    {
-        Control = control;
-        Result = DiffResult.Missing;
-        Target = target;
-    }
+    public DiffResult Result => DiffResult.Missing;
 }
diff --git a/src/AngleSharp.Diffing/Core/Diffs/MissingNodeDiff.cs b/src/AngleSharp.Diffing/Core/Diffs/MissingNodeDiff.cs
index e85dae1..6334838 100644
--- a/src/AngleSharp.Diffing/Core/Diffs/MissingNodeDiff.cs
+++ b/src/AngleSharp.Diffing/Core/Diffs/MissingNodeDiff.cs
@@ -3,8 +3,7 @@ namespace AngleSharp.Diffing.Core;
 /// 
 /// Represents a missing node in the test DOM tree.
 /// 
-[DebuggerDisplay("Missing {Target}: Control = {Control.Path}")]
-public class MissingNodeDiff : MissingDiffBase
+public record class MissingNodeDiff : MissingDiffBase
 {
     /// 
     /// Creates a .
diff --git a/src/AngleSharp.Diffing/Core/Diffs/NodeDiff.cs b/src/AngleSharp.Diffing/Core/Diffs/NodeDiff.cs
index 0afd036..6fa5cec 100644
--- a/src/AngleSharp.Diffing/Core/Diffs/NodeDiff.cs
+++ b/src/AngleSharp.Diffing/Core/Diffs/NodeDiff.cs
@@ -3,8 +3,7 @@ namespace AngleSharp.Diffing.Core;
 /// 
 /// Represents a difference between two nodes.
 /// 
-[DebuggerDisplay("{Target} diff: Control = {Control.Path}, Test = {Test.Path}")]
-public class NodeDiff : DiffBase
+public record class NodeDiff : DiffBase
 {
     /// 
     /// Creates a .
@@ -12,4 +11,4 @@ public class NodeDiff : DiffBase
     public NodeDiff(in Comparison comparison) : base(comparison.Control, comparison.Test, comparison.Control.Node.NodeType.ToDiffTarget())
     {
     }
-}
+}
\ No newline at end of file
diff --git a/src/AngleSharp.Diffing/Core/Diffs/StylesheetDiff.cs b/src/AngleSharp.Diffing/Core/Diffs/StylesheetDiff.cs
new file mode 100644
index 0000000..99a97fa
--- /dev/null
+++ b/src/AngleSharp.Diffing/Core/Diffs/StylesheetDiff.cs
@@ -0,0 +1,14 @@
+namespace AngleSharp.Diffing.Core;
+
+/// 
+/// Represents a difference between the applied style of two nodes
+/// 
+public record class StylesheetDiff : NodeDiff
+{
+    /// 
+    /// Creates a .
+    /// 
+    public StylesheetDiff(in Comparison comparison) : base(comparison)
+    {
+    }
+}
diff --git a/src/AngleSharp.Diffing/Core/Diffs/TextDiff.cs b/src/AngleSharp.Diffing/Core/Diffs/TextDiff.cs
new file mode 100644
index 0000000..6598593
--- /dev/null
+++ b/src/AngleSharp.Diffing/Core/Diffs/TextDiff.cs
@@ -0,0 +1,14 @@
+namespace AngleSharp.Diffing.Core;
+
+/// 
+/// Represents a difference between two texts.
+/// 
+public record class TextDiff : NodeDiff
+{
+    /// 
+    /// Creates a .
+    /// 
+    public TextDiff(in Comparison comparison) : base(comparison)
+    {
+    }
+}
diff --git a/src/AngleSharp.Diffing/Core/Diffs/UnexpectedAttrDiff.cs b/src/AngleSharp.Diffing/Core/Diffs/UnexpectedAttrDiff.cs
index c85d58c..7a7e364 100644
--- a/src/AngleSharp.Diffing/Core/Diffs/UnexpectedAttrDiff.cs
+++ b/src/AngleSharp.Diffing/Core/Diffs/UnexpectedAttrDiff.cs
@@ -3,8 +3,7 @@ namespace AngleSharp.Diffing.Core;
 /// 
 /// Represents an unexpected attribute found in the test DOM tree.
 /// 
-[DebuggerDisplay("Unexpected Attribute: Test = {Test.Path}")]
-public class UnexpectedAttrDiff : UnexpectedDiffBase
+public record class UnexpectedAttrDiff : UnexpectedDiffBase
 {
     /// 
     /// Creates a .
diff --git a/src/AngleSharp.Diffing/Core/Diffs/UnexpectedDiffBase.cs b/src/AngleSharp.Diffing/Core/Diffs/UnexpectedDiffBase.cs
index c675ae5..04c62be 100644
--- a/src/AngleSharp.Diffing/Core/Diffs/UnexpectedDiffBase.cs
+++ b/src/AngleSharp.Diffing/Core/Diffs/UnexpectedDiffBase.cs
@@ -3,26 +3,10 @@ namespace AngleSharp.Diffing.Core;
 /// 
 /// Represents an unexpected node or attribute in the test DOM tree.
 /// 
-public abstract class UnexpectedDiffBase : IDiff where T : struct
+/// The source of the unexpected item in the test DOM tree.
+///  Gets the target type in that failed the comparison.
+public abstract record class UnexpectedDiffBase(T Test, DiffTarget Target) : IDiff where T : struct
 {
-    /// 
-    /// The source of the unexpected item in the test DOM tree.
-    /// 
-    public T Test { get; }
-
-    /// 
-    public DiffResult Result { get; }
-
     /// 
-    public DiffTarget Target { get; }
-
-    /// 
-    /// Creates a .
-    /// 
-    protected UnexpectedDiffBase(in T test, DiffTarget target)
-    {
-        Test = test;
-        Result = DiffResult.Unexpected;
-        Target = target;
-    }
+    public DiffResult Result => DiffResult.Unexpected;
 }
diff --git a/src/AngleSharp.Diffing/Core/Diffs/UnexpectedNodeDiff.cs b/src/AngleSharp.Diffing/Core/Diffs/UnexpectedNodeDiff.cs
index 43b5d02..a67eb50 100644
--- a/src/AngleSharp.Diffing/Core/Diffs/UnexpectedNodeDiff.cs
+++ b/src/AngleSharp.Diffing/Core/Diffs/UnexpectedNodeDiff.cs
@@ -3,8 +3,7 @@ namespace AngleSharp.Diffing.Core;
 /// 
 /// Represents an unexpected node in the test DOM tree.
 /// 
-[DebuggerDisplay("Unexpected {Target}: Test = {Test.Path}")]
-public class UnexpectedNodeDiff : UnexpectedDiffBase
+public record class UnexpectedNodeDiff : UnexpectedDiffBase
 {
     /// 
     /// Creates a .
diff --git a/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs b/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs
index f0f9aa8..8065dac 100644
--- a/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs
+++ b/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs
@@ -1,3 +1,5 @@
+using AngleSharp.Diffing.Core.Diffs;
+
 namespace AngleSharp.Diffing.Core;
 
 /// 
@@ -85,9 +87,9 @@ private IEnumerable CompareNode(in Comparison comparison)
         }
 
         var compareRes = _diffingStrategy.Compare(comparison);
-        if (compareRes.HasFlag(CompareResult.Different))
+        if (compareRes.Decision.HasFlag(CompareDecision.Different))
         {
-            IDiff diff = new NodeDiff(comparison);
+            IDiff diff = compareRes.Diff ?? new NodeDiff(comparison);
             return new[] { diff };
         }
 
@@ -99,16 +101,16 @@ private IEnumerable CompareElement(in Comparison comparison)
         var result = new List();
 
         var compareRes = _diffingStrategy.Compare(comparison);
-        if (compareRes.HasFlag(CompareResult.Different))
+        if (compareRes.Decision.HasFlag(CompareDecision.Different))
         {
-            result.Add(new NodeDiff(comparison));
+            result.Add(compareRes.Diff ?? new ElementDiff(comparison, ElementDiffKind.Unspecified));
         }
 
-        if (!compareRes.HasFlag(CompareResult.Skip))
+        if (!compareRes.Decision.HasFlag(CompareDecision.Skip))
         {
-            if (!compareRes.HasFlag(CompareResult.SkipAttributes))
+            if (!compareRes.Decision.HasFlag(CompareDecision.SkipAttributes))
                 result.AddRange(CompareElementAttributes(comparison));
-            if (!compareRes.HasFlag(CompareResult.SkipChildren))
+            if (!compareRes.Decision.HasFlag(CompareDecision.SkipChildren))
                 result.AddRange(CompareChildNodes(comparison));
         }
 
@@ -184,8 +186,10 @@ private IEnumerable CompareAttributes(IEnumerable co
         foreach (var comparison in comparisons)
         {
             var compareRes = _diffingStrategy.Compare(comparison);
-            if (compareRes == CompareResult.Different)
-                yield return new AttrDiff(comparison);
+            if (compareRes.Decision == CompareDecision.Different)
+            {
+                yield return compareRes.Diff ?? new AttrDiff(comparison, AttrDiffKind.Unspecified);
+            }
         }
     }
 }
diff --git a/src/AngleSharp.Diffing/Extensions/UnexpectedDOMTreeStructureException.cs b/src/AngleSharp.Diffing/Extensions/UnexpectedDOMTreeStructureException.cs
index 014cf85..cd77df0 100644
--- a/src/AngleSharp.Diffing/Extensions/UnexpectedDOMTreeStructureException.cs
+++ b/src/AngleSharp.Diffing/Extensions/UnexpectedDOMTreeStructureException.cs
@@ -1,6 +1,4 @@
-using System.Runtime.Serialization;
-
-namespace AngleSharp.Diffing.Extensions;
+namespace AngleSharp.Diffing.Extensions;
 
 /// 
 /// Represents an exception that is thrown when a part of the DOM tree is not as expected.
diff --git a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/AttributeComparer.cs b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/AttributeComparer.cs
index 6c935c2..782773d 100644
--- a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/AttributeComparer.cs
+++ b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/AttributeComparer.cs
@@ -17,7 +17,7 @@ public static class AttributeComparer
     /// 
     public static CompareResult Compare(in AttributeComparison comparison, CompareResult currentDecision)
     {
-        if (currentDecision.IsSameOrSkip())
+        if (currentDecision.IsSameOrSkip)
             return currentDecision;
 
         var (ignoreCase, isRegexValueCompare) = GetComparisonModifiers(comparison);
@@ -25,7 +25,7 @@ public static CompareResult Compare(in AttributeComparison comparison, CompareRe
         var hasSameName = CompareAttributeNames(comparison, ignoreCase, isRegexValueCompare);
 
         if (!hasSameName)
-            return CompareResult.Different;
+            return CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Name));
 
         var hasSameValue = isRegexValueCompare
             ? CompareAttributeValuesByRegex(comparison, ignoreCase)
@@ -33,7 +33,7 @@ public static CompareResult Compare(in AttributeComparison comparison, CompareRe
 
         return hasSameValue
             ? CompareResult.Same
-            : CompareResult.Different;
+            : CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value));
     }
 
     private static (bool ignoreCase, bool isRegexCompare) GetComparisonModifiers(in AttributeComparison comparison)
diff --git a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/BooleanAttributeComparer.cs b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/BooleanAttributeComparer.cs
index 2658b63..889badc 100644
--- a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/BooleanAttributeComparer.cs
+++ b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/BooleanAttributeComparer.cs
@@ -55,10 +55,10 @@ public BooleanAttributeComparer(BooleanAttributeComparision mode)
     /// 
     public CompareResult Compare(in AttributeComparison comparison, CompareResult currentDecision)
     {
-        if (currentDecision.IsSameOrSkip())
+        if (currentDecision.IsSameOrSkip)
             return currentDecision;
         if (!IsAttributeNamesEqual(comparison))
-            return CompareResult.Different;
+            return CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Name));
         if (!BooleanAttributesSet.Contains(comparison.Control.Attribute.Name))
             return currentDecision;
 
@@ -66,7 +66,9 @@ public CompareResult Compare(in AttributeComparison comparison, CompareResult cu
             ? CompareStrict(comparison)
             : true;
 
-        return hasSameValue ? CompareResult.Same : CompareResult.Different;
+        return hasSameValue ?
+            CompareResult.Same :
+            CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value));
     }
 
     private static bool IsAttributeNamesEqual(in AttributeComparison comparison)
diff --git a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/ClassAttributeComparer.cs b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/ClassAttributeComparer.cs
index 8f0d56e..cfe136b 100644
--- a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/ClassAttributeComparer.cs
+++ b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/ClassAttributeComparer.cs
@@ -12,19 +12,19 @@ public static class ClassAttributeComparer
     /// 
     public static CompareResult Compare(in AttributeComparison comparison, CompareResult currentDecision)
     {
-        if (currentDecision.IsSameOrSkip())
+        if (currentDecision.IsSameOrSkip)
             return currentDecision;
 
         if (!IsBothClassAttributes(comparison))
-            return CompareResult.Different;
+            return CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Name));
 
         var (ctrlElm, testElm) = comparison.GetAttributeElements();
         if (ctrlElm.ClassList.Length != testElm.ClassList.Length)
-            return CompareResult.Different;
+            return CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value));
 
         return ctrlElm.ClassList.All(x => testElm.ClassList.Contains(x))
             ? CompareResult.Same
-            : CompareResult.Different;
+            : CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value));
     }
 
     private static bool IsBothClassAttributes(in AttributeComparison comparison)
diff --git a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeComparer.cs b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeComparer.cs
index bafb648..c74f619 100644
--- a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeComparer.cs
+++ b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/IgnoreAttributeComparer.cs
@@ -12,7 +12,7 @@ public static class IgnoreAttributeComparer
     /// 
     public static CompareResult Compare(in AttributeComparison comparison, CompareResult currentDecision)
     {
-        if (currentDecision.IsSameOrSkip())
+        if (currentDecision.IsSameOrSkip)
             return currentDecision;
 
         return comparison.Control.Attribute.Name.EndsWith(DIFF_IGNORE_POSTFIX, StringComparison.OrdinalIgnoreCase)
diff --git a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/OrderingStyleAttributeComparer.cs b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/OrderingStyleAttributeComparer.cs
index 5e3e73d..4e3d830 100644
--- a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/OrderingStyleAttributeComparer.cs
+++ b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/OrderingStyleAttributeComparer.cs
@@ -10,7 +10,7 @@ public static class OrderingStyleAttributeComparer
     /// 
     public static CompareResult Compare(in AttributeComparison comparison, CompareResult currentDecision)
     {
-        if (currentDecision.IsSameOrSkip())
+        if (currentDecision.IsSameOrSkip)
             return currentDecision;
 
         return IsStyleAttributeComparison(comparison)
@@ -18,6 +18,12 @@ public static CompareResult Compare(in AttributeComparison comparison, CompareRe
             : currentDecision;
     }
 
+    private static bool IsStyleAttributeComparison(in AttributeComparison comparison)
+    {
+        return comparison.Control.Attribute.Name.Equals(AttributeNames.Style, StringComparison.Ordinal) &&
+            comparison.Test.Attribute.Name.Equals(AttributeNames.Style, StringComparison.Ordinal);
+    }
+
     private static CompareResult CompareElementStyle(in AttributeComparison comparison)
     {
         var (ctrlElm, testElm) = comparison.GetAttributeElements();
@@ -25,13 +31,7 @@ private static CompareResult CompareElementStyle(in AttributeComparison comparis
         var testStyle = testElm.GetStyle();
         return CompareCssStyleDeclarations(ctrlStyle, testStyle)
             ? CompareResult.Same
-            : CompareResult.Different;
-    }
-
-    private static bool IsStyleAttributeComparison(in AttributeComparison comparison)
-    {
-        return comparison.Control.Attribute.Name.Equals(AttributeNames.Style, StringComparison.Ordinal) &&
-            comparison.Test.Attribute.Name.Equals(AttributeNames.Style, StringComparison.Ordinal);
+            : CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value));
     }
 
     private static bool CompareCssStyleDeclarations(ICssStyleDeclaration control, ICssStyleDeclaration test)
diff --git a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/StyleAttributeComparer.cs b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/StyleAttributeComparer.cs
index 76ee635..00dfaee 100644
--- a/src/AngleSharp.Diffing/Strategies/AttributeStrategies/StyleAttributeComparer.cs
+++ b/src/AngleSharp.Diffing/Strategies/AttributeStrategies/StyleAttributeComparer.cs
@@ -10,7 +10,7 @@ public static class StyleAttributeComparer
     /// 
     public static CompareResult Compare(in AttributeComparison comparison, CompareResult currentDecision)
     {
-        if (currentDecision.IsSameOrSkip())
+        if (currentDecision.IsSameOrSkip)
             return currentDecision;
 
         return IsStyleAttributeComparison(comparison)
@@ -18,6 +18,12 @@ public static CompareResult Compare(in AttributeComparison comparison, CompareRe
             : currentDecision;
     }
 
+    private static bool IsStyleAttributeComparison(in AttributeComparison comparison)
+    {
+        return comparison.Control.Attribute.Name.Equals(AttributeNames.Style, StringComparison.Ordinal) &&
+            comparison.Test.Attribute.Name.Equals(AttributeNames.Style, StringComparison.Ordinal);
+    }
+
     private static CompareResult CompareElementStyle(in AttributeComparison comparison)
     {
         var (ctrlElm, testElm) = comparison.GetAttributeElements();
@@ -25,13 +31,7 @@ private static CompareResult CompareElementStyle(in AttributeComparison comparis
         var testStyle = testElm.GetStyle();
         return CompareCssStyleDeclarations(ctrlStyle, testStyle)
             ? CompareResult.Same
-            : CompareResult.Different;
-    }
-
-    private static bool IsStyleAttributeComparison(in AttributeComparison comparison)
-    {
-        return comparison.Control.Attribute.Name.Equals(AttributeNames.Style, StringComparison.Ordinal) &&
-            comparison.Test.Attribute.Name.Equals(AttributeNames.Style, StringComparison.Ordinal);
+            : CompareResult.FromDiff(new AttrDiff(comparison, AttrDiffKind.Value));
     }
 
     private static bool CompareCssStyleDeclarations(ICssStyleDeclaration control, ICssStyleDeclaration test)
diff --git a/src/AngleSharp.Diffing/Strategies/CommentStrategies/CommentComparer.cs b/src/AngleSharp.Diffing/Strategies/CommentStrategies/CommentComparer.cs
index d06359e..7ea0bc5 100644
--- a/src/AngleSharp.Diffing/Strategies/CommentStrategies/CommentComparer.cs
+++ b/src/AngleSharp.Diffing/Strategies/CommentStrategies/CommentComparer.cs
@@ -10,10 +10,14 @@ public static class CommentComparer
     /// 
     public static CompareResult Compare(in Comparison comparison, CompareResult currentDecision)
     {
-        if (currentDecision.IsSameOrSkip())
+        if (currentDecision.IsSameOrSkip)
+            return currentDecision;
+
+        if (comparison.TryGetNodesAsType(out var controlComment, out var testComment))
+            return controlComment.Data.Equals(testComment.Data, StringComparison.Ordinal)
+                ? CompareResult.Same
+                : CompareResult.FromDiff(new CommentDiff(comparison));
+        else
             return currentDecision;
-        return comparison.Control.Node.NodeType == NodeType.Comment && comparison.AreNodeTypesEqual
-            ? CompareResult.Same
-            : CompareResult.Different;
     }
 }
diff --git a/src/AngleSharp.Diffing/Strategies/ElementStrategies/ElementComparer.cs b/src/AngleSharp.Diffing/Strategies/ElementStrategies/ElementComparer.cs
index ef85c93..4c5d12a 100644
--- a/src/AngleSharp.Diffing/Strategies/ElementStrategies/ElementComparer.cs
+++ b/src/AngleSharp.Diffing/Strategies/ElementStrategies/ElementComparer.cs
@@ -1,3 +1,4 @@
+using AngleSharp.Diffing.Core.Diffs;
 using AngleSharp.Html.Parser.Tokens;
 
 namespace AngleSharp.Diffing.Strategies.ElementStrategies;
@@ -30,24 +31,27 @@ public ElementComparer(bool enforceTagClosing)
     /// 
     public CompareResult Compare(in Comparison comparison, CompareResult currentDecision)
     {
-        if (currentDecision.IsSameOrSkip())
+        if (currentDecision.IsSameOrSkip)
             return currentDecision;
 
-        var result = comparison.Control.Node.NodeType == NodeType.Element && comparison.AreNodeTypesEqual
+        if (!comparison.TryGetNodesAsType(out var controlElement, out var testElement))
+            return currentDecision;
+
+        var result = comparison.AreNodeTypesEqual
             ? CompareResult.Same
-            : CompareResult.Different;
+            : CompareResult.FromDiff(new ElementDiff(comparison, ElementDiffKind.Name));
 
         if (EnforceTagClosing && result == CompareResult.Same)
         {
-            if (comparison.Test.Node is not IElement testElement || testElement.SourceReference is not HtmlTagToken testTag)
+            if (testElement.SourceReference is not HtmlTagToken testTag)
                 throw new InvalidOperationException("No source reference attached to test element, cannot determine element tag closing style.");
 
-            if (comparison.Control.Node is not IElement controlElement || controlElement.SourceReference is not HtmlTagToken controlTag)
+            if (controlElement.SourceReference is not HtmlTagToken controlTag)
                 throw new InvalidOperationException("No source reference attached to test element, cannot determine element tag closing style.");
 
             return testTag.IsSelfClosing == controlTag.IsSelfClosing
                 ? result
-                : CompareResult.Different;
+                : CompareResult.FromDiff(new ElementDiff(comparison, ElementDiffKind.ClosingStyle));
         }
 
         return result;
diff --git a/src/AngleSharp.Diffing/Strategies/TextNodeStrategies/StyleSheetTextNodeComparer.cs b/src/AngleSharp.Diffing/Strategies/TextNodeStrategies/StyleSheetTextNodeComparer.cs
index f8784e1..8284a2a 100644
--- a/src/AngleSharp.Diffing/Strategies/TextNodeStrategies/StyleSheetTextNodeComparer.cs
+++ b/src/AngleSharp.Diffing/Strategies/TextNodeStrategies/StyleSheetTextNodeComparer.cs
@@ -10,10 +10,10 @@ public static class StyleSheetTextNodeComparer
     /// 
     public static CompareResult Compare(in Comparison comparison, CompareResult currentDecision)
     {
-        if (currentDecision.IsSameOrSkip())
+        if (currentDecision.IsSameOrSkip)
             return currentDecision;
         if (TryGetStyleDeclaretions(comparison, out var controlStyles, out var testStyles))
-            return Compare(controlStyles, testStyles);
+            return Compare(comparison, controlStyles, testStyles);
         else
             return currentDecision;
     }
@@ -36,13 +36,13 @@ private static bool TryGetStyleDeclaretions(in Comparison comparison, [NotNullWh
             return false;
     }
 
-    private static CompareResult Compare(IStyleSheet controlStyles, IStyleSheet testStyles)
+    private static CompareResult Compare(in Comparison comparison, IStyleSheet controlStyles, IStyleSheet testStyles)
     {
         var control = controlStyles.ToCss();
         var test = testStyles.ToCss();
 
         return control.Equals(test, StringComparison.Ordinal)
             ? CompareResult.Same
-            : CompareResult.Different;
+            : CompareResult.FromDiff(new StylesheetDiff(comparison));
     }
 }
diff --git a/src/AngleSharp.Diffing/Strategies/TextNodeStrategies/TextNodeComparer.cs b/src/AngleSharp.Diffing/Strategies/TextNodeStrategies/TextNodeComparer.cs
index af458a4..bfe64f0 100644
--- a/src/AngleSharp.Diffing/Strategies/TextNodeStrategies/TextNodeComparer.cs
+++ b/src/AngleSharp.Diffing/Strategies/TextNodeStrategies/TextNodeComparer.cs
@@ -37,16 +37,16 @@ public TextNodeComparer(WhitespaceOption option = WhitespaceOption.Preserve, boo
     /// 
     public CompareResult Compare(in Comparison comparison, CompareResult currentDecision)
     {
-        if (currentDecision.IsSameOrSkip())
+        if (currentDecision.IsSameOrSkip)
             return currentDecision;
 
         if (comparison.TryGetNodesAsType(out var controlTextNode, out var testTextNode))
-            return Compare(controlTextNode, testTextNode);
+            return Compare(comparison, controlTextNode, testTextNode);
         else
             return currentDecision;
     }
 
-    private CompareResult Compare(IText controlTextNode, IText testTextNode)
+    private CompareResult Compare(in Comparison comparison, IText controlTextNode, IText testTextNode)
     {
         var option = GetWhitespaceOption(controlTextNode);
         var compareMethod = GetCompareMethod(controlTextNode);
@@ -68,11 +68,11 @@ private CompareResult Compare(IText controlTextNode, IText testTextNode)
         var isRegexCompare = GetIsRegexComparison(controlTextNode);
 
         return isRegexCompare
-            ? PerformRegexCompare(compareMethod, controlText, testText)
-            : PerformStringCompare(compareMethod, controlText, testText);
+            ? PerformRegexCompare(comparison, compareMethod, controlText, testText)
+            : PerformStringCompare(comparison, compareMethod, controlText, testText);
     }
 
-    private static CompareResult PerformRegexCompare(StringComparison compareMethod, string controlText, string testText)
+    private static CompareResult PerformRegexCompare(in Comparison comparison, StringComparison compareMethod, string controlText, string testText)
     {
         var regexOptions = compareMethod == StringComparison.OrdinalIgnoreCase
             ? RegexOptions.IgnoreCase
@@ -80,14 +80,14 @@ private static CompareResult PerformRegexCompare(StringComparison compareMethod,
 
         return Regex.IsMatch(testText, controlText, regexOptions, TimeSpan.FromSeconds(5))
             ? CompareResult.Same
-            : CompareResult.Different;
+            : CompareResult.FromDiff(new TextDiff(comparison));
     }
 
-    private static CompareResult PerformStringCompare(StringComparison compareMethod, string controlText, string testText)
+    private static CompareResult PerformStringCompare(in Comparison comparison, StringComparison compareMethod, string controlText, string testText)
     {
         return controlText.Equals(testText, compareMethod)
             ? CompareResult.Same
-            : CompareResult.Different;
+            : CompareResult.FromDiff(new TextDiff(comparison));
     }
 
     private static bool GetIsRegexComparison(IText controlTextNode)
diff --git a/src/AngleSharp.Diffing/System.Runtime.CompilerServices/IsExternalInit.cs b/src/AngleSharp.Diffing/System.Runtime.CompilerServices/IsExternalInit.cs
new file mode 100644
index 0000000..6492a5e
--- /dev/null
+++ b/src/AngleSharp.Diffing/System.Runtime.CompilerServices/IsExternalInit.cs
@@ -0,0 +1,66 @@
+// 
+//   This code file has automatically been added by the "IsExternalInit" NuGet package (https://www.nuget.org/packages/IsExternalInit).
+//   Please see https://github.com/manuelroemer/IsExternalInit for more information.
+//
+//   IMPORTANT:
+//   DO NOT DELETE THIS FILE if you are using a "packages.config" file to manage your NuGet references.
+//   Consider migrating to PackageReferences instead:
+//   https://docs.microsoft.com/en-us/nuget/consume-packages/migrate-packages-config-to-package-reference
+//   Migrating brings the following benefits:
+//   * The "IsExternalInit" folder and the "IsExternalInit.cs" file don't appear in your project.
+//   * The added file is immutable and can therefore not be modified by coincidence.
+//   * Updating/Uninstalling the package will work flawlessly.
+// 
+
+#region License
+// MIT License
+// 
+// Copyright (c) Manuel Römer
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+#endregion
+
+#if !ISEXTERNALINIT_DISABLE
+#nullable enable
+#pragma warning disable
+
+namespace System.Runtime.CompilerServices
+{
+    using global::System.Diagnostics;
+    using global::System.Diagnostics.CodeAnalysis;
+
+    /// 
+    ///     Reserved to be used by the compiler for tracking metadata.
+    ///     This class should not be used by developers in source code.
+    /// 
+    /// 
+    ///     This definition is provided by the IsExternalInit NuGet package (https://www.nuget.org/packages/IsExternalInit).
+    ///     Please see https://github.com/manuelroemer/IsExternalInit for more information.
+    /// 
+#if !ISEXTERNALINIT_INCLUDE_IN_CODE_COVERAGE
+    [ExcludeFromCodeCoverage, DebuggerNonUserCode]
+#endif
+    internal static class IsExternalInit
+    {
+    }
+}
+
+#pragma warning restore
+#nullable restore
+#endif // ISEXTERNALINIT_DISABLE
\ No newline at end of file
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index ad2c443..3a59f95 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -1,6 +1,6 @@
 
   
-    0.18.1
+    0.18.2