Skip to content

Commit 78ca649

Browse files
committed
Class attribute comparer
1 parent fe5b60a commit 78ca649

File tree

5 files changed

+286
-1
lines changed

5 files changed

+286
-1
lines changed

src/Core/AttributeComparison.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System;
1+
using AngleSharp.Dom;
2+
using System;
3+
using System.Diagnostics.CodeAnalysis;
24

35
namespace Egil.AngleSharp.Diffing.Core
46
{
@@ -14,6 +16,15 @@ public AttributeComparison(in AttributeComparisonSource control, in AttributeCom
1416
Test = test;
1517
}
1618

19+
public bool AttributeNameEquals(string attributeName)
20+
{
21+
return Control.Attribute.Name.Equals(attributeName, StringComparison.OrdinalIgnoreCase) &&
22+
Test.Attribute.Name.Equals(attributeName, StringComparison.OrdinalIgnoreCase);
23+
}
24+
25+
public (IElement ControlElement, IElement TestElement) GetNodesAsElements()
26+
=> ((IElement)Control.ElementSource.Node, (IElement)Test.ElementSource.Node);
27+
1728
#region Equals and HashCode
1829
public bool Equals(AttributeComparison other) => Control == other.Control && Test == other.Test;
1930
public override bool Equals(object obj) => obj is AttributeComparison other && Equals(other);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using System.Text.RegularExpressions;
5+
using System.Threading.Tasks;
6+
using AngleSharp.Dom;
7+
using Egil.AngleSharp.Diffing.Core;
8+
9+
namespace Egil.AngleSharp.Diffing.Strategies.AttributeStrategies
10+
{
11+
12+
public class AttributeComparer
13+
{
14+
private const string IGNORE_CASE_POSTFIX = ":ignorecase";
15+
private const string REGEX_POSTFIX = ":regex";
16+
private const string IGNORE_CASE_REGEX_POSTFIX = IGNORE_CASE_POSTFIX + REGEX_POSTFIX;
17+
private const string REGEX_IGNORE_CASE_POSTFIX = REGEX_POSTFIX + IGNORE_CASE_POSTFIX;
18+
19+
public CompareResult Compare(in AttributeComparison comparison, CompareResult currentDecision)
20+
{
21+
if (currentDecision.IsDecisionFinal()) return currentDecision;
22+
23+
var (ignoreCase, isRegexValueCompare) = GetComparisonModifiers(comparison);
24+
var hasSameName = CompareAttributeNames(comparison, ignoreCase, isRegexValueCompare);
25+
var hasSameValue = isRegexValueCompare
26+
? CompareAttributeValuesByRegex(comparison, ignoreCase)
27+
: CompareAttributeValues(comparison, ignoreCase);
28+
29+
return hasSameName && hasSameValue
30+
? CompareResult.Same
31+
: CompareResult.Different;
32+
}
33+
34+
private static (bool ignoreCase, bool isRegexCompare) GetComparisonModifiers(in AttributeComparison comparison)
35+
{
36+
var ctrlName = comparison.Control.Attribute.Name;
37+
if (ctrlName.EndsWith(IGNORE_CASE_REGEX_POSTFIX, StringComparison.OrdinalIgnoreCase) || ctrlName.EndsWith(REGEX_IGNORE_CASE_POSTFIX, StringComparison.OrdinalIgnoreCase))
38+
return (ignoreCase: true, isRegexCompare: true);
39+
else if (ctrlName.EndsWith(IGNORE_CASE_POSTFIX, StringComparison.OrdinalIgnoreCase))
40+
return (ignoreCase: true, isRegexCompare: false);
41+
else if (ctrlName.EndsWith(REGEX_POSTFIX, StringComparison.OrdinalIgnoreCase))
42+
return (ignoreCase: false, isRegexCompare: true);
43+
else
44+
return (ignoreCase: false, isRegexCompare: false);
45+
}
46+
47+
private static bool CompareAttributeValues(in AttributeComparison comparison, bool ignoreCase)
48+
{
49+
var comparisonType = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
50+
return comparison.Control.Attribute.Value.Equals(comparison.Test.Attribute.Value, comparisonType);
51+
}
52+
53+
private static bool CompareAttributeValuesByRegex(in AttributeComparison comparison, bool ignoreCase)
54+
{
55+
var comparisonType = ignoreCase ? RegexOptions.IgnoreCase : RegexOptions.None;
56+
return Regex.IsMatch(comparison.Test.Attribute.Value, comparison.Control.Attribute.Value, comparisonType, TimeSpan.FromSeconds(5));
57+
}
58+
59+
private static bool CompareAttributeNames(in AttributeComparison comparison, bool hasIgnorePostfix, bool hasRegexPostfix)
60+
{
61+
var ctrlName = comparison.Control.Attribute.Name;
62+
63+
if (hasIgnorePostfix && !hasRegexPostfix)
64+
ctrlName = ctrlName.Substring(0, ctrlName.Length - IGNORE_CASE_POSTFIX.Length);
65+
else if (hasRegexPostfix && !hasIgnorePostfix)
66+
ctrlName = ctrlName.Substring(0, ctrlName.Length - REGEX_POSTFIX.Length);
67+
else if (hasIgnorePostfix && hasRegexPostfix)
68+
ctrlName = ctrlName.Substring(0, ctrlName.Length - IGNORE_CASE_REGEX_POSTFIX.Length);
69+
70+
return ctrlName.Equals(comparison.Test.Attribute.Name, StringComparison.OrdinalIgnoreCase);
71+
}
72+
}
73+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Linq;
2+
using Egil.AngleSharp.Diffing.Core;
3+
4+
namespace Egil.AngleSharp.Diffing.Strategies.AttributeStrategies
5+
{
6+
public class ClassAttributeComparer
7+
{
8+
public CompareResult Compare(in AttributeComparison comparison, CompareResult currentDecision)
9+
{
10+
if (currentDecision.IsDecisionFinal()) return currentDecision;
11+
if (!comparison.AttributeNameEquals("class")) return currentDecision;
12+
13+
var (ctrlElm, testElm) = comparison.GetNodesAsElements();
14+
var sameLength = ctrlElm.ClassList.Length == testElm.ClassList.Length;
15+
if (!sameLength) return CompareResult.Different;
16+
return ctrlElm.ClassList.All(x => testElm.ClassList.Contains(x))
17+
? CompareResult.Same
18+
: CompareResult.Different;
19+
}
20+
}
21+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using Egil.AngleSharp.Diffing.Core;
2+
using Shouldly;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
using Xunit;
9+
10+
namespace Egil.AngleSharp.Diffing.Strategies.AttributeStrategies
11+
{
12+
13+
public class AttributeComparerTest : DiffingTestBase
14+
{
15+
// Name comparer (attr)
16+
[Fact(DisplayName = "When compare is called with a current decision of Same or SameAndBreak, the current decision is returned")]
17+
public void Test001()
18+
{
19+
var sut = new AttributeComparer();
20+
var comparison = ToAttributeComparison(@"<b foo>", "foo",
21+
"<b bar>", "bar");
22+
23+
sut.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same);
24+
sut.Compare(comparison, CompareResult.SameAndBreak).ShouldBe(CompareResult.SameAndBreak);
25+
}
26+
27+
[Fact(DisplayName = "When two attributes has the same name and no value, the compare result is Same")]
28+
public void Test002()
29+
{
30+
var sut = new AttributeComparer();
31+
var comparison = ToAttributeComparison(@"<b foo>", "foo",
32+
"<b foo>", "foo");
33+
34+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
35+
}
36+
37+
[Fact(DisplayName = "When two attributes does not have the same name, the compare result is Different")]
38+
public void Test003()
39+
{
40+
var sut = new AttributeComparer();
41+
var comparison = ToAttributeComparison(@"<b foo>", "foo",
42+
"<b bar>", "bar");
43+
44+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
45+
}
46+
47+
[Fact(DisplayName = "When two attribute values are the same, the compare result is Same")]
48+
public void Test004()
49+
{
50+
var sut = new AttributeComparer();
51+
var comparison = ToAttributeComparison(@"<b foo=""bar"">", "foo",
52+
@"<b foo=""bar"">", "foo");
53+
54+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
55+
}
56+
57+
[Fact(DisplayName = "When two attribute values are different, the compare result is Different")]
58+
public void Test005()
59+
{
60+
var sut = new AttributeComparer();
61+
var comparison = ToAttributeComparison(@"<b foo=""bar"">", "foo",
62+
@"<b foo=""baz"">", "foo");
63+
64+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
65+
}
66+
67+
[Fact(DisplayName = "When the control attribute is postfixed with :ignoreCase, " +
68+
"a case insensitive comparison between control and test attributes is performed")]
69+
public void Test006()
70+
{
71+
var sut = new AttributeComparer();
72+
var comparison = ToAttributeComparison(@"<b foo:ignoreCase=""BAR"">", "foo:ignorecase",
73+
@"<b foo=""bar"">", "foo");
74+
75+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
76+
}
77+
78+
[Fact(DisplayName = "When the control attribute is postfixed with :regex, " +
79+
"the control attributes value is assumed to be a regular expression and " +
80+
"that is used to match against the test attributes value")]
81+
public void Test007()
82+
{
83+
var sut = new AttributeComparer();
84+
var comparison = ToAttributeComparison(@"<b foo:regex=""foobar-\d{4}"">", "foo:regex",
85+
@"<b foo=""foobar-2000"">", "foo");
86+
87+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
88+
}
89+
90+
[Theory(DisplayName = "When the control attribute is postfixed with :regex:ignoreCase " +
91+
"or :ignoreCase:regex, the control attributes value is assumed " +
92+
"to be a regular expression and that is used to do a case insensitive " +
93+
"match against the test attributes value")]
94+
[InlineData(":regex:ignorecase")]
95+
[InlineData(":ignorecase:regex")]
96+
public void Test008(string attrNamePostfix)
97+
{
98+
var sut = new AttributeComparer();
99+
var controlAttrName = $"foo{attrNamePostfix}";
100+
var comparison = ToAttributeComparison($@"<b {controlAttrName}=""foobar-\d{{4}}"">", controlAttrName,
101+
@"<b foo=""FOOBAR-2000"">", "foo");
102+
103+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
104+
}
105+
106+
//[Theory(DisplayName = "When a control attribute is a boolean attribute, its presents represent " +
107+
// "a truthy value, and its absence a falsy value, independent of the actual value")]
108+
//[InlineData(@"<p required>", @"<p required=""required"">")]
109+
//public void Test0009(string controlHtml, string testHtml)
110+
//{
111+
// var sut = new AttributeComparer();
112+
// var comparison = ToAttributeComparison(controlHtml, "required", testHtml, "required");
113+
114+
// sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
115+
//}
116+
117+
// Boolean-attribute comparer (attr)
118+
}
119+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using Egil.AngleSharp.Diffing.Core;
2+
using Shouldly;
3+
using Xunit;
4+
5+
namespace Egil.AngleSharp.Diffing.Strategies.AttributeStrategies
6+
{
7+
public class ClassAttributeComparerTest : DiffingTestBase
8+
{
9+
[Theory(DisplayName = "When a class attribute is compared, the order of individual " +
10+
"classes and multiple whitespace is ignored")]
11+
[InlineData("", "")]
12+
[InlineData(" foo", "foo ")]
13+
[InlineData("foo bar", " foo bar ")]
14+
[InlineData("foo bar", "bar foo ")]
15+
[InlineData("foo bar baz", "bar foo baz ")]
16+
public void Test009(string controlClasses, string testClasses)
17+
{
18+
var sut = new ClassAttributeComparer();
19+
var comparison = ToAttributeComparison($@"<p class=""{controlClasses}"">", "class",
20+
$@"<p class=""{testClasses}"">", "class");
21+
22+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
23+
}
24+
25+
[Fact(DisplayName = "When a class attribute is matched up with another attribute, the result is different")]
26+
public void Test010()
27+
{
28+
var sut = new ClassAttributeComparer();
29+
var comparison = ToAttributeComparison(@"<p class=""foo"">", "class",
30+
@"<p bar=""bar"">", "bar");
31+
32+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
33+
}
34+
35+
[Theory(DisplayName = "When there are different number of classes in the class attributes the result is different")]
36+
[InlineData("foo bar baz", "baz foo")]
37+
[InlineData("bar baz", "bar baz foo")]
38+
public void Test011(string controlClasses, string testClasses)
39+
{
40+
var sut = new ClassAttributeComparer();
41+
var comparison = ToAttributeComparison($@"<p class=""{controlClasses}"">", "class",
42+
$@"<p class=""{testClasses}"">", "class");
43+
44+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
45+
}
46+
47+
[Theory(DisplayName = "When the classes in the class attributes are different the result is different")]
48+
[InlineData("foo", "bar")]
49+
[InlineData("foo bar", "baz bin")]
50+
[InlineData("foo bar", "foo bin")]
51+
[InlineData("foo bar", "baz bar")]
52+
public void Test012(string controlClasses, string testClasses)
53+
{
54+
var sut = new ClassAttributeComparer();
55+
var comparison = ToAttributeComparison($@"<p class=""{controlClasses}"">", "class",
56+
$@"<p class=""{testClasses}"">", "class");
57+
58+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)