1
1
<?php
2
+ require_once ('lib/CSSUrlUtils.php ' );
2
3
require_once ('lib/CSSProperties.php ' );
3
4
require_once ('lib/CSSList.php ' );
4
5
require_once ('lib/CSSRuleSet.php ' );
10
11
* @package html
11
12
* CSSParser class parses CSS from text into a data structure.
12
13
*/
13
- class CSSParser {
14
+ class CSSParser {
15
+ /**
16
+ * User options
17
+ **/
18
+ protected $ aOptions = array (
19
+ 'ignore_charset_rules ' => false ,
20
+ 'resolve_imports ' => false ,
21
+ 'absolute_urls ' => false ,
22
+ 'base_url ' => null ,
23
+ 'default_charset ' => 'utf-8 '
24
+ );
25
+
26
+ /**
27
+ * Parser internal pointers
28
+ **/
14
29
private $ sText ;
15
30
private $ iCurrentPosition ;
16
31
private $ iLength ;
32
+
33
+ /**
34
+ * Data for resolving imports
35
+ **/
36
+ const IMPORT_FILE = 'file ' ;
37
+ const IMPORT_URL = 'url ' ;
38
+ const IMPORT_NONE = 'none ' ;
39
+ private $ sImportMode = 'none ' ;
40
+
41
+ /**
42
+ * flags
43
+ **/
44
+ private $ bIgnoreCharsetRules = false ;
45
+ private $ bIgnoreImportRules = false ;
46
+ private $ bIsAbsBaseUrl ;
17
47
18
- public function __construct ($ sText , $ sDefaultCharset = 'utf-8 ' ) {
19
- $ this ->sText = $ sText ;
20
- $ this ->iCurrentPosition = 0 ;
21
- $ this ->setCharset ($ sDefaultCharset );
22
- }
48
+ /**
49
+ * @param $aOptions array of options
50
+ *
51
+ * Valid options:
52
+ * * default_charset:
53
+ * default charset used by the parser.
54
+ * Will be overriden by @charset rules unless ignore_charset_rules is set to true
55
+ * * ignore_charset_rules:
56
+ * @charset rules are parsed but do not change the parser's default charset
57
+ * * resolve_imports:
58
+ * recursively import embedded stylesheets
59
+ * * absolute_urls:
60
+ * make all urls absolute
61
+ * * base_url:
62
+ * the base url to use for absolute urls and resolving imports
63
+ * if not set, will be computed from the file path
64
+ **/
65
+ public function __construct (array $ aOptions =array ()) {
66
+ $ this ->aOptions = array_merge ($ this ->aOptions , $ aOptions );
67
+ $ this ->bIgnoreCharsetRules = $ this ->aOptions ['ignore_charset_rules ' ];
68
+ $ this ->bIsAbsBaseUrl = CSSUrlUtils::isAbsUrl ($ this ->aOptions ['base_url ' ]);
69
+ }
23
70
24
71
public function setCharset ($ sCharset ) {
25
72
$ this ->sCharset = $ sCharset ;
@@ -29,12 +76,99 @@ public function setCharset($sCharset) {
29
76
public function getCharset () {
30
77
return $ this ->sCharset ;
31
78
}
32
-
33
- public function parse () {
79
+
80
+ /**
81
+ * @param $sPath string path to a file to load
82
+ **/
83
+ public function parseFile ($ sPath )
84
+ {
85
+ if (!$ this ->aOptions ['base_url ' ]) {
86
+ $ this ->aOptions ['base_url ' ] = dirname ($ sPath );
87
+ }
88
+ if ($ this ->aOptions ['absolute_urls ' ]) {
89
+ $ this ->aOptions ['base_url ' ] = realpath ($ this ->aOptions ['base_url ' ]);
90
+ }
91
+ //printf(">>> Parsing %s in %s\n", $sPath, $this->aOptions['base_url']);
92
+ $ this ->sImportMode = self ::IMPORT_FILE ;
93
+ $ sCss = file_get_contents ($ sPath );
94
+ return $ this ->parseString ($ sCss );
95
+ }
96
+
97
+ public function parseURL ($ sPath )
98
+ {
99
+ if (!$ this ->aOptions ['base_url ' ]) {
100
+ $ this ->aOptions ['base_url ' ] = dirname ($ sPath );
101
+ }
102
+ //if($this->aOptions['absolute_urls'] && !CSSUrlUtils::isAbsUrl($this->aOptions['base_url'])) {
103
+ //$sProtocol = $_SERVER['HTTPS'] ? 'https' : 'http';
104
+ //$sPath = $sProtocol.'://'.$_SERVER['HTTP_HOST'].'/'.ltrim($sPath, '/');
105
+ //$this->aOptions['base_url'] = dirname($sPath);
106
+ //}
107
+ $ this ->sImportMode = self ::IMPORT_URL ;
108
+ $ sCss = CSSUrlUtils::loadURL ($ sPath );
109
+ return $ this ->parseString ($ sCss );
110
+ }
111
+
112
+
113
+ public function parseString ($ sString ) {
114
+ $ this ->sText = $ sString ;
115
+ $ this ->iCurrentPosition = 0 ;
116
+ $ this ->setCharset ($ this ->aOptions ['default_charset ' ]);
34
117
$ oResult = new CSSDocument ();
35
118
$ this ->parseDocument ($ oResult );
119
+ $ this ->postParse ($ oResult );
36
120
return $ oResult ;
37
- }
121
+ }
122
+
123
+ public function postParse ($ oDoc )
124
+ {
125
+ $ aImports = array ();
126
+ $ aCharsets = array ();
127
+ $ aContents = $ oDoc ->getContents ();
128
+ foreach ($ aContents as $ i => $ oItem ) {
129
+ if ($ oItem instanceof CSSIgnoredValue) {
130
+ unset($ aContents [$ i ]);
131
+ } else if ($ oItem instanceof CSSImport) {
132
+ $ aImports [] = $ oItem ;
133
+ unset($ aContents [$ i ]);
134
+ } else if ($ oItem instanceof CSSCharset) {
135
+ $ aCharsets [] = $ oItem ;
136
+ unset($ aContents [$ i ]);
137
+ }
138
+ }
139
+ $ aImportedItems = array ();
140
+ $ aImportOptions = array_merge ($ this ->aOptions , array (
141
+ 'default_charset ' => $ this ->sCharset ,
142
+ 'ignore_charset_rules ' => true ,
143
+ 'base_url ' => null
144
+ ));
145
+ foreach ($ aImports as $ oImport ) {
146
+ if ($ this ->aOptions ['resolve_imports ' ]) {
147
+ $ parser = new CSSParser ($ aImportOptions );
148
+ $ sPath = $ oImport ->getLocation ()->getURL ()->getString ();
149
+ if ($ this ->sImportMode == self ::IMPORT_URL ) {
150
+ $ oImportedDoc = $ parser ->parseURL ($ sPath , null , $ this ->getCharset ());
151
+ $ aImportedContents = $ oImportedDoc ->getContents ();
152
+ } else if ($ this ->sImportMode == self ::IMPORT_FILE ) {
153
+ $ oImportedDoc = $ parser ->parseFile ($ sPath );
154
+ $ aImportedContents = $ oImportedDoc ->getContents ();
155
+ }
156
+ if ($ oImport ->getMediaQuery () !== null ) {
157
+ $ sMediaQuery = $ oImport ->getMediaQuery ();
158
+ $ oMediaQuery = new CSSMediaQuery ();
159
+ $ oMediaQuery ->setQuery ($ sMediaQuery );
160
+ $ oMediaQuery ->setContents ($ aImportedContents );
161
+ $ aImportedContents = array ($ oMediaQuery );
162
+ }
163
+ } else {
164
+ $ aImportedContents = array ($ oImport );
165
+ }
166
+ $ aImportedItems = array_merge ($ aImportedItems , $ aImportedContents );
167
+ }
168
+ $ aContents = array_merge ($ aImportedItems , $ aContents );
169
+ if (isset ($ aCharsets [0 ])) array_unshift ($ aContents , $ aCharsets [0 ]);
170
+ $ oDoc ->setContents ($ aContents );
171
+ }
38
172
39
173
private function parseDocument (CSSDocument $ oDocument ) {
40
174
$ this ->consumeWhiteSpace ();
@@ -53,6 +187,8 @@ private function parseList(CSSList $oList, $bIsRoot = false) {
53
187
return ;
54
188
}
55
189
} else {
190
+ $ this ->bIgnoreCharsetRules = true ;
191
+ $ this ->bIgnoreImportRules = true ;
56
192
$ oList ->append ($ this ->parseSelector ());
57
193
}
58
194
$ this ->consumeWhiteSpace ();
@@ -65,31 +201,45 @@ private function parseList(CSSList $oList, $bIsRoot = false) {
65
201
private function parseAtRule () {
66
202
$ this ->consume ('@ ' );
67
203
$ sIdentifier = $ this ->parseIdentifier ();
68
- $ this ->consumeWhiteSpace ();
204
+ $ this ->consumeWhiteSpace ();
69
205
if ($ sIdentifier === 'media ' ) {
206
+ $ this ->bIgnoreCharsetRules = true ;
207
+ $ this ->bIgnoreImportRules = true ;
70
208
$ oResult = new CSSMediaQuery ();
71
209
$ oResult ->setQuery (trim ($ this ->consumeUntil ('{ ' )));
72
210
$ this ->consume ('{ ' );
73
211
$ this ->consumeWhiteSpace ();
74
212
$ this ->parseList ($ oResult );
75
213
return $ oResult ;
76
214
} else if ($ sIdentifier === 'import ' ) {
215
+ $ this ->bIgnoreCharsetRules = true ;
77
216
$ oLocation = $ this ->parseURLValue ();
78
217
$ this ->consumeWhiteSpace ();
79
218
$ sMediaQuery = null ;
80
219
if (!$ this ->comes ('; ' )) {
81
220
$ sMediaQuery = $ this ->consumeUntil ('; ' );
82
221
}
83
222
$ this ->consume ('; ' );
84
- return new CSSImport ($ oLocation , $ sMediaQuery );
223
+ if ($ this ->bIgnoreImportRules ) {
224
+ return new CSSIgnoredValue ();
225
+ }
226
+ return new CSSImport ($ oLocation , $ sMediaQuery );
85
227
} else if ($ sIdentifier === 'charset ' ) {
86
- $ sCharset = $ this ->parseStringValue ();
87
- $ this ->consumeWhiteSpace ();
88
- $ this ->consume ('; ' );
89
- $ this ->setCharset ($ sCharset ->getString ());
90
- return new CSSCharset ($ sCharset );
228
+ $ sCharset = $ this ->parseStringValue ();
229
+ $ this ->consumeWhiteSpace ();
230
+ $ this ->consume ('; ' );
231
+ // Only the first charset rule should exist !
232
+ if ($ this ->bIgnoreCharsetRules ) {
233
+ return new CSSIgnoredValue ();
234
+ } else {
235
+ $ this ->setCharset ($ sCharset ->getString ());
236
+ $ this ->bIgnoreCharsetRules = true ;
237
+ return new CSSCharset ($ sCharset );
238
+ }
91
239
} else {
92
240
//Unknown other at rule (font-face or such)
241
+ $ this ->bIgnoreCharsetRules = true ;
242
+ $ this ->bIgnoreImportRules = true ;
93
243
$ this ->consume ('{ ' );
94
244
$ this ->consumeWhiteSpace ();
95
245
$ oAtRule = new CSSAtRule ($ sIdentifier );
@@ -176,7 +326,7 @@ private function parseCharacter($bIsForIdentifier) {
176
326
return iconv ('utf-32le ' , $ this ->sCharset , $ sUtf32 );
177
327
}
178
328
if ($ bIsForIdentifier ) {
179
- if (preg_match ('/[a-zA-Z0-9]|-|_/u ' , $ this ->peek ()) === 1 ) {
329
+ if (preg_match ('/\*| [a-zA-Z0-9]|-|_/u ' , $ this ->peek ()) === 1 ) {
180
330
return $ this ->consume (1 );
181
331
} else if (ord ($ this ->peek ()) > 0xa1 ) {
182
332
return $ this ->consume (1 );
@@ -375,7 +525,22 @@ private function parseURLValue() {
375
525
$ this ->consume ('( ' );
376
526
}
377
527
$ this ->consumeWhiteSpace ();
378
- $ oResult = new CSSURL ($ this ->parseStringValue ());
528
+ $ sValue = $ this ->parseStringValue ();
529
+ if ($ this ->aOptions ['absolute_urls ' ] || $ this ->aOptions ['resolve_imports ' ]) {
530
+ $ sURL = $ sValue ->getString ();
531
+ // resolve only if:
532
+ // (url is not absolute) OR IF (url is absolute path AND base_url is absolute)
533
+ $ bIsAbsPath = CSSUrlUtils::isAbsPath ($ sURL );
534
+ $ bIsAbsUrl = CSSUrlUtils::isAbsUrl ($ sURL );
535
+ if ( (!$ bIsAbsUrl && !$ bIsAbsPath )
536
+ || ($ bIsAbsPath && $ this ->bIsAbsBaseUrl )) {
537
+ $ sURL = CSSUrlUtils::joinPaths (
538
+ $ this ->aOptions ['base_url ' ], $ sURL
539
+ );
540
+ $ sValue = new CSSString ($ sURL );
541
+ }
542
+ }
543
+ $ oResult = new CSSURL ($ sValue );
379
544
if ($ bUseUrl ) {
380
545
$ this ->consumeWhiteSpace ();
381
546
$ this ->consume (') ' );
0 commit comments