Skip to content

Commit 37f5593

Browse files
committed
With stylesheet comparer support
1 parent bd85031 commit 37f5593

File tree

8 files changed

+153
-4
lines changed

8 files changed

+153
-4
lines changed

docs/Strategies.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ var diffs = DiffBuilder
3939
.WithAttributeNameMatcher()
4040
.WithNodeNameComparer()
4141
.WithIgnoreElementSupport()
42+
.WithStyleSheetComparer()
4243
.WithTextComparer(WhitespaceOption.Normalize, ignoreCase: false)
4344
.WithAttributeComparer()
4445
.WithClassAttributeComparer()
@@ -204,7 +205,7 @@ var diffs = DiffBuilder
204205
.Build();
205206
```
206207

207-
### Ignore element attribute
208+
#### Ignore element attribute
208209
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.
209210

210211
In this example, the `<h1>` tag, it's attribute and children are considered the same as the element it is matched with:
@@ -228,6 +229,8 @@ var diffs = DiffBuilder
228229
### Text (text nodes) strategies
229230
The built-in text strategies offer a bunch of ways to control how text (text nodes) is handled during the diffing process.
230231

232+
**NOTE:** It is on the issues list to enable a more intelligent, e.g. whitespace aware, comparison of JavaScript (text) inside `<script>`-tags and event-attributes.
233+
231234
#### Whitespace handling
232235
Whitespace can be a source of false-positives when comparing two HTML fragments. Thus, the whitespace handling strategy offer different ways to deal with it during a comparison.
233236

@@ -261,8 +264,6 @@ To configure/override whitespace rules on a specific subtree in the comparison,
261264
<pre diff:whitespace="RemoveWhitespaceNodes">...</pre>
262265
```
263266

264-
**NOTE:** It is on the issues list to deal with whitespace properly inside `<style>` and `<script>`-tags, e.g. inside strings.
265-
266267
#### Perform case-_insensitve_ text comparison
267268
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.:
268269

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

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

299+
#### Style sheet text comparer
300+
Different rules whitespace apply to style sheets (style information) inside `<style>` tags and `style="..."` attributes, than to HTML5. This comparer will parse the style information inside `<style>` tags and `style="..."` attributes and compare the result of the parsing, instead doing a direct string comparison. This should remove false-positives where e.g. insignificant whitespace makes two otherwise equal set of style informations result in a diff.
301+
302+
To add this comparer, use the `WithStyleSheetComparer()` method on the `DiffBuilder` class, e.g.:
303+
304+
```csharp
305+
var diffs = DiffBuilder
306+
.Compare(controlHtml)
307+
.WithTest(testHtml)
308+
.WithStyleSheetComparer()
309+
.Build();
310+
```
311+
298312
### Attribute Compare options
299313
The library supports various ways to perform attribute comparison.
300314

src/Egil.AngleSharp.Diffing.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
<ItemGroup>
1414
<PackageReference Include="AngleSharp" Version="$(AngleSharpVersion)" />
15+
<PackageReference Include="AngleSharp.Css" Version="$(AngleSharpVersion)" />
1516
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="$(FxCopAnalyzersVersion)">
1617
<PrivateAssets>all</PrivateAssets>
1718
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

src/Strategies/DiffingStrategyPipelineExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public static DiffingStrategyPipeline WithDefaultOptions(this DiffingStrategyPip
2626
.WithAttributeNameMatcher()
2727
.WithNodeNameComparer()
2828
.WithIgnoreElementSupport()
29+
.WithStyleSheetComparer()
2930
.WithTextComparer(WhitespaceOption.Normalize, ignoreCase: false)
3031
.WithAttributeComparer()
3132
.WithClassAttributeComparer()

src/Strategies/TextNodeStrategies/DiffingStrategyPipelineExtensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,13 @@ public static DiffingStrategyPipeline WithTextComparer(this DiffingStrategyPipel
2020
return pipeline;
2121
}
2222

23+
/// <summary>
24+
/// Enables the special style-tag style sheet text comparer.
25+
/// </summary>
26+
public static DiffingStrategyPipeline WithStyleSheetComparer(this DiffingStrategyPipeline pipeline)
27+
{
28+
pipeline.AddComparer(StyleSheetTextNodeComparer.Compare, isSpecializedComparer: true);
29+
return pipeline;
30+
}
2331
}
2432
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
using AngleSharp;
9+
using AngleSharp.Css.Dom;
10+
using AngleSharp.Css.Parser;
11+
using AngleSharp.Dom;
12+
using Egil.AngleSharp.Diffing.Core;
13+
14+
namespace Egil.AngleSharp.Diffing.Strategies.TextNodeStrategies
15+
{
16+
public static class StyleSheetTextNodeComparer
17+
{
18+
public static CompareResult Compare(in Comparison comparison, CompareResult currentDecision)
19+
{
20+
if (currentDecision.IsDecisionFinal()) return currentDecision;
21+
if (TryGetStyleDeclaretions(comparison, out var controlStyles, out var testStyles))
22+
return Compare(controlStyles, testStyles);
23+
else
24+
return currentDecision;
25+
}
26+
27+
private static bool TryGetStyleDeclaretions(in Comparison comparison, [NotNullWhen(true)]out IStyleSheet? controlStyles, [NotNullWhen(true)]out IStyleSheet? testStyles)
28+
{
29+
controlStyles = default;
30+
testStyles = default;
31+
32+
if (comparison.TryGetNodesAsType<IText>(out var controlTextNode, out var testTextNode))
33+
{
34+
var controlParentStyle = controlTextNode.ParentElement as ILinkStyle;
35+
var testParentStyle = testTextNode.ParentElement as ILinkStyle;
36+
controlStyles = controlParentStyle?.Sheet;
37+
testStyles = testParentStyle?.Sheet;
38+
39+
return controlStyles is { } && testStyles is { };
40+
}
41+
else
42+
return false;
43+
}
44+
45+
private static CompareResult Compare(IStyleSheet controlStyles, IStyleSheet testStyles)
46+
{
47+
var control = controlStyles.ToCss();
48+
var test = testStyles.ToCss();
49+
50+
return control.Equals(test, StringComparison.Ordinal)
51+
? CompareResult.Same
52+
: CompareResult.Different;
53+
}
54+
}
55+
}

tests/Core/DiffingTestBase.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ namespace Egil.AngleSharp.Diffing.Core
77
{
88
public abstract class DiffingTestBase
99
{
10-
private readonly IBrowsingContext _context = BrowsingContext.New();
10+
private readonly IBrowsingContext _context;
1111
private readonly IHtmlParser _htmlParser;
1212
private readonly IDocument _document;
1313

1414
protected INodeList EmptyNodeList => ToNodeList("");
1515

1616
protected DiffingTestBase()
1717
{
18+
var config = Configuration.Default.WithCss();
19+
_context = BrowsingContext.New(config);
1820
_htmlParser = _context.GetService<IHtmlParser>();
1921
_document = _context.OpenNewAsync().Result;
2022
}

tests/Egil.AngleSharp.DiffingTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
<ItemGroup>
1111
<PackageReference Include="AngleSharp" Version="$(AngleSharpVersion)" />
12+
<PackageReference Include="AngleSharp.Css" Version="$(AngleSharpVersion)" />
1213
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="$(FxCopAnalyzersVersion)">
1314
<PrivateAssets>all</PrivateAssets>
1415
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
using Shouldly;
8+
using Xunit;
9+
10+
namespace Egil.AngleSharp.Diffing.Strategies.TextNodeStrategies
11+
{
12+
public class StyleSheetTextNodeComparerTest : TextNodeTestBase
13+
{
14+
[Fact(DisplayName = "When input node is not a IText node, comparer does not run nor change the current decision")]
15+
public void Test000()
16+
{
17+
var comparison = ToComparison("<p></p>", "<p></p>");
18+
19+
StyleSheetTextNodeComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
20+
StyleSheetTextNodeComparer.Compare(comparison, CompareResult.DifferentAndBreak).ShouldBe(CompareResult.DifferentAndBreak);
21+
StyleSheetTextNodeComparer.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same);
22+
StyleSheetTextNodeComparer.Compare(comparison, CompareResult.SameAndBreak).ShouldBe(CompareResult.SameAndBreak);
23+
}
24+
25+
[Fact(DisplayName = "When input node is a IText node inside an element that is NOT a style tag, comparer does not run nor change the current decision")]
26+
public void Test0001()
27+
{
28+
var comparison = ToComparison("<p>h1{background:#000;}</p>", "<p>h1{background:#000;}</p>");
29+
30+
StyleSheetTextNodeComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
31+
StyleSheetTextNodeComparer.Compare(comparison, CompareResult.DifferentAndBreak).ShouldBe(CompareResult.DifferentAndBreak);
32+
StyleSheetTextNodeComparer.Compare(comparison, CompareResult.Same).ShouldBe(CompareResult.Same);
33+
StyleSheetTextNodeComparer.Compare(comparison, CompareResult.SameAndBreak).ShouldBe(CompareResult.SameAndBreak);
34+
}
35+
36+
37+
[Fact(DisplayName = "The comparer responses with Different when style information is different")]
38+
public void Test001()
39+
{
40+
var comparison = ToStyleComparison(@"h1{background:#000;}", @"h1{color:#000;}");
41+
42+
StyleSheetTextNodeComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Different);
43+
}
44+
45+
[Theory(DisplayName = "The comparer ignores insignificant whitespace")]
46+
[InlineData(" ")]
47+
[InlineData(" ")]
48+
[InlineData("\t")]
49+
[InlineData("\n")]
50+
[InlineData("\n\r")]
51+
public void Test003(string whitespace)
52+
{
53+
var comparison = ToStyleComparison($@"h1{whitespace}{{{whitespace}color:{whitespace}#000;{whitespace}}}", @"h1{color:#000;}");
54+
55+
StyleSheetTextNodeComparer.Compare(comparison, CompareResult.Different).ShouldBe(CompareResult.Same);
56+
}
57+
58+
private Comparison ToStyleComparison(string controlStyleText, string testStyleText)
59+
{
60+
var controlStyle = ToComparisonSource($@"<style type=""text/css"">{controlStyleText}</style>");
61+
var controlSource = new ComparisonSource(controlStyle.Node.FirstChild, 0, controlStyle.Path, ComparisonSourceType.Control);
62+
var testStyle = ToComparisonSource($@"<style type=""text/css"">{testStyleText}</style>");
63+
var testSource = new ComparisonSource(testStyle.Node.FirstChild, 0, testStyle.Path, ComparisonSourceType.Test);
64+
return new Comparison(controlSource, testSource);
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)