@@ -21,74 +21,101 @@ namespace TheArtOfDev.HtmlRenderer.Core.Dom
2121 /// Resolves CSS 2.1 §12.4 named counters (<c>counter-reset</c>/<c>counter-increment</c>) into a
2222 /// per-box snapshot of counter values, consulted by <c>content: counter()</c>
2323 /// (<see cref="CssContentEngine"/>).<br/>
24- /// Deliberately a subset of the full spec algorithm: counters thread forward through document order
25- /// (a box inherits its parent's counters for its first child, and each child's resulting counters
26- /// feed the next sibling) which covers ordinary sequential/nested counter usage, but
27- /// <c>reversed(<name>)</c> and <c>counter-set</c> are not implemented, and a nested
28- /// <c>counter-reset</c> for a name already in scope always starts a fresh inner value rather than
29- /// the spec's more precise "new scope only among this element's own following siblings" rule -
30- /// in practice indistinguishable from the full rule for the common case of one counter-reset per
31- /// counter name in a document, which is what this port's test coverage exercises.
24+ /// <c>reversed(<name>)</c> and <c>counter-set</c> are not implemented.
3225 /// </summary>
3326 internal static class CssCounterEngine
3427 {
3528 /// <summary>
36- /// Walks <paramref name="box "/> and its descendants in document order, applying each box's own
29+ /// Walks <paramref name="root "/> and its descendants in document order, applying each box's own
3730 /// <c>counter-reset</c>/<c>counter-increment</c> (plus the implicit <c>list-item</c> counter a
38- /// <c>display:list-item</c> box always increments) on top of <paramref name="inherited"/>, and
39- /// storing the result on <see cref="CssBoxProperties.Counters"/>. Must run once, before layout,
40- /// after every box's CSS properties are resolved (<c>counter-reset</c>/<c>counter-increment</c>
41- /// need their final cascaded values).
31+ /// <c>display:list-item</c> box always increments) and storing the values in scope on
32+ /// <see cref="CssBoxProperties.Counters"/>. Must run once, before layout, after every box's CSS
33+ /// properties are resolved.
4234 /// </summary>
43- /// <returns>
44- /// The counters visible immediately after <paramref name="box"/> and its whole subtree - what a
45- /// following sibling should inherit.
46- /// </returns>
47- public static Dictionary < string , int > ResolveCounters ( CssBox box , Dictionary < string , int > inherited )
35+ public static void ResolveCounters ( CssBox root )
4836 {
49- var counters = new Dictionary < string , int > ( inherited ) ;
50-
51- ApplyCounterReset ( box , counters ) ;
52- ApplyCounterIncrement ( box , counters ) ;
37+ ResolveSiblings ( new [ ] { root } , new Dictionary < string , List < int > > ( ) ) ;
38+ }
5339
54- box . Counters = new Dictionary < string , int > ( counters ) ;
40+ /// <summary>
41+ /// Each counter name maps to a stack of instances. A reset creates a new instance that stays in
42+ /// scope for the box's following siblings and all their descendants, and goes out of scope again
43+ /// once the parent ends (§12.4.1). Boxes with <c>display:none</c> don't touch counters (§12.4.3).
44+ /// </summary>
45+ private static void ResolveSiblings ( IEnumerable < CssBox > boxes , Dictionary < string , List < int > > counters )
46+ {
47+ var created = new HashSet < string > ( ) ;
5548
56- foreach ( var child in box . Boxes )
49+ foreach ( var box in boxes )
5750 {
58- counters = ResolveCounters ( child , counters ) ;
51+ if ( box . Display == CssConstants . None )
52+ continue ;
53+
54+ ApplyCounterReset ( box , counters , created ) ;
55+ ApplyCounterIncrement ( box , counters , created ) ;
56+
57+ var snapshot = new Dictionary < string , int > ( counters . Count ) ;
58+ foreach ( var pair in counters )
59+ snapshot [ pair . Key ] = pair . Value [ pair . Value . Count - 1 ] ;
60+ box . Counters = snapshot ;
61+
62+ ResolveSiblings ( box . Boxes , counters ) ;
5963 }
6064
61- return counters ;
65+ foreach ( var name in created )
66+ {
67+ var instances = counters [ name ] ;
68+ instances . RemoveAt ( instances . Count - 1 ) ;
69+ if ( instances . Count == 0 )
70+ counters . Remove ( name ) ;
71+ }
6272 }
6373
64- private static void ApplyCounterReset ( CssBox box , Dictionary < string , int > counters )
74+ private static void ApplyCounterReset ( CssBox box , Dictionary < string , List < int > > counters , HashSet < string > created )
6575 {
6676 if ( string . IsNullOrEmpty ( box . CounterReset ) || box . CounterReset == CssConstants . None ) return ;
6777
68- ForEachNameValuePair ( box . CounterReset , defaultValue : 0 , ( name , value ) => counters [ name ] = value ) ;
78+ ForEachNameValuePair ( box . CounterReset , defaultValue : 0 , ( name , value ) => Reset ( counters , created , name , value ) ) ;
6979 }
7080
71- private static void ApplyCounterIncrement ( CssBox box , Dictionary < string , int > counters )
81+ private static void ApplyCounterIncrement ( CssBox box , Dictionary < string , List < int > > counters , HashSet < string > created )
7282 {
7383 if ( box . Display == CssConstants . ListItem && ! CounterNameAppears ( box . CounterIncrement , CssConstants . ListItem ) )
7484 {
75- counters [ CssConstants . ListItem ] = GetOrDefault ( counters , CssConstants . ListItem ) + 1 ;
85+ Increment ( counters , created , CssConstants . ListItem , 1 ) ;
7686 }
7787
7888 if ( string . IsNullOrEmpty ( box . CounterIncrement ) || box . CounterIncrement == CssConstants . None ) return ;
7989
80- ForEachNameValuePair ( box . CounterIncrement , defaultValue : 1 ,
81- ( name , value ) => counters [ name ] = GetOrDefault ( counters , name ) + value ) ;
90+ ForEachNameValuePair ( box . CounterIncrement , defaultValue : 1 , ( name , value ) => Increment ( counters , created , name , value ) ) ;
8291 }
8392
84- /// <summary>
85- /// <see cref="Dictionary{TKey,TValue}.GetValueOrDefault(TKey)"/> stand-in: this project also
86- /// targets netstandard2.0/net462, where that method doesn't exist.
87- /// </summary>
88- private static int GetOrDefault ( Dictionary < string , int > counters , string name )
93+ private static void Reset ( Dictionary < string , List < int > > counters , HashSet < string > created , string name , int value )
8994 {
90- int value ;
91- return counters . TryGetValue ( name , out value ) ? value : 0 ;
95+ // A second reset among the same siblings replaces that instance instead of nesting a new one.
96+ if ( created . Add ( name ) )
97+ {
98+ if ( ! counters . TryGetValue ( name , out var instances ) )
99+ counters [ name ] = instances = new List < int > ( ) ;
100+ instances . Add ( value ) ;
101+ }
102+ else
103+ {
104+ var instances = counters [ name ] ;
105+ instances [ instances . Count - 1 ] = value ;
106+ }
107+ }
108+
109+ private static void Increment ( Dictionary < string , List < int > > counters , HashSet < string > created , string name , int value )
110+ {
111+ // Incrementing a counter that isn't in scope behaves as if this box had reset it to 0.
112+ if ( ! counters . TryGetValue ( name , out var instances ) )
113+ {
114+ Reset ( counters , created , name , 0 ) ;
115+ instances = counters [ name ] ;
116+ }
117+
118+ instances [ instances . Count - 1 ] += value ;
92119 }
93120
94121 private static bool CounterNameAppears ( string counterPropertyValue , string counterName )
0 commit comments