Skip to content

added support for merging imported stylesheets #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 201 additions & 18 deletions CSSParser.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php
require_once('lib/CSSUrlUtils.php');
require_once('lib/CSSProperties.php');
require_once('lib/CSSList.php');
require_once('lib/CSSRuleSet.php');
Expand All @@ -10,16 +11,63 @@
* @package html
* CSSParser class parses CSS from text into a data structure.
*/
class CSSParser {
class CSSParser {
/**
* User options
**/
protected $aOptions = array(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor now accepts an array of user-defined options.
Parsing now occurs in the parseFile, parseURL adn/or parseString methods.

'ignore_charset_rules' => false,
'resolve_imports' => false,
'absolute_urls' => false,
'base_url' => null,
'default_charset' => 'utf-8'
);

/**
* Parser internal pointers
**/
private $sText;
private $iCurrentPosition;
private $iLength;
private $aLoadedFiles = array();

/**
* Data for resolving imports
**/
const IMPORT_FILE = 'file';
const IMPORT_URL = 'url';
const IMPORT_NONE = 'none';
private $sImportMode = 'none';

/**
* flags
**/
private $bIgnoreCharsetRules = false;
private $bIgnoreImportRules = false;
private $bIsAbsBaseUrl;

public function __construct($sText, $sDefaultCharset = 'utf-8') {
$this->sText = $sText;
$this->iCurrentPosition = 0;
$this->setCharset($sDefaultCharset);
}
/**
* @param $aOptions array of options
*
* Valid options:
* * default_charset:
* default charset used by the parser.
* Will be overriden by @charset rules unless ignore_charset_rules is set to true
* * ignore_charset_rules:
* @charset rules are parsed but do not change the parser's default charset
* * resolve_imports:
* recursively import embedded stylesheets
* * absolute_urls:
* make all urls absolute
* * base_url:
* the base url to use for absolute urls and resolving imports
* if not set, will be computed from the file path
**/
public function __construct(array $aOptions=array()) {
$this->aOptions = array_merge($this->aOptions, $aOptions);
$this->bIgnoreCharsetRules = $this->aOptions['ignore_charset_rules'];
$this->bIsAbsBaseUrl = CSSUrlUtils::isAbsUrl($this->aOptions['base_url']);
}

public function setCharset($sCharset) {
$this->sCharset = $sCharset;
Expand All @@ -29,12 +77,116 @@ public function setCharset($sCharset) {
public function getCharset() {
return $this->sCharset;
}

public function parse() {

public function getLoadedFiles() {
return $this->aLoadedFiles;
}

/**
* @param $sPath string path to a file to load
**/
public function parseFile($sPath, $aLoadedFiles=array())
{
if(!$this->aOptions['base_url']) {
$this->aOptions['base_url'] = dirname($sPath);
}
if($this->aOptions['absolute_urls']) {
$this->aOptions['base_url'] = realpath($this->aOptions['base_url']);
}
//printf(">>> Parsing %s in %s\n", $sPath, $this->aOptions['base_url']);
$this->sImportMode = self::IMPORT_FILE;
$sPath = realpath($sPath);
$aLoadedFiles[] = $sPath;
$this->aLoadedFiles = array_merge($this->aLoadedFiles, $aLoadedFiles);
$sCss = file_get_contents($sPath);
return $this->parseString($sCss);
}

public function parseURL($sPath, $aLoadedFiles=array())
{
if(!$this->aOptions['base_url']) {
$this->aOptions['base_url'] = dirname($sPath);
}
//printf(">>> Parsing %s in %s\n", $sPath, $this->aOptions['base_url']);
//if($this->aOptions['absolute_urls'] && !CSSUrlUtils::isAbsUrl($this->aOptions['base_url'])) {
//$sProtocol = $_SERVER['HTTPS'] ? 'https' : 'http';
//$sPath = $sProtocol.'://'.$_SERVER['HTTP_HOST'].'/'.ltrim($sPath, '/');
//$this->aOptions['base_url'] = dirname($sPath);
//}
$this->sImportMode = self::IMPORT_URL;
$aLoadedFiles[] =$sPath;
$this->aLoadedFiles = array_merge($this->aLoadedFiles, $aLoadedFiles);
$sCss = CSSUrlUtils::loadURL($sPath);
return $this->parseString($sCss);
}


public function parseString($sString) {
$this->sText = $sString;
$this->iCurrentPosition = 0;
$this->setCharset($this->aOptions['default_charset']);
$oResult = new CSSDocument();
$this->parseDocument($oResult);
$this->postParse($oResult);
return $oResult;
}
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actual import resolving happens here.

public function postParse($oDoc)
{
$aImports = array();
$aCharsets = array();
$aContents = $oDoc->getContents();
foreach($aContents as $i => $oItem) {
if($oItem instanceof CSSIgnoredValue) {
unset($aContents[$i]);
} else if($oItem instanceof CSSImport) {
$aImports[] = $oItem;
unset($aContents[$i]);
} else if ($oItem instanceof CSSCharset) {
$aCharsets[] = $oItem;
unset($aContents[$i]);
}
}
$aImportedItems = array();
$aImportOptions = array_merge($this->aOptions, array(
'default_charset' => $this->sCharset,
'ignore_charset_rules' => true,
'base_url' => null
));
foreach($aImports as $oImport) {
if($this->aOptions['resolve_imports']) {
$parser = new CSSParser($aImportOptions);
$sPath = $oImport->getLocation()->getURL()->getString();
if($this->sImportMode == self::IMPORT_URL) {
if(!in_array($sPath, $this->aLoadedFiles)) {
$oImportedDoc = $parser->parseURL($sPath, $this->aLoadedFiles);
$this->aLoadedFiles = $parser->getLoadedFiles();
$aImportedContents = $oImportedDoc->getContents();
}
} else if($this->sImportMode == self::IMPORT_FILE) {
$sPath = realpath($sPath);
if(!in_array($sPath, $this->aLoadedFiles)) {
$oImportedDoc = $parser->parseFile($sPath, $this->aLoadedFiles);
$this->aLoadedFiles = $parser->getLoadedFiles();
$aImportedContents = $oImportedDoc->getContents();
}
}
if($oImport->getMediaQuery() !== null) {
$sMediaQuery = $oImport->getMediaQuery();
$oMediaQuery = new CSSMediaQuery();
$oMediaQuery->setQuery($sMediaQuery);
$oMediaQuery->setContents($aImportedContents);
$aImportedContents = array($oMediaQuery);
}
} else {
$aImportedContents = array($oImport);
}
$aImportedItems = array_merge($aImportedItems, $aImportedContents);
}
$aContents = array_merge($aImportedItems, $aContents);
if(isset($aCharsets[0])) array_unshift($aContents, $aCharsets[0]);
$oDoc->setContents($aContents);
}

private function parseDocument(CSSDocument $oDocument) {
$this->consumeWhiteSpace();
Expand All @@ -53,6 +205,8 @@ private function parseList(CSSList $oList, $bIsRoot = false) {
return;
}
} else {
$this->bIgnoreCharsetRules = true;
$this->bIgnoreImportRules = true;
$oList->append($this->parseSelector());
}
$this->consumeWhiteSpace();
Expand All @@ -65,31 +219,45 @@ private function parseList(CSSList $oList, $bIsRoot = false) {
private function parseAtRule() {
$this->consume('@');
$sIdentifier = $this->parseIdentifier();
$this->consumeWhiteSpace();
$this->consumeWhiteSpace();
if($sIdentifier === 'media') {
$this->bIgnoreCharsetRules = true;
$this->bIgnoreImportRules = true;
$oResult = new CSSMediaQuery();
$oResult->setQuery(trim($this->consumeUntil('{')));
$this->consume('{');
$this->consumeWhiteSpace();
$this->parseList($oResult);
return $oResult;
} else if($sIdentifier === 'import') {
$this->bIgnoreCharsetRules = true;
$oLocation = $this->parseURLValue();
$this->consumeWhiteSpace();
$sMediaQuery = null;
if(!$this->comes(';')) {
$sMediaQuery = $this->consumeUntil(';');
}
$this->consume(';');
return new CSSImport($oLocation, $sMediaQuery);
if($this->bIgnoreImportRules) {
return new CSSIgnoredValue();
}
return new CSSImport($oLocation, $sMediaQuery);
} else if($sIdentifier === 'charset') {
$sCharset = $this->parseStringValue();
$this->consumeWhiteSpace();
$this->consume(';');
$this->setCharset($sCharset->getString());
return new CSSCharset($sCharset);
$sCharset = $this->parseStringValue();
$this->consumeWhiteSpace();
$this->consume(';');
// Only the first charset rule should exist !
if($this->bIgnoreCharsetRules) {
return new CSSIgnoredValue();
} else {
$this->setCharset($sCharset->getString());
$this->bIgnoreCharsetRules = true;
return new CSSCharset($sCharset);
}
} else {
//Unknown other at rule (font-face or such)
$this->bIgnoreCharsetRules = true;
$this->bIgnoreImportRules = true;
$this->consume('{');
$this->consumeWhiteSpace();
$oAtRule = new CSSAtRule($sIdentifier);
Expand Down Expand Up @@ -176,7 +344,7 @@ private function parseCharacter($bIsForIdentifier) {
return iconv('utf-32le', $this->sCharset, $sUtf32);
}
if($bIsForIdentifier) {
if(preg_match('/[a-zA-Z0-9]|-|_/u', $this->peek()) === 1) {
if(preg_match('/\*|[a-zA-Z0-9]|-|_/u', $this->peek()) === 1) {
return $this->consume(1);
} else if(ord($this->peek()) > 0xa1) {
return $this->consume(1);
Expand Down Expand Up @@ -375,7 +543,22 @@ private function parseURLValue() {
$this->consume('(');
}
$this->consumeWhiteSpace();
$oResult = new CSSURL($this->parseStringValue());
$sValue = $this->parseStringValue();
if($this->aOptions['absolute_urls'] || $this->aOptions['resolve_imports']) {
$sURL = $sValue->getString();
// resolve only if:
// (url is not absolute) OR IF (url is absolute path AND base_url is absolute)
$bIsAbsPath = CSSUrlUtils::isAbsPath($sURL);
$bIsAbsUrl = CSSUrlUtils::isAbsUrl($sURL);
if( (!$bIsAbsUrl && !$bIsAbsPath)
|| ($bIsAbsPath && $this->bIsAbsBaseUrl)) {
$sURL = CSSUrlUtils::joinPaths(
$this->aOptions['base_url'], $sURL
);
$sValue = new CSSString($sURL);
}
}
$oResult = new CSSURL($sValue);
if($bUseUrl) {
$this->consumeWhiteSpace();
$this->consume(')');
Expand Down
24 changes: 23 additions & 1 deletion lib/CSSList.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,29 @@ abstract class CSSList {
public function __construct() {
$this->aContents = array();
}

public function prepend($oItem)
{
array_unshift($this->aContents, $oItem);
}

public function append($oItem) {
$this->aContents[] = $oItem;
}

public function extend(Array $aItems) {
foreach ($aItems as $oItem) {
$this->aContents[] = $oItem;
}
}

public function removeItemAt($iIndex) {
unset($this->aContents[$iIndex]);
}

public function insertItemsAt(Array $oItems, $iIndex) {
array_splice($this->aContents, $iIndex, 0, $oItems);
}

public function __toString() {
$sResult = '';
Expand All @@ -25,7 +44,10 @@ public function __toString() {

public function getContents() {
return $this->aContents;
}
}
public function setContents(Array $aContents) {
$this->aContents = $aContents;
}

protected function allDeclarationBlocks(&$aResult) {
foreach($this->aContents as $mContent) {
Expand Down
12 changes: 8 additions & 4 deletions lib/CSSProperties.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ public function __construct(CSSURL $oLocation, $sMediaQuery) {
}

public function setLocation($oLocation) {
$this->oLocation = $oLocation;
$this->oLocation = $oLocation;
}

public function getLocation() {
return $this->oLocation;
return $this->oLocation;
}

public function getMediaQuery() {
return $this->sMediaQuery;
}

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

public function setCharset($sCharset) {
$this->sCharset = $sCharset;
$this->sCharset = $sCharset;
}

public function getCharset() {
return $this->sCharset;
return $this->sCharset;
}

public function __toString() {
Expand Down
Loading