11using System ;
22using System . Collections . Generic ;
3- using System . Diagnostics ;
43using System . Linq ;
54using AngleSharp . Dom ;
65
76namespace 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