Skip to content

Commit 220d330

Browse files
committed
Regex textnode compare done
1 parent 19dee4f commit 220d330

File tree

6 files changed

+169
-22
lines changed

6 files changed

+169
-22
lines changed

src/CompareResult.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public static class CompareResultExtensions
2626
public static bool IsSameAndBreak(this CompareResult compareResult) => compareResult == CompareResult.SameAndBreak;
2727
public static bool IsDifferent(this CompareResult compareResult) => compareResult == CompareResult.Different;
2828
public static bool IsDifferentAndBreak(this CompareResult compareResult) => compareResult == CompareResult.DifferentAndBreak;
29+
public static bool IsDecisionFinal(this CompareResult compareResult) => !compareResult.IsDifferent();
2930
}
3031
}
3132

src/Core/Comparison.cs

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

35
namespace Egil.AngleSharp.Diffing.Core
46
{
@@ -18,6 +20,24 @@ public Comparison(in ComparisonSource control, in ComparisonSource test)
1820
Test = test;
1921
}
2022

23+
public bool AreNodeTypesEqual() => Control.Node.NodeType == Test.Node.NodeType && Control.Node.NodeName == Test.Node.NodeName;
24+
25+
public bool TryGetNodesAsType<TNode>([NotNullWhen(true)]out TNode? controlNode, [NotNullWhen(true)]out TNode? testNode) where TNode : class, INode
26+
{
27+
if (Control.Node is TNode ctrl && Test.Node is TNode test)
28+
{
29+
controlNode = ctrl;
30+
testNode = test;
31+
return true;
32+
}
33+
else
34+
{
35+
controlNode = default;
36+
testNode = default;
37+
return false;
38+
}
39+
}
40+
2141
#region Equals and HashCode
2242
public bool Equals(Comparison other) => Control == other.Control && Test == other.Test;
2343
public override bool Equals(object obj) => obj is Comparison other && Equals(other);

src/Extensions/ElementExtensions.cs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,43 @@
11
using System;
2+
using System.Diagnostics.CodeAnalysis;
23
using AngleSharp.Dom;
34

45
namespace Egil.AngleSharp.Diffing.Extensions
56
{
67
public static class ElementExtensions
78
{
8-
public static bool TryGetAttrValue<T>(this IElement element, string attributeName, out T result) where T : struct
9+
public static bool TryGetAttrValue(this IElement element, string attributeName, out bool result)
10+
=> TryGetAttrValue(element, attributeName, x => string.IsNullOrWhiteSpace(x) || bool.Parse(x), out result);
11+
12+
public static bool TryGetAttrValue<T>(this IElement element, string attributeName, out T result) where T : System.Enum
13+
{
14+
return TryGetAttrValue(element, attributeName, ParseEnum, out result);
15+
16+
static T ParseEnum(string enumValue)
17+
{
18+
return (T)Enum.Parse(typeof(T), enumValue, true);
19+
}
20+
}
21+
22+
public static bool TryGetAttrValue<T>(this IElement element, string attributeName, Func<string, T> resultFunc, [NotNullWhen(true)] out T result)
923
{
1024
if (element is null) throw new ArgumentNullException(nameof(element));
25+
if (resultFunc is null) throw new ArgumentNullException(nameof(resultFunc));
26+
1127
result = default;
12-
return element.Attributes[attributeName] is IAttr optAttr
13-
&& Enum.TryParse(optAttr.Value, true, out result);
28+
if (element.Attributes[attributeName] is IAttr optAttr)
29+
{
30+
result = resultFunc(optAttr.Value);
31+
return true;
32+
}
33+
return false;
1434
}
1535

1636
public static TEnum GetInlineOptionOrDefault<TEnum>(this IElement startElement, string optionName, TEnum defaultValue)
1737
where TEnum : System.Enum => GetInlineOptionOrDefault(startElement, optionName, x => x.Parse<TEnum>(), defaultValue);
1838

1939
public static bool GetInlineOptionOrDefault(this IElement startElement, string optionName, bool defaultValue)
20-
=> GetInlineOptionOrDefault(startElement, optionName, x => bool.Parse(x), defaultValue);
40+
=> GetInlineOptionOrDefault(startElement, optionName, x => string.IsNullOrWhiteSpace(x) || bool.Parse(x), defaultValue);
2141

2242
public static T GetInlineOptionOrDefault<T>(this IElement startElement, string optionName, Func<string, T> resultFunc, T defaultValue)
2343
{

src/NotNullWhenAttribute.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace System.Diagnostics.CodeAnalysis
2+
{
3+
// TODO: Figure out how to do multi-platform targeting, such that nullable types is part of the assembly for folks using .net core 3.
4+
// This is added since it is not available in .net standard 2.0
5+
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
6+
public sealed class NotNullWhenAttribute : Attribute
7+
{
8+
public bool ReturnValue { get; }
9+
10+
public NotNullWhenAttribute(bool returnValue)
11+
{
12+
ReturnValue = returnValue;
13+
}
14+
}
15+
}

src/Strategies/TextNodeStrategies/TextNodeComparer.cs

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,16 @@ public TextNodeComparer(WhitespaceOption option = WhitespaceOption.Preserve, boo
2525

2626
public CompareResult Compare(in Comparison comparison, CompareResult currentDecision)
2727
{
28-
if (currentDecision.IsSame() || currentDecision.IsSameAndBreak())
29-
return currentDecision;
30-
if (comparison.Control.Node is IText controlTextNode && comparison.Test.Node is IText testTextNode)
31-
return Compare(controlTextNode, testTextNode, currentDecision);
28+
if (currentDecision.IsDecisionFinal()) return currentDecision;
29+
if (!comparison.AreNodeTypesEqual()) return CompareResult.Different;
30+
31+
if (comparison.TryGetNodesAsType<IText>(out var controlTextNode, out var testTextNode))
32+
return Compare(controlTextNode, testTextNode);
3233
else
3334
return currentDecision;
3435
}
3536

36-
private CompareResult Compare(IText controlTextNode, IText testTextNode, CompareResult currentDecision)
37+
private CompareResult Compare(IText controlTextNode, IText testTextNode)
3738
{
3839
var option = GetWhitespaceOption(controlTextNode);
3940
var compareMethod = GetCompareMethod(controlTextNode);
@@ -44,14 +45,39 @@ private CompareResult Compare(IText controlTextNode, IText testTextNode, Compare
4445
{
4546
controlText = WhitespaceReplace.Replace(controlText.Trim(), " ");
4647
testText = WhitespaceReplace.Replace(controlText.Trim(), " ");
47-
}
48+
}
4849

49-
if (controlText.Equals(testText, compareMethod))
50-
return CompareResult.Same;
51-
else
52-
return currentDecision;
50+
var isRegexCompare = GetIsRegexComparison(controlTextNode);
51+
52+
return isRegexCompare
53+
? PerformRegexCompare(compareMethod, controlText, testText)
54+
: PerformStringCompare(compareMethod, controlText, testText);
5355
}
54-
56+
57+
private static CompareResult PerformRegexCompare(StringComparison compareMethod, string controlText, string testText)
58+
{
59+
var regexOptions = compareMethod == StringComparison.OrdinalIgnoreCase
60+
? RegexOptions.IgnoreCase
61+
: RegexOptions.None;
62+
63+
return Regex.IsMatch(testText, controlText, regexOptions, TimeSpan.FromSeconds(5))
64+
? CompareResult.Same
65+
: CompareResult.Different;
66+
}
67+
68+
private static CompareResult PerformStringCompare(StringComparison compareMethod, string controlText, string testText)
69+
{
70+
return controlText.Equals(testText, compareMethod)
71+
? CompareResult.Same
72+
: CompareResult.Different;
73+
}
74+
75+
private static bool GetIsRegexComparison(IText controlTextNode)
76+
{
77+
var parent = controlTextNode.ParentElement;
78+
return parent is { } && parent.TryGetAttrValue("diff:regex", out bool isRegex) && isRegex;
79+
}
80+
5581
private WhitespaceOption GetWhitespaceOption(IText textNode)
5682
{
5783
var parent = textNode.ParentElement;

tests/Strategies/TextNodeStrategies/TextNodeComparerTest.cs

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ namespace Egil.AngleSharp.Diffing.Strategies.TextNodeStrategies
66
{
77
public class TextNodeComparerTest : TextnodeStrategyTestBase
88
{
9+
[Fact(DisplayName = "When input node is not a IText node, comparer does not run nor change the current decision")]
10+
public void Test2()
11+
{
12+
var comparison = new Comparison(ToComparisonSource("<p></p>", ComparisonSourceType.Control), ToComparisonSource("<p></p>", ComparisonSourceType.Test));
13+
var sut = new TextNodeComparer();
14+
15+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
16+
sut.Compare(comparison, CompareResult.DifferentAndBreak).ShouldBe(CompareResult.DifferentAndBreak);
17+
sut.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same);
18+
sut.Compare(comparison, CompareResult.SameAndBreak).ShouldBe(CompareResult.SameAndBreak);
19+
}
20+
921
[Theory(DisplayName = "When option is Preserve or RemoveWhitespaceNodes, comparer does not run nor change the current decision")]
1022
[InlineData(WhitespaceOption.Preserve)]
1123
[InlineData(WhitespaceOption.RemoveWhitespaceNodes)]
@@ -97,7 +109,7 @@ public void Test004()
97109
var sut = new TextNodeComparer(ignoreCase: true);
98110
var comparison = new Comparison(ToComparisonSource("HELLO WoRlD", ComparisonSourceType.Control),
99111
ToComparisonSource("hello world", ComparisonSourceType.Test));
100-
112+
101113
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
102114
}
103115

@@ -125,21 +137,74 @@ public void Test006()
125137
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
126138
}
127139

128-
[Fact(DisplayName = "When IgnoreCase='true' inline attribute is present in a parent element, a string ordinal ignore case comparison is performed")]
129-
public void Test007()
140+
[Theory(DisplayName = "When IgnoreCase='true' inline attribute is present in a parent element, a string ordinal ignore case comparison is performed")]
141+
[InlineData(@"<header><h1><em diff:ignoreCase=""true"">HELLO WoRlD</em></h1></header>")]
142+
[InlineData(@"<header><h1 diff:ignoreCase=""True""><em>HELLO WoRlD</em></h1></header>")]
143+
[InlineData(@"<header diff:ignoreCase=""TRUE""><h1><em>HELLO WoRlD</em></h1></header>")]
144+
public void Test008(string controlHtml)
130145
{
131-
var sut = new TextNodeComparer(ignoreCase: false);
132-
var pre = ToComparisonSource("<h1 diff:ignoreCase=\"True\">HELLO WoRlD</pre>");
133-
var controlSource = new ComparisonSource(pre.Node.FirstChild, 0, pre.Path, ComparisonSourceType.Control);
146+
var sut = new TextNodeComparer(ignoreCase: false);
147+
var rootSource = ToComparisonSource(controlHtml);
148+
var controlSource = new ComparisonSource(rootSource.Node.FirstChild.FirstChild.FirstChild, 0, rootSource.Path, ComparisonSourceType.Control);
149+
var testSource = ToComparisonSource("hello world", ComparisonSourceType.Test);
150+
var comparison = new Comparison(controlSource, testSource);
151+
152+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
153+
}
154+
155+
[Theory(DisplayName = "When IgnoreCase='false' inline attribute is present in a parent element, a string ordinal case comparison is performed")]
156+
[InlineData(@"<header><h1><em diff:ignoreCase=""false"">HELLO WoRlD</em></h1></header>")]
157+
[InlineData(@"<header><h1 diff:ignoreCase=""False""><em>HELLO WoRlD</em></h1></header>")]
158+
[InlineData(@"<header diff:ignoreCase=""FALSE""><h1><em>HELLO WoRlD</em></h1></header>")]
159+
public void Test009(string controlHtml)
160+
{
161+
var sut = new TextNodeComparer(ignoreCase: true);
162+
var rootSource = ToComparisonSource(controlHtml);
163+
var controlSource = new ComparisonSource(rootSource.Node.FirstChild.FirstChild.FirstChild, 0, rootSource.Path, ComparisonSourceType.Control);
134164
var testSource = ToComparisonSource("hello world", ComparisonSourceType.Test);
135165
var comparison = new Comparison(controlSource, testSource);
136166

167+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
168+
}
169+
170+
[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.")]
171+
[InlineData(@"<p diff:regex>\d{4}</p>")]
172+
[InlineData(@"<p diff:regex=""true"">\d{4}</p>")]
173+
public void Test010(string controlHtml)
174+
{
175+
var sut = new TextNodeComparer();
176+
var paragraphSource = ToComparisonSource(controlHtml);
177+
var controlSource = new ComparisonSource(paragraphSource.Node.FirstChild, 0, paragraphSource.Path, ComparisonSourceType.Control);
178+
var testSource = ToComparisonSource("1234", ComparisonSourceType.Test);
179+
var comparison = new Comparison(controlSource, testSource);
137180

138181
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
139182
}
140183

184+
[Fact(DisplayName = "When diff:regex attribute is found on the immediate parent element and ignoreCase is true, the regex compare is done as case insensitive.")]
185+
public void Test011()
186+
{
187+
var sut = new TextNodeComparer(ignoreCase: true);
188+
var paragraphSource = ToComparisonSource(@"<p diff:regex>FOO\d{4}</p>");
189+
var controlSource = new ComparisonSource(paragraphSource.Node.FirstChild, 0, paragraphSource.Path, ComparisonSourceType.Control);
190+
var testSource = ToComparisonSource("foo1234", ComparisonSourceType.Test);
191+
var comparison = new Comparison(controlSource, testSource);
141192

142-
// When diff:regex attribute is found on the containing element, the control text is expected to a regex and that used when comparing to the test text node.
193+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
194+
}
195+
196+
[Theory(DisplayName = "When diff:regex='false' attribute is found on the immediate parent element, a string ordinal case comparison is performed.")]
197+
[InlineData(@"<p diff:regex=""false"">1234</p>")]
198+
public void Test012(string controlHtml)
199+
{
200+
var sut = new TextNodeComparer();
201+
var paragraphSource = ToComparisonSource(controlHtml);
202+
var controlSource = new ComparisonSource(paragraphSource.Node.FirstChild, 0, paragraphSource.Path, ComparisonSourceType.Control);
203+
var testSource = ToComparisonSource("1234", ComparisonSourceType.Test);
204+
var comparison = new Comparison(controlSource, testSource);
205+
206+
sut.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
207+
}
143208
}
144209
}
145210

0 commit comments

Comments
 (0)