11namespace 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}
0 commit comments