1
1
namespace AngleSharp . Css . RenderTree
2
2
{
3
3
using AngleSharp . Css . Dom ;
4
+ using AngleSharp . Css . Values ;
4
5
using AngleSharp . Dom ;
6
+ using System ;
5
7
using System . Collections . Generic ;
6
8
using System . Linq ;
7
9
8
- class RenderTreeBuilder
10
+ sealed class RenderTreeBuilder
9
11
{
12
+ private readonly IBrowsingContext _context ;
10
13
private readonly IWindow _window ;
11
14
private readonly IEnumerable < ICssStyleSheet > _defaultSheets ;
12
15
private readonly IRenderDevice _device ;
@@ -15,49 +18,176 @@ public RenderTreeBuilder(IWindow window, IRenderDevice device = null)
15
18
{
16
19
var ctx = window . Document . Context ;
17
20
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 ) ;
20
24
_window = window ;
21
25
}
22
26
23
27
public IRenderNode RenderDocument ( )
24
28
{
25
29
var document = _window . Document ;
26
30
var currentSheets = document . GetStyleSheets ( ) . OfType < ICssStyleSheet > ( ) ;
27
- var stylesheets = _defaultSheets . Concat ( currentSheets ) ;
31
+ var stylesheets = _defaultSheets . Concat ( currentSheets ) . ToList ( ) ;
28
32
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 ) ;
30
36
}
31
37
32
- private ElementRenderNode RenderElement ( IElement reference , StyleCollection collection , ICssStyleDeclaration parent = null )
38
+ private ElementRenderNode RenderElement ( double rootFontSize , IElement reference , StyleCollection collection , ICssStyleDeclaration ? parent = null )
33
39
{
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
+ }
35
46
var children = new List < IRenderNode > ( ) ;
36
47
37
48
foreach ( var child in reference . ChildNodes )
38
49
{
39
50
if ( child is IText text )
40
51
{
41
- children . Add ( RenderText ( text , collection ) ) ;
52
+ children . Add ( RenderText ( text ) ) ;
42
53
}
43
- else if ( child is IElement element )
54
+ else if ( child is IElement element )
44
55
{
45
- children . Add ( RenderElement ( element , collection , style ) ) ;
56
+ children . Add ( RenderElement ( rootFontSize , element , collection , computedStyle ) ) ;
46
57
}
47
58
}
48
59
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 )
50
62
{
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 ;
56
92
}
57
93
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 )
59
97
{
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
+ }
62
192
}
63
193
}
0 commit comments