1010// - Sun Tsu,
1111// "The Art of War"
1212
13+ using System ;
14+ using System . Linq ;
1315using System . Text ;
16+ using TheArtOfDev . HtmlRenderer . Core . CssEngine ;
17+ using TheArtOfDev . HtmlRenderer . Core . Parse ;
1418using TheArtOfDev . HtmlRenderer . Core . Utils ;
1519
1620namespace TheArtOfDev . HtmlRenderer . Core . Dom
@@ -23,7 +27,12 @@ namespace TheArtOfDev.HtmlRenderer.Core.Dom
2327 /// <c>attr()</c>, and the four quote keywords are supported; <c>counter()</c>/<c>counters()</c>/
2428 /// <c>string()</c>/<c>content()</c> are not (CSS counters and named strings remain out of scope
2529 /// for this backport - an unrecognized component is simply skipped, the same graceful-ignore
26- /// behavior this engine already applies to unsupported CSS elsewhere).
30+ /// behavior this engine already applies to unsupported CSS elsewhere).<br/>
31+ /// Tokenizes via the shared CSS lexer (<see cref="CssValueParser.GetCssTokens"/>) rather than
32+ /// hand-parsing the CSS text, so string literals (e.g. <c>"\A"</c>, the standard UA-stylesheet
33+ /// line-break trick used by <c>br:before</c>) get the same correct escape decoding the lexer
34+ /// already applies when the stylesheet is first parsed - see CalcParser/BackgroundPositionGrammar
35+ /// for the same "don't write two independent parsers for the same grammar" precedent.
2736 /// </summary>
2837 internal static class CssContentEngine
2938 {
@@ -50,82 +59,41 @@ public static void ApplyContent(CssBox box)
5059 private static string Resolve ( CssBox box , string content )
5160 {
5261 var sb = new StringBuilder ( ) ;
53- var i = 0 ;
54- while ( i < content . Length )
62+
63+ foreach ( var token in CssValueParser . GetCssTokens ( content ) )
5564 {
56- if ( char . IsWhiteSpace ( content [ i ] ) )
65+ if ( token is StringToken stringToken )
5766 {
58- i ++ ;
59- continue ;
67+ sb . Append ( stringToken . Data ) ;
6068 }
61-
62- if ( content [ i ] == '"' || content [ i ] == ' \' ' )
69+ else if ( token is FunctionToken functionToken &&
70+ functionToken . Data . Equals ( FunctionNames . Attr , StringComparison . OrdinalIgnoreCase ) )
6371 {
64- var quote = content [ i ] ;
65- var start = i + 1 ;
66- var j = start ;
67- while ( j < content . Length && content [ j ] != quote )
72+ if ( functionToken . ArgumentTokens . FirstOrDefault ( ) is KeywordToken attrNameToken )
6873 {
69- if ( content [ j ] == ' \\ ' && j + 1 < content . Length )
70- j ++ ;
71- j ++ ;
74+ var sourceBox = box . IsPseudoElement && box . ParentBox != null ? box . ParentBox : box ;
75+ var attrValue = sourceBox . GetAttribute ( attrNameToken . Data , string . Empty ) ;
76+ sb . Append ( attrValue ) ;
7277 }
73- sb . Append ( Unescape ( content . Substring ( start , j - start ) ) ) ;
74- i = j + 1 ;
75- continue ;
7678 }
77-
78- var tokenEnd = i ;
79- while ( tokenEnd < content . Length && ! char . IsWhiteSpace ( content [ tokenEnd ] ) )
80- tokenEnd ++ ;
81- var token = content . Substring ( i , tokenEnd - i ) ;
82- i = tokenEnd ;
83-
84- if ( token . StartsWith ( "attr(" , System . StringComparison . OrdinalIgnoreCase ) && token . EndsWith ( ")" ) )
79+ else if ( token is KeywordToken keywordToken )
8580 {
86- var attrName = token . Substring ( 5 , token . Length - 6 ) . Trim ( ) ;
87- var sourceBox = box . IsPseudoElement && box . ParentBox != null ? box . ParentBox : box ;
88- var attrValue = sourceBox . GetAttribute ( attrName , string . Empty ) ;
89- sb . Append ( attrValue ) ;
90- }
91- else if ( token . Equals ( "open-quote" , System . StringComparison . OrdinalIgnoreCase ) )
92- {
93- sb . Append ( '“' ) ;
94- }
95- else if ( token . Equals ( "close-quote" , System . StringComparison . OrdinalIgnoreCase ) )
96- {
97- sb . Append ( '”' ) ;
98- }
99- else if ( token . Equals ( "no-open-quote" , System . StringComparison . OrdinalIgnoreCase ) ||
100- token . Equals ( "no-close-quote" , System . StringComparison . OrdinalIgnoreCase ) )
101- {
102- // produces no text, but still counts as a quoting depth change per spec - depth
103- // tracking isn't implemented here (no "quotes" property support), so this is a no-op.
81+ if ( keywordToken . Data . Equals ( Keywords . OpenQuote , StringComparison . OrdinalIgnoreCase ) )
82+ {
83+ sb . Append ( '“' ) ;
84+ }
85+ else if ( keywordToken . Data . Equals ( Keywords . CloseQuote , StringComparison . OrdinalIgnoreCase ) )
86+ {
87+ sb . Append ( '”' ) ;
88+ }
89+ // no-open-quote/no-close-quote produce no text, but still count as a quoting depth
90+ // change per spec - depth tracking isn't implemented here (no "quotes" property
91+ // support), so they (like any other unrecognized keyword) are a no-op.
10492 }
10593 // counter()/counters()/string()/content() and anything else unrecognized: skip.
10694 }
10795
10896 return sb . ToString ( ) ;
10997 }
110-
111- private static string Unescape ( string s )
112- {
113- if ( s . IndexOf ( '\\ ' ) < 0 ) return s ;
114-
115- var sb = new StringBuilder ( s . Length ) ;
116- for ( var i = 0 ; i < s . Length ; i ++ )
117- {
118- if ( s [ i ] == '\\ ' && i + 1 < s . Length )
119- {
120- i ++ ;
121- sb . Append ( s [ i ] ) ;
122- }
123- else
124- {
125- sb . Append ( s [ i ] ) ;
126- }
127- }
128- return sb . ToString ( ) ;
129- }
13098 }
13199}
0 commit comments