Skip to content

Commit a5bcabf

Browse files
committed
Improved render tree
1 parent e97935a commit a5bcabf

File tree

6 files changed

+184
-29
lines changed

6 files changed

+184
-29
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public async Task DownloadResources()
3939
};
4040
var config = Configuration.Default
4141
.WithDefaultLoader(loaderOptions)
42+
.WithRenderDevice()
4243
.WithCss();
4344
var document = "<style>div { background: url('https://avatars1.githubusercontent.com/u/10828168?s=200&v=4'); }</style><div></div>".ToHtmlDocument(config);
4445
var tree = document.DefaultView.Render();

src/AngleSharp.Css/Dom/Internal/CssProperty.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ public String Value
5252

5353
public Boolean HasValue => _value != null;
5454

55+
public PropertyFlags Flags => _flags;
56+
5557
public Boolean IsInherited => (((_flags & PropertyFlags.Inherited) == PropertyFlags.Inherited) && IsInitial) || (HasValue && _value.CssText.Is(CssKeywords.Inherit));
5658

5759
public Boolean CanBeInherited => (_flags & PropertyFlags.Inherited) == PropertyFlags.Inherited;

src/AngleSharp.Css/RenderTree/ElementRenderNode.cs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,27 @@ namespace AngleSharp.Css.RenderTree
33
using AngleSharp.Css.Dom;
44
using AngleSharp.Dom;
55
using System.Collections.Generic;
6-
using System.Linq;
76

8-
class ElementRenderNode : IRenderNode
7+
sealed class ElementRenderNode : IRenderNode
98
{
10-
public INode Ref { get; set; }
9+
public ElementRenderNode(IElement reference, IEnumerable<IRenderNode> children, ICssStyleDeclaration specifiedStyle, ICssStyleDeclaration computedStyle)
10+
{
11+
Ref = reference;
12+
Children = children;
13+
SpecifiedStyle = specifiedStyle;
14+
ComputedStyle = computedStyle;
15+
}
1116

12-
public IEnumerable<IRenderNode> Children { get; set; } = Enumerable.Empty<IRenderNode>();
17+
public IElement Ref { get; }
1318

14-
public ICssStyleDeclaration SpecifiedStyle { get; set; }
19+
INode IRenderNode.Ref => Ref;
1520

16-
public ICssStyleDeclaration ComputedStyle { get; set; }
21+
public IEnumerable<IRenderNode> Children { get; }
1722

18-
public RenderValues UsedValue { get; set; }
23+
public IRenderNode? Parent { get; set; }
1924

20-
public RenderValues ActualValue { get; set; }
25+
public ICssStyleDeclaration SpecifiedStyle { get; }
26+
27+
public ICssStyleDeclaration ComputedStyle { get; }
2128
}
2229
}
Lines changed: 149 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
namespace AngleSharp.Css.RenderTree
22
{
33
using AngleSharp.Css.Dom;
4+
using AngleSharp.Css.Values;
45
using AngleSharp.Dom;
6+
using System;
57
using System.Collections.Generic;
68
using System.Linq;
79

8-
class RenderTreeBuilder
10+
sealed class RenderTreeBuilder
911
{
12+
private readonly IBrowsingContext _context;
1013
private readonly IWindow _window;
1114
private readonly IEnumerable<ICssStyleSheet> _defaultSheets;
1215
private readonly IRenderDevice _device;
@@ -15,49 +18,176 @@ public RenderTreeBuilder(IWindow window, IRenderDevice device = null)
1518
{
1619
var ctx = window.Document.Context;
1720
var defaultStyleSheetProvider = ctx.GetServices<ICssDefaultStyleSheetProvider>();
18-
_device = device ?? ctx.GetService<IRenderDevice>();
19-
_defaultSheets = defaultStyleSheetProvider.Select(m => m.Default).Where(m => m != null);
21+
_context = ctx;
22+
_device = device ?? ctx.GetService<IRenderDevice>() ?? throw new ArgumentNullException(nameof(device));
23+
_defaultSheets = defaultStyleSheetProvider.Select(m => m.Default).Where(m => m is not null);
2024
_window = window;
2125
}
2226

2327
public IRenderNode RenderDocument()
2428
{
2529
var document = _window.Document;
2630
var currentSheets = document.GetStyleSheets().OfType<ICssStyleSheet>();
27-
var stylesheets = _defaultSheets.Concat(currentSheets);
31+
var stylesheets = _defaultSheets.Concat(currentSheets).ToList();
2832
var collection = new StyleCollection(stylesheets, _device);
29-
return RenderElement(document.DocumentElement, collection);
33+
var rootStyle = collection.ComputeCascadedStyle(document.DocumentElement);
34+
var rootFontSize = ((Length?)rootStyle.GetProperty(PropertyNames.FontSize)?.RawValue)?.Value ?? 16;
35+
return RenderElement(rootFontSize, document.DocumentElement, collection);
3036
}
3137

32-
private ElementRenderNode RenderElement(IElement reference, StyleCollection collection, ICssStyleDeclaration parent = null)
38+
private ElementRenderNode RenderElement(double rootFontSize, IElement reference, StyleCollection collection, ICssStyleDeclaration? parent = null)
3339
{
34-
var style = collection.ComputeCascadedStyle(reference, parent);
40+
var style = collection.ComputeCascadedStyle(reference);
41+
var computedStyle = Compute(rootFontSize, style, parent);
42+
if (parent != null)
43+
{
44+
computedStyle.UpdateDeclarations(parent);
45+
}
3546
var children = new List<IRenderNode>();
3647

3748
foreach (var child in reference.ChildNodes)
3849
{
3950
if (child is IText text)
4051
{
41-
children.Add(RenderText(text, collection));
52+
children.Add(RenderText(text));
4253
}
43-
else if (child is IElement element)
54+
else if (child is IElement element)
4455
{
45-
children.Add(RenderElement(element, collection, style));
56+
children.Add(RenderElement(rootFontSize, element, collection, computedStyle));
4657
}
4758
}
4859

49-
return new ElementRenderNode
60+
// compute unitless line-height after rendering children
61+
if (computedStyle.GetProperty(PropertyNames.LineHeight).RawValue is Length { Type: Length.Unit.None } unitlessLineHeight)
5062
{
51-
Ref = reference,
52-
SpecifiedStyle = style,
53-
ComputedStyle = style.Compute(_device),
54-
Children = children,
55-
};
63+
var fontSize = computedStyle.GetProperty(PropertyNames.FontSize).RawValue is Length { Type: Length.Unit.Px } fontSizeLength ? fontSizeLength.Value : rootFontSize;
64+
var pixelValue = unitlessLineHeight.Value * fontSize;
65+
var computedLineHeight = new Length(pixelValue, Length.Unit.Px);
66+
67+
// create a new property because SetProperty would change the parent value
68+
var lineHeightProperty = _context.CreateProperty(PropertyNames.LineHeight);
69+
lineHeightProperty.RawValue = computedLineHeight;
70+
computedStyle.SetDeclarations(new[] { lineHeightProperty });
71+
}
72+
73+
var node = new ElementRenderNode(reference, children, style, computedStyle);
74+
75+
foreach (var child in children)
76+
{
77+
if (child is ElementRenderNode elementChild)
78+
{
79+
elementChild.Parent = node;
80+
}
81+
else if (child is TextRenderNode textChild)
82+
{
83+
textChild.Parent = node;
84+
}
85+
else
86+
{
87+
throw new InvalidOperationException();
88+
}
89+
}
90+
91+
return node;
5692
}
5793

58-
private IRenderNode RenderText(IText text, StyleCollection collection) => new TextRenderNode
94+
private IRenderNode RenderText(IText text) => new TextRenderNode(text);
95+
96+
private CssStyleDeclaration Compute(Double rootFontSize, ICssStyleDeclaration style, ICssStyleDeclaration? parentStyle)
5997
{
60-
Ref = text,
61-
};
98+
var computedStyle = new CssStyleDeclaration(_context);
99+
var parentFontSize = ((Length?)parentStyle?.GetProperty(PropertyNames.FontSize)?.RawValue)?.ToPixel(_device) ?? rootFontSize;
100+
var fontSize = parentFontSize;
101+
// compute font-size first because other properties may depend on it
102+
if (style.GetProperty(PropertyNames.FontSize) is { RawValue: not null } fontSizeProperty)
103+
{
104+
fontSize = GetFontSizeInPixels(fontSizeProperty.RawValue);
105+
}
106+
var declarations = style.OfType<CssProperty>().Select(property =>
107+
{
108+
var name = property.Name;
109+
var value = property.RawValue;
110+
if (name == PropertyNames.FontSize)
111+
{
112+
// font-size was already computed
113+
value = new Length(fontSize, Length.Unit.Px);
114+
}
115+
else if (value is Length { IsAbsolute: true, Type: not Length.Unit.Px } absoluteLength)
116+
{
117+
value = new Length(absoluteLength.ToPixel(_device), Length.Unit.Px);
118+
}
119+
else if (value is Length { Type: Length.Unit.Percent } percentLength)
120+
{
121+
if (name == PropertyNames.VerticalAlign || name == PropertyNames.LineHeight)
122+
{
123+
var pixelValue = percentLength.Value / 100 * fontSize;
124+
value = new Length(pixelValue, Length.Unit.Px);
125+
}
126+
else
127+
{
128+
// TODO: compute for other properties that should be absolute
129+
}
130+
}
131+
else if (value is Length { IsRelative: true, Type: not Length.Unit.None } relativeLength)
132+
{
133+
var pixelValue = relativeLength.Type switch
134+
{
135+
Length.Unit.Em => relativeLength.Value * fontSize,
136+
Length.Unit.Rem => relativeLength.Value * rootFontSize,
137+
_ => relativeLength.ToPixel(_device),
138+
};
139+
value = new Length(pixelValue, Length.Unit.Px);
140+
}
141+
142+
return new CssProperty(name, property.Converter, property.Flags, value, property.IsImportant);
143+
});
144+
145+
computedStyle.SetDeclarations(declarations);
146+
147+
return computedStyle;
148+
149+
Double GetFontSizeInPixels(ICssValue value) => value switch
150+
{
151+
Constant<Length> constLength when constLength.CssText == CssKeywords.XxSmall => 9D / 16 * rootFontSize,
152+
Constant<Length> constLength when constLength.CssText == CssKeywords.XSmall => 10D / 16 * rootFontSize,
153+
Constant<Length> constLength when constLength.CssText == CssKeywords.Small => 13D / 16 * rootFontSize,
154+
Constant<Length> constLength when constLength.CssText == CssKeywords.Medium => 16D / 16 * rootFontSize,
155+
Constant<Length> constLength when constLength.CssText == CssKeywords.Large => 18D / 16 * rootFontSize,
156+
Constant<Length> constLength when constLength.CssText == CssKeywords.XLarge => 24D / 16 * rootFontSize,
157+
Constant<Length> constLength when constLength.CssText == CssKeywords.XxLarge => 32D / 16 * rootFontSize,
158+
Constant<Length> constLength when constLength.CssText == CssKeywords.XxxLarge => 48D / 16 * rootFontSize,
159+
Constant<Length> constLength when constLength.CssText == CssKeywords.Smaller => ComputeRelativeFontSize(constLength),
160+
Constant<Length> constLength when constLength.CssText == CssKeywords.Larger => ComputeRelativeFontSize(constLength),
161+
Length { Type: Length.Unit.Px } length => length.Value,
162+
Length { IsAbsolute: true } length => length.ToPixel(_device),
163+
Length { Type: Length.Unit.Vh or Length.Unit.Vw or Length.Unit.Vmax or Length.Unit.Vmin } length => length.ToPixel(_device),
164+
Length { IsRelative: true } length => ComputeRelativeFontSize(length),
165+
ICssSpecialValue specialValue when specialValue.CssText == CssKeywords.Inherit || specialValue.CssText == CssKeywords.Unset => parentFontSize,
166+
ICssSpecialValue specialValue when specialValue.CssText == CssKeywords.Initial => rootFontSize,
167+
_ => throw new InvalidOperationException("Font size must be a length"),
168+
};
169+
170+
Double ComputeRelativeFontSize(ICssValue value)
171+
{
172+
var ancestorValue = parentStyle?.GetProperty(PropertyNames.FontSize)?.RawValue;
173+
var ancestorPixels = ancestorValue switch
174+
{
175+
Length { IsAbsolute: true } ancestorLength => ancestorLength.ToPixel(_device),
176+
null => rootFontSize,
177+
_ => throw new InvalidOperationException(),
178+
};
179+
180+
// set a minimum size of 9px for relative sizes
181+
return Math.Max(9, value switch
182+
{
183+
Constant<Length> constLength when constLength.CssText == CssKeywords.Smaller => ancestorPixels / 1.2,
184+
Constant<Length> constLength when constLength.CssText == CssKeywords.Larger => ancestorPixels * 1.2,
185+
Length { Type: Length.Unit.Rem } length => length.Value * rootFontSize,
186+
Length { Type: Length.Unit.Em } length => length.Value * ancestorPixels,
187+
Length { Type: Length.Unit.Percent } length => length.Value / 100 * ancestorPixels,
188+
_ => throw new InvalidOperationException(),
189+
});
190+
}
191+
}
62192
}
63193
}

src/AngleSharp.Css/RenderTree/TextRenderNode.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@ namespace AngleSharp.Css.RenderTree
44
using System.Collections.Generic;
55
using System.Linq;
66

7-
class TextRenderNode : IRenderNode
7+
sealed class TextRenderNode : IRenderNode
88
{
9-
public INode Ref { get; set; }
9+
public TextRenderNode(INode reference)
10+
{
11+
Ref = reference;
12+
}
13+
14+
public INode Ref { get; }
1015

1116
public IEnumerable<IRenderNode> Children => Enumerable.Empty<IRenderNode>();
17+
18+
public IRenderNode? Parent { get; set; }
1219
}
1320
}

src/AngleSharp.Css/Values/Primitives/Length.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,14 @@ public static Unit GetUnit(String s)
267267
}
268268
}
269269

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

0 commit comments

Comments
 (0)