Skip to content

Commit

Permalink
Added render tree extensions for downloading content
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianRappl committed Apr 6, 2020
1 parent d20ea49 commit 6b7015e
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 14 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# 0.14.0

Released on Tuesday, March 31 2020.
Released on Tuesday, April 7 2020.

- Added a way to compute relative dimensions (#3)
- Added render tree information incl. utilities (#4)
- Fixed issue with empty content (#42)
- Added debugger display attribute to CSS rules (#43)
- Fixed handling of CSS gradients (#45)
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ AngleSharp.Css contains code written by (in order of first pull request / commit
* [Florian Rappl](https://github.com/FlorianRappl)
* [Michał Kostrzewski](https://github.com/zeaposs)
* [Jochen Kühner](https://github.com/jogibear9988)
* [Tom Hazell](https://github.com/The-Nutty)

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

Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ var config = Configuration.Default

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

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.,

```cs
var tree = document.DefaultView.Render();
var node = tree.Find(document.QuerySelector("div"));
await node.DownloadResources();
```

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.

## Advantages of AngleSharp.Css

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:
Expand Down
30 changes: 29 additions & 1 deletion src/AngleSharp.Css.Tests/Extensions/Elements.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
namespace AngleSharp.Css.Tests.Extensions
{
using NUnit.Framework;
using AngleSharp.Css.Dom;
using AngleSharp.Css.RenderTree;
using AngleSharp.Dom;
using AngleSharp.Io;
using NUnit.Framework;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

[TestFixture]
public class ElementsTests
Expand All @@ -19,5 +23,29 @@ public void SetAllStyles()
Assert.AreEqual("rgba(255, 0, 0, 1)", divs.Skip(1).First().GetStyle().GetBackground());
Assert.AreEqual("rgba(255, 0, 0, 1)", divs.Skip(2).First().GetStyle().GetBackground());
}

[Test]
public async Task DownloadResources()
{
var urls = new List<Url>();
var loaderOptions = new LoaderOptions
{
IsResourceLoadingEnabled = true,
Filter = (req) =>
{
urls.Add(req.Address);
return true;
},
};
var config = Configuration.Default
.WithDefaultLoader(loaderOptions)
.WithCss();
var document = "<style>div { background: url('https://avatars1.githubusercontent.com/u/10828168?s=200&v=4'); }</style><div></div>".ToHtmlDocument(config);
var tree = document.DefaultView.Render();
var node = tree.Find(document.QuerySelector("div"));
await node.DownloadResources();
Assert.AreEqual(1, urls.Count);
Assert.AreEqual("https://avatars1.githubusercontent.com/u/10828168?s=200&v=4", urls[0].Href);
}
}
}
4 changes: 2 additions & 2 deletions src/AngleSharp.Css/Extensions/CssValueExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,10 @@ public static Int32 AsRgba(this ICssValue value)
}

/// <summary>
/// Tries to convert the value to an RGBA integer.
/// Tries to convert the value to a URL.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <returns>The resulting number.</returns>
/// <returns>The resulting URL.</returns>
public static String AsUrl(this ICssValue value)
{
if (value is CssUrlValue res)
Expand Down
14 changes: 7 additions & 7 deletions src/AngleSharp.Css/Extensions/ElementExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public static String GetInnerText(this IElement element)

if (!String.IsNullOrEmpty(css?.GetDisplay()))
{
hidden = css.GetDisplay() == "none";
hidden = css.GetDisplay() == CssKeywords.None;
}
}

Expand Down Expand Up @@ -167,12 +167,12 @@ private static void ItcInCssBox(ICssStyleDeclaration elementStyle, ICssStyleDecl
{
if (!String.IsNullOrEmpty(elementStyle.GetDisplay()))
{
elementHidden = elementStyle.GetDisplay() == "none";
elementHidden = elementStyle.GetDisplay() == CssKeywords.None;
}

if (!String.IsNullOrEmpty(elementStyle.GetVisibility()) && elementHidden != true)
{
elementHidden = elementStyle.GetVisibility() != "visible";
elementHidden = elementStyle.GetVisibility() != CssKeywords.Visible;
}
}

Expand Down Expand Up @@ -202,25 +202,25 @@ private static void ItcInCssBox(ICssStyleDeclaration elementStyle, ICssStyleDecl
{
sb.Append(Symbols.LineFeed);
}
else if ((node is IHtmlTableCellElement && String.IsNullOrEmpty(elementStyle.GetDisplay())) || elementStyle.GetDisplay() == "table-cell")
else if ((node is IHtmlTableCellElement && String.IsNullOrEmpty(elementStyle.GetDisplay())) || elementStyle.GetDisplay() == CssKeywords.TableCell)
{
if (node.NextSibling is IElement nextSibling)
{
var nextSiblingCss = nextSibling.ComputeCurrentStyle();

if (nextSibling is IHtmlTableCellElement && String.IsNullOrEmpty(nextSiblingCss.GetDisplay()) || nextSiblingCss.GetDisplay() == "table-cell")
if (nextSibling is IHtmlTableCellElement && String.IsNullOrEmpty(nextSiblingCss.GetDisplay()) || nextSiblingCss.GetDisplay() == CssKeywords.TableCell)
{
sb.Append(Symbols.Tab);
}
}
}
else if ((node is IHtmlTableRowElement && String.IsNullOrEmpty(elementStyle.GetDisplay())) || elementStyle.GetDisplay() == "table-row")
else if ((node is IHtmlTableRowElement && String.IsNullOrEmpty(elementStyle.GetDisplay())) || elementStyle.GetDisplay() == CssKeywords.TableRow)
{
if (node.NextSibling is IElement nextSibling)
{
var nextSiblingCss = nextSibling.ComputeCurrentStyle();

if (nextSibling is IHtmlTableRowElement && String.IsNullOrEmpty(nextSiblingCss.GetDisplay()) || nextSiblingCss.GetDisplay() == "table-row")
if (nextSibling is IHtmlTableRowElement && String.IsNullOrEmpty(nextSiblingCss.GetDisplay()) || nextSiblingCss.GetDisplay() == CssKeywords.TableRow)
{
sb.Append(Symbols.LineFeed);
}
Expand Down
107 changes: 107 additions & 0 deletions src/AngleSharp.Css/Extensions/RenderNodeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
namespace AngleSharp.Css.RenderTree
{
using AngleSharp.Css.Dom;
using AngleSharp.Css.Values;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Io;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// Extensions for the rendering nodes
/// </summary>
public static class RenderNodeExtensions
{
/// <summary>
/// Downloads the referenced resources from the node if visible.
///
/// Included resources:
///
/// - Background images
/// </summary>
/// <param name="node">The node to use as a starting base.</param>
/// <param name="cancellationToken">The cancellation token to use, if any.</param>
public static Task DownloadResources(this IRenderNode node, CancellationToken cancellationToken = default)
{
var context = node.Ref.Owner?.Context ?? throw new InvalidOperationException("The node needs to be inside a browsing context.");
var loader = context.GetService<IResourceLoader>() ?? throw new InvalidOperationException("A resource loader is required. Check your configuration.");
var tasks = new List<Task>();

if (node.IsVisible() && node is ElementRenderNode element)
{
var elementRef = element.Ref as IElement;
var style = element.ComputedStyle;
var value = style.GetProperty(PropertyNames.BackgroundImage).RawValue;

if (value is CssListValue list)
{
var url = new Url(list.AsUrl());
var request = new ResourceRequest(elementRef, url);
var download = loader.FetchAsync(request);
cancellationToken.Register(download.Cancel);
tasks.Add(download.Task);
}
}

return Task.WhenAll(tasks);
}

/// <summary>
/// Checks if the provided render node is visible.
/// </summary>
/// <param name="node">The node to check for visibility.</param>
/// <returns>True if its visible, otherwise false.</returns>
public static Boolean IsVisible(this IRenderNode node)
{
var hasOwner = node.Ref.Owner != null;

if (hasOwner)
{
if (node is ElementRenderNode element)
{
var style = element.ComputedStyle;

if (element.Ref is IHtmlElement htmlElement && htmlElement.IsHidden)
{
return false;
}
else if (style.GetDisplay() == CssKeywords.None)
{
return false;
}
else if (style.GetVisibility() == CssKeywords.Hidden)
{
return false;
}
}

return true;
}

return false;
}

/// <summary>
/// Finds a particular render node based on the given reference node.
/// </summary>
/// <param name="node">The render tree root.</param>
/// <param name="reference">The reference node.</param>
/// <returns>The related render tree node, if any.</returns>
public static IRenderNode Find(this IRenderNode node, INode reference)
{
if (!Object.ReferenceEquals(node.Ref, reference))
{
return node.Children
.Select(child => child.Find(reference))
.Where(child => child != null)
.FirstOrDefault();
}

return node;
}
}
}
16 changes: 16 additions & 0 deletions src/AngleSharp.Css/Extensions/WindowExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ namespace AngleSharp.Dom
using AngleSharp.Attributes;
using AngleSharp.Css;
using AngleSharp.Css.Dom;
using AngleSharp.Css.RenderTree;
using System;
using System.Linq;

Expand Down Expand Up @@ -111,5 +112,20 @@ public static ICssStyleDeclaration ComputeRawStyle(this IWindow window, IElement
// --> computed
throw new NotImplementedException();
}

/// <summary>
/// Renders the currently available document into a render tree rooted at the returned render node.
/// a render tree is essentially the combination of DOM nodes with their CSSOM computed style declarations.
///
/// In case no render device is supplied the context's default render device is chosen.
/// </summary>
/// <param name="window">The window to extend.</param>
/// <param name="renderDevice">The device for rendering, if any. </param>
/// <returns>The created render node.</returns>
public static IRenderNode Render(this IWindow window, IRenderDevice renderDevice = null)
{
var builder = new RenderTreeBuilder(window, renderDevice);
return builder.RenderDocument();
}
}
}
11 changes: 10 additions & 1 deletion src/AngleSharp.Css/RenderTree/IRenderNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,19 @@ namespace AngleSharp.Css.RenderTree
using AngleSharp.Dom;
using System.Collections.Generic;

interface IRenderNode
/// <summary>
/// Represents a render node.
/// </summary>
public interface IRenderNode
{
/// <summary>
/// References the original DOM node.
/// </summary>
INode Ref { get; }

/// <summary>
/// References the contained render children.
/// </summary>
IEnumerable<IRenderNode> Children { get; }
}
}
4 changes: 2 additions & 2 deletions src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ class RenderTreeBuilder
private readonly IEnumerable<ICssStyleSheet> _defaultSheets;
private readonly IRenderDevice _device;

public RenderTreeBuilder(IWindow window)
public RenderTreeBuilder(IWindow window, IRenderDevice device = null)
{
var ctx = window.Document.Context;
var defaultStyleSheetProvider = ctx.GetServices<ICssDefaultStyleSheetProvider>();
_device = ctx.GetService<IRenderDevice>();
_device = device ?? ctx.GetService<IRenderDevice>();
_defaultSheets = defaultStyleSheetProvider.Select(m => m.Default).Where(m => m != null);
_window = window;
}
Expand Down

0 comments on commit 6b7015e

Please sign in to comment.