11'use strict'
22
3+ // Escapes sequences that could break out of an HTML <style> context.
4+ // Uses CSS unicode escaping (\3c = '<') which is valid CSS and parsed
5+ // correctly by all compliant CSS consumers.
6+ const STYLE_TAG = / ( < ) ( \/ ? s t y l e \b ) / gi
7+ const COMMENT_OPEN = / ( < ) ( ! - - ) / g
8+
9+ function escapeHTMLInCSS ( str ) {
10+ if ( typeof str !== 'string' ) return str
11+ if ( ! str . includes ( '<' ) ) return str
12+ return str . replace ( STYLE_TAG , '\\3c $2' ) . replace ( COMMENT_OPEN , '\\3c $2' )
13+ }
14+
315const DEFAULT_RAW = {
416 after : '\n' ,
517 beforeClose : '\n' ,
@@ -38,7 +50,7 @@ class Stringifier {
3850 this . block ( node , name + params )
3951 } else {
4052 let end = ( node . raws . between || '' ) + ( semicolon ? ';' : '' )
41- this . builder ( name + params + end , node )
53+ this . builder ( escapeHTMLInCSS ( name + params + end ) , node )
4254 }
4355 }
4456
@@ -73,7 +85,7 @@ class Stringifier {
7385
7486 block ( node , start ) {
7587 let between = this . raw ( node , 'between' , 'beforeOpen' )
76- this . builder ( start + between + '{' , node , 'start' )
88+ this . builder ( escapeHTMLInCSS ( start + between ) + '{' , node , 'start' )
7789
7890 let after
7991 if ( node . nodes && node . nodes . length ) {
@@ -83,7 +95,7 @@ class Stringifier {
8395 after = this . raw ( node , 'after' , 'emptyBody' )
8496 }
8597
86- if ( after ) this . builder ( after )
98+ if ( after ) this . builder ( escapeHTMLInCSS ( after ) )
8799 this . builder ( '}' , node , 'end' )
88100 }
89101
@@ -95,18 +107,19 @@ class Stringifier {
95107 }
96108
97109 let semicolon = this . raw ( node , 'semicolon' )
110+ let isDocument = node . type === 'document'
98111 for ( let i = 0 ; i < node . nodes . length ; i ++ ) {
99112 let child = node . nodes [ i ]
100113 let before = this . raw ( child , 'before' )
101- if ( before ) this . builder ( before )
114+ if ( before ) this . builder ( isDocument ? before : escapeHTMLInCSS ( before ) )
102115 this . stringify ( child , last !== i || semicolon )
103116 }
104117 }
105118
106119 comment ( node ) {
107120 let left = this . raw ( node , 'left' , 'commentLeft' )
108121 let right = this . raw ( node , 'right' , 'commentRight' )
109- this . builder ( '/*' + left + node . text + right + '*/' , node )
122+ this . builder ( escapeHTMLInCSS ( '/*' + left + node . text + right + '*/' ) , node )
110123 }
111124
112125 decl ( node , semicolon ) {
@@ -118,7 +131,7 @@ class Stringifier {
118131 }
119132
120133 if ( semicolon ) string += ';'
121- this . builder ( string , node )
134+ this . builder ( escapeHTMLInCSS ( string ) , node )
122135 }
123136
124137 document ( node ) {
@@ -324,13 +337,17 @@ class Stringifier {
324337
325338 root ( node ) {
326339 this . body ( node )
327- if ( node . raws . after ) this . builder ( node . raws . after )
340+ if ( node . raws . after ) {
341+ let after = node . raws . after
342+ let isDocument = node . parent && node . parent . type === 'document'
343+ this . builder ( isDocument ? after : escapeHTMLInCSS ( after ) )
344+ }
328345 }
329346
330347 rule ( node ) {
331348 this . block ( node , this . rawValue ( node , 'selector' ) )
332349 if ( node . raws . ownSemicolon ) {
333- this . builder ( node . raws . ownSemicolon , node , 'end' )
350+ this . builder ( escapeHTMLInCSS ( node . raws . ownSemicolon ) , node , 'end' )
334351 }
335352 }
336353
0 commit comments