Skip to content

Commit bd85031

Browse files
committed
Strategies and pipeline
1 parent 15156f3 commit bd85031

30 files changed

+834
-95
lines changed

docs/Strategies.md

Lines changed: 47 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ var testHtml = "<p>World, I say hello</p>";
2222
var diffs = DiffBuilder
2323
.Compare(controlHtml)
2424
.WithTest(testHtml)
25-
.UseDefaultOptions()
25+
.WithDefaultOptions()
2626
.Build();
2727
```
2828

@@ -32,17 +32,18 @@ Calling the `UseDefaultOptions()` method is equivalent to specifying the followi
3232
var diffs = DiffBuilder
3333
.Compare(controlHtml)
3434
.WithTest(testHtml)
35-
.IgnoreComments()
3635
.IgnoreDiffAttributes()
37-
.WithCssSelectorMatcher()
36+
.IgnoreComments()
3837
.WithSearchingNodeMatcher()
39-
.WithOneToOneAttributeMatcher()
38+
.WithCssSelectorMatcher()
39+
.WithAttributeNameMatcher()
4040
.WithNodeNameComparer()
41+
.WithIgnoreElementSupport()
4142
.WithTextComparer(WhitespaceOption.Normalize, ignoreCase: false)
42-
.EnableIgnoreAttribute()
4343
.WithAttributeComparer()
4444
.WithClassAttributeComparer()
4545
.WithBooleanAttributeComparer(BooleanAttributeComparision.Strict)
46+
.WithInlineAttributeIgnoreSupport()
4647
.Build();
4748
```
4849

@@ -64,8 +65,8 @@ var diffs = DiffBuilder
6465

6566
_**NOTE**: Currently, the ignore comment strategy does NOT remove comments from CSS or JavaScript embedded in `<style>` or `<script>` tags._
6667

67-
### Ignoring special `diff:` attributes
68-
Any attributes that starts with `diff:` are automatically filtered out before matching/comparing happens. E.g. `diff:whitespace="..."` does not show up as a missing diff when added to an control element.
68+
### Ignoring special "diff"-attributes
69+
Any attributes that starts with `diff:` are automatically filtered out before matching/comparing happens. E.g. `diff:whitespace="..."` does not show up as a missing diff when added to an control element.
6970

7071
To enable this option, use the `IgnoreDiffAttributes()` method on the `DiffBuilder` class, e.g.:
7172

@@ -161,7 +162,7 @@ The following control node will be compared against the `<h1>` in the `<header>`
161162

162163
One use case of the CSS-selector element matcher is where you only want to test one part of a sub-tree, and ignore the rest. The example above will report the unmatched test nodes as *unexpected*, but those "diffs" can be ignored, since that is expected. This approach can save you from specifying all the needed control nodes, if only part of a sub tree needs to be compared.
163164

164-
To choose this matcher, use the `WithSearchingMatcher()` method on the `DiffBuilder` class, e.g.:
165+
To choose this matcher, use the `WithCssSelectorMatcher()` method on the `DiffBuilder` class, e.g.:
165166

166167
```csharp
167168
var diffs = DiffBuilder
@@ -174,28 +175,26 @@ var diffs = DiffBuilder
174175
### Attribute matching strategies
175176
These are the built-in attribute matching strategies.
176177

177-
#### One-to-one attribute name matcher
178+
#### Attribute name matcher
178179
This selector will match attributes on a control element with attributes on a test element using the attribute's name. If an *control* attribute is not matched, it is reported as *missing* and if a *test* attribute is not matched, it is reported as *unexpected*.
179180

180-
To choose this matcher, use the `WithOneToOneAttributeMatcher()` method on the `DiffBuilder` class, e.g.:
181+
To choose this matcher, use the `WithAttributeNameMatcher()` method on the `DiffBuilder` class, e.g.:
181182

182183
```csharp
183184
var diffs = DiffBuilder
184185
.Compare(controlHtml)
185186
.WithTest(testHtml)
186-
.WithOneToOneAttributeMatcher()
187+
.WithAttributeNameMatcher()
187188
.Build();
188189
```
189190

190-
**Note:** this matcher will not count any diff-`:postfix` modifiers on attributes as part of the name when matching, thus, `id:regex="..."` will be matched with `id="..."`.
191-
192191
## Comparing strategies
193192
These are the built-in comparing strategies.
194193

195-
### Node compare strategy
194+
### Node and element compare strategy
196195
The basic node compare strategy will simply check if the node's types and node's name are equal.
197196

198-
To choose this matcher, use the `WithNodeNameComparer()` method on the `DiffBuilder` class, e.g.:
197+
To choose this comparer, use the `WithNodeNameComparer()` method on the `DiffBuilder` class, e.g.:
199198

200199
```csharp
201200
var diffs = DiffBuilder
@@ -205,7 +204,7 @@ var diffs = DiffBuilder
205204
.Build();
206205
```
207206

208-
### Ignore attribute
207+
### Ignore element attribute
209208
If the `diff:ignore="true"` attribute is used on a control element (`="true"` implicit/optional), all their attributes and child nodes are skipped/ignored during comparison, including those of the test element, the control element is matched with.
210209

211210
In this example, the `<h1>` tag, it's attribute and children are considered the same as the element it is matched with:
@@ -216,13 +215,13 @@ In this example, the `<h1>` tag, it's attribute and children are considered the
216215
</header>
217216
```
218217

219-
Activate this strategy by calling the `EnableIgnoreAttribute()` method on a `DiffBuilder` instance, e.g.:
218+
Activate this strategy by calling the `WithIgnoreElementSupport()` method on a `DiffBuilder` instance, e.g.:
220219

221220
```csharp
222221
var diffs = DiffBuilder
223222
.Compare(controlHtml)
224223
.WithTest(testHtml)
225-
.EnableIgnoreAttribute()
224+
.WithIgnoreElementSupport()
226225
.Build();
227226
```
228227

@@ -265,13 +264,13 @@ To configure/override whitespace rules on a specific subtree in the comparison,
265264
**NOTE:** It is on the issues list to deal with whitespace properly inside `<style>` and `<script>`-tags, e.g. inside strings.
266265

267266
#### Perform case-_insensitve_ text comparison
268-
To compare the text in two text nodes to each other using a case-insensitive comparison, call the `WithTextComparer(WhitespaceOption, ignoreCase: true)` method on a `DiffBuilder` instance, e.g.:
267+
To compare the text in two text nodes to each other using a case-insensitive comparison, call the `WithTextComparer(ignoreCase: true)` method on a `DiffBuilder` instance, e.g.:
269268

270269
```csharp
271270
var diffs = DiffBuilder
272271
.Compare(controlHtml)
273272
.WithTest(testHtml)
274-
.WithTextComparer(WhitespaceOption.Normalize, ignoreCase: true)
273+
.WithTextComparer(ignoreCase: true)
275274
.Build();
276275
```
277276

@@ -296,25 +295,6 @@ By using the inline attribute `diff:regex` on the element containing the text no
296295

297296
The above control text would use case-insensitive regular expression to match against a test text string (e.g. "HELLO WORLD 2020").
298297

299-
### Ignore postfix for attributes
300-
To ignore a specific attribute during comparison, add the `:ignore` postfix to the attribute on the control element. Thus will simply skip comparing the two attributes and not report any differences between them. E.g. to ignore the `class` attribute, do:
301-
302-
```html
303-
<header>
304-
<h1 class:ignore="heading-1">Hello world</h1>
305-
</header>
306-
```
307-
308-
Activate this strategy by calling the `EnableIgnoreAttribute()` method on a `DiffBuilder` instance, e.g.:
309-
310-
```csharp
311-
var diffs = DiffBuilder
312-
.Compare(controlHtml)
313-
.WithTest(testHtml)
314-
.EnableIgnoreAttribute()
315-
.Build();
316-
```
317-
318298
### Attribute Compare options
319299
The library supports various ways to perform attribute comparison.
320300

@@ -325,7 +305,15 @@ The *"name and value comparison"* is the base comparison option, and that will t
325305
- `attr="foo"` is the NOT same as `attr="bar"`
326306
- `foo="attr"` is the NOT same as `bar="attr"`
327307

328-
This comparison mode is on by default.
308+
To choose this comparer, use the `WithAttributeComparer()` method on the `DiffBuilder` class, e.g.:
309+
310+
```csharp
311+
var diffs = DiffBuilder
312+
.Compare(controlHtml)
313+
.WithTest(testHtml)
314+
.WithAttributeComparer()
315+
.Build();
316+
```
329317

330318
#### RegEx attribute value comparer
331319
It is possible to specify a regular expression in the control attributes value, and add the `:regex` postfix to the *control* attributes name, to have the comparison performed using a Regex match test. E.g.
@@ -378,4 +366,23 @@ var diffs = DiffBuilder
378366
.WithTest(testHtml)
379367
.WithBooleanAttributeComparer(BooleanAttributeComparision.Strict)
380368
.Build();
369+
```
370+
371+
### Ignore attributes during diffing
372+
To ignore a specific attribute during comparison, add the `:ignore` postfix to the attribute on the control element. Thus will simply skip comparing the two attributes and not report any differences between them. E.g. to ignore the `class` attribute, do:
373+
374+
```html
375+
<header>
376+
<h1 class:ignore="heading-1">Hello world</h1>
377+
</header>
378+
```
379+
380+
Activate this strategy by calling the `WithInlineAttributeIgnoreSupport()` method on a `DiffBuilder` instance, e.g.:
381+
382+
```csharp
383+
var diffs = DiffBuilder
384+
.Compare(controlHtml)
385+
.WithTest(testHtml)
386+
.WithInlineAttributeIgnoreSupport()
387+
.Build();
381388
```

src/Core/SourceMap.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ public AttributeComparisonSource this[string name]
2222
{
2323
get
2424
{
25-
if (!_sources.ContainsKey(name))
26-
throw new ArgumentException($"The map does not contain an attribute comparison source that matches the name '{name}'.");
25+
//if (!_sources.ContainsKey(name))
26+
// throw new ArgumentException($"The map does not contain an attribute comparison source that matches the name '{name}'.");
2727
return _sources[name];
2828
}
2929
}
@@ -46,6 +46,8 @@ public SourceMap(in ComparisonSource elementSource)
4646

4747
public bool Contains(string name) => _sources.ContainsKey(name);
4848

49+
public bool IsUnmatched(string name) => !_matched.Contains(name);
50+
4951
public IEnumerable<AttributeComparisonSource> GetUnmatched()
5052
{
5153
foreach (var source in _sources.Values)

src/DiffingStrategyPipeline.cs

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ public IEnumerable<Comparison> Match(DiffContext context, SourceCollection contr
3232
}
3333
}
3434
}
35-
3635
public IEnumerable<AttributeComparison> Match(DiffContext context, SourceMap controlAttrSources, SourceMap testAttrSources)
3736
{
3837
foreach (var matcher in _attrsMatchers)
@@ -46,19 +45,101 @@ public IEnumerable<AttributeComparison> Match(DiffContext context, SourceMap con
4645
}
4746
}
4847

49-
public CompareResult Compare(in Comparison comparison)
48+
public CompareResult Compare(in Comparison comparison)
5049
=> Compare(comparison, _nodeComparers, CompareResult.DifferentAndBreak);
5150
public CompareResult Compare(in AttributeComparison comparison)
5251
=> Compare(comparison, _attrComparers, CompareResult.Different);
5352

54-
public void AddFilter(FilterStrategy<ComparisonSource> filterStrategy) => _nodeFilters.Add(filterStrategy);
55-
public void AddFilter(FilterStrategy<AttributeComparisonSource> filterStrategy) => _attrsFilters.Add(filterStrategy);
53+
/// <summary>
54+
/// Adds a node filter to the pipeline.
55+
/// Specialized filters always execute after any generalized filters in the pipeline.
56+
/// That enables them to correct for the generic filters decision.
57+
/// </summary>
58+
/// <param name="filterStrategy"></param>
59+
/// <param name="isSpecializedFilter">true if <paramref name="filterStrategy"/> is a specialized filter, false if it is a generalized filter</param>
60+
public void AddFilter(FilterStrategy<ComparisonSource> filterStrategy, bool isSpecializedFilter)
61+
{
62+
if (isSpecializedFilter)
63+
_nodeFilters.Add(filterStrategy);
64+
else
65+
_nodeFilters.Insert(0, filterStrategy);
66+
}
67+
68+
/// <summary>
69+
/// Adds an attribute filter to the pipeline.
70+
/// Specialized filters always execute after any generalized filters in the pipeline.
71+
/// That enables them to correct for the generic filters decision.
72+
/// </summary>
73+
/// <param name="filterStrategy"></param>
74+
/// <param name="isSpecializedFilter">true if <paramref name="filterStrategy"/> is a specialized filter, false if it is a generalized filter</param>
75+
public void AddFilter(FilterStrategy<AttributeComparisonSource> filterStrategy, bool isSpecializedFilter)
76+
{
77+
if (isSpecializedFilter)
78+
_attrsFilters.Add(filterStrategy);
79+
else
80+
_attrsFilters.Insert(0, filterStrategy);
81+
}
82+
83+
/// <summary>
84+
/// Adds a node matcher to the pipeline.
85+
/// Specialized matchers always execute before any generalized matchers in the pipeline.
86+
/// This enables the special matchers to handle special matching cases before the more simple generalized matchers process the rest.
87+
/// </summary>
88+
/// <param name="matchStrategy"></param>
89+
/// <param name="isSpecializedMatcher">true if <paramref name="matchStrategy"/> is a specialized matcher, false if it is a generalized matcher</param>
90+
public void AddMatcher(MatchStrategy<SourceCollection, Comparison> matchStrategy, bool isSpecializedMatcher)
91+
{
92+
if (isSpecializedMatcher)
93+
_nodeMatchers.Insert(0, matchStrategy);
94+
else
95+
_nodeMatchers.Add(matchStrategy);
96+
}
5697

57-
public void AddMatcher(MatchStrategy<SourceCollection, Comparison> matchStrategy) => _nodeMatchers.Add(matchStrategy);
58-
public void AddMatcher(MatchStrategy<SourceMap, AttributeComparison> matchStrategy) => _attrsMatchers.Add(matchStrategy);
98+
/// <summary>
99+
/// Adds an attribute matcher to the pipeline.
100+
/// Specialized matchers always execute before any generalized matchers in the pipeline.
101+
/// This enables the special matchers to handle special matching cases before the more simple generalized matchers process the rest.
102+
/// </summary>
103+
/// <param name="matchStrategy"></param>
104+
/// <param name="isSpecializedMatcher">true if <paramref name="matchStrategy"/> is a specialized matcher, false if it is a generalized matcher</param>
59105

60-
public void AddComparer(CompareStrategy<Comparison> compareStrategy) => _nodeComparers.Add(compareStrategy);
61-
public void AddComparer(CompareStrategy<AttributeComparison> compareStrategy) => _attrComparers.Add(compareStrategy);
106+
public void AddMatcher(MatchStrategy<SourceMap, AttributeComparison> matchStrategy, bool isSpecializedMatcher)
107+
{
108+
if (isSpecializedMatcher)
109+
_attrsMatchers.Insert(0, matchStrategy);
110+
else
111+
_attrsMatchers.Add(matchStrategy);
112+
}
113+
114+
/// <summary>
115+
/// Adds a node comparer to the pipeline.
116+
/// Specialized comparers always execute after any generalized comparers in the pipeline.
117+
/// That enables them to correct for the generic comparers decision.
118+
/// </summary>
119+
/// <param name="compareStrategy"></param>
120+
/// <param name="isSpecializedComparer">true if <paramref name="compareStrategy"/> is a specialized comparer, false if it is a generalized comparer</param>
121+
public void AddComparer(CompareStrategy<Comparison> compareStrategy, bool isSpecializedComparer)
122+
{
123+
if (isSpecializedComparer)
124+
_nodeComparers.Add(compareStrategy);
125+
else
126+
_nodeComparers.Insert(0, compareStrategy);
127+
}
128+
129+
/// <summary>
130+
/// Adds a attribute comparer to the pipeline.
131+
/// Specialized comparers always execute after any generalized comparers in the pipeline.
132+
/// That enables them to correct for the generic comparers decision.
133+
/// </summary>
134+
/// <param name="compareStrategy"></param>
135+
/// <param name="isSpecializedComparer">true if <paramref name="compareStrategy"/> is a specialized comparer, false if it is a generalized comparer</param>
136+
public void AddComparer(CompareStrategy<AttributeComparison> compareStrategy, bool isSpecializedComparer)
137+
{
138+
if (isSpecializedComparer)
139+
_attrComparers.Add(compareStrategy);
140+
else
141+
_attrComparers.Insert(0, compareStrategy);
142+
}
62143

63144
private FilterDecision Filter<T>(in T source, List<FilterStrategy<T>> filterStrategies)
64145
{

src/Strategies/AttributeStrategies/AttributeComparer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using Egil.AngleSharp.Diffing.Core;
44

55
namespace Egil.AngleSharp.Diffing.Strategies.AttributeStrategies
6-
{
6+
{
77
public static class AttributeComparer
88
{
99
private const string IGNORE_CASE_POSTFIX = ":ignorecase";
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.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Egil.AngleSharp.Diffing.Core;
7+
8+
namespace Egil.AngleSharp.Diffing.Strategies.AttributeStrategies
9+
{
10+
public static class AttributeNameMatcher
11+
{
12+
public static IEnumerable<AttributeComparison> Match(DiffContext context, SourceMap controlSources, SourceMap testSources)
13+
{
14+
if (controlSources is null) throw new ArgumentNullException(nameof(controlSources));
15+
if (testSources is null) throw new ArgumentNullException(nameof(testSources));
16+
17+
foreach (var control in controlSources.GetUnmatched())
18+
{
19+
if (testSources.Contains(control.Attribute.Name) && testSources.IsUnmatched(control.Attribute.Name))
20+
yield return new AttributeComparison(control, testSources[control.Attribute.Name]);
21+
}
22+
23+
yield break;
24+
}
25+
}
26+
}

0 commit comments

Comments
 (0)