Skip to content

Commit

Permalink
Improved render tree
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianRappl committed Jan 17, 2024
1 parent e97935a commit a5bcabf
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 29 deletions.
1 change: 1 addition & 0 deletions src/AngleSharp.Css.Tests/Extensions/Elements.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public async Task DownloadResources()
};
var config = Configuration.Default
.WithDefaultLoader(loaderOptions)
.WithRenderDevice()
.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();
Expand Down
2 changes: 2 additions & 0 deletions src/AngleSharp.Css/Dom/Internal/CssProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ public String Value

public Boolean HasValue => _value != null;

public PropertyFlags Flags => _flags;

public Boolean IsInherited => (((_flags & PropertyFlags.Inherited) == PropertyFlags.Inherited) && IsInitial) || (HasValue && _value.CssText.Is(CssKeywords.Inherit));

public Boolean CanBeInherited => (_flags & PropertyFlags.Inherited) == PropertyFlags.Inherited;
Expand Down
23 changes: 15 additions & 8 deletions src/AngleSharp.Css/RenderTree/ElementRenderNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,27 @@ namespace AngleSharp.Css.RenderTree
using AngleSharp.Css.Dom;
using AngleSharp.Dom;
using System.Collections.Generic;
using System.Linq;

class ElementRenderNode : IRenderNode
sealed class ElementRenderNode : IRenderNode
{
public INode Ref { get; set; }
public ElementRenderNode(IElement reference, IEnumerable<IRenderNode> children, ICssStyleDeclaration specifiedStyle, ICssStyleDeclaration computedStyle)
{
Ref = reference;
Children = children;
SpecifiedStyle = specifiedStyle;
ComputedStyle = computedStyle;
}

public IEnumerable<IRenderNode> Children { get; set; } = Enumerable.Empty<IRenderNode>();
public IElement Ref { get; }

public ICssStyleDeclaration SpecifiedStyle { get; set; }
INode IRenderNode.Ref => Ref;

public ICssStyleDeclaration ComputedStyle { get; set; }
public IEnumerable<IRenderNode> Children { get; }

public RenderValues UsedValue { get; set; }
public IRenderNode? Parent { get; set; }

public RenderValues ActualValue { get; set; }
public ICssStyleDeclaration SpecifiedStyle { get; }

public ICssStyleDeclaration ComputedStyle { get; }
}
}
168 changes: 149 additions & 19 deletions src/AngleSharp.Css/RenderTree/RenderTreeBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
namespace AngleSharp.Css.RenderTree
{
using AngleSharp.Css.Dom;
using AngleSharp.Css.Values;
using AngleSharp.Dom;
using System;
using System.Collections.Generic;
using System.Linq;

class RenderTreeBuilder
sealed class RenderTreeBuilder
{
private readonly IBrowsingContext _context;
private readonly IWindow _window;
private readonly IEnumerable<ICssStyleSheet> _defaultSheets;
private readonly IRenderDevice _device;
Expand All @@ -15,49 +18,176 @@ public RenderTreeBuilder(IWindow window, IRenderDevice device = null)
{
var ctx = window.Document.Context;
var defaultStyleSheetProvider = ctx.GetServices<ICssDefaultStyleSheetProvider>();
_device = device ?? ctx.GetService<IRenderDevice>();
_defaultSheets = defaultStyleSheetProvider.Select(m => m.Default).Where(m => m != null);
_context = ctx;
_device = device ?? ctx.GetService<IRenderDevice>() ?? throw new ArgumentNullException(nameof(device));
_defaultSheets = defaultStyleSheetProvider.Select(m => m.Default).Where(m => m is not null);
_window = window;
}

public IRenderNode RenderDocument()
{
var document = _window.Document;
var currentSheets = document.GetStyleSheets().OfType<ICssStyleSheet>();
var stylesheets = _defaultSheets.Concat(currentSheets);
var stylesheets = _defaultSheets.Concat(currentSheets).ToList();
var collection = new StyleCollection(stylesheets, _device);
return RenderElement(document.DocumentElement, collection);
var rootStyle = collection.ComputeCascadedStyle(document.DocumentElement);
var rootFontSize = ((Length?)rootStyle.GetProperty(PropertyNames.FontSize)?.RawValue)?.Value ?? 16;
return RenderElement(rootFontSize, document.DocumentElement, collection);
}

private ElementRenderNode RenderElement(IElement reference, StyleCollection collection, ICssStyleDeclaration parent = null)
private ElementRenderNode RenderElement(double rootFontSize, IElement reference, StyleCollection collection, ICssStyleDeclaration? parent = null)
{
var style = collection.ComputeCascadedStyle(reference, parent);
var style = collection.ComputeCascadedStyle(reference);
var computedStyle = Compute(rootFontSize, style, parent);
if (parent != null)
{
computedStyle.UpdateDeclarations(parent);
}
var children = new List<IRenderNode>();

foreach (var child in reference.ChildNodes)
{
if (child is IText text)
{
children.Add(RenderText(text, collection));
children.Add(RenderText(text));
}
else if (child is IElement element)
else if (child is IElement element)
{
children.Add(RenderElement(element, collection, style));
children.Add(RenderElement(rootFontSize, element, collection, computedStyle));
}
}

return new ElementRenderNode
// compute unitless line-height after rendering children
if (computedStyle.GetProperty(PropertyNames.LineHeight).RawValue is Length { Type: Length.Unit.None } unitlessLineHeight)
{
Ref = reference,
SpecifiedStyle = style,
ComputedStyle = style.Compute(_device),
Children = children,
};
var fontSize = computedStyle.GetProperty(PropertyNames.FontSize).RawValue is Length { Type: Length.Unit.Px } fontSizeLength ? fontSizeLength.Value : rootFontSize;
var pixelValue = unitlessLineHeight.Value * fontSize;
var computedLineHeight = new Length(pixelValue, Length.Unit.Px);

// create a new property because SetProperty would change the parent value
var lineHeightProperty = _context.CreateProperty(PropertyNames.LineHeight);
lineHeightProperty.RawValue = computedLineHeight;
computedStyle.SetDeclarations(new[] { lineHeightProperty });
}

var node = new ElementRenderNode(reference, children, style, computedStyle);

foreach (var child in children)
{
if (child is ElementRenderNode elementChild)
{
elementChild.Parent = node;
}
else if (child is TextRenderNode textChild)
{
textChild.Parent = node;
}
else
{
throw new InvalidOperationException();
}
}

return node;
}

private IRenderNode RenderText(IText text, StyleCollection collection) => new TextRenderNode
private IRenderNode RenderText(IText text) => new TextRenderNode(text);

private CssStyleDeclaration Compute(Double rootFontSize, ICssStyleDeclaration style, ICssStyleDeclaration? parentStyle)
{
Ref = text,
};
var computedStyle = new CssStyleDeclaration(_context);
var parentFontSize = ((Length?)parentStyle?.GetProperty(PropertyNames.FontSize)?.RawValue)?.ToPixel(_device) ?? rootFontSize;
var fontSize = parentFontSize;
// compute font-size first because other properties may depend on it
if (style.GetProperty(PropertyNames.FontSize) is { RawValue: not null } fontSizeProperty)
{
fontSize = GetFontSizeInPixels(fontSizeProperty.RawValue);
}
var declarations = style.OfType<CssProperty>().Select(property =>
{
var name = property.Name;
var value = property.RawValue;
if (name == PropertyNames.FontSize)
{
// font-size was already computed
value = new Length(fontSize, Length.Unit.Px);
}
else if (value is Length { IsAbsolute: true, Type: not Length.Unit.Px } absoluteLength)
{
value = new Length(absoluteLength.ToPixel(_device), Length.Unit.Px);
}
else if (value is Length { Type: Length.Unit.Percent } percentLength)
{
if (name == PropertyNames.VerticalAlign || name == PropertyNames.LineHeight)
{
var pixelValue = percentLength.Value / 100 * fontSize;
value = new Length(pixelValue, Length.Unit.Px);
}
else
{
// TODO: compute for other properties that should be absolute
}
}
else if (value is Length { IsRelative: true, Type: not Length.Unit.None } relativeLength)
{
var pixelValue = relativeLength.Type switch
{
Length.Unit.Em => relativeLength.Value * fontSize,
Length.Unit.Rem => relativeLength.Value * rootFontSize,
_ => relativeLength.ToPixel(_device),
};
value = new Length(pixelValue, Length.Unit.Px);
}

return new CssProperty(name, property.Converter, property.Flags, value, property.IsImportant);
});

computedStyle.SetDeclarations(declarations);

return computedStyle;

Double GetFontSizeInPixels(ICssValue value) => value switch
{
Constant<Length> constLength when constLength.CssText == CssKeywords.XxSmall => 9D / 16 * rootFontSize,
Constant<Length> constLength when constLength.CssText == CssKeywords.XSmall => 10D / 16 * rootFontSize,
Constant<Length> constLength when constLength.CssText == CssKeywords.Small => 13D / 16 * rootFontSize,
Constant<Length> constLength when constLength.CssText == CssKeywords.Medium => 16D / 16 * rootFontSize,
Constant<Length> constLength when constLength.CssText == CssKeywords.Large => 18D / 16 * rootFontSize,
Constant<Length> constLength when constLength.CssText == CssKeywords.XLarge => 24D / 16 * rootFontSize,
Constant<Length> constLength when constLength.CssText == CssKeywords.XxLarge => 32D / 16 * rootFontSize,
Constant<Length> constLength when constLength.CssText == CssKeywords.XxxLarge => 48D / 16 * rootFontSize,
Constant<Length> constLength when constLength.CssText == CssKeywords.Smaller => ComputeRelativeFontSize(constLength),
Constant<Length> constLength when constLength.CssText == CssKeywords.Larger => ComputeRelativeFontSize(constLength),
Length { Type: Length.Unit.Px } length => length.Value,
Length { IsAbsolute: true } length => length.ToPixel(_device),
Length { Type: Length.Unit.Vh or Length.Unit.Vw or Length.Unit.Vmax or Length.Unit.Vmin } length => length.ToPixel(_device),
Length { IsRelative: true } length => ComputeRelativeFontSize(length),
ICssSpecialValue specialValue when specialValue.CssText == CssKeywords.Inherit || specialValue.CssText == CssKeywords.Unset => parentFontSize,
ICssSpecialValue specialValue when specialValue.CssText == CssKeywords.Initial => rootFontSize,
_ => throw new InvalidOperationException("Font size must be a length"),
};

Double ComputeRelativeFontSize(ICssValue value)
{
var ancestorValue = parentStyle?.GetProperty(PropertyNames.FontSize)?.RawValue;
var ancestorPixels = ancestorValue switch
{
Length { IsAbsolute: true } ancestorLength => ancestorLength.ToPixel(_device),
null => rootFontSize,
_ => throw new InvalidOperationException(),
};

// set a minimum size of 9px for relative sizes
return Math.Max(9, value switch
{
Constant<Length> constLength when constLength.CssText == CssKeywords.Smaller => ancestorPixels / 1.2,
Constant<Length> constLength when constLength.CssText == CssKeywords.Larger => ancestorPixels * 1.2,
Length { Type: Length.Unit.Rem } length => length.Value * rootFontSize,
Length { Type: Length.Unit.Em } length => length.Value * ancestorPixels,
Length { Type: Length.Unit.Percent } length => length.Value / 100 * ancestorPixels,
_ => throw new InvalidOperationException(),
});
}
}
}
}
11 changes: 9 additions & 2 deletions src/AngleSharp.Css/RenderTree/TextRenderNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ namespace AngleSharp.Css.RenderTree
using System.Collections.Generic;
using System.Linq;

class TextRenderNode : IRenderNode
sealed class TextRenderNode : IRenderNode
{
public INode Ref { get; set; }
public TextRenderNode(INode reference)
{
Ref = reference;
}

public INode Ref { get; }

public IEnumerable<IRenderNode> Children => Enumerable.Empty<IRenderNode>();

public IRenderNode? Parent { get; set; }
}
}
8 changes: 8 additions & 0 deletions src/AngleSharp.Css/Values/Primitives/Length.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,14 @@ public static Unit GetUnit(String s)
}
}

/// <summary>
/// Converts the length to a number of pixels, if possible. If the
/// current unit is relative, then an exception will be thrown.
/// </summary>
/// <param name="renderDimensions">the render device used to calculate relative units, can be null if units are absolute.</param>
/// <returns>The number of pixels represented by the current length.</returns>
public Double ToPixel(IRenderDimensions renderDimensions) => ToPixel(renderDimensions, RenderMode.Horizontal);

/// <summary>
/// Converts the length to a number of pixels, if possible. If the
/// current unit is relative, then an exception will be thrown.
Expand Down

0 comments on commit a5bcabf

Please sign in to comment.