Skip to content

Commit ee5afeb

Browse files
committed
Boolean attribute comparison
1 parent 78ca649 commit ee5afeb

File tree

8 files changed

+180
-38
lines changed

8 files changed

+180
-38
lines changed

src/Core/AttributeComparison.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,7 @@ public AttributeComparison(in AttributeComparisonSource control, in AttributeCom
1515
Control = control;
1616
Test = test;
1717
}
18-
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-
18+
2519
public (IElement ControlElement, IElement TestElement) GetNodesAsElements()
2620
=> ((IElement)Control.ElementSource.Node, (IElement)Test.ElementSource.Node);
2721

src/Strategies/AttributeStrategies/AttributeComparer.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.Text;
43
using System.Text.RegularExpressions;
54
using System.Threading.Tasks;
@@ -9,6 +8,7 @@
98
namespace Egil.AngleSharp.Diffing.Strategies.AttributeStrategies
109
{
1110

11+
1212
public class AttributeComparer
1313
{
1414
private const string IGNORE_CASE_POSTFIX = ":ignorecase";
@@ -21,12 +21,16 @@ public CompareResult Compare(in AttributeComparison comparison, CompareResult cu
2121
if (currentDecision.IsDecisionFinal()) return currentDecision;
2222

2323
var (ignoreCase, isRegexValueCompare) = GetComparisonModifiers(comparison);
24+
2425
var hasSameName = CompareAttributeNames(comparison, ignoreCase, isRegexValueCompare);
26+
27+
if(!hasSameName) return CompareResult.Different;
28+
2529
var hasSameValue = isRegexValueCompare
2630
? CompareAttributeValuesByRegex(comparison, ignoreCase)
2731
: CompareAttributeValues(comparison, ignoreCase);
2832

29-
return hasSameName && hasSameValue
33+
return hasSameValue
3034
? CompareResult.Same
3135
: CompareResult.Different;
3236
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using AngleSharp.Dom;
4+
using Egil.AngleSharp.Diffing.Core;
5+
6+
namespace Egil.AngleSharp.Diffing.Strategies.AttributeStrategies
7+
{
8+
public class BooleanAttributeComparer
9+
{
10+
private static readonly HashSet<string> BooleanAttributesSet = new HashSet<string>()
11+
{
12+
"allowfullscreen",
13+
"allowpaymentrequest",
14+
"async",
15+
"autofocus",
16+
"autoplay",
17+
"checked",
18+
"controls",
19+
"default",
20+
"defer",
21+
"disabled",
22+
"formnovalidate",
23+
"hidden",
24+
"ismap",
25+
"itemscope",
26+
"loop",
27+
"multiple",
28+
"muted",
29+
"nomodule",
30+
"novalidate",
31+
"open",
32+
"readonly",
33+
"required",
34+
"reversed",
35+
"selected",
36+
"typemustmatch"
37+
};
38+
39+
public static IReadOnlyCollection<string> BooleanAttributes => BooleanAttributesSet;
40+
41+
private readonly BooleanAttributeComparision _mode;
42+
43+
public BooleanAttributeComparer(BooleanAttributeComparision mode)
44+
{
45+
_mode = mode;
46+
}
47+
48+
public CompareResult Compare(in AttributeComparison comparison, CompareResult currentDecision)
49+
{
50+
if (currentDecision.IsDecisionFinal()) return currentDecision;
51+
if (!IsAttributeNamesEqual(comparison)) return CompareResult.Different;
52+
if (!BooleanAttributesSet.Contains(comparison.Control.Attribute.Name)) return currentDecision;
53+
54+
var hasSameValue = _mode == BooleanAttributeComparision.Strict
55+
? CompareStrict(comparison)
56+
: true;
57+
58+
return hasSameValue ? CompareResult.Same : CompareResult.Different;
59+
}
60+
61+
private static bool IsAttributeNamesEqual(in AttributeComparison comparison)
62+
{
63+
return comparison.Control.Attribute.Name.Equals(comparison.Test.Attribute.Name, StringComparison.OrdinalIgnoreCase);
64+
}
65+
66+
private static bool CompareStrict(in AttributeComparison comparison)
67+
{
68+
return IsAttributeStrictlyTruthy(comparison.Control.Attribute) && IsAttributeStrictlyTruthy(comparison.Test.Attribute);
69+
}
70+
71+
private static bool IsAttributeStrictlyTruthy(IAttr attr)
72+
=> string.IsNullOrWhiteSpace(attr.Value) || attr.Value.Equals(attr.Name, StringComparison.OrdinalIgnoreCase);
73+
}
74+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Egil.AngleSharp.Diffing.Strategies.AttributeStrategies
2+
{
3+
public enum BooleanAttributeComparision
4+
{
5+
Loose,
6+
Strict
7+
}
8+
}
Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
1-
using System.Linq;
1+
using System;
2+
using System.Linq;
23
using Egil.AngleSharp.Diffing.Core;
34

45
namespace Egil.AngleSharp.Diffing.Strategies.AttributeStrategies
56
{
6-
public class ClassAttributeComparer
7+
public static class ClassAttributeComparer
78
{
8-
public CompareResult Compare(in AttributeComparison comparison, CompareResult currentDecision)
9+
private const string CLASS_ATTRIBUTE_NAME = "class";
10+
11+
public static CompareResult Compare(in AttributeComparison comparison, CompareResult currentDecision)
912
{
1013
if (currentDecision.IsDecisionFinal()) return currentDecision;
11-
if (!comparison.AttributeNameEquals("class")) return currentDecision;
14+
if (!IsClassAttributes(comparison)) return currentDecision;
15+
16+
var (ctrlElm, testElm) = comparison.GetNodesAsElements();
17+
if (ctrlElm.ClassList.Length != testElm.ClassList.Length) return CompareResult.Different;
1218

13-
var (ctrlElm, testElm) = comparison.GetNodesAsElements();
14-
var sameLength = ctrlElm.ClassList.Length == testElm.ClassList.Length;
15-
if (!sameLength) return CompareResult.Different;
1619
return ctrlElm.ClassList.All(x => testElm.ClassList.Contains(x))
1720
? CompareResult.Same
1821
: CompareResult.Different;
1922
}
23+
24+
private static bool IsClassAttributes(in AttributeComparison comparison)
25+
{
26+
return comparison.Control.Attribute.Name.Equals(CLASS_ATTRIBUTE_NAME, StringComparison.OrdinalIgnoreCase) &&
27+
comparison.Test.Attribute.Name.Equals(CLASS_ATTRIBUTE_NAME, StringComparison.OrdinalIgnoreCase);
28+
}
29+
2030
}
2131
}

tests/Strategies/AttributeStrategies/AttributeComparerTest.cs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ namespace Egil.AngleSharp.Diffing.Strategies.AttributeStrategies
1212

1313
public class AttributeComparerTest : DiffingTestBase
1414
{
15-
// Name comparer (attr)
1615
[Fact(DisplayName = "When compare is called with a current decision of Same or SameAndBreak, the current decision is returned")]
1716
public void Test001()
1817
{
@@ -102,18 +101,6 @@ public void Test008(string attrNamePostfix)
102101

103102
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
104103
}
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)
118104
}
105+
119106
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using Egil.AngleSharp.Diffing.Core;
2+
using Shouldly;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using Xunit;
6+
7+
namespace Egil.AngleSharp.Diffing.Strategies.AttributeStrategies
8+
{
9+
public class BooleanAttributeComparerTest : DiffingTestBase
10+
{
11+
public static IEnumerable<object[]> BooleanAttributes = BooleanAttributeComparer.BooleanAttributes.Select(x => new string[] { x }).ToArray();
12+
13+
[Fact(DisplayName = "When attribute names are not the same comparer returns different")]
14+
public void Test001()
15+
{
16+
var sut = new BooleanAttributeComparer(BooleanAttributeComparision.Strict);
17+
var comparison = ToAttributeComparison("<b foo>", "foo", "<b bar>", "bar");
18+
19+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
20+
}
21+
22+
[Fact(DisplayName = "When attribute name is not an boolean attribute, its current result is returned")]
23+
public void Test002()
24+
{
25+
var sut = new BooleanAttributeComparer(BooleanAttributeComparision.Strict);
26+
var comparison = ToAttributeComparison(@"<b class="""">", "class", @"<b class="""">", "class");
27+
28+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
29+
}
30+
31+
[Theory(DisplayName = "When attributes is boolean and mode is strict, " +
32+
"the attribute is considered truthy if conforms to the html5 spec and" +
33+
"the result is Same if both control and test attribute are truthy")]
34+
[MemberData(nameof(BooleanAttributes))]
35+
public void Test003(string attrName)
36+
{
37+
var sut = new BooleanAttributeComparer(BooleanAttributeComparision.Strict);
38+
var c1 = ToAttributeComparison($@"<b {attrName}="""">", attrName, $@"<b {attrName}=""{attrName}"">", attrName);
39+
var c2 = ToAttributeComparison($@"<b {attrName}=""{attrName}"">", attrName, $@"<b {attrName}="""">", attrName);
40+
var c3 = ToAttributeComparison($@"<b {attrName}>", attrName, $@"<b {attrName}="""">", attrName);
41+
var c4 = ToAttributeComparison($@"<b {attrName}>", attrName, $@"<b {attrName}=""{attrName}"">", attrName);
42+
var c5 = ToAttributeComparison($@"<b {attrName}="""">", attrName, $@"<b {attrName}>", attrName);
43+
var c6 = ToAttributeComparison($@"<b {attrName}=""{attrName}"">", attrName, $@"<b {attrName}>", attrName);
44+
45+
sut.Compare(c1, CompareResult.Different).ShouldBe(CompareResult.Same);
46+
sut.Compare(c2, CompareResult.Different).ShouldBe(CompareResult.Same);
47+
sut.Compare(c3, CompareResult.Different).ShouldBe(CompareResult.Same);
48+
sut.Compare(c4, CompareResult.Different).ShouldBe(CompareResult.Same);
49+
sut.Compare(c5, CompareResult.Different).ShouldBe(CompareResult.Same);
50+
sut.Compare(c6, CompareResult.Different).ShouldBe(CompareResult.Same);
51+
}
52+
53+
[Theory(DisplayName = "When attributes is boolean and mode is loose, the presence of " +
54+
"two attributes with the same name returns compare result Same, no matter what their value is")]
55+
[MemberData(nameof(BooleanAttributes))]
56+
public void Test004(string attrName)
57+
{
58+
var sut = new BooleanAttributeComparer(BooleanAttributeComparision.Loose);
59+
var c1 = ToAttributeComparison($@"<b {attrName}=""foo"">", attrName, $@"<b {attrName}=""bar"">", attrName);
60+
var c2 = ToAttributeComparison($@"<b {attrName}=""true"">", attrName, $@"<b {attrName}=""true"">", attrName);
61+
var c3 = ToAttributeComparison($@"<b {attrName}=""true"">", attrName, $@"<b {attrName}=""false"">", attrName);
62+
63+
64+
sut.Compare(c1, CompareResult.Different).ShouldBe(CompareResult.Same);
65+
sut.Compare(c2, CompareResult.Different).ShouldBe(CompareResult.Same);
66+
sut.Compare(c3, CompareResult.Different).ShouldBe(CompareResult.Same);
67+
}
68+
}
69+
}

tests/Strategies/AttributeStrategies/ClassAttributeComparerTest.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,30 @@ public class ClassAttributeComparerTest : DiffingTestBase
1515
[InlineData("foo bar baz", "bar foo baz ")]
1616
public void Test009(string controlClasses, string testClasses)
1717
{
18-
var sut = new ClassAttributeComparer();
1918
var comparison = ToAttributeComparison($@"<p class=""{controlClasses}"">", "class",
2019
$@"<p class=""{testClasses}"">", "class");
2120

22-
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
21+
ClassAttributeComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
2322
}
2423

2524
[Fact(DisplayName = "When a class attribute is matched up with another attribute, the result is different")]
2625
public void Test010()
2726
{
28-
var sut = new ClassAttributeComparer();
2927
var comparison = ToAttributeComparison(@"<p class=""foo"">", "class",
3028
@"<p bar=""bar"">", "bar");
3129

32-
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
30+
ClassAttributeComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
3331
}
3432

3533
[Theory(DisplayName = "When there are different number of classes in the class attributes the result is different")]
3634
[InlineData("foo bar baz", "baz foo")]
3735
[InlineData("bar baz", "bar baz foo")]
3836
public void Test011(string controlClasses, string testClasses)
3937
{
40-
var sut = new ClassAttributeComparer();
4138
var comparison = ToAttributeComparison($@"<p class=""{controlClasses}"">", "class",
4239
$@"<p class=""{testClasses}"">", "class");
4340

44-
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
41+
ClassAttributeComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
4542
}
4643

4744
[Theory(DisplayName = "When the classes in the class attributes are different the result is different")]
@@ -51,11 +48,10 @@ public void Test011(string controlClasses, string testClasses)
5148
[InlineData("foo bar", "baz bar")]
5249
public void Test012(string controlClasses, string testClasses)
5350
{
54-
var sut = new ClassAttributeComparer();
5551
var comparison = ToAttributeComparison($@"<p class=""{controlClasses}"">", "class",
5652
$@"<p class=""{testClasses}"">", "class");
5753

58-
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
54+
ClassAttributeComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
5955
}
6056
}
6157
}

0 commit comments

Comments
 (0)