Skip to content

Commit 6b7015e

Browse files
committed
Added render tree extensions for downloading content
1 parent d20ea49 commit 6b7015e

File tree

10 files changed

+187
-14
lines changed

10 files changed

+187
-14
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# 0.14.0
22

3-
Released on Tuesday, March 31 2020.
3+
Released on Tuesday, April 7 2020.
44

5+
- Added a way to compute relative dimensions (#3)
6+
- Added render tree information incl. utilities (#4)
57
- Fixed issue with empty content (#42)
68
- Added debugger display attribute to CSS rules (#43)
79
- Fixed handling of CSS gradients (#45)

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ AngleSharp.Css contains code written by (in order of first pull request / commit
77
* [Florian Rappl](https://github.com/FlorianRappl)
88
* [Michał Kostrzewski](https://github.com/zeaposs)
99
* [Jochen Kühner](https://github.com/jogibear9988)
10+
* [Tom Hazell](https://github.com/The-Nutty)
1011

1112
Without these awesome people AngleSharp.Css could not exist. Thanks to everyone for your contributions! :beers:
1213

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ var config = Configuration.Default
4343

4444
If no specific `IRenderDevice` (e.g., via creating an `DefaultRenderDevice` object) instance is created a default implementation will be set.
4545

46+
Going a bit further it is possible to `Render` the current document. This render tree information can then be used to retrieve or other information, e.g.,
47+
48+
```cs
49+
var tree = document.DefaultView.Render();
50+
var node = tree.Find(document.QuerySelector("div"));
51+
await node.DownloadResources();
52+
```
53+
54+
The previous snippet renders the current document. Afterwards it retrieves a particular render tree node, which is related to the first found `div`. Then all (CSS introduced) resources are downloaded for the node, if visible.
55+
4656
## Advantages of AngleSharp.Css
4757

4858
The core library already contains the CSS selector parser and the most basic classes and interfaces for dealing with the CSSOM. AngleSharp.Css brings the following advantages and use cases to life:

src/AngleSharp.Css.Tests/Extensions/Elements.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
namespace AngleSharp.Css.Tests.Extensions
22
{
3-
using NUnit.Framework;
43
using AngleSharp.Css.Dom;
4+
using AngleSharp.Css.RenderTree;
55
using AngleSharp.Dom;
6+
using AngleSharp.Io;
7+
using NUnit.Framework;
8+
using System.Collections.Generic;
69
using System.Linq;
10+
using System.Threading.Tasks;
711

812
[TestFixture]
913
public class ElementsTests
@@ -19,5 +23,29 @@ public void SetAllStyles()
1923
Assert.AreEqual("rgba(255, 0, 0, 1)", divs.Skip(1).First().GetStyle().GetBackground());
2024
Assert.AreEqual("rgba(255, 0, 0, 1)", divs.Skip(2).First().GetStyle().GetBackground());
2125
}
26+
27+
[Test]
28+
public async Task DownloadResources()
29+
{
30+
var urls = new List<Url>();
31+
var loaderOptions = new LoaderOptions
32+
{
33+
IsResourceLoadingEnabled = true,
34+
Filter = (req) =>
35+
{
36+
urls.Add(req.Address);
37+
return true;
38+
},
39+
};
40+
var config = Configuration.Default
41+
.WithDefaultLoader(loaderOptions)
42+
.WithCss();
43+
var document = "<style>div { background: url('https://avatars1.githubusercontent.com/u/10828168?s=200&v=4'); }</style><div></div>".ToHtmlDocument(config);
44+
var tree = document.DefaultView.Render();
45+
var node = tree.Find(document.QuerySelector("div"));
46+
await node.DownloadResources();
47+
Assert.AreEqual(1, urls.Count);
48+
Assert.AreEqual("https://avatars1.githubusercontent.com/u/10828168?s=200&v=4", urls[0].Href);
49+
}
2250
}
2351
}

src/AngleSharp.Css/Extensions/CssValueExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,10 @@ public static Int32 AsRgba(this ICssValue value)
175175
}
176176

177177
/// <summary>
178-
/// Tries to convert the value to an RGBA integer.
178+
/// Tries to convert the value to a URL.
179179
/// </summary>
180180
/// <param name="value">The value to convert.</param>
181-
/// <returns>The resulting number.</returns>
181+
/// <returns>The resulting URL.</returns>
182182
public static String AsUrl(this ICssValue value)
183183
{
184184
if (value is CssUrlValue res)

src/AngleSharp.Css/Extensions/ElementExtensions.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public static String GetInnerText(this IElement element)
5353

5454
if (!String.IsNullOrEmpty(css?.GetDisplay()))
5555
{
56-
hidden = css.GetDisplay() == "none";
56+
hidden = css.GetDisplay() == CssKeywords.None;
5757
}
5858
}
5959

@@ -167,12 +167,12 @@ private static void ItcInCssBox(ICssStyleDeclaration elementStyle, ICssStyleDecl
167167
{
168168
if (!String.IsNullOrEmpty(elementStyle.GetDisplay()))
169169
{
170-
elementHidden = elementStyle.GetDisplay() == "none";
170+
elementHidden = elementStyle.GetDisplay() == CssKeywords.None;
171171
}
172172

173173
if (!String.IsNullOrEmpty(elementStyle.GetVisibility()) && elementHidden != true)
174174
{
175-
elementHidden = elementStyle.GetVisibility() != "visible";
175+
elementHidden = elementStyle.GetVisibility() != CssKeywords.Visible;
176176
}
177177
}
178178

@@ -202,25 +202,25 @@ private static void ItcInCssBox(ICssStyleDeclaration elementStyle, ICssStyleDecl
202202
{
203203
sb.Append(Symbols.LineFeed);
204204
}
205-
else if ((node is IHtmlTableCellElement && String.IsNullOrEmpty(elementStyle.GetDisplay())) || elementStyle.GetDisplay() == "table-cell")
205+
else if ((node is IHtmlTableCellElement && String.IsNullOrEmpty(elementStyle.GetDisplay())) || elementStyle.GetDisplay() == CssKeywords.TableCell)
206206
{
207207
if (node.NextSibling is IElement nextSibling)
208208
{
209209
var nextSiblingCss = nextSibling.ComputeCurrentStyle();
210210

211-
if (nextSibling is IHtmlTableCellElement && String.IsNullOrEmpty(nextSiblingCss.GetDisplay()) || nextSiblingCss.GetDisplay() == "table-cell")
211+
if (nextSibling is IHtmlTableCellElement && String.IsNullOrEmpty(nextSiblingCss.GetDisplay()) || nextSiblingCss.GetDisplay() == CssKeywords.TableCell)
212212
{
213213
sb.Append(Symbols.Tab);
214214
}
215215
}
216216
}
217-
else if ((node is IHtmlTableRowElement && String.IsNullOrEmpty(elementStyle.GetDisplay())) || elementStyle.GetDisplay() == "table-row")
217+
else if ((node is IHtmlTableRowElement && String.IsNullOrEmpty(elementStyle.GetDisplay())) || elementStyle.GetDisplay() == CssKeywords.TableRow)
218218
{
219219
if (node.NextSibling is IElement nextSibling)
220220
{
221221
var nextSiblingCss = nextSibling.ComputeCurrentStyle();
222222

223-
if (nextSibling is IHtmlTableRowElement && String.IsNullOrEmpty(nextSiblingCss.GetDisplay()) || nextSiblingCss.GetDisplay() == "table-row")
223+
if (nextSibling is IHtmlTableRowElement && String.IsNullOrEmpty(nextSiblingCss.GetDisplay()) || nextSiblingCss.GetDisplay() == CssKeywords.TableRow)
224224
{
225225
sb.Append(Symbols.LineFeed);
226226
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
namespace AngleSharp.Css.RenderTree
2+
{
3+
using AngleSharp.Css.Dom;
4+
using AngleSharp.Css.Values;
5+
using AngleSharp.Dom;
6+
using AngleSharp.Html.Dom;
7+
using AngleSharp.Io;
8+
using System;
9+
using System.Collections.Generic;
10+
using System.Linq;
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
14+
/// <summary>
15+
/// Extensions for the rendering nodes
16+
/// </summary>
17+
public static class RenderNodeExtensions
18+
{
19+
/// <summary>
20+
/// Downloads the referenced resources from the node if visible.
21+
///
22+
/// Included resources:
23+
///
24+
/// - Background images
25+
/// </summary>
26+
/// <param name="node">The node to use as a starting base.</param>
27+
/// <param name="cancellationToken">The cancellation token to use, if any.</param>
28+
public static Task DownloadResources(this IRenderNode node, CancellationToken cancellationToken = default)
29+
{
30+
var context = node.Ref.Owner?.Context ?? throw new InvalidOperationException("The node needs to be inside a browsing context.");
31+
var loader = context.GetService<IResourceLoader>() ?? throw new InvalidOperationException("A resource loader is required. Check your configuration.");
32+
var tasks = new List<Task>();
33+
34+
if (node.IsVisible() && node is ElementRenderNode element)
35+
{
36+
var elementRef = element.Ref as IElement;
37+
var style = element.ComputedStyle;
38+
var value = style.GetProperty(PropertyNames.BackgroundImage).RawValue;
39+
40+
if (value is CssListValue list)
41+
{
42+
var url = new Url(list.AsUrl());
43+
var request = new ResourceRequest(elementRef, url);
44+
var download = loader.FetchAsync(request);
45+
cancellationToken.Register(download.Cancel);
46+
tasks.Add(download.Task);
47+
}
48+
}
49+
50+
return Task.WhenAll(tasks);
51+
}
52+
53+
/// <summary>
54+
/// Checks if the provided render node is visible.
55+
/// </summary>
56+
/// <param name="node">The node to check for visibility.</param>
57+
/// <returns>True if its visible, otherwise false.</returns>
58+
public static Boolean IsVisible(this IRenderNode node)
59+
{
60+
var hasOwner = node.Ref.Owner != null;
61+
62+
if (hasOwner)
63+
{
64+
if (node is ElementRenderNode element)
65+
{
66+
var style = element.ComputedStyle;
67+
68+
if (element.Ref is IHtmlElement htmlElement && htmlElement.IsHidden)
69+
{
70+
return false;
71+
}
72+
else if (style.GetDisplay() == CssKeywords.None)
73+
{
74+
return false;
75+
}
76+
else if (style.GetVisibility() == CssKeywords.Hidden)
77+
{
78+
return false;
79+
}
80+
}
81+
82+
return true;
83+
}
84+
85+
return false;
86+
}
87+
88+
/// <summary>
89+
/// Finds a particular render node based on the given reference node.
90+
/// </summary>
91+
/// <param name="node">The render tree root.</param>
92+
/// <param name="reference">The reference node.</param>
93+
/// <returns>The related render tree node, if any.</returns>
94+
public static IRenderNode Find(this IRenderNode node, INode reference)
95+
{
96+
if (!Object.ReferenceEquals(node.Ref, reference))
97+
{
98+
return node.Children
99+
.Select(child => child.Find(reference))
100+
.Where(child => child != null)
101+
.FirstOrDefault();
102+
}
103+
104+
return node;
105+
}
106+
}
107+
}

src/AngleSharp.Css/Extensions/WindowExtensions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ namespace AngleSharp.Dom
33
using AngleSharp.Attributes;
44
using AngleSharp.Css;
55
using AngleSharp.Css.Dom;
6+
using AngleSharp.Css.RenderTree;
67
using System;
78
using System.Linq;
89

@@ -111,5 +112,20 @@ public static ICssStyleDeclaration ComputeRawStyle(this IWindow window, IElement
111112
// --> computed
112113
throw new NotImplementedException();
113114
}
115+
116+
/// <summary>
117+
/// Renders the currently available document into a render tree rooted at the returned render node.
118+
/// a render tree is essentially the combination of DOM nodes with their CSSOM computed style declarations.
119+
///
120+
/// In case no render device is supplied the context's default render device is chosen.
121+
/// </summary>
122+
/// <param name="window">The window to extend.</param>
123+
/// <param name="renderDevice">The device for rendering, if any. </param>
124+
/// <returns>The created render node.</returns>
125+
public static IRenderNode Render(this IWindow window, IRenderDevice renderDevice = null)
126+
{
127+
var builder = new RenderTreeBuilder(window, renderDevice);
128+
return builder.RenderDocument();
129+
}
114130
}
115131
}

src/AngleSharp.Css/RenderTree/IRenderNode.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,19 @@ namespace AngleSharp.Css.RenderTree
33
using AngleSharp.Dom;
44
using System.Collections.Generic;
55

6-
interface IRenderNode
6+
/// <summary>
7+
/// Represents a render node.
8+
/// </summary>
9+
public interface IRenderNode
710
{
11+
/// <summary>
12+
/// References the original DOM node.
13+
/// </summary>
814
INode Ref { get; }
915

16+
/// <summary>
17+
/// References the contained render children.
18+
/// </summary>
1019
IEnumerable<IRenderNode> Children { get; }
1120
}
1221
}

src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ class RenderTreeBuilder
1111
private readonly IEnumerable<ICssStyleSheet> _defaultSheets;
1212
private readonly IRenderDevice _device;
1313

14-
public RenderTreeBuilder(IWindow window)
14+
public RenderTreeBuilder(IWindow window, IRenderDevice device = null)
1515
{
1616
var ctx = window.Document.Context;
1717
var defaultStyleSheetProvider = ctx.GetServices<ICssDefaultStyleSheetProvider>();
18-
_device = ctx.GetService<IRenderDevice>();
18+
_device = device ?? ctx.GetService<IRenderDevice>();
1919
_defaultSheets = defaultStyleSheetProvider.Select(m => m.Default).Where(m => m != null);
2020
_window = window;
2121
}

0 commit comments

Comments
 (0)