Skip to content

Commit decf478

Browse files
author
ju1ius
committed
added support for merging imported stylesheet
1 parent b03ab00 commit decf478

11 files changed

+365
-46
lines changed

CSSParser.php

Lines changed: 183 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<?php
2+
require_once('lib/CSSUrlUtils.php');
23
require_once('lib/CSSProperties.php');
34
require_once('lib/CSSList.php');
45
require_once('lib/CSSRuleSet.php');
@@ -10,16 +11,62 @@
1011
* @package html
1112
* CSSParser class parses CSS from text into a data structure.
1213
*/
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+
**/
1429
private $sText;
1530
private $iCurrentPosition;
1631
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;
1747

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+
}
2370

2471
public function setCharset($sCharset) {
2572
$this->sCharset = $sCharset;
@@ -29,12 +76,99 @@ public function setCharset($sCharset) {
2976
public function getCharset() {
3077
return $this->sCharset;
3178
}
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']);
34117
$oResult = new CSSDocument();
35118
$this->parseDocument($oResult);
119+
$this->postParse($oResult);
36120
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+
}
38172

39173
private function parseDocument(CSSDocument $oDocument) {
40174
$this->consumeWhiteSpace();
@@ -53,6 +187,8 @@ private function parseList(CSSList $oList, $bIsRoot = false) {
53187
return;
54188
}
55189
} else {
190+
$this->bIgnoreCharsetRules = true;
191+
$this->bIgnoreImportRules = true;
56192
$oList->append($this->parseSelector());
57193
}
58194
$this->consumeWhiteSpace();
@@ -65,31 +201,45 @@ private function parseList(CSSList $oList, $bIsRoot = false) {
65201
private function parseAtRule() {
66202
$this->consume('@');
67203
$sIdentifier = $this->parseIdentifier();
68-
$this->consumeWhiteSpace();
204+
$this->consumeWhiteSpace();
69205
if($sIdentifier === 'media') {
206+
$this->bIgnoreCharsetRules = true;
207+
$this->bIgnoreImportRules = true;
70208
$oResult = new CSSMediaQuery();
71209
$oResult->setQuery(trim($this->consumeUntil('{')));
72210
$this->consume('{');
73211
$this->consumeWhiteSpace();
74212
$this->parseList($oResult);
75213
return $oResult;
76214
} else if($sIdentifier === 'import') {
215+
$this->bIgnoreCharsetRules = true;
77216
$oLocation = $this->parseURLValue();
78217
$this->consumeWhiteSpace();
79218
$sMediaQuery = null;
80219
if(!$this->comes(';')) {
81220
$sMediaQuery = $this->consumeUntil(';');
82221
}
83222
$this->consume(';');
84-
return new CSSImport($oLocation, $sMediaQuery);
223+
if($this->bIgnoreImportRules) {
224+
return new CSSIgnoredValue();
225+
}
226+
return new CSSImport($oLocation, $sMediaQuery);
85227
} 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+
}
91239
} else {
92240
//Unknown other at rule (font-face or such)
241+
$this->bIgnoreCharsetRules = true;
242+
$this->bIgnoreImportRules = true;
93243
$this->consume('{');
94244
$this->consumeWhiteSpace();
95245
$oAtRule = new CSSAtRule($sIdentifier);
@@ -176,7 +326,7 @@ private function parseCharacter($bIsForIdentifier) {
176326
return iconv('utf-32le', $this->sCharset, $sUtf32);
177327
}
178328
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) {
180330
return $this->consume(1);
181331
} else if(ord($this->peek()) > 0xa1) {
182332
return $this->consume(1);
@@ -375,7 +525,22 @@ private function parseURLValue() {
375525
$this->consume('(');
376526
}
377527
$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);
379544
if($bUseUrl) {
380545
$this->consumeWhiteSpace();
381546
$this->consume(')');

lib/CSSList.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,29 @@ abstract class CSSList {
1010
public function __construct() {
1111
$this->aContents = array();
1212
}
13+
14+
public function prepend($oItem)
15+
{
16+
array_unshift($this->aContents, $oItem);
17+
}
1318

1419
public function append($oItem) {
1520
$this->aContents[] = $oItem;
1621
}
22+
23+
public function extend(Array $aItems) {
24+
foreach ($aItems as $oItem) {
25+
$this->aContents[] = $oItem;
26+
}
27+
}
28+
29+
public function removeItemAt($iIndex) {
30+
unset($this->aContents[$iIndex]);
31+
}
32+
33+
public function insertItemsAt(Array $oItems, $iIndex) {
34+
array_splice($this->aContents, $iIndex, 0, $oItems);
35+
}
1736

1837
public function __toString() {
1938
$sResult = '';
@@ -25,7 +44,10 @@ public function __toString() {
2544

2645
public function getContents() {
2746
return $this->aContents;
28-
}
47+
}
48+
public function setContents(Array $aContents) {
49+
$this->aContents = $aContents;
50+
}
2951

3052
protected function allDeclarationBlocks(&$aResult) {
3153
foreach($this->aContents as $mContent) {

lib/CSSProperties.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ public function __construct(CSSURL $oLocation, $sMediaQuery) {
1313
}
1414

1515
public function setLocation($oLocation) {
16-
$this->oLocation = $oLocation;
16+
$this->oLocation = $oLocation;
1717
}
1818

1919
public function getLocation() {
20-
return $this->oLocation;
20+
return $this->oLocation;
2121
}
22+
23+
public function getMediaQuery() {
24+
return $this->sMediaQuery;
25+
}
2226

2327
public function __toString() {
2428
return "@import ".$this->oLocation->__toString().($this->sMediaQuery === null ? '' : ' '.$this->sMediaQuery).';';
@@ -40,11 +44,11 @@ public function __construct($sCharset) {
4044
}
4145

4246
public function setCharset($sCharset) {
43-
$this->sCharset = $sCharset;
47+
$this->sCharset = $sCharset;
4448
}
4549

4650
public function getCharset() {
47-
return $this->sCharset;
51+
return $this->sCharset;
4852
}
4953

5054
public function __toString() {

0 commit comments

Comments
 (0)