diff --git a/CSSParser.php b/CSSParser.php
index 54779275..e63040b1 100644
--- a/CSSParser.php
+++ b/CSSParser.php
@@ -1,4 +1,6 @@
false,
+ 'absolute_urls' => false,
+ 'base_url' => null,
+ 'input_encoding' => null,
+ 'output_encoding' => null
+ );
+
+ /**
+ * 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 are:
+ *
+ * -
+ * input_encoding:
+ * Force the input to be read with this encoding.
+ * This also force encoding for all imported stylesheets if resolve_imports is set to true.
+ * If not specified, the input encoding will be detected according to:
+ * http://www.w3.org/TR/CSS2/syndata.html#charset
+ *
+ * -
+ * output_encoding:
+ * Converts the output to given encoding.
+ *
+ * -
+ * 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 specified, will be computed from the file path or url.
+ *
+ *
+ **/
+ public function __construct(array $aOptions=array()) {
+ $this->setOptions($aOptions);
+ }
+
+ /**
+ * Gets an option value.
+ *
+ * @param string $sName The option name
+ * @param mixed $mDefault The default value (null by default)
+ *
+ * @return mixed The option value or the default value
+ */
+ public function getOption($sName, $mDefault=null) {
+ return isset($this->aOptions[$sName]) ? $this->aOptions[$sName] : $mDefault;
+ }
+ /**
+ * Sets an option value.
+ *
+ * @param string $sName The option name
+ * @param mixed $mValue The default value
+ *
+ * @return CSSParser The current CSSParser instance
+ */
+ public function setOption($sName, $mValue) {
+ $this->aOptions[$sName] = $mValue;
+ return $this;
+ }
+
+ /**
+ * Returns the options of the current instance.
+ *
+ * @return array The current instance's options
+ **/
+ public function getOptions() {
+ return $this->aOptions;
+ }
+
+ /**
+ * Merge given options with the current options
+ *
+ * @param array $aOptions The options to merge
+ *
+ * @return CSSParser The current CSSParser instance
+ **/
+ public function setOptions(array $aOptions) {
+ $this->aOptions = array_merge($this->aOptions, $aOptions);
+ return $this;
+ }
+
+ /**
+ * @todo Access should be private, since calling this method
+ * from the outside world could lead to unpredicable results.
+ **/
public function setCharset($sCharset) {
$this->sCharset = $sCharset;
$this->iLength = mb_strlen($this->sText, $this->sCharset);
}
public function getCharset() {
- return $this->sCharset;
- }
-
- public function parse() {
+ return $this->sCharset;
+ }
+
+ /**
+ * Returns an array of all the loaded stylesheets.
+ *
+ * @return array The loaded stylesheets
+ **/
+ public function getLoadedFiles() {
+ return $this->aLoadedFiles;
+ }
+
+ /**
+ * Parses a local stylesheet into a CSSDocument object.
+ *
+ * @param string $sPath Path to a file to load
+ * @param array $aLoadedFiles An array of files to exclude
+ *
+ * @return CSSDocument the resulting CSSDocument
+ **/
+ public function parseFile($sPath, $aLoadedFiles=array()) {
+ if(!$this->getOption('base_url')) {
+ $this->setOption('base_url', dirname($sPath));
+ }
+ if($this->getOption('absolute_urls') && !CSSUrlUtils::isAbsUrl($this->getOption('base_url'))) {
+ $this->setOption('base_url', realpath($this->getOption('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);
+ }
+
+ /**
+ * Parses a remote stylesheet into a CSSDocument object.
+ *
+ * @param string $sPath URL of a file to load
+ * @param array $aLoadedFiles An array of files to exclude
+ *
+ * @return CSSDocument the resulting CSSDocument
+ **/
+ public function parseURL($sPath, $aLoadedFiles=array()) {
+ if(!$this->getOption('base_url')) {
+ $this->setOption('base_url', CSSUrlUtils::dirname($sPath));
+ }
+ $this->sImportMode = self::IMPORT_URL;
+ $aLoadedFiles[] =$sPath;
+ $this->aLoadedFiles = array_merge($this->aLoadedFiles, $aLoadedFiles);
+ $aResult = CSSUrlUtils::loadURL($sPath);
+ $sResponse = $aResult['response'];
+ // charset from Content-Type HTTP header
+ // TODO: what do we do if the header returns a wrong charset ?
+ if($aResult['charset']) {
+ return $this->parseString($sResponse, $aResult['charset']);
+ }
+ return $this->parseString($sResponse);
+ }
+
+ /**
+ * Parses a string into a CSSDocument object.
+ *
+ * @param string $sString A CSS String
+ * @param array $sCharset An optional charset to use (overridden by the "input_encoding" option).
+ *
+ * @return CSSDocument the resulting CSSDocument
+ **/
+
+ public function parseString($sString, $sCharset=null) {
+ $this->bIsAbsBaseUrl = CSSUrlUtils::isAbsUrl($this->getOption('base_url'));
+ if($this->getOption('input_encoding')) {
+ // The input encoding has been overriden by user.
+ $sCharset = $this->getOption('input_encoding');
+ $this->bIgnoreCharsetRules = true;
+ }
+ if(!$sCharset) {
+ // detect charset from BOM and/or @charset rule
+ $sCharset = CSSCharsetUtils::detectCharset($sString);
+ if(!$sCharset) {
+ $sCharset = 'UTF-8';
+ }
+ }
+ $sString = CSSCharsetUtils::removeBOM($sString);
+ if($this->getOption('output_encoding')) {
+ $sString = CSSCharsetUtils::convert($sString, $sCharset, $this->getOption('output_encoding'));
+ $sCharset = $this->getOption('output_encoding');
+ $this->bIgnoreCharsetRules = true;
+ }
+ $this->sText = $sString;
+ $this->iCurrentPosition = 0;
+ $this->setCharset($sCharset);
$oResult = new CSSDocument();
$this->parseDocument($oResult);
+ $this->postParse($oResult);
return $oResult;
- }
+ }
+
+ /**
+ * Post processes the parsed CSSDocument object.
+ *
+ * Handles removal of ignored values and resolving of @import rules.
+ *
+ * @todo Should CSSIgnoredValue exist ?
+ * Another solution would be to add values only if they are not === null,
+ * i.e. in CSSList::append(), CSSRule::addValue() etc...
+ **/
+ private function postParse($oDoc) {
+ $aCharsets = array();
+ $aImports = array();
+ $aContents = $oDoc->getContents();
+ foreach($aContents as $i => $oItem) {
+ if($oItem instanceof CSSIgnoredValue) {
+ unset($aContents[$i]);
+ } else if($oItem instanceof CSSCharset) {
+ $aCharsets[] = $oItem;
+ unset($aContents[$i]);
+ } else if($oItem instanceof CSSImport) {
+ $aImports[] = $oItem;
+ unset($aContents[$i]);
+ }
+ }
+ $aImportedItems = array();
+ $aImportOptions = array_merge($this->getOptions(), array(
+ 'output_encoding' => $this->sCharset,
+ 'base_url' => null
+ ));
+ foreach($aImports as $oImport) {
+ if($this->getOption('resolve_imports')) {
+ $parser = new CSSParser($aImportOptions);
+ $sPath = $oImport->getLocation()->getURL()->getString();
+ $bIsAbsUrl = CSSUrlUtils::isAbsUrl($sPath);
+ if($this->sImportMode == self::IMPORT_URL || $bIsAbsUrl) {
+ 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();
@@ -53,6 +317,8 @@ private function parseList(CSSList $oList, $bIsRoot = false) {
return;
}
} else {
+ $this->bIgnoreCharsetRules = true;
+ $this->bIgnoreImportRules = true;
$oList->append($this->parseSelector());
}
$this->consumeWhiteSpace();
@@ -65,8 +331,10 @@ 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('{');
@@ -74,6 +342,7 @@ private function parseAtRule() {
$this->parseList($oResult);
return $oResult;
} else if($sIdentifier === 'import') {
+ $this->bIgnoreCharsetRules = true;
$oLocation = $this->parseURLValue();
$this->consumeWhiteSpace();
$sMediaQuery = null;
@@ -81,15 +350,25 @@ private function parseAtRule() {
$sMediaQuery = $this->consumeUntil(';');
}
$this->consume(';');
- return new CSSImport($oLocation, $sMediaQuery);
+ $oImport = new CSSImport($oLocation, $sMediaQuery);
+ if($this->bIgnoreImportRules) {
+ return new CSSIgnoredValue($oImport);
+ }
+ return $oImport;
} 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(';');
+ $oCharset = new CSSCharset($sCharset);
+ if($this->bIgnoreCharsetRules) {
+ return new CSSIgnoredValue($oCharset);
+ }
+ $this->bIgnoreCharsetRules = true;
+ return $oCharset;
} else {
//Unknown other at rule (font-face or such)
+ $this->bIgnoreCharsetRules = true;
+ $this->bIgnoreImportRules = true;
$this->consume('{');
$this->consumeWhiteSpace();
$oAtRule = new CSSAtRule($sIdentifier);
@@ -173,10 +452,10 @@ private function parseCharacter($bIsForIdentifier) {
$sUtf32 .= chr($iUnicode & 0xff);
$iUnicode = $iUnicode >> 8;
}
- return iconv('utf-32le', $this->sCharset, $sUtf32);
+ return CSSCharsetUtils::convert($sUtf32, 'UTF-32LE', $this->sCharset);
}
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);
@@ -375,7 +654,22 @@ private function parseURLValue() {
$this->consume('(');
}
$this->consumeWhiteSpace();
- $oResult = new CSSURL($this->parseStringValue());
+ $sValue = $this->parseStringValue();
+ if($this->getOption('absolute_urls') || $this->getOption('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->getOption('base_url'), $sURL
+ );
+ $sValue = new CSSString($sURL);
+ }
+ }
+ $oResult = new CSSURL($sValue);
if($bUseUrl) {
$this->consumeWhiteSpace();
$this->consume(')');
diff --git a/lib/CSSCharsetUtils.php b/lib/CSSCharsetUtils.php
new file mode 100644
index 00000000..b9f3158c
--- /dev/null
+++ b/lib/CSSCharsetUtils.php
@@ -0,0 +1,252 @@
+ '#^\xEF\xBB\xBF\x40\x63\x68\x61\x72\x73\x65\x74\x20\x22([\x20-\x7F]*)\x22\x3B#',
+ 'charset' => null,
+ 'endianness' => null
+ ),
+ array(
+ 'pattern' => '#^\xEF\xBB\xBF#',
+ 'charset' => "UTF-8",
+ 'endianness' => null
+ ),
+ array(
+ 'pattern' => '#^\x40\x63\x68\x61\x72\x73\x65\x74\x20\x22([\x20-\x7F]*)\x22\x3B#',
+ 'charset' => null,
+ 'endianness' => null
+ ),
+ array(
+ 'pattern' => '#^\xFE\xFF\x00\x40\x00\x63\x00\x68\x00\x61\x00\x72\x00\x73\x00\x65\x00\x74\x00\x20\x00\x22((?:\x00[\x20-\x7F])*)\x00\x22\x00\x3B#',
+ 'charset' => null,
+ 'endianness' => 'BE'
+ ),
+ array(
+ 'pattern' => '#^\x00\x40\x00\x63\x00\x68\x00\x61\x00\x72\x00\x73\x00\x65\x00\x74\x00\x20\x00\x22((?:\x00[\x20-\x7F])*)\x00\x22\x00\x3B#',
+ 'charset' => null,
+ 'endianness' => 'BE'
+ ),
+ array(
+ 'pattern' => '#^\xFF\xFE\x40\x00\x63\x00\x68\x00\x61\x00\x72\x00\x73\x00\x65\x00\x74\x00\x20\x00\x22\x00((?:\x00[\x20-\x7F])*)\x22\x00\x3B\x00#',
+ 'charset' => null,
+ 'endianness' => 'BE'
+ ),
+ array(
+ 'pattern' => '#^\x40\x00\x63\x00\x68\x00\x61\x00\x72\x00\x73\x00\x65\x00\x74\x00\x20\x00\x22\x00((?:\x00[\x20-\x7F])*)\x22\x00\x3B\x00#',
+ 'charset' => null,
+ 'endianness' => 'LE'
+ ),
+ array(
+ 'pattern' => '#^\x00\x00\xFE\xFF\x00\x00\x00\x40\x00\x00\x00\x63\x00\x00\x00\x68\x00\x00\x00\x61\x00\x00\x00\x72\x00\x00\x00\x73\x00\x00\x00\x65\x00\x00\x00\x74\x00\x00\x00\x20\x00\x00\x00\x22((?:\x00\x00\x00[\x20-\x7F])*)\x00\x00\x00\x22\x00\x00\x00\x3B#',
+ 'charset' => null,
+ 'endianness' => 'BE'
+ ),
+ array(
+ 'pattern' => '#^\x00\x00\x00\x40\x00\x00\x00\x63\x00\x00\x00\x68\x00\x00\x00\x61\x00\x00\x00\x72\x00\x00\x00\x73\x00\x00\x00\x65\x00\x00\x00\x74\x00\x00\x00\x20\x00\x00\x00\x22((?:\x00\x00\x00[\x20-\x7F])*)\x00\x00\x00\x22\x00\x00\x00\x3B#',
+ 'charset' => null,
+ 'endianness' => 'BE'
+ ),
+ array(
+ 'pattern' => '#^\x00\x00\xFF\xFE\x00\x00\x40\x00\x00\x00\x63\x00\x00\x00\x68\x00\x00\x00\x61\x00\x00\x00\x72\x00\x00\x00\x73\x00\x00\x00\x65\x00\x00\x00\x74\x00\x00\x00\x20\x00\x00\x00\x22\x00((?:\x00\x00[\x20-\x7F]\x00)*)\x00\x00\x22\x00\x00\x00\x3B\x00#',
+ 'charset' => null,
+ 'endianness' => '2143'
+ ),
+ array(
+ 'pattern' => '#^\x00\x00\x40\x00\x00\x00\x63\x00\x00\x00\x68\x00\x00\x00\x61\x00\x00\x00\x72\x00\x00\x00\x73\x00\x00\x00\x65\x00\x00\x00\x74\x00\x00\x00\x20\x00\x00\x00\x22\x00((?:\x00\x00[\x20-\x7F]\x00)*)\x00\x00\x22\x00\x00\x00\x3B\x00#',
+ 'charset' => null,
+ 'endianness' => '2143'
+ ),
+ array(
+ 'pattern' => '#^\xFE\xFF\x00\x00\x00\x40\x00\x00\x00\x63\x00\x00\x00\x68\x00\x00\x00\x61\x00\x00\x00\x72\x00\x00\x00\x73\x00\x00\x00\x65\x00\x00\x00\x74\x00\x00\x00\x20\x00\x00\x00\x22\x00\x00((?:\x00[\x20-\x7F]\x00\x00)*)\x00\x22\x00\x00\x00\x3B\x00\x00#',
+ 'charset' => null,
+ 'endianness' => '3412'
+ ),
+ array(
+ 'pattern' => '#^\x00\x40\x00\x00\x00\x63\x00\x00\x00\x68\x00\x00\x00\x61\x00\x00\x00\x72\x00\x00\x00\x73\x00\x00\x00\x65\x00\x00\x00\x74\x00\x00\x00\x20\x00\x00\x00\x22\x00\x00((?:\x00[\x20-\x7F]\x00\x00)*)\x00\x22\x00\x00\x00\x3B\x00\x00#',
+ 'charset' => null,
+ 'endianness' => '3412'
+ ),
+ array(
+ 'pattern' => '#^\xFF\xFE\x00\x00\x40\x00\x00\x00\x63\x00\x00\x00\x68\x00\x00\x00\x61\x00\x00\x00\x72\x00\x00\x00\x73\x00\x00\x00\x65\x00\x00\x00\x74\x00\x00\x00\x20\x00\x00\x00\x22\x00\x00\x00((?:[\x20-\x7F]\x00\x00\x00)*)\x22\x00\x00\x00\x3B\x00\x00\x00#',
+ 'charset' => null,
+ 'endianness' => 'LE'
+ ),
+ array(
+ 'pattern' => '#^\x40\x00\x00\x00\x63\x00\x00\x00\x68\x00\x00\x00\x61\x00\x00\x00\x72\x00\x00\x00\x73\x00\x00\x00\x65\x00\x00\x00\x74\x00\x00\x00\x20\x00\x00\x00\x22\x00\x00\x00((?:[\x20-\x7F]\x00\x00\x00)*)\x22\x00\x00\x00\x3B\x00\x00\x00#',
+ 'charset' => null,
+ 'endianness' => 'LE'
+ ),
+ array(
+ 'pattern' => '#^\x00\x00\xFE\xFF#',
+ 'charset' => 'UTF-32BE',
+ 'endianness' => null
+ ),
+ array(
+ 'pattern' => '#^\xFF\xFE\x00\x00#',
+ 'charset' => 'UTF-32LE',
+ 'endianness' => null
+ ),
+ array(
+ 'pattern' => '#^\x00\x00\xFF\xFE#',
+ 'charset' => 'UTF-32-2143',
+ 'endianness' => null
+ ),
+ array(
+ 'pattern' => '#^\xFE\xFF\x00\x00#',
+ 'charset' => 'UTF-32-3412',
+ 'endianness' => null
+ ),
+ array(
+ 'pattern' => '#^\xFE\xFF#',
+ 'charset' => "UTF-16BE",
+ 'endianness' => null
+ ),
+ array(
+ 'pattern' => '#^\xFF\xFE#',
+ 'charset' => 'UTF-16LE',
+ 'endianness' => null
+ ),
+ /**
+ * These encodings are not supported by mbstring extension.
+ **/
+ //array(
+ //'pattern' => '/^\x7C\x83\x88\x81\x99\xA2\x85\xA3\x40\x7F(YY)*\x7F\x5E/',
+ //'charset' => null,
+ //'endianness' => null,
+ //'transcoded-from' => 'EBCDIC'
+ //),
+ //array(
+ //'pattern' => '/^\xAE\x83\x88\x81\x99\xA2\x85\xA3\x40\xFC(YY)*\xFC\x5E/',
+ //'charset' => null,
+ //'endianness' => null,
+ //'transcoded-from' => 'IBM1026'
+ //),
+ //array(
+ //'pattern' => '/^\x00\x63\x68\x61\x72\x73\x65\x74\x20\x22(YY)*\x22\x3B/',
+ //'charset' => null,
+ //'endianness' => null
+ //'transcoded-from' => 'GSM 03.38'
+ //),
+ );
+
+ static function detectCharset($sText) {
+ $aSupportedEncodings = mb_list_encodings();
+ foreach (self::$CHARSET_DETECTION_MAP as $aCharsetMap) {
+ $sPattern = $aCharsetMap['pattern'];
+ $aMatches = array();
+ if(preg_match($sPattern, $sText, $aMatches)) {
+ if($aCharsetMap['charset']) {
+ $sCharset = $aCharsetMap['charset'];
+ } else {
+ $sCharset = $aMatches[1];
+ }
+ return $sCharset;
+ }
+ }
+ return false;
+ }
+
+ static function convert($sSubject, $sFromCharset, $sToCharset) {
+ return mb_convert_encoding($sSubject, $sToCharset, $sFromCharset);
+ //return iconv($sFromCharset, $sToCharset, $sSubject);
+ }
+
+ static function removeBOM($sText) {
+ $iLen = strlen($sText);
+ if($iLen > 3) {
+ switch ($sText[0]) {
+ case "\xEF":
+ if(("\xBB" == $sText[1]) && ("\xBF" == $sText[2])) {
+ // EF BB BF UTF-) encoded BOM
+ return substr($sText, 3);
+ }
+ break;
+ case "\xFE":
+ if (("\xFF" == $sText[1]) && ("\x00" == $sText[2]) && ("\x00" == $sText[3])) {
+ // FE FF 00 00 UCS-4, unusual octet order BOM (3412)
+ return substr($sText, 4);
+ } else if ("\xFF" == $sText[1]) {
+ // FE FF UTF-16, big endian BOM
+ return substr($sText, 2);
+ }
+ break;
+ case "\x00":
+ if (("\x00" == $sText[1]) && ("\xFE" == $sText[2]) && ("\xFF" == $sText[3])) {
+ // 00 00 FE FF UTF-32, big-endian BOM
+ return substr($sText, 4);
+ } else if (("\x00" == $sText[1]) && ("\xFF" == $sText[2]) && ("\xFE" == $sText[3])) {
+ // 00 00 FF FE UCS-4, unusual octet order BOM (2143)
+ return substr($sText, 4);
+ }
+ break;
+ case "\xFF":
+ if (("\xFE" == $sText[1]) && ("\x00" == $sText[2]) && ("\x00" == $sText[3])) {
+ // FF FE 00 00 UTF-32, little-endian BOM
+ return substr($sText, 4);
+ } else if ("\xFE" == $sText[1]) {
+ // FF FE UTF-16, little endian BOM
+ return substr($sText, 2);
+ }
+ break;
+ }
+ }
+ return $sText;
+ }
+
+ static function checkForBOM($sText) {
+ $iLen = strlen($sText);
+ if($iLen > 3) {
+ switch ($sText[0]) {
+ case "\xEF":
+ if(("\xBB" == $sText[1]) && ("\xBF" == $sText[2])) {
+ // EF BB BF UTF-) encoded BOM
+ return 'UTF-8';
+ }
+ break;
+ case "\xFE":
+ if (("\xFF" == $sText[1]) && ("\x00" == $sText[2]) && ("\x00" == $sText[3])) {
+ // FE FF 00 00 UCS-4, unusual octet order BOM (3412)
+ return "X-ISO-10646-UCS-4-3412";
+ } else if ("\xFF" == $sText[1]) {
+ // FE FF UTF-16, big endian BOM
+ return "UTF-16BE";
+ }
+ break;
+ case "\x00":
+ if (("\x00" == $sText[1]) && ("\xFE" == $sText[2]) && ("\xFF" == $sText[3])) {
+ // 00 00 FE FF UTF-32, big-endian BOM
+ return "UTF-32BE";
+ } else if (("\x00" == $sText[1]) && ("\xFF" == $sText[2]) && ("\xFE" == $sText[3])) {
+ // 00 00 FF FE UCS-4, unusual octet order BOM (2143)
+ return "X-ISO-10646-UCS-4-2143";
+ }
+ break;
+ case "\xFF":
+ if (("\xFE" == $sText[1]) && ("\x00" == $sText[2]) && ("\x00" == $sText[3])) {
+ // FF FE 00 00 UTF-32, little-endian BOM
+ return "UTF-32LE";
+ } else if ("\xFE" == $sText[1]) {
+ // FF FE UTF-16, little endian BOM
+ return "UTF-16LE";
+ }
+ break;
+ }
+ }
+ return false;
+ }
+
+ static function printBytes($sString, $iLen=null)
+ {
+ if($iLen == null) $iLen = strlen($sString);
+ $aBytes = array();
+ for($i = 0; $i < $iLen; $i++) {
+ $aBytes[] = "0x".dechex(ord($sString[$i]));
+ }
+ return implode(' ', $aBytes);
+ }
+
+}
diff --git a/lib/CSSList.php b/lib/CSSList.php
index b89d9e65..bf3be0f2 100644
--- a/lib/CSSList.php
+++ b/lib/CSSList.php
@@ -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 = '';
@@ -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) {
diff --git a/lib/CSSProperties.php b/lib/CSSProperties.php
index 15c9edbd..28f6a498 100644
--- a/lib/CSSProperties.php
+++ b/lib/CSSProperties.php
@@ -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).';';
@@ -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() {
diff --git a/lib/CSSUrlUtils.php b/lib/CSSUrlUtils.php
new file mode 100644
index 00000000..10f90600
--- /dev/null
+++ b/lib/CSSUrlUtils.php
@@ -0,0 +1,145 @@
+ the charset of the response as specified by the
+ * HTTP Content-Type header, if specified
+ * 'response' => the response body
+ **/
+ static public function loadURL($sURL) {
+ $rCurl = curl_init();
+ curl_setopt($rCurl, CURLOPT_URL, $sURL);
+ //curl_setopt($rCurl, CURLOPT_HEADER, true);
+ curl_setopt($rCurl, CURLOPT_ENCODING, 'deflate,gzip');
+ curl_setopt($rCurl, CURLOPT_FOLLOWLOCATION, true);
+ curl_setopt($rCurl, CURLOPT_USERAGENT, 'PHP-CSS-Parser v0.1');
+ curl_setopt($rCurl, CURLOPT_RETURNTRANSFER, true);
+ $mResponse = curl_exec($rCurl);
+ $aInfos = curl_getinfo($rCurl);
+ curl_close($rCurl);
+ if(false === $mResponse) return false;
+ $aResult = array(
+ 'charset' => null,
+ 'response' => $mResponse
+ );
+ if($aInfos['content_type']) {
+ if(preg_match('/charset=([a-zA-Z0-9-]*)/', $aInfos['content_type'], $aMatches)) {
+ $aResult['charset'] = $aMatches[0];
+ }
+ }
+ return $aResult;
+ }
+
+ /**
+ * CSSUrlUtils::joinPaths( string $head, string $tail [, string $...] )
+ *
+ * @param string $head the head component of the path
+ * @param string $tail at least one path component
+ * @returns string the resulting path
+ **/
+ static public function joinPaths() {
+ $num_args = func_num_args();
+ if($num_args < 1) return '';
+ $args = func_get_args();
+ if($num_args == 1) return rtrim($args[0], DIRECTORY_SEPARATOR);
+
+ $head = array_shift($args);
+ $head = rtrim($head, DIRECTORY_SEPARATOR);
+ $output = array($head);
+ foreach ($args as $arg) {
+ $output[] = trim($arg, DIRECTORY_SEPARATOR);
+ }
+ return implode(DIRECTORY_SEPARATOR, $output);
+ }
+
+ /**
+ * Returns boolean based on whether given path is absolute or not.
+ *
+ * @param string $path Given path
+ * @return boolean True if the path is absolute, false if it is not
+ */
+ static public function isAbsPath($sPath) {
+ if (preg_match('#(?:/|\\\)\.\.(?=/|$)#', $sPath)) {
+ return false;
+ }
+ if (OS_WIN32) {
+ return (($sPath[0] == '/') || preg_match('#^[a-zA-Z]:(\\\|/)#', $sPath));
+ }
+ return ($sPath[0] == '/') || ($sPath[0] == '~');
+ }
+
+ /**
+ * Tests if an URL is absolute
+ *
+ * @param string $sURL
+ * @return boolean
+ **/
+ static public function isAbsURL($sURL) {
+ return preg_match('#^(http|https|ftp)://#', $sURL);
+ }
+
+ /**
+ * Returns the parent path of an URL or path
+ *
+ * @param string $sURL an URL
+ * @returns string an URL
+ **/
+ static public function dirname($sURL) {
+ $aURL = parse_url($sURL);
+ if(isset($aURL['path'])) {
+ $sPath = dirname($aURL['path']);
+ if($sPath == '/') {
+ unset($aURL['path']);
+ } else {
+ $aURL['path'] = $sPath;
+ }
+ }
+ return self::buildURL($aURL);
+ }
+
+ /**
+ * Builds an URL from an array of URL parts
+ *
+ * @param array $aURL URL parts in the format returned by parse_url
+ * @return string the builded URL
+ * @see http://php.net/manual/function.parse-url.php
+ **/
+ static public function buildURL(array $aURL)
+ {
+ $sURL = '';
+ if(isset($aURL['scheme'])) {
+ $sURL .= $aURL['scheme'] . '://';
+ }
+ if(isset($aURL['user'])) {
+ $sURL .= $aURL['user'];
+ if(isset($aURL['pass'])) {
+ $sURL .= ':' . $aURL['pass'];
+ }
+ $sURL .= '@';
+ }
+ if(isset($aURL['host'])) {
+ $sURL .= $aURL['host'];
+ }
+ if(isset($aURL['port'])) {
+ $sURL .= ':' . $aURL['port'];
+ }
+ if(isset($aURL['path'])) {
+ if(strpos($aURL['path'], '/') !== 0) $sURL .= '/';
+ $sURL .= $aURL['path'];
+ }
+ if(isset($aURL['query'])) {
+ $sURL .= '?' . $aURL['query'];
+ }
+ if(isset($aURL['fragment'])) {
+ $sURL .= '#' . $aURL['fragment'];
+ }
+ return $sURL;
+ }
+}
diff --git a/lib/CSSValue.php b/lib/CSSValue.php
index 020cd684..458d9eb8 100644
--- a/lib/CSSValue.php
+++ b/lib/CSSValue.php
@@ -8,6 +8,19 @@ abstract class CSSPrimitiveValue extends CSSValue {
}
+class CSSIgnoredValue extends CSSValue {
+ private $mValue;
+ public function __construct($mValue) {
+ $this->mValue = $mValue;
+ }
+ public function getValue() {
+ return $this->mValue;
+ }
+ public function __toString() {
+ return $this->mValue->__toString();
+ }
+}
+
class CSSSize extends CSSPrimitiveValue {
private $fSize;
private $sUnit;
@@ -45,7 +58,7 @@ public function isColorComponent() {
*/
public function isSize() {
$aNonSizeUnits = array('deg', 'grad', 'rad', 'turns', 's', 'ms', 'Hz', 'kHz');
- if(in_array($this->sUnit), $aNonSizeUnits) {
+ if(in_array($this->sUnit, $aNonSizeUnits)) {
return false;
}
return !$this->isColorComponent();
diff --git a/tests/CSSDeclarationBlockTest.php b/tests/CSSDeclarationBlockTest.php
index 550db934..4aa1f9b5 100644
--- a/tests/CSSDeclarationBlockTest.php
+++ b/tests/CSSDeclarationBlockTest.php
@@ -10,8 +10,8 @@ class CSSDeclarationBlockTest extends PHPUnit_Framework_TestCase
**/
public function testExpandBorderShorthand($sCss, $sExpected)
{
- $oParser = new CSSParser($sCss);
- $oDoc = $oParser->parse();
+ $oParser = new CSSParser();
+ $oDoc = $oParser->parseString($sCss);
foreach($oDoc->getAllDeclarationBlocks() as $oDeclaration)
{
$oDeclaration->expandBorderShorthand();
@@ -35,8 +35,8 @@ public function expandBorderShorthandProvider()
**/
public function testExpandFontShorthand($sCss, $sExpected)
{
- $oParser = new CSSParser($sCss);
- $oDoc = $oParser->parse();
+ $oParser = new CSSParser();
+ $oDoc = $oParser->parseString($sCss);
foreach($oDoc->getAllDeclarationBlocks() as $oDeclaration)
{
$oDeclaration->expandFontShorthand();
@@ -78,8 +78,8 @@ public function expandFontShorthandProvider()
**/
public function testExpandBackgroundShorthand($sCss, $sExpected)
{
- $oParser = new CSSParser($sCss);
- $oDoc = $oParser->parse();
+ $oParser = new CSSParser();
+ $oDoc = $oParser->parseString($sCss);
foreach($oDoc->getAllDeclarationBlocks() as $oDeclaration)
{
$oDeclaration->expandBackgroundShorthand();
@@ -103,8 +103,8 @@ public function expandBackgroundShorthandProvider()
**/
public function testExpandDimensionsShorthand($sCss, $sExpected)
{
- $oParser = new CSSParser($sCss);
- $oDoc = $oParser->parse();
+ $oParser = new CSSParser();
+ $oDoc = $oParser->parseString($sCss);
foreach($oDoc->getAllDeclarationBlocks() as $oDeclaration)
{
$oDeclaration->expandDimensionsShorthand();
@@ -127,8 +127,8 @@ public function expandDimensionsShorthandProvider()
**/
public function testCreateBorderShorthand($sCss, $sExpected)
{
- $oParser = new CSSParser($sCss);
- $oDoc = $oParser->parse();
+ $oParser = new CSSParser();
+ $oDoc = $oParser->parseString($sCss);
foreach($oDoc->getAllDeclarationBlocks() as $oDeclaration)
{
$oDeclaration->createBorderShorthand();
@@ -150,8 +150,8 @@ public function createBorderShorthandProvider()
**/
public function testCreateFontShorthand($sCss, $sExpected)
{
- $oParser = new CSSParser($sCss);
- $oDoc = $oParser->parse();
+ $oParser = new CSSParser();
+ $oDoc = $oParser->parseString($sCss);
foreach($oDoc->getAllDeclarationBlocks() as $oDeclaration)
{
$oDeclaration->createFontShorthand();
@@ -175,8 +175,8 @@ public function createFontShorthandProvider()
**/
public function testCreateDimensionsShorthand($sCss, $sExpected)
{
- $oParser = new CSSParser($sCss);
- $oDoc = $oParser->parse();
+ $oParser = new CSSParser();
+ $oDoc = $oParser->parseString($sCss);
foreach($oDoc->getAllDeclarationBlocks() as $oDeclaration)
{
$oDeclaration->createDimensionsShorthand();
@@ -199,8 +199,8 @@ public function createDimensionsShorthandProvider()
**/
public function testCreateBackgroundShorthand($sCss, $sExpected)
{
- $oParser = new CSSParser($sCss);
- $oDoc = $oParser->parse();
+ $oParser = new CSSParser();
+ $oDoc = $oParser->parseString($sCss);
foreach($oDoc->getAllDeclarationBlocks() as $oDeclaration)
{
$oDeclaration->createBackgroundShorthand();
diff --git a/tests/CSSImportTest.php b/tests/CSSImportTest.php
new file mode 100644
index 00000000..59dcafef
--- /dev/null
+++ b/tests/CSSImportTest.php
@@ -0,0 +1,59 @@
+ true,
+ 'base_url' => $sBaseUrl
+ ));
+ $oDoc = $parser->parseString($sCSS);
+ $this->assertEquals($oDoc->__toString(), $sExpected);
+ }
+ public function testAbsoluteUrlsProvider()
+ {
+ return array(
+ array(
+ '@import "styles.css";body{background: url("image.jpg")}',
+ 'http://foobar.biz',
+ '@import url("http://foobar.biz/styles.css");body {background: url("http://foobar.biz/image.jpg");}'
+ ),
+ array(
+ '@import "sub/styles.css";body{background: url("/root/image.jpg")}',
+ 'http://foobar.biz',
+ '@import url("http://foobar.biz/sub/styles.css");body {background: url("http://foobar.biz/root/image.jpg");}'
+ ),
+ array(
+ '@import "sub/styles.css";body{background: url("/root/image.jpg")}',
+ '/home/user/www',
+ '@import url("/home/user/www/sub/styles.css");body {background: url("/root/image.jpg");}'
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider testResolveImportsProvider
+ **/
+ public function testResolveImports($sFile, $sExpected)
+ {
+ $oParser = new CSSParser(array('resolve_imports' => true));
+ $oDoc = $oParser->parseFile($sFile);
+ $this->assertEquals((string)$oDoc, $sExpected);
+ }
+ public function testResolveImportsProvider()
+ {
+ return array(
+ array(
+ dirname(__FILE__)."/files/import.css",
+ '@font-face {font-family: "CrassRoots";src: url("/home/ju1ius/code/php/third-party/PHP-CSS-Parser/tests/files/../media/cr.ttf");}html, body {font-size: 1.6em;}header {width: 618px;height: 120px;}.euc-jp {content: "もっと強く";}body.im_utf-32 {color: green;}div.im_utf-16 {background: url("/home/ju1ius/code/php/third-party/PHP-CSS-Parser/tests/files/import/barfoo.png");}body#icomelast {color: fuschia;padding: 5px;background-image: url("http://foobar.com");}'
+ )
+ );
+ }
+
+}
diff --git a/tests/CSSParserTests.php b/tests/CSSParserTest.php
similarity index 97%
rename from tests/CSSParserTests.php
rename to tests/CSSParserTest.php
index 3d461c77..e659123f 100644
--- a/tests/CSSParserTests.php
+++ b/tests/CSSParserTest.php
@@ -4,7 +4,7 @@
require_once(dirname(__FILE__).'/../CSSParser.php');
-class CSSParserTests extends PHPUnit_Framework_TestCase {
+class CSSParserTest extends PHPUnit_Framework_TestCase {
function testCssFiles() {
$sDirectory = dirname(__FILE__).DIRECTORY_SEPARATOR.'files';
@@ -21,9 +21,10 @@ function testCssFiles() {
//Either a file which SHOULD fail or a future test of a as-of-now missing feature
continue;
}
- $oParser = new CSSParser(file_get_contents($sDirectory.DIRECTORY_SEPARATOR.$sFileName));
+ $oParser = new CSSParser();
+ $oDoc = $oParser->parseString(file_get_contents($sDirectory.DIRECTORY_SEPARATOR.$sFileName));
try {
- $this->assertNotEquals('', $oParser->parse()->__toString());
+ $this->assertNotEquals('', $oDoc->__toString());
} catch(Exception $e) {
$this->fail($e);
}
@@ -236,7 +237,7 @@ function testCreateShorthands() {
function parsedStructureForFile($sFileName) {
$sFile = dirname(__FILE__).DIRECTORY_SEPARATOR.'files'.DIRECTORY_SEPARATOR."$sFileName.css";
- $oParser = new CSSParser(file_get_contents($sFile));
- return $oParser->parse();
+ $oParser = new CSSParser();
+ return $oParser->parseString(file_get_contents($sFile));
}
}
diff --git a/tests/files/atrules.css b/tests/files/atrules.css
index adfa9f99..f6233831 100644
--- a/tests/files/atrules.css
+++ b/tests/files/atrules.css
@@ -6,5 +6,5 @@
}
html, body {
- font-size: 1.6em
+ font-size: 1.6em;
}
diff --git a/tests/files/import.css b/tests/files/import.css
new file mode 100644
index 00000000..5efed294
--- /dev/null
+++ b/tests/files/import.css
@@ -0,0 +1,10 @@
+@import "imported.css";
+@import "import/utf-16.css";
+
+body#icomelast{
+ color: fuschia;
+ padding: 5px;
+ background-image: url(http://foobar.com);
+}
+
+@import "nope.css";
diff --git a/tests/files/import/subdir/euc-jp.css b/tests/files/import/subdir/euc-jp.css
new file mode 100644
index 00000000..c4d3d40a
--- /dev/null
+++ b/tests/files/import/subdir/euc-jp.css
@@ -0,0 +1,5 @@
+@charset "EUC-JP";
+
+.euc-jp {
+ content: "äȶ";
+}
diff --git a/tests/files/import/subdir/utf-32.css b/tests/files/import/subdir/utf-32.css
new file mode 100644
index 00000000..3dd69ed3
Binary files /dev/null and b/tests/files/import/subdir/utf-32.css differ
diff --git a/tests/files/import/utf-16.css b/tests/files/import/utf-16.css
new file mode 100644
index 00000000..056cb1ab
Binary files /dev/null and b/tests/files/import/utf-16.css differ
diff --git a/tests/files/imported.css b/tests/files/imported.css
new file mode 100644
index 00000000..2da8342a
--- /dev/null
+++ b/tests/files/imported.css
@@ -0,0 +1,6 @@
+@import "atrules.css";
+
+header{
+ width: 618px;
+ height: 120px;
+}