diff --git a/CHANGELOG.md b/CHANGELOG.md index 0721ce5..068a2d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# 0.17.0 + +Released on Wednesday, September 8, 2021. + +- Added the ability to ignore an elements children or its attributes. By [@grishat](https://github.com/grishat). + # 0.16.0 Released on Wednesday, June 24, 2021. diff --git a/src/AngleSharp.Diffing.Tests/Core/HtmlDifferenceEngineTest.cs b/src/AngleSharp.Diffing.Tests/Core/HtmlDifferenceEngineTest.cs index 302f713..6068afb 100644 --- a/src/AngleSharp.Diffing.Tests/Core/HtmlDifferenceEngineTest.cs +++ b/src/AngleSharp.Diffing.Tests/Core/HtmlDifferenceEngineTest.cs @@ -357,6 +357,44 @@ public void Test2() results.ShouldBeEmpty(); } + [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) + { + 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"), + attrMatcher: AttributeNameMatcher, + attrFilter: NoneAttrFilter, + attrComparer: SameResultAttrComparer + ); + + var results = sut.Compare(ToNodeList(@"

foo

"), ToNodeList(@"

baz

")); + + results.ShouldBeEmpty(); + } + + [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) + { + var sut = CreateHtmlDiffer( + nodeMatcher: OneToOneNodeListMatcher, + nodeFilter: NoneNodeFilter, + nodeComparer: c => compareResult, + attrMatcher: AttributeNameMatcher, + attrFilter: NoneAttrFilter, + attrComparer: SameResultAttrComparer + ); + + var results = sut.Compare(ToNodeList(@"

"), ToNodeList(@"

")); + + results.ShouldBeEmpty(); + } + #region NodeFilters private static FilterDecision NoneNodeFilter(ComparisonSource source) => FilterDecision.Keep; private static FilterDecision RemoveCommentNodeFilter(ComparisonSource source) => source.Node.NodeType == NodeType.Comment ? FilterDecision.Exclude : FilterDecision.Keep; diff --git a/src/AngleSharp.Diffing.Tests/DiffBuilderTest.cs b/src/AngleSharp.Diffing.Tests/DiffBuilderTest.cs index 8d973c2..be8914f 100644 --- a/src/AngleSharp.Diffing.Tests/DiffBuilderTest.cs +++ b/src/AngleSharp.Diffing.Tests/DiffBuilderTest.cs @@ -89,5 +89,41 @@ public void Test004() nodeComparerCalled.ShouldBeTrue(); attrComparerCalled.ShouldBeTrue(); } + + [Theory(DisplayName = "When a control element has 'diff:ignoreChildren', calling Build() with DefaultOptions() returns empty diffs")] + [InlineData(@"

hello world

", + @"

world says hello

")] + [InlineData(@"

hello

", + @"

world says hello

")] + [InlineData(@"

hello world

", + @"

world says

")] + public void Test005(string control, string test) + { + var diffs = DiffBuilder + .Compare(control) + .WithTest(test) + .Build() + .ToList(); + + diffs.ShouldBeEmpty(); + } + + [Theory(DisplayName = "When a control element has 'diff:ignoreAttributes', calling Build() with DefaultOptions() returns empty diffs")] + [InlineData(@"

", + @"

")] + [InlineData(@"

", + @"

")] + [InlineData(@"

", + @"

")] + public void Test006(string control, string test) + { + var diffs = DiffBuilder + .Compare(control) + .WithTest(test) + .Build() + .ToList(); + + diffs.ShouldBeEmpty(); + } } } diff --git a/src/AngleSharp.Diffing.Tests/Strategies/ElementStrategies/IgnoreAttributesElementComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/ElementStrategies/IgnoreAttributesElementComparerTest.cs new file mode 100644 index 0000000..6959de7 --- /dev/null +++ b/src/AngleSharp.Diffing.Tests/Strategies/ElementStrategies/IgnoreAttributesElementComparerTest.cs @@ -0,0 +1,44 @@ +using System.Linq; + +using AngleSharp.Diffing.Core; + +using Shouldly; + +using Xunit; + +namespace AngleSharp.Diffing.Strategies.ElementStrategies +{ + public class IgnoreAttributesElementComparerTest : DiffingTestBase + { + public IgnoreAttributesElementComparerTest(DiffingTestFixture fixture) : base(fixture) + { + } + + [Theory(DisplayName = "When a control element does not contain the 'diff:ignoreAttributes' attribute or it is 'diff:ignoreAttributes=false', the current decision is returned")] + [InlineData(@"

")] + [InlineData(@"

")] + [InlineData(@"

")] + [InlineData(@"

")] + public void Test001(string controlHtml) + { + var comparison = ToComparison(controlHtml, "

"); + + IgnoreAttributesElementComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different); + IgnoreAttributesElementComparer.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same); + IgnoreAttributesElementComparer.Compare(comparison, CompareResult.Skip).ShouldBe(CompareResult.Skip); + } + + [Theory(DisplayName = "When a control element has 'diff:ignoreAttributes' attribute, CompareResult.SkipAttributes flag is returned")] + [InlineData(@"

")] + [InlineData(@"

")] + [InlineData(@"

")] + [InlineData(@"

")] + public void Test002(string controlHtml) + { + var comparison = ToComparison(controlHtml, "

"); + + IgnoreAttributesElementComparer.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same | CompareResult.SkipAttributes); + IgnoreAttributesElementComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different | CompareResult.SkipAttributes); + } + } +} diff --git a/src/AngleSharp.Diffing.Tests/Strategies/ElementStrategies/IgnoreChildrenElementComparerTest.cs b/src/AngleSharp.Diffing.Tests/Strategies/ElementStrategies/IgnoreChildrenElementComparerTest.cs new file mode 100644 index 0000000..c02e7b0 --- /dev/null +++ b/src/AngleSharp.Diffing.Tests/Strategies/ElementStrategies/IgnoreChildrenElementComparerTest.cs @@ -0,0 +1,44 @@ +using System.Linq; + +using AngleSharp.Diffing.Core; + +using Shouldly; + +using Xunit; + +namespace AngleSharp.Diffing.Strategies.ElementStrategies +{ + public class IgnoreChildrenElementComparerTest : DiffingTestBase + { + public IgnoreChildrenElementComparerTest(DiffingTestFixture fixture) : base(fixture) + { + } + + [Theory(DisplayName = "When a control element does not contain the 'diff:ignoreChildren' attribute or it is 'diff:ignoreChildren=false', the current decision is returned")] + [InlineData(@"

")] + [InlineData(@"

")] + [InlineData(@"

")] + [InlineData(@"

")] + public void Test001(string controlHtml) + { + var comparison = ToComparison(controlHtml, "

"); + + IgnoreChildrenElementComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different); + IgnoreChildrenElementComparer.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same); + IgnoreChildrenElementComparer.Compare(comparison, CompareResult.Skip).ShouldBe(CompareResult.Skip); + } + + [Theory(DisplayName = "When a control element has 'diff:ignoreChildren' attribute, CompareResult.SkipChildren flag is returned")] + [InlineData(@"

")] + [InlineData(@"

")] + [InlineData(@"

")] + [InlineData(@"

")] + public void Test002(string controlHtml) + { + var comparison = ToComparison(controlHtml, "

"); + + IgnoreChildrenElementComparer.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same | CompareResult.SkipChildren); + IgnoreChildrenElementComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different | CompareResult.SkipChildren); + } + } +} diff --git a/src/AngleSharp.Diffing/Core/CompareResult.cs b/src/AngleSharp.Diffing/Core/CompareResult.cs index 4ec46ea..7b5ea20 100644 --- a/src/AngleSharp.Diffing/Core/CompareResult.cs +++ b/src/AngleSharp.Diffing/Core/CompareResult.cs @@ -1,22 +1,33 @@ -namespace AngleSharp.Diffing.Core +using System; + +namespace AngleSharp.Diffing.Core { /// /// Represents a result of a comparison. /// + [Flags] public enum CompareResult { /// /// Use when the two compared nodes or attributes are the same. /// - Same, + Same = 1, /// /// Use when the two compared nodes or attributes are the different. /// - Different, + Different = 2, /// /// Use when the comparison should be skipped and any child-nodes or attributes skipped as well. /// - Skip + 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/HtmlDifferenceEngine.cs b/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs index e4b6a2d..16577f2 100644 --- a/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs +++ b/src/AngleSharp.Diffing/Core/HtmlDifferenceEngine.cs @@ -92,7 +92,7 @@ private IEnumerable CompareNode(in Comparison comparison) } var compareRes = _diffingStrategy.Compare(comparison); - if (compareRes == CompareResult.Different) + if (compareRes.HasFlag(CompareResult.Different)) { IDiff diff = new NodeDiff(comparison); return new[] { diff }; @@ -106,15 +106,17 @@ private IEnumerable CompareElement(in Comparison comparison) var result = new List(); var compareRes = _diffingStrategy.Compare(comparison); - if (compareRes == CompareResult.Different) + if (compareRes.HasFlag(CompareResult.Different)) { result.Add(new NodeDiff(comparison)); } - if (compareRes != CompareResult.Skip) + if (!compareRes.HasFlag(CompareResult.Skip)) { - result.AddRange(CompareElementAttributes(comparison)); - result.AddRange(CompareChildNodes(comparison)); + if (!compareRes.HasFlag(CompareResult.SkipAttributes)) + result.AddRange(CompareElementAttributes(comparison)); + if (!compareRes.HasFlag(CompareResult.SkipChildren)) + result.AddRange(CompareChildNodes(comparison)); } return result; diff --git a/src/AngleSharp.Diffing/Strategies/DiffingStrategyPipelineBuilderExtensions.cs b/src/AngleSharp.Diffing/Strategies/DiffingStrategyPipelineBuilderExtensions.cs index 3bd094a..9454b08 100644 --- a/src/AngleSharp.Diffing/Strategies/DiffingStrategyPipelineBuilderExtensions.cs +++ b/src/AngleSharp.Diffing/Strategies/DiffingStrategyPipelineBuilderExtensions.cs @@ -24,8 +24,10 @@ public static IDiffingStrategyCollection AddDefaultOptions(this IDiffingStrategy .AddAttributeComparer() .AddClassAttributeComparer() .AddBooleanAttributeComparer(BooleanAttributeComparision.Strict) - .AddStyleAttributeComparer(); - ; + .AddStyleAttributeComparer() + .AddIgnoreChildrenElementSupport() + .AddIgnoreAttributesElementSupport() + ; } } } diff --git a/src/AngleSharp.Diffing/Strategies/ElementStrategies/DiffingStrategyPipelineBuilderExtensions.cs b/src/AngleSharp.Diffing/Strategies/ElementStrategies/DiffingStrategyPipelineBuilderExtensions.cs index a853542..3b0b475 100644 --- a/src/AngleSharp.Diffing/Strategies/ElementStrategies/DiffingStrategyPipelineBuilderExtensions.cs +++ b/src/AngleSharp.Diffing/Strategies/ElementStrategies/DiffingStrategyPipelineBuilderExtensions.cs @@ -33,5 +33,27 @@ public static IDiffingStrategyCollection AddIgnoreElementSupport(this IDiffingSt builder.AddComparer(IgnoreElementComparer.Compare, StrategyType.Specialized); return builder; } + + /// + /// Enables the ignore children element `diff:ignoreChildren` attribute during diffing. + /// + /// + /// + public static IDiffingStrategyCollection AddIgnoreChildrenElementSupport(this IDiffingStrategyCollection builder) + { + builder.AddComparer(IgnoreChildrenElementComparer.Compare, StrategyType.Specialized); + return builder; + } + + /// + /// Enables the ignore attributes element `diff:ignoreAttributes` attribute during diffing. + /// + /// + /// + public static IDiffingStrategyCollection AddIgnoreAttributesElementSupport(this IDiffingStrategyCollection builder) + { + builder.AddComparer(IgnoreAttributesElementComparer.Compare, StrategyType.Specialized); + return builder; + } } } diff --git a/src/AngleSharp.Diffing/Strategies/ElementStrategies/IgnoreAttributesElementComparer.cs b/src/AngleSharp.Diffing/Strategies/ElementStrategies/IgnoreAttributesElementComparer.cs new file mode 100644 index 0000000..7dc682a --- /dev/null +++ b/src/AngleSharp.Diffing/Strategies/ElementStrategies/IgnoreAttributesElementComparer.cs @@ -0,0 +1,34 @@ +using AngleSharp.Diffing.Core; +using AngleSharp.Diffing.Extensions; +using AngleSharp.Dom; + +namespace AngleSharp.Diffing.Strategies.ElementStrategies +{ + /// + /// Represents the ignore attributes element comparer. + /// + public static class IgnoreAttributesElementComparer + { + private const string DIFF_IGNORE_ATTRIBUTES_ATTRIBUTE = "diff:ignoreattributes"; + + /// + /// The ignore attributes element comparer. + /// + public static CompareResult Compare(in Comparison comparison, CompareResult currentDecision) + { + if (currentDecision == CompareResult.Skip) + return currentDecision; + + return ControlHasTruthyIgnoreAttributesAttribute(comparison) + ? currentDecision | CompareResult.SkipAttributes + : currentDecision; + } + + private static bool ControlHasTruthyIgnoreAttributesAttribute(in Comparison comparison) + { + return comparison.Control.Node is IElement element && + element.TryGetAttrValue(DIFF_IGNORE_ATTRIBUTES_ATTRIBUTE, out bool shouldIgnore) && + shouldIgnore; + } + } +} diff --git a/src/AngleSharp.Diffing/Strategies/ElementStrategies/IgnoreChildrenElementComparer.cs b/src/AngleSharp.Diffing/Strategies/ElementStrategies/IgnoreChildrenElementComparer.cs new file mode 100644 index 0000000..ce239af --- /dev/null +++ b/src/AngleSharp.Diffing/Strategies/ElementStrategies/IgnoreChildrenElementComparer.cs @@ -0,0 +1,34 @@ +using AngleSharp.Diffing.Core; +using AngleSharp.Diffing.Extensions; +using AngleSharp.Dom; + +namespace AngleSharp.Diffing.Strategies.ElementStrategies +{ + /// + /// Represents the ignore children element comparer. + /// + public static class IgnoreChildrenElementComparer + { + private const string DIFF_IGNORE_CHILDREN_ATTRIBUTE = "diff:ignorechildren"; + + /// + /// The ignore children element comparer. + /// + public static CompareResult Compare(in Comparison comparison, CompareResult currentDecision) + { + if (currentDecision == CompareResult.Skip) + return currentDecision; + + return ControlHasTruthyIgnoreChildrenAttribute(comparison) + ? currentDecision | CompareResult.SkipChildren + : currentDecision; + } + + private static bool ControlHasTruthyIgnoreChildrenAttribute(in Comparison comparison) + { + return comparison.Control.Node is IElement element && + element.TryGetAttrValue(DIFF_IGNORE_CHILDREN_ATTRIBUTE, out bool shouldIgnore) && + shouldIgnore; + } + } +}