Skip to content

Commit 0940e86

Browse files
committed
Simple attribute compare
1 parent 8c4ff19 commit 0940e86

File tree

7 files changed

+276
-137
lines changed

7 files changed

+276
-137
lines changed

src/Comparison.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
3+
namespace Egil.AngleSharp.Diffing
4+
{
5+
public readonly struct Comparison : IEquatable<Comparison>
6+
{
7+
public ComparisonSource Control { get; }
8+
9+
public ComparisonSource? Test { get; }
10+
11+
public MatchStatus Status => Test is null ? MatchStatus.TestNodeNotFound : MatchStatus.TestNodeFound;
12+
13+
public Comparison(in ComparisonSource control, in ComparisonSource? test)
14+
{
15+
Control = control;
16+
Test = test;
17+
}
18+
19+
#region Equals and HashCode
20+
public bool Equals(Comparison other) => Control == other.Control && Test == other.Test;
21+
public override bool Equals(object obj) => obj is Comparison other && Equals(other);
22+
public override int GetHashCode() => (Control, Test).GetHashCode();
23+
public static bool operator ==(Comparison left, Comparison right) => left.Equals(right);
24+
public static bool operator !=(Comparison left, Comparison right) => !(left == right);
25+
#endregion
26+
}
27+
}

src/ComparisonSource.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using AngleSharp.Dom;
3+
4+
namespace Egil.AngleSharp.Diffing
5+
{
6+
public readonly struct ComparisonSource : IEquatable<ComparisonSource>
7+
{
8+
public INode Node { get; }
9+
public int Index { get; }
10+
11+
public ComparisonSource(INode node, int index)
12+
{
13+
Node = node;
14+
Index = index;
15+
}
16+
17+
#region Equals and HashCode
18+
public bool Equals(ComparisonSource other) => Node == other.Node && Index == other.Index;
19+
public override int GetHashCode() => (Node, Index).GetHashCode();
20+
public override bool Equals(object obj) => obj is ComparisonSource other && Equals(other);
21+
public static bool operator ==(ComparisonSource left, ComparisonSource right) => left.Equals(right);
22+
public static bool operator !=(ComparisonSource left, ComparisonSource right) => !(left == right);
23+
#endregion
24+
}
25+
}

src/Diff.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.Diagnostics;
3+
4+
namespace Egil.AngleSharp.Diffing
5+
{
6+
[DebuggerDisplay("Diff={Type} Control={Control?.Node.NodeName}[{Control?.Index}] Test={Test?.Node.NodeName}[{Test?.Index}]")]
7+
public readonly struct Diff : IEquatable<Diff>
8+
{
9+
public DiffType Type { get; }
10+
11+
public ComparisonSource? Control { get; }
12+
13+
public ComparisonSource? Test { get; }
14+
15+
public Diff(DiffType type, in ComparisonSource? control = null, in ComparisonSource? test = null)
16+
{
17+
Type = type;
18+
Control = control;
19+
Test = test;
20+
}
21+
22+
#region Equals and Hashcode
23+
public bool Equals(Diff other) => Type == other.Type;
24+
public override int GetHashCode() => (Type).GetHashCode();
25+
public override bool Equals(object obj) => obj is Diff other && Equals(other);
26+
public static bool operator ==(Diff left, Diff right) => left.Equals(right);
27+
public static bool operator !=(Diff left, Diff right) => !(left == right);
28+
#endregion
29+
}
30+
31+
public enum DiffType
32+
{
33+
DifferentComment,
34+
DifferentElementTagName,
35+
DifferentTextNode,
36+
MissingComment,
37+
MissingElement,
38+
MissingTextNode,
39+
UnexpectedComment,
40+
UnexpectedElement,
41+
UnexpectedTextNode,
42+
DifferentAttribute
43+
}
44+
}

src/EmptyNodeList.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using AngleSharp;
6+
using AngleSharp.Dom;
7+
8+
namespace Egil.AngleSharp.Diffing
9+
{
10+
public class EmptyNodeList : INodeList
11+
{
12+
public INode this[int index] => throw new IndexOutOfRangeException("There are no nodes in an empty node list");
13+
14+
public int Length => 0;
15+
16+
private EmptyNodeList() { }
17+
18+
public IEnumerator<INode> GetEnumerator() { yield break; }
19+
20+
public void ToHtml(TextWriter writer, IMarkupFormatter formatter) { }
21+
22+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
23+
24+
public static readonly INodeList Instance = new EmptyNodeList();
25+
}
26+
}

src/HtmlDifferenceEngine.cs

Lines changed: 68 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Diagnostics;
43
using System.Linq;
54
using AngleSharp.Dom;
65

76
namespace Egil.AngleSharp.Diffing
87
{
8+
99
public enum CompareResult
1010
{
1111
Same,
@@ -16,7 +16,8 @@ public interface IHtmlCompareStrategy
1616
{
1717
bool NodeFilter(INode node);
1818
bool AttributeFilter(IAttr attribute, IElement owningElement);
19-
CompareResult Compare(in Comparison comparison);
19+
CompareResult CompareNode(in Comparison comparison);
20+
CompareResult CompareAttribute(string attributeName, in Comparison comparisonContext);
2021
}
2122

2223
public class HtmlDifferenceEngine
@@ -28,48 +29,58 @@ public HtmlDifferenceEngine(IHtmlCompareStrategy strategy)
2829
_strategy = strategy;
2930
}
3031

31-
public IReadOnlyCollection<Diff> Compare(INodeList controlNodes, INodeList testNodes) => DoCompare(controlNodes, testNodes);
32+
public IReadOnlyList<Diff> Compare(INodeList controlNodes, INodeList testNodes) => DoCompare(controlNodes, testNodes);
3233

33-
private IReadOnlyCollection<Diff> DoCompare(INodeList controlNodes, INodeList testNodes)
34+
private IReadOnlyList<Diff> DoCompare(INodeList rootControlNodes, INodeList rootTestNodes)
3435
{
35-
if (controlNodes is null) throw new ArgumentNullException(nameof(controlNodes));
36-
if (testNodes is null) throw new ArgumentNullException(nameof(testNodes));
36+
if (rootControlNodes is null) throw new ArgumentNullException(nameof(rootControlNodes));
37+
if (rootTestNodes is null) throw new ArgumentNullException(nameof(rootTestNodes));
3738

3839
var result = new List<Diff>();
3940

40-
var comparisons = GetComparisons(controlNodes, testNodes);
41+
var compareQueue = new Queue<(INodeList ControlNodes, INodeList TestNodes)>();
42+
compareQueue.Enqueue((rootControlNodes, rootTestNodes));
4143

42-
foreach (var comparison in comparisons)
44+
while (compareQueue.Count > 0)
4345
{
44-
result.AddRange(GetDifferences(in comparison));
45-
//// Compare child nodes
46-
//if (comparison.Control.HasChildNodes)
47-
// foreach (var diff in Compare(comparison.Control.ChildNodes, comparison.Test.ChildNodes))
48-
// yield return diff;
49-
}
46+
var (controlNodes, testNodes) = compareQueue.Dequeue();
47+
var comparisons = GetComparisons(controlNodes, testNodes);
5048

51-
var matchedTestNodes = comparisons.Where(x => x.Test.HasValue).Select(x => x.Test.Value);
52-
var unmatchedTestNodes = testNodes.Select((n, i) => new ComparisonSource(n, i)).Except(matchedTestNodes);
49+
foreach (var comparison in comparisons)
50+
{
51+
result.AddRange(GetDifferences(in comparison));
52+
}
5353

54-
// detect unmatched test nodes
55-
foreach (var node in unmatchedTestNodes)
56-
{
57-
result.Add(node.Node.NodeType switch
54+
var matchedTestNodes = comparisons.Where(x => x.Test.HasValue).Select(x => x.Test.Value);
55+
var unmatchedTestNodes = testNodes.Select((n, i) => new ComparisonSource(n, i)).Except(matchedTestNodes);
56+
57+
// detect unmatched test nodes
58+
foreach (var node in unmatchedTestNodes)
5859
{
59-
NodeType.Comment => new Diff(DiffType.UnexpectedComment, test: node),
60-
NodeType.Element => new Diff(DiffType.UnexpectedElement, test: node),
61-
NodeType.Text => new Diff(DiffType.UnexpectedTextNode, test: node),
62-
_ => throw new InvalidOperationException($"Unexpected nodetype, {node.Node.NodeType}, in test nodes list.")
63-
});
60+
result.Add(node.Node.NodeType switch
61+
{
62+
NodeType.Comment => new Diff(DiffType.UnexpectedComment, test: node),
63+
NodeType.Element => new Diff(DiffType.UnexpectedElement, test: node),
64+
NodeType.Text => new Diff(DiffType.UnexpectedTextNode, test: node),
65+
_ => throw new InvalidOperationException($"Unexpected nodetype, {node.Node.NodeType}, in test nodes list.")
66+
});
67+
}
68+
69+
foreach (var c in comparisons)
70+
{
71+
if (c.Status == MatchStatus.TestNodeNotFound) continue;
72+
if (c.Control.Node.HasChildNodes || (c.Test?.Node.HasChildNodes ?? false))
73+
compareQueue.Enqueue((c.Control.Node.ChildNodes, c.Test?.Node.ChildNodes ?? EmptyNodeList.Instance));
74+
}
6475
}
6576

6677
return result;
6778
}
6879

69-
private ICollection<Comparison> GetComparisons(INodeList controlNodes, INodeList testNodes)
80+
private List<Comparison> GetComparisons(INodeList controlNodes, INodeList testNodes)
7081
{
7182
var evenTreeBranch = controlNodes.Length == testNodes.Length;
72-
var result = new Comparison[controlNodes.Length];
83+
var result = new List<Comparison>(controlNodes.Length);
7384
var lastFoundIndex = -1;
7485

7586
for (int index = 0; index < controlNodes.Length; index++)
@@ -83,13 +94,13 @@ private ICollection<Comparison> GetComparisons(INodeList controlNodes, INodeList
8394
? EqualTreeSizeNodeMatcher(in controlSource)
8495
: ForwardSearchingNodeMatcher(in controlSource);
8596

86-
result[index] = new Comparison(controlSource, testSource);
97+
result.Add(new Comparison(controlSource, testSource));
8798
}
8899

89100
return result;
90101

91102
//bool ShouldSkipNode(INode node) => !_strategy.NodeFilter(node);
92-
103+
93104
ComparisonSource? EqualTreeSizeNodeMatcher(in ComparisonSource comparisonSource)
94105
{
95106
// Consider skipping strategy effect
@@ -135,99 +146,39 @@ private ICollection<Diff> GetDifferences(in Comparison comparison) // in
135146
_ => throw new InvalidOperationException($"Unexpected nodetype, {comparison.Control.Node.NodeType}, in test nodes list.")
136147
});
137148
}
138-
else if (comparison.Status == MatchStatus.TestNodeFound && _strategy.Compare(in comparison) == CompareResult.Different)
149+
else
139150
{
140-
result.Add(comparison.Control.Node.NodeType switch
151+
if (_strategy.CompareNode(in comparison) == CompareResult.Different)
141152
{
142-
NodeType.Comment => new Diff(DiffType.DifferentComment, comparison.Control, comparison.Test),
143-
NodeType.Element => new Diff(DiffType.DifferentElementTagName, comparison.Control, comparison.Test),
144-
NodeType.Text => new Diff(DiffType.DifferentTextNode, comparison.Control, comparison.Test),
145-
_ => throw new InvalidOperationException($"Unexpected nodetype, {comparison.Control.Node.NodeType}, in test nodes list.")
146-
});
147-
}
148-
149-
return result;
150-
}
151-
}
152-
153-
public enum DiffType
154-
{
155-
DifferentComment,
156-
DifferentElementTagName,
157-
DifferentTextNode,
158-
MissingComment,
159-
MissingElement,
160-
MissingTextNode,
161-
UnexpectedComment,
162-
UnexpectedElement,
163-
UnexpectedTextNode
164-
}
165-
166-
[DebuggerDisplay("Diff={Type} Control={Control?.Node.NodeName}[{Control?.Index}] Test={Test?.Node.NodeName}[{Test?.Index}]")]
167-
public readonly struct Diff : IEquatable<Diff>
168-
{
169-
public DiffType Type { get; }
170-
171-
public ComparisonSource? Control { get; }
172-
173-
public ComparisonSource? Test { get; }
174-
175-
public Diff(DiffType type, in ComparisonSource? control = null, in ComparisonSource? test = null)
176-
{
177-
Type = type;
178-
Control = control;
179-
Test = test;
180-
}
181-
182-
#region Equals and Hashcode
183-
public bool Equals(Diff other) => Type == other.Type;
184-
public override int GetHashCode() => (Type).GetHashCode();
185-
public override bool Equals(object obj) => obj is Diff other && Equals(other);
186-
public static bool operator ==(Diff left, Diff right) => left.Equals(right);
187-
public static bool operator !=(Diff left, Diff right) => !(left == right);
188-
#endregion
189-
}
190-
191-
public readonly struct Comparison : IEquatable<Comparison>
192-
{
193-
public ComparisonSource Control { get; }
194-
195-
public ComparisonSource? Test { get; }
196-
197-
public MatchStatus Status => Test is null ? MatchStatus.TestNodeNotFound : MatchStatus.TestNodeFound;
198-
199-
public Comparison(in ComparisonSource control, in ComparisonSource? test)
200-
{
201-
Control = control;
202-
Test = test;
203-
}
153+
result.Add(comparison.Control.Node.NodeType switch
154+
{
155+
NodeType.Comment => new Diff(DiffType.DifferentComment, comparison.Control, comparison.Test),
156+
NodeType.Element => new Diff(DiffType.DifferentElementTagName, comparison.Control, comparison.Test),
157+
NodeType.Text => new Diff(DiffType.DifferentTextNode, comparison.Control, comparison.Test),
158+
_ => throw new InvalidOperationException($"Unexpected nodetype, {comparison.Control.Node.NodeType}, in test nodes list.")
159+
});
160+
}
204161

205-
#region Equals and HashCode
206-
public bool Equals(Comparison other) => Control == other.Control && Test == other.Test;
207-
public override bool Equals(object obj) => obj is Comparison other && Equals(other);
208-
public override int GetHashCode() => (Control, Test).GetHashCode();
209-
public static bool operator ==(Comparison left, Comparison right) => left.Equals(right);
210-
public static bool operator !=(Comparison left, Comparison right) => !(left == right);
211-
#endregion
212-
}
162+
var controlElm = comparison.Control.Node as IElement;
163+
var testElm = comparison.Test?.Node as IElement;
213164

214-
public readonly struct ComparisonSource : IEquatable<ComparisonSource>
215-
{
216-
public INode Node { get; }
217-
public int Index { get; }
165+
if (controlElm is { } && testElm is { })
166+
{
167+
foreach (var controlAttr in controlElm.Attributes)
168+
{
169+
if (testElm.HasAttribute(controlAttr.Name))
170+
{
171+
var attrCompareRes = _strategy.CompareAttribute(controlAttr.Name, in comparison);
172+
if(attrCompareRes== CompareResult.Different)
173+
{
174+
result.Add(new Diff(DiffType.DifferentAttribute, comparison.Control, comparison.Test));
175+
}
176+
}
177+
}
178+
}
179+
}
218180

219-
public ComparisonSource(INode node, int index)
220-
{
221-
Node = node;
222-
Index = index;
181+
return result;
223182
}
224-
225-
#region Equals and HashCode
226-
public bool Equals(ComparisonSource other) => Node == other.Node && Index == other.Index;
227-
public override int GetHashCode() => (Node, Index).GetHashCode();
228-
public override bool Equals(object obj) => obj is ComparisonSource other && Equals(other);
229-
public static bool operator ==(ComparisonSource left, ComparisonSource right) => left.Equals(right);
230-
public static bool operator !=(ComparisonSource left, ComparisonSource right) => !(left == right);
231-
#endregion
232183
}
233184
}

0 commit comments

Comments
 (0)