PDF rausgenommen

This commit is contained in:
aschwarz
2023-01-23 11:03:31 +01:00
parent 82d562a322
commit a6523903eb
28078 changed files with 4247552 additions and 2 deletions

View File

@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit827e61deccfa9f79cdf08e64e91986f6::getLoader();

View File

@ -0,0 +1,445 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see http://www.php-fig.org/psr/psr-0/
* @see http://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
private $classMapAuthoritative = false;
private $missingClasses = array();
private $apcuPrefix;
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', $this->prefixesPsr0);
}
return array();
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*/
function includeFile($file)
{
include $file;
}

View File

@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,13 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'FasterImage\\Exception\\InvalidImageException' => $vendorDir . '/fasterimage/fasterimage/src/FasterImage/Exception/InvalidImageException.php',
'FasterImage\\ExifParser' => $vendorDir . '/fasterimage/fasterimage/src/FasterImage/ExifParser.php',
'FasterImage\\FasterImage' => $vendorDir . '/fasterimage/fasterimage/src/FasterImage/FasterImage.php',
'FasterImage\\ImageParser' => $vendorDir . '/fasterimage/fasterimage/src/FasterImage/ImageParser.php',
);

View File

@ -0,0 +1,10 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Sabberworm\\CSS' => array($vendorDir . '/sabberworm/php-css-parser/lib'),
);

View File

@ -0,0 +1,12 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'cweagans\\Composer\\' => array($vendorDir . '/cweagans/composer-patches/src'),
'WillWashburn\\' => array($vendorDir . '/willwashburn/stream/src'),
'Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\' => array($vendorDir . '/dealerdirect/phpcodesniffer-composer-installer/src'),
);

View File

@ -0,0 +1,52 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit827e61deccfa9f79cdf08e64e91986f6
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit827e61deccfa9f79cdf08e64e91986f6', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit827e61deccfa9f79cdf08e64e91986f6', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit827e61deccfa9f79cdf08e64e91986f6::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
$loader->register(true);
return $loader;
}
}

View File

@ -0,0 +1,66 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit827e61deccfa9f79cdf08e64e91986f6
{
public static $prefixLengthsPsr4 = array (
'c' =>
array (
'cweagans\\Composer\\' => 18,
),
'W' =>
array (
'WillWashburn\\' => 13,
),
'D' =>
array (
'Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\' => 55,
),
);
public static $prefixDirsPsr4 = array (
'cweagans\\Composer\\' =>
array (
0 => __DIR__ . '/..' . '/cweagans/composer-patches/src',
),
'WillWashburn\\' =>
array (
0 => __DIR__ . '/..' . '/willwashburn/stream/src',
),
'Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\' =>
array (
0 => __DIR__ . '/..' . '/dealerdirect/phpcodesniffer-composer-installer/src',
),
);
public static $prefixesPsr0 = array (
'S' =>
array (
'Sabberworm\\CSS' =>
array (
0 => __DIR__ . '/..' . '/sabberworm/php-css-parser/lib',
),
),
);
public static $classMap = array (
'FasterImage\\Exception\\InvalidImageException' => __DIR__ . '/..' . '/fasterimage/fasterimage/src/FasterImage/Exception/InvalidImageException.php',
'FasterImage\\ExifParser' => __DIR__ . '/..' . '/fasterimage/fasterimage/src/FasterImage/ExifParser.php',
'FasterImage\\FasterImage' => __DIR__ . '/..' . '/fasterimage/fasterimage/src/FasterImage/FasterImage.php',
'FasterImage\\ImageParser' => __DIR__ . '/..' . '/fasterimage/fasterimage/src/FasterImage/ImageParser.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit827e61deccfa9f79cdf08e64e91986f6::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit827e61deccfa9f79cdf08e64e91986f6::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInit827e61deccfa9f79cdf08e64e91986f6::$prefixesPsr0;
$loader->classMap = ComposerStaticInit827e61deccfa9f79cdf08e64e91986f6::$classMap;
}, null, ClassLoader::class);
}
}

View File

@ -0,0 +1,470 @@
[
{
"name": "cweagans/composer-patches",
"version": "1.6.5",
"version_normalized": "1.6.5.0",
"source": {
"type": "git",
"url": "https://github.com/cweagans/composer-patches.git",
"reference": "2ec4f00ff5fb64de584c8c4aea53bf9053ecb0b3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cweagans/composer-patches/zipball/2ec4f00ff5fb64de584c8c4aea53bf9053ecb0b3",
"reference": "2ec4f00ff5fb64de584c8c4aea53bf9053ecb0b3",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.0",
"php": ">=5.3.0"
},
"require-dev": {
"composer/composer": "~1.0",
"phpunit/phpunit": "~4.6"
},
"time": "2018-05-11T18:00:16+00:00",
"type": "composer-plugin",
"extra": {
"class": "cweagans\\Composer\\Patches"
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"cweagans\\Composer\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Cameron Eagans",
"email": "me@cweagans.net"
}
],
"description": "Provides a way to patch Composer packages."
},
{
"name": "dealerdirect/phpcodesniffer-composer-installer",
"version": "v0.5.0",
"version_normalized": "0.5.0.0",
"source": {
"type": "git",
"url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git",
"reference": "e749410375ff6fb7a040a68878c656c2e610b132"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/e749410375ff6fb7a040a68878c656c2e610b132",
"reference": "e749410375ff6fb7a040a68878c656c2e610b132",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.0",
"php": "^5.3|^7",
"squizlabs/php_codesniffer": "^2|^3"
},
"require-dev": {
"composer/composer": "*",
"phpcompatibility/php-compatibility": "^9.0",
"sensiolabs/security-checker": "^4.1.0"
},
"time": "2018-10-26T13:21:45+00:00",
"type": "composer-plugin",
"extra": {
"class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin"
},
"installation-source": "source",
"autoload": {
"psr-4": {
"Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Franck Nijhof",
"email": "franck.nijhof@dealerdirect.com",
"homepage": "http://www.frenck.nl",
"role": "Developer / IT Manager"
}
],
"description": "PHP_CodeSniffer Standards Composer Installer Plugin",
"homepage": "http://www.dealerdirect.com",
"keywords": [
"PHPCodeSniffer",
"PHP_CodeSniffer",
"code quality",
"codesniffer",
"composer",
"installer",
"phpcs",
"plugin",
"qa",
"quality",
"standard",
"standards",
"style guide",
"stylecheck",
"tests"
]
},
{
"name": "fasterimage/fasterimage",
"version": "v1.4.0",
"version_normalized": "1.4.0.0",
"source": {
"type": "git",
"url": "https://github.com/willwashburn/fasterimage.git",
"reference": "2a85079bef8090c2820bcad5c649cdc41f7b53ab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/willwashburn/fasterimage/zipball/2a85079bef8090c2820bcad5c649cdc41f7b53ab",
"reference": "2a85079bef8090c2820bcad5c649cdc41f7b53ab",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"willwashburn/stream": ">=1.0"
},
"require-dev": {
"mockery/mockery": "~0.9",
"phpunit/phpunit": "~4.0"
},
"time": "2019-03-07T05:20:21+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"classmap": [
"src"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Will Washburn",
"email": "will@tailwindapp.com"
}
],
"description": "FasterImage finds the size or type of a set of images given their uris by fetching as little as needed, in parallel. Originally ported by Tom Moor.",
"homepage": "https://github.com/willwashburn/fasterimage",
"keywords": [
"fast image",
"faster image",
"fasterimage",
"fastimage",
"getimagesize",
"image size",
"parallel"
]
},
{
"name": "phpcompatibility/php-compatibility",
"version": "9.1.1",
"version_normalized": "9.1.1.0",
"source": {
"type": "git",
"url": "https://github.com/PHPCompatibility/PHPCompatibility.git",
"reference": "2b63c5d284ab8857f7b1d5c240ddb507a6b2293c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/2b63c5d284ab8857f7b1d5c240ddb507a6b2293c",
"reference": "2b63c5d284ab8857f7b1d5c240ddb507a6b2293c",
"shasum": ""
},
"require": {
"php": ">=5.3",
"squizlabs/php_codesniffer": "^2.3 || ^3.0.2"
},
"conflict": {
"squizlabs/php_codesniffer": "2.6.2"
},
"require-dev": {
"phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0"
},
"suggest": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.4.3 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.",
"roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues."
},
"time": "2018-12-30T23:16:27+00:00",
"type": "phpcodesniffer-standard",
"installation-source": "dist",
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-3.0-or-later"
],
"authors": [
{
"name": "Contributors",
"homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors"
},
{
"name": "Wim Godden",
"homepage": "https://github.com/wimg",
"role": "lead"
},
{
"name": "Juliette Reinders Folmer",
"homepage": "https://github.com/jrfnl",
"role": "lead"
}
],
"description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.",
"homepage": "http://techblog.wimgodden.be/tag/codesniffer/",
"keywords": [
"compatibility",
"phpcs",
"standards"
]
},
{
"name": "sabberworm/php-css-parser",
"version": "8.3.0",
"version_normalized": "8.3.0.0",
"source": {
"type": "git",
"url": "https://github.com/sabberworm/PHP-CSS-Parser.git",
"reference": "91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sabberworm/PHP-CSS-Parser/zipball/91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f",
"reference": "91bcc3e3fdb7386c9a2e0e0aa09ca75cc43f121f",
"shasum": ""
},
"require": {
"php": ">=5.3.2"
},
"require-dev": {
"codacy/coverage": "^1.4",
"phpunit/phpunit": "~4.8"
},
"time": "2019-02-22T07:42:52+00:00",
"type": "library",
"extra": {
"patches_applied": {
"PHP-CSS-Parser: Fix parsing CSS selectors which contain commas <https://github.com/sabberworm/PHP-CSS-Parser/pull/138>": "patches/php-css-parser-mods.diff"
}
},
"installation-source": "source",
"autoload": {
"psr-0": {
"Sabberworm\\CSS": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Raphael Schweikert"
}
],
"description": "Parser for CSS Files written in PHP",
"homepage": "http://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"keywords": [
"css",
"parser",
"stylesheet"
]
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.4.2",
"version_normalized": "3.4.2.0",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8",
"reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8",
"shasum": ""
},
"require": {
"ext-simplexml": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"time": "2019-04-10T23:49:02+00:00",
"bin": [
"bin/phpcs",
"bin/phpcbf"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"installation-source": "source",
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Greg Sherwood",
"role": "lead"
}
],
"description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
"homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
"keywords": [
"phpcs",
"standards"
]
},
{
"name": "willwashburn/stream",
"version": "v1.0.0",
"version_normalized": "1.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/willwashburn/stream.git",
"reference": "345b3062493e3899d987dbdd1fec1c13ee28c903"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/willwashburn/stream/zipball/345b3062493e3899d987dbdd1fec1c13ee28c903",
"reference": "345b3062493e3899d987dbdd1fec1c13ee28c903",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"mockery/mockery": "~0.9",
"phpunit/phpunit": "~4.0"
},
"time": "2016-03-15T10:54:35+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"WillWashburn\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Will Washburn",
"email": "will.washburn@gmail.com"
}
],
"description": "model a sequence of data elements made available over time ",
"homepage": "https://github.com/willwashburn/stream",
"keywords": [
"peek",
"read",
"stream",
"streamable"
]
},
{
"name": "wp-coding-standards/wpcs",
"version": "2.1.0",
"version_normalized": "2.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git",
"reference": "8c7a2e7682de9ef5955251874b639deda51ef470"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/WordPress-Coding-Standards/WordPress-Coding-Standards/zipball/8c7a2e7682de9ef5955251874b639deda51ef470",
"reference": "8c7a2e7682de9ef5955251874b639deda51ef470",
"shasum": ""
},
"require": {
"php": ">=5.4",
"squizlabs/php_codesniffer": "^3.3.1"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.5.0",
"phpcompatibility/php-compatibility": "^9.0",
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"suggest": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.5.0 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically."
},
"time": "2019-04-08T10:53:57+00:00",
"type": "phpcodesniffer-standard",
"installation-source": "source",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Contributors",
"homepage": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/graphs/contributors"
}
],
"description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions",
"keywords": [
"phpcs",
"standards",
"wordpress"
]
},
{
"name": "xwp/wp-dev-lib",
"version": "1.1.1",
"version_normalized": "1.1.1.0",
"source": {
"type": "git",
"url": "https://github.com/xwp/wp-dev-lib.git",
"reference": "4caaf0f554778bb59062c8797755999b5fae400d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/xwp/wp-dev-lib/zipball/4caaf0f554778bb59062c8797755999b5fae400d",
"reference": "4caaf0f554778bb59062c8797755999b5fae400d",
"shasum": ""
},
"time": "2019-04-06T06:37:44+00:00",
"type": "library",
"installation-source": "dist",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Weston Ruter",
"email": "weston@xwp.co",
"homepage": "https://weston.ruter.net"
},
{
"name": "XWP",
"email": "engage@xwp.co",
"homepage": "https://xwp.co"
}
],
"description": "Common code used during development of WordPress plugins and themes",
"homepage": "https://github.com/xwp/wp-dev-lib",
"keywords": [
"wordpress"
]
}
]

View File

@ -0,0 +1,8 @@
<?php namespace FasterImage\Exception;
/**
* Class InvalidImageException
*
* @package FasterImage\Exception
*/
class InvalidImageException extends \Exception {}

View File

@ -0,0 +1,134 @@
<?php namespace FasterImage;
use FasterImage\Exception\InvalidImageException;
use WillWashburn\Stream\StreamableInterface;
/**
* Class ExifParser
*
* @package FasterImage
*/
class ExifParser
{
/**
* @var int
*/
protected $width;
/**
* @var int
*/
protected $height;
/**
* @var
*/
protected $short;
/**
* @var
*/
protected $long;
/**
* @var StreamableInterface
*/
protected $stream;
/**
* @var int
*/
protected $orientation;
/**
* ExifParser constructor.
*
* @param StreamableInterface $stream
*/
public function __construct(StreamableInterface $stream)
{
$this->stream = $stream;
$this->parseExifIfd();
}
/**
* @return int
*/
public function getHeight()
{
return $this->height;
}
/**
* @return int
*/
public function getWidth()
{
return $this->width;
}
/**
* @return bool
*/
public function isRotated()
{
return (! empty($this->orientation) && $this->orientation >= 5 && $this->orientation <= 8);
}
/**
* @return bool
* @throws \FasterImage\Exception\InvalidImageException
*/
protected function parseExifIfd()
{
$byte_order = $this->stream->read(2);
switch ( $byte_order ) {
case 'II':
$this->short = 'v';
$this->long = 'V';
break;
case 'MM':
$this->short = 'n';
$this->long = 'N';
break;
default:
throw new InvalidImageException;
break;
}
$this->stream->read(2);
$offset = current(unpack($this->long, $this->stream->read(4)));
$this->stream->read($offset - 8);
$tag_count = current(unpack($this->short, $this->stream->read(2)));
for ( $i = $tag_count; $i > 0; $i-- ) {
$type = current(unpack($this->short, $this->stream->read(2)));
$this->stream->read(6);
$data = current(unpack($this->short, $this->stream->read(2)));
switch ( $type ) {
case 0x0100:
$this->width = $data;
break;
case 0x0101:
$this->height = $data;
break;
case 0x0112:
$this->orientation = $data;
break;
}
if ( isset($this->width) && isset($this->height) && isset($this->orientation) ) {
return true;
}
$this->stream->read(2);
}
return false;
}
}

View File

@ -0,0 +1,280 @@
<?php namespace FasterImage;
use FasterImage\Exception\InvalidImageException;
use WillWashburn\Stream\Exception\StreamBufferTooSmallException;
use WillWashburn\Stream\Stream;
/**
* FasterImage - Because sometimes you just want the size, and you want them in
* parallel!
*
* Based on the PHP stream implementation by Tom Moor (http://tommoor.com)
* which was based on the original Ruby Implementation by Steven Sykes
* (https://github.com/sdsykes/fastimage)
*
* MIT Licensed
*/
class FasterImage
{
/**
* The default timeout.
*
* @var int
*/
protected $timeout = 10;
/**
* The default buffer size.
*
* @var int
*/
protected $bufferSize = 256;
/**
* The default for whether to verify SSL peer.
*
* @var bool
*/
protected $sslVerifyPeer = false;
/**
* The default for whether to verify SSL host.
*
* @var bool
*/
protected $sslVerifyHost = false;
/**
* If the content length should be included in the result set.
*
* @var bool
*/
protected $includeContentLength = false;
/**
* The default user agent to set for requests.
*
* @var string
*/
protected $userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36';
/**
* Get the size of each of the urls in a list
*
* @param array $urls
*
* @return array
* @throws \Exception
*/
public function batch(array $urls)
{
$multi = curl_multi_init();
$results = array();
// Create the curl handles and add them to the multi_request
foreach ( array_values($urls) as $count => $uri ) {
$results[$uri] = [];
$$count = $this->handle($uri, $results[$uri]);
$code = curl_multi_add_handle($multi, $$count);
if ( $code != CURLM_OK ) {
throw new \Exception("Curl handle for $uri could not be added");
}
}
// Perform the requests
do {
while ( ($mrc = curl_multi_exec($multi, $active)) == CURLM_CALL_MULTI_PERFORM ) ;
if ( $mrc != CURLM_OK && $mrc != CURLM_CALL_MULTI_PERFORM ) {
throw new \Exception("Curl error code: $mrc");
}
if ( $active && curl_multi_select($multi) === -1 ) {
// Perform a usleep if a select returns -1.
// See: https://bugs.php.net/bug.php?id=61141
usleep(250);
}
} while ( $active );
// Figure out why individual requests may have failed
foreach ( array_values($urls) as $count => $uri ) {
$error = curl_error($$count);
if ( $error ) {
$results[$uri]['failure_reason'] = $error;
}
}
return $results;
}
/**
* @param int $seconds
*/
public function setTimeout($seconds)
{
$this->timeout = (int) $seconds;
}
/**
* @param int $bufferSize
*/
public function setBufferSize($bufferSize)
{
$this->bufferSize = (int) $bufferSize;
}
/**
* @param bool $sslVerifyPeer
*/
public function setSslVerifyPeer($sslVerifyPeer)
{
$this->sslVerifyPeer = (bool) $sslVerifyPeer;
}
/**
* @param bool $sslVerifyHost
*/
public function setSslVerifyHost($sslVerifyHost)
{
$this->sslVerifyHost = (bool) $sslVerifyHost;
}
/**
* @param bool $bool
*/
public function setIncludeContentLength($bool)
{
$this->includeContentLength = (bool) $bool;
}
/**
* @param string $userAgent
*/
public function setUserAgent($userAgent)
{
$this->userAgent = $userAgent;
}
/**
* Create the handle for the curl request
*
* @param $url
* @param $result
*
* @return resource
*/
protected function handle($url, & $result)
{
$stream = new Stream();
$parser = new ImageParser($stream);
$result['rounds'] = 0;
$result['bytes'] = 0;
$result['size'] = 'failed';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_BUFFERSIZE, $this->bufferSize);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->sslVerifyPeer ? 1 : 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $this->sslVerifyHost ? 2 : 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
# Some web servers require the useragent to be not a bot. So we are liars.
curl_setopt($ch, CURLOPT_USERAGENT, $this->userAgent);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Accept: image/webp,image/*,*/*;q=0.8",
"Cache-Control: max-age=0",
"Connection: keep-alive",
"Keep-Alive: 300",
"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7",
"Accept-Language: en-us,en;q=0.5",
"Pragma: ", // browsers keep this blank.
]);
curl_setopt($ch, CURLOPT_ENCODING, "");
/*
* We parse the headers to find the content-length. This is added to the
* result array and can be useful to determine the overall image filesize.
*/
if ($this->includeContentLength) {
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$result) {
$len = strlen($header);
$header = explode(':', $header, 2);
if ( strtolower($header[0]) === 'content-length' ) {
$result['content-length'] = trim($header[1]);
}
return $len;
});
}
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $str) use (& $result, & $parser, & $stream, $url) {
$result['rounds']++;
$result['bytes'] += strlen($str);
$stream->write($str);
try {
// store the type in the result array by looking at the bits
$result['type'] = $parser->parseType();
/*
* We try here to parse the buffer of characters we already have
* for the size.
*/
$result['size'] = $parser->parseSize() ?: 'failed';
}
catch (StreamBufferTooSmallException $e) {
/*
* If this exception is thrown, we don't have enough of the stream buffered
* so in order to tell curl to keep streaming we need to return the number
* of bytes we have already handled
*
* We set the 'size' to 'failed' in the case that we've done
* the entire image and we couldn't figure it out. Otherwise
* it'll get overwritten with the next round.
*/
$result['size'] = 'failed';
return strlen($str);
}
catch (InvalidImageException $e) {
/*
* This means we've determined that we're lost and don't know
* how to parse this image.
*
* We set the size to invalid and move on
*/
$result['size'] = 'invalid';
return -1;
}
/*
* We return -1 to abort the transfer when we have enough buffered
* to find the size
*/
//
// hey curl! this is an error. But really we just are stopping cause
// we already have what we wwant
return -1;
});
return $ch;
}
}

View File

@ -0,0 +1,449 @@
<?php namespace FasterImage;
use WillWashburn\Stream\Exception\StreamBufferTooSmallException;
use WillWashburn\Stream\Stream;
use WillWashburn\Stream\StreamableInterface;
/**
* Parses the stream of the image and determines the size and type of the image
*
* @package FasterImage
*/
class ImageParser
{
/**
* The type of image we've determined this is
*
* @var string
*/
protected $type;
/**
* @var StreamableInterface $stream
*/
private $stream;
/**
* ImageParser constructor.
*
* @param StreamableInterface $stream
*/
public function __construct(StreamableInterface & $stream)
{
$this->stream = $stream;
}
/**
* @return array|bool|null
*/
public function parseSize()
{
$this->stream->resetPointer();
switch ( $this->type ) {
case 'png':
return $this->parseSizeForPNG();
case 'ico':
case 'cur':
return $this->parseSizeForIco();
case 'gif':
return $this->parseSizeForGIF();
case 'bmp':
return $this->parseSizeForBMP();
case 'jpeg':
return $this->parseSizeForJPEG();
case 'tiff':
return $this->parseSizeForTiff();
case 'psd':
return $this->parseSizeForPSD();
case 'webp':
return $this->parseSizeForWebp();
case 'svg':
return $this->parseSizeForSvg();
}
return null;
}
/**
* @return array
*/
public function parseSizeForIco()
{
$this->stream->read(6);
$b1 = $this->getByte();
$b2 = $this->getByte();
return [
$b1 == 0 ? 256 : $b1,
$b2 == 0 ? 256 : $b2
];
}
/**
* @return array
*/
protected function parseSizeForPSD() {
$this->stream->read(14);
$sizes = unpack("N*",$this->stream->read(12));
return [
$sizes[2],
$sizes[1]
];
}
/**
* Reads and returns the type of the image
*
* @return bool|string
*/
public function parseType()
{
if ( ! $this->type ) {
$this->stream->resetPointer();
switch ( $this->stream->read(2) ) {
case "BM":
return $this->type = 'bmp';
case "GI":
return $this->type = 'gif';
case chr(0xFF) . chr(0xd8):
return $this->type = 'jpeg';
case "\0\0":
switch ( $this->readByte($this->stream->peek(1)) ) {
case 1:
return $this->type = 'ico';
case 2:
return $this->type = 'cur';
}
return false;
case chr(0x89) . 'P':
return $this->type = 'png';
case "RI":
if ( substr($this->stream->read(10), 6, 4) == 'WEBP' ) {
return $this->type = 'webp';
}
return false;
case'8B':
return $this->type = 'psd';
case "II":
case "MM":
return $this->type = 'tiff';
default:
$this->stream->resetPointer();
// Keep reading bytes until we find '<svg'.
while ( true ) {
$byte = $this->stream->read( 1 );
if ( '<' === $byte && 'svg' === $this->stream->peek( 3 ) ) {
$this->type = 'svg';
return $this->type;
}
}
return false;
}
}
return $this->type;
}
/**
* @return array
*/
protected function parseSizeForBMP()
{
$chars = $this->stream->read(29);
$chars = substr($chars, 14, 14);
$type = unpack('C', $chars);
$size = (reset($type) == 40) ? unpack('l*', substr($chars, 4)) : unpack('l*', substr($chars, 4, 8));
return [
current($size),
abs(next($size))
];
}
/**
* @return array
*/
protected function parseSizeForGIF()
{
$chars = $this->stream->read(11);
$size = unpack("S*", substr($chars, 6, 4));
return [
current($size),
next($size)
];
}
/**
* @return array|bool
*/
protected function parseSizeForJPEG()
{
$state = null;
while ( true ) {
switch ( $state ) {
default:
$this->stream->read(2);
$state = 'started';
break;
case 'started':
$b = $this->getByte();
if ( $b === false ) return false;
$state = $b == 0xFF ? 'sof' : 'started';
break;
case 'sof':
$b = $this->getByte();
if ( $b === 0xe1 ) {
$data = $this->stream->read($this->readInt($this->stream->read(2)) - 2);
$stream = new Stream;
$stream->write($data);
if ( $stream->read(4) === 'Exif' ) {
$stream->read(2);
// Some Exif data is broken/wrong so we'll ignore
// any exceptions here
try {
$exif = new ExifParser($stream);
} catch (\Exception $e) {}
}
break;
}
if ( in_array($b, range(0xe0, 0xef)) ) {
$state = 'skipframe';
break;
}
if ( in_array($b, array_merge(range(0xC0, 0xC3), range(0xC5, 0xC7), range(0xC9, 0xCB), range(0xCD, 0xCF))) ) {
$state = 'readsize';
break;
}
if ( $b == 0xFF ) {
$state = 'sof';
break;
}
$state = 'skipframe';
break;
case 'skipframe':
$skip = $this->readInt($this->stream->read(2)) - 2;
$this->stream->read($skip);
$state = 'started';
break;
case 'readsize':
$c = $this->stream->read(7);
$size = array($this->readInt(substr($c, 5, 2)), $this->readInt(substr($c, 3, 2)));
if ( isset($exif) && $exif->isRotated() ) {
return array_reverse($size);
}
return $size;
}
}
return false;
}
/**
* @return array
*/
protected function parseSizeForPNG()
{
$chars = $this->stream->read(25);
$size = unpack("N*", substr($chars, 16, 8));
return [
current($size),
next($size)
];
}
/**
* @return array|bool
* @throws \FasterImage\Exception\InvalidImageException
* @throws StreamBufferTooSmallException
*/
protected function parseSizeForTiff()
{
$exif = new ExifParser($this->stream);
if ( $exif->isRotated() ) {
return [$exif->getHeight(), $exif->getWidth()];
}
return [$exif->getWidth(), $exif->getHeight()];
}
/**
* @return null
* @throws StreamBufferTooSmallException
*/
protected function parseSizeForWebp()
{
$vp8 = substr($this->stream->read(16), 12, 4);
$len = unpack("V", $this->stream->read(4));
switch ( trim($vp8) ) {
case 'VP8':
$this->stream->read(6);
$width = current(unpack("v", $this->stream->read(2)));
$height = current(unpack("v", $this->stream->read(2)));
return [
$width & 0x3fff,
$height & 0x3fff
];
case 'VP8L':
$this->stream->read(1);
$b1 = $this->getByte();
$b2 = $this->getByte();
$b3 = $this->getByte();
$b4 = $this->getByte();
$width = 1 + ((($b2 & 0x3f) << 8) | $b1);
$height = 1 + ((($b4 & 0xf) << 10) | ($b3 << 2) | (($b2 & 0xc0) >> 6));
return [$width, $height];
case 'VP8X':
$flags = current(unpack("C", $this->stream->read(4)));
$b1 = $this->getByte();
$b2 = $this->getByte();
$b3 = $this->getByte();
$b4 = $this->getByte();
$b5 = $this->getByte();
$b6 = $this->getByte();
$width = 1 + $b1 + ($b2 << 8) + ($b3 << 16);
$height = 1 + $b4 + ($b5 << 8) + ($b6 << 16);
return [$width, $height];
default:
return null;
}
}
/**
* Parse size for SVG.
*
* @return array|null Size or null.
*/
protected function parseSizeForSvg()
{
$this->stream->resetPointer();
// Keep reading bytes until we find the complete <svg> start tag.
$inside = false;
$markup = '';
while ( true ) {
$byte = $this->stream->read(1);
// Open a tag if not in a tag and the byte is '<'.
if ( ! $inside && '<' === $byte ) {
$inside = true;
$markup .= $byte;
}
// Close the current tag if the tag is open, the byte is '>', and the last characters weren't a comment token.
elseif ( $inside && '>' === $byte && '--' !== substr( $markup, -2 ) ) {
$inside = false;
$markup .= $byte;
// Break the loop if this tag started with '<svg', as we have now found the SVG start tag.
if ( '<svg' === strtolower( substr( $markup, 0, 4 ) ) ) {
break;
}
// Clear out the markup since we consumed the end of the tag/comment.
$markup = '';
}
// Append the bte to the current tag if the tag is open.
elseif ( $inside ) {
$markup .= $byte;
}
}
$width = null;
$height = null;
if ( preg_match( '/\swidth=([\'"])(\d+(\.\d+)?)(px)?\1/', $markup, $matches ) ) {
$width = floatval( $matches[2] );
}
if ( preg_match( '/\sheight=([\'"])(\d+(\.\d+)?)(px)?\1/', $markup, $matches ) ) {
$height = floatval( $matches[2] );
}
if ( $width && $height ) {
return [ $width, $height ];
}
if ( preg_match( '/\sviewBox=([\'"])[^\1]*(?:,|\s)+(?P<width>\d+(?:\.\d+)?)(?:px)?(?:,|\s)+(?P<height>\d+(?:\.\d+)?)(?:px)?\s*\1/', $markup, $matches ) ) {
return [
floatval( $matches['width'] ),
floatval( $matches['height'] )
];
}
return null;
}
/**
* @return mixed
*/
private function getByte()
{
return $this->readByte($this->stream->read(1));
}
/**
* @param $string
*
* @return mixed
*/
private function readByte($string)
{
return current(unpack("C", $string));
}
/**
* @param $str
*
* @return int
*/
private function readInt($str)
{
$size = unpack("C*", $str);
return ($size[1] << 8) + $size[2];
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\Property\AtRule;
/**
* A BlockList constructed by an unknown @-rule. @media rules are rendered into AtRuleBlockList objects.
*/
class AtRuleBlockList extends CSSBlockList implements AtRule {
private $sType;
private $sArgs;
public function __construct($sType, $sArgs = '', $iLineNo = 0) {
parent::__construct($iLineNo);
$this->sType = $sType;
$this->sArgs = $sArgs;
}
public function atRuleName() {
return $this->sType;
}
public function atRuleArgs() {
return $this->sArgs;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sArgs = $this->sArgs;
if($sArgs) {
$sArgs = ' ' . $sArgs;
}
$sResult = $oOutputFormat->sBeforeAtRuleBlock;
$sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult .= '}';
$sResult .= $oOutputFormat->sAfterAtRuleBlock;
return $sResult;
}
public function isRootList() {
return false;
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\RuleSet;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Rule\Rule;
use Sabberworm\CSS\Value\ValueList;
use Sabberworm\CSS\Value\CSSFunction;
/**
* A CSSBlockList is a CSSList whose DeclarationBlocks are guaranteed to contain valid declaration blocks or at-rules.
* Most CSSLists conform to this category but some at-rules (such as @keyframes) do not.
*/
abstract class CSSBlockList extends CSSList {
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
}
protected function allDeclarationBlocks(&$aResult) {
foreach ($this->aContents as $mContent) {
if ($mContent instanceof DeclarationBlock) {
$aResult[] = $mContent;
} else if ($mContent instanceof CSSBlockList) {
$mContent->allDeclarationBlocks($aResult);
}
}
}
protected function allRuleSets(&$aResult) {
foreach ($this->aContents as $mContent) {
if ($mContent instanceof RuleSet) {
$aResult[] = $mContent;
} else if ($mContent instanceof CSSBlockList) {
$mContent->allRuleSets($aResult);
}
}
}
protected function allValues($oElement, &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false) {
if ($oElement instanceof CSSBlockList) {
foreach ($oElement->getContents() as $oContent) {
$this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
} else if ($oElement instanceof RuleSet) {
foreach ($oElement->getRules($sSearchString) as $oRule) {
$this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
} else if ($oElement instanceof Rule) {
$this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments);
} else if ($oElement instanceof ValueList) {
if ($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) {
foreach ($oElement->getListComponents() as $mComponent) {
$this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
}
} else {
//Non-List Value or CSSString (CSS identifier)
$aResult[] = $oElement;
}
}
protected function allSelectors(&$aResult, $sSpecificitySearch = null) {
$aDeclarationBlocks = array();
$this->allDeclarationBlocks($aDeclarationBlocks);
foreach ($aDeclarationBlocks as $oBlock) {
foreach ($oBlock->getSelectors() as $oSelector) {
if ($sSpecificitySearch === null) {
$aResult[] = $oSelector;
} else {
$sComparison = "\$bRes = {$oSelector->getSpecificity()} $sSpecificitySearch;";
eval($sComparison);
if ($bRes) {
$aResult[] = $oSelector;
}
}
}
}
}
}

View File

@ -0,0 +1,348 @@
<?php
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Property\AtRule;
use Sabberworm\CSS\Property\Charset;
use Sabberworm\CSS\Property\CSSNamespace;
use Sabberworm\CSS\Property\Import;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\RuleSet\AtRuleSet;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\RuleSet;
use Sabberworm\CSS\Value\CSSString;
use Sabberworm\CSS\Value\URL;
use Sabberworm\CSS\Value\Value;
/**
* A CSSList is the most generic container available. Its contents include RuleSet as well as other CSSList objects.
* Also, it may contain Import and Charset objects stemming from @-rules.
*/
abstract class CSSList implements Renderable, Commentable {
protected $aComments;
protected $aContents;
protected $iLineNo;
public function __construct($iLineNo = 0) {
$this->aComments = array();
$this->aContents = array();
$this->iLineNo = $iLineNo;
}
public static function parseList(ParserState $oParserState, CSSList $oList) {
$bIsRoot = $oList instanceof Document;
if(is_string($oParserState)) {
$oParserState = new ParserState($oParserState);
}
$bLenientParsing = $oParserState->getSettings()->bLenientParsing;
while(!$oParserState->isEnd()) {
$comments = $oParserState->consumeWhiteSpace();
$oListItem = null;
if($bLenientParsing) {
try {
$oListItem = self::parseListItem($oParserState, $oList);
} catch (UnexpectedTokenException $e) {
$oListItem = false;
}
} else {
$oListItem = self::parseListItem($oParserState, $oList);
}
if($oListItem === null) {
// List parsing finished
return;
}
if($oListItem) {
$oListItem->setComments($comments);
$oList->append($oListItem);
}
$oParserState->consumeWhiteSpace();
}
if(!$bIsRoot && !$bLenientParsing) {
throw new SourceException("Unexpected end of document", $oParserState->currentLine());
}
}
private static function parseListItem(ParserState $oParserState, CSSList $oList) {
$bIsRoot = $oList instanceof Document;
if ($oParserState->comes('@')) {
$oAtRule = self::parseAtRule($oParserState);
if($oAtRule instanceof Charset) {
if(!$bIsRoot) {
throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $oParserState->currentLine());
}
if(count($oList->getContents()) > 0) {
throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $oParserState->currentLine());
}
$oParserState->setCharset($oAtRule->getCharset()->getString());
}
return $oAtRule;
} else if ($oParserState->comes('}')) {
$oParserState->consume('}');
if ($bIsRoot) {
if ($oParserState->getSettings()->bLenientParsing) {
while ($oParserState->comes('}')) $oParserState->consume('}');
return DeclarationBlock::parse($oParserState);
} else {
throw new SourceException("Unopened {", $oParserState->currentLine());
}
} else {
return null;
}
} else {
return DeclarationBlock::parse($oParserState);
}
}
private static function parseAtRule(ParserState $oParserState) {
$oParserState->consume('@');
$sIdentifier = $oParserState->parseIdentifier();
$iIdentifierLineNum = $oParserState->currentLine();
$oParserState->consumeWhiteSpace();
if ($sIdentifier === 'import') {
$oLocation = URL::parse($oParserState);
$oParserState->consumeWhiteSpace();
$sMediaQuery = null;
if (!$oParserState->comes(';')) {
$sMediaQuery = $oParserState->consumeUntil(';');
}
$oParserState->consume(';');
return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
} else if ($sIdentifier === 'charset') {
$sCharset = CSSString::parse($oParserState);
$oParserState->consumeWhiteSpace();
$oParserState->consume(';');
return new Charset($sCharset, $iIdentifierLineNum);
} else if (self::identifierIs($sIdentifier, 'keyframes')) {
$oResult = new KeyFrame($iIdentifierLineNum);
$oResult->setVendorKeyFrame($sIdentifier);
$oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true)));
CSSList::parseList($oParserState, $oResult);
return $oResult;
} else if ($sIdentifier === 'namespace') {
$sPrefix = null;
$mUrl = Value::parsePrimitiveValue($oParserState);
if (!$oParserState->comes(';')) {
$sPrefix = $mUrl;
$mUrl = Value::parsePrimitiveValue($oParserState);
}
$oParserState->consume(';');
if ($sPrefix !== null && !is_string($sPrefix)) {
throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
}
if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum);
}
return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
} else {
//Unknown other at rule (font-face or such)
$sArgs = trim($oParserState->consumeUntil('{', false, true));
if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) {
if($oParserState->getSettings()->bLenientParsing) {
return NULL;
} else {
throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine());
}
}
$bUseRuleSet = true;
foreach(explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
if(self::identifierIs($sIdentifier, $sBlockRuleName)) {
$bUseRuleSet = false;
break;
}
}
if($bUseRuleSet) {
$oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
RuleSet::parseRuleSet($oParserState, $oAtRule);
} else {
$oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
CSSList::parseList($oParserState, $oAtRule);
}
return $oAtRule;
}
}
/**
* Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too.
*/
private static function identifierIs($sIdentifier, $sMatch) {
return (strcasecmp($sIdentifier, $sMatch) === 0)
?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
/**
* Prepend item to list of contents.
*
* @param object $oItem Item.
*/
public function prepend($oItem) {
array_unshift($this->aContents, $oItem);
}
/**
* Append item to list of contents.
*
* @param object $oItem Item.
*/
public function append($oItem) {
$this->aContents[] = $oItem;
}
/**
* Splice the list of contents.
*
* @param int $iOffset Offset.
* @param int $iLength Length. Optional.
* @param RuleSet[] $mReplacement Replacement. Optional.
*/
public function splice($iOffset, $iLength = null, $mReplacement = null) {
array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
}
/**
* Removes an item from the CSS list.
* @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
* @return bool Whether the item was removed.
*/
public function remove($oItemToRemove) {
$iKey = array_search($oItemToRemove, $this->aContents, true);
if ($iKey !== false) {
unset($this->aContents[$iKey]);
return true;
}
return false;
}
/**
* Replaces an item from the CSS list.
* @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
*/
public function replace($oOldItem, $oNewItem) {
$iKey = array_search($oOldItem, $this->aContents, true);
if ($iKey !== false) {
array_splice($this->aContents, $iKey, 1, $oNewItem);
return true;
}
return false;
}
/**
* Set the contents.
* @param array $aContents Objects to set as content.
*/
public function setContents(array $aContents) {
$this->aContents = array();
foreach ($aContents as $content) {
$this->append($content);
}
}
/**
* Removes a declaration block from the CSS list if it matches all given selectors.
* @param array|string $mSelector The selectors to match.
* @param boolean $bRemoveAll Whether to stop at the first declaration block found or remove all blocks
*/
public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) {
if ($mSelector instanceof DeclarationBlock) {
$mSelector = $mSelector->getSelectors();
}
if (!is_array($mSelector)) {
$mSelector = explode(',', $mSelector);
}
foreach ($mSelector as $iKey => &$mSel) {
if (!($mSel instanceof Selector)) {
$mSel = new Selector($mSel);
}
}
foreach ($this->aContents as $iKey => $mItem) {
if (!($mItem instanceof DeclarationBlock)) {
continue;
}
if ($mItem->getSelectors() == $mSelector) {
unset($this->aContents[$iKey]);
if (!$bRemoveAll) {
return;
}
}
}
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sResult = '';
$bIsFirst = true;
$oNextLevel = $oOutputFormat;
if(!$this->isRootList()) {
$oNextLevel = $oOutputFormat->nextLevel();
}
foreach ($this->aContents as $oContent) {
$sRendered = $oOutputFormat->safely(function() use ($oNextLevel, $oContent) {
return $oContent->render($oNextLevel);
});
if($sRendered === null) {
continue;
}
if($bIsFirst) {
$bIsFirst = false;
$sResult .= $oNextLevel->spaceBeforeBlocks();
} else {
$sResult .= $oNextLevel->spaceBetweenBlocks();
}
$sResult .= $sRendered;
}
if(!$bIsFirst) {
// Had some output
$sResult .= $oOutputFormat->spaceAfterBlocks();
}
return $sResult;
}
/**
* Return true if the list can not be further outdented. Only important when rendering.
*/
public abstract function isRootList();
public function getContents() {
return $this->aContents;
}
/**
* @param array $aComments Array of comments.
*/
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array
*/
public function getComments() {
return $this->aComments;
}
/**
* @param array $aComments Array containing Comment objects.
*/
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\Parsing\ParserState;
/**
* The root CSSList of a parsed file. Contains all top-level css contents, mostly declaration blocks, but also any @-rules encountered.
*/
class Document extends CSSBlockList {
/**
* Document constructor.
* @param int $iLineNo
*/
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
}
public static function parse(ParserState $oParserState) {
$oDocument = new Document($oParserState->currentLine());
CSSList::parseList($oParserState, $oDocument);
return $oDocument;
}
/**
* Gets all DeclarationBlock objects recursively.
*/
public function getAllDeclarationBlocks() {
$aResult = array();
$this->allDeclarationBlocks($aResult);
return $aResult;
}
/**
* @deprecated use getAllDeclarationBlocks()
*/
public function getAllSelectors() {
return $this->getAllDeclarationBlocks();
}
/**
* Returns all RuleSet objects found recursively in the tree.
*/
public function getAllRuleSets() {
$aResult = array();
$this->allRuleSets($aResult);
return $aResult;
}
/**
* Returns all Value objects found recursively in the tree.
* @param (object|string) $mElement the CSSList or RuleSet to start the search from (defaults to the whole document). If a string is given, it is used as rule name filter (@see{RuleSet->getRules()}).
* @param (bool) $bSearchInFunctionArguments whether to also return Value objects used as Function arguments.
*/
public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) {
$sSearchString = null;
if ($mElement === null) {
$mElement = $this;
} else if (is_string($mElement)) {
$sSearchString = $mElement;
$mElement = $this;
}
$aResult = array();
$this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments);
return $aResult;
}
/**
* Returns all Selector objects found recursively in the tree.
* Note that this does not yield the full DeclarationBlock that the selector belongs to (and, currently, there is no way to get to that).
* @param $sSpecificitySearch An optional filter by specificity. May contain a comparison operator and a number or just a number (defaults to "==").
* @example getSelectorsBySpecificity('>= 100')
*/
public function getSelectorsBySpecificity($sSpecificitySearch = null) {
if (is_numeric($sSpecificitySearch) || is_numeric($sSpecificitySearch[0])) {
$sSpecificitySearch = "== $sSpecificitySearch";
}
$aResult = array();
$this->allSelectors($aResult, $sSpecificitySearch);
return $aResult;
}
/**
* Expands all shorthand properties to their long value
*/
public function expandShorthands() {
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->expandShorthands();
}
}
/**
* Create shorthands properties whenever possible
*/
public function createShorthands() {
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->createShorthands();
}
}
// Override render() to make format argument optional
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat = null) {
if($oOutputFormat === null) {
$oOutputFormat = new \Sabberworm\CSS\OutputFormat();
}
return parent::render($oOutputFormat);
}
public function isRootList() {
return true;
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\Property\AtRule;
class KeyFrame extends CSSList implements AtRule {
private $vendorKeyFrame;
private $animationName;
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
$this->vendorKeyFrame = null;
$this->animationName = null;
}
public function setVendorKeyFrame($vendorKeyFrame) {
$this->vendorKeyFrame = $vendorKeyFrame;
}
public function getVendorKeyFrame() {
return $this->vendorKeyFrame;
}
public function setAnimationName($animationName) {
$this->animationName = $animationName;
}
public function getAnimationName() {
return $this->animationName;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult .= '}';
return $sResult;
}
public function isRootList() {
return false;
}
public function atRuleName() {
return $this->vendorKeyFrame;
}
public function atRuleArgs() {
return $this->animationName;
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Sabberworm\CSS\Comment;
use Sabberworm\CSS\Renderable;
class Comment implements Renderable {
protected $iLineNo;
protected $sComment;
public function __construct($sComment = '', $iLineNo = 0) {
$this->sComment = $sComment;
$this->iLineNo = $iLineNo;
}
/**
* @return string
*/
public function getComment() {
return $this->sComment;
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
/**
* @return string
*/
public function setComment($sComment) {
$this->sComment = $sComment;
}
/**
* @return string
*/
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* @return string
*/
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return '/*' . $this->sComment . '*/';
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Sabberworm\CSS\Comment;
interface Commentable {
/**
* @param array $aComments Array of comments.
*/
public function addComments(array $aComments);
/**
* @return array
*/
public function getComments();
/**
* @param array $aComments Array containing Comment objects.
*/
public function setComments(array $aComments);
}

View File

@ -0,0 +1,322 @@
<?php
namespace Sabberworm\CSS;
use Sabberworm\CSS\Parsing\OutputException;
/**
* Class OutputFormat
*
* @method OutputFormat setSemicolonAfterLastRule( bool $bSemicolonAfterLastRule ) Set whether semicolons are added after last rule.
*/
class OutputFormat {
/**
* Value format
*/
// " means double-quote, ' means single-quote
public $sStringQuotingType = '"';
// Output RGB colors in hash notation if possible
public $bRGBHashNotation = true;
/**
* Declaration format
*/
// Semicolon after the last rule of a declaration block can be omitted. To do that, set this false.
public $bSemicolonAfterLastRule = true;
/**
* Spacing
* Note that these strings are not sanity-checked: the value should only consist of whitespace
* Any newline character will be indented according to the current level.
* The triples (After, Before, Between) can be set using a wildcard (e.g. `$oFormat->set('Space*Rules', "\n");`)
*/
public $sSpaceAfterRuleName = ' ';
public $sSpaceBeforeRules = '';
public $sSpaceAfterRules = '';
public $sSpaceBetweenRules = '';
public $sSpaceBeforeBlocks = '';
public $sSpaceAfterBlocks = '';
public $sSpaceBetweenBlocks = "\n";
// Content injected in and around @-rule blocks.
public $sBeforeAtRuleBlock = '';
public $sAfterAtRuleBlock = '';
// This is whats printed before and after the comma if a declaration block contains multiple selectors.
public $sSpaceBeforeSelectorSeparator = '';
public $sSpaceAfterSelectorSeparator = ' ';
// This is whats printed after the comma of value lists
public $sSpaceBeforeListArgumentSeparator = '';
public $sSpaceAfterListArgumentSeparator = '';
public $sSpaceBeforeOpeningBrace = ' ';
// Content injected in and around declaration blocks.
public $sBeforeDeclarationBlock = '';
public $sAfterDeclarationBlockSelectors = '';
public $sAfterDeclarationBlock = '';
/**
* Indentation
*/
// Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings.
public $sIndentation = "\t";
/**
* Output exceptions.
*/
public $bIgnoreExceptions = false;
private $oFormatter = null;
private $oNextLevelFormat = null;
private $iIndentationLevel = 0;
public function __construct() {
}
public function get($sName) {
$aVarPrefixes = array('a', 's', 'm', 'b', 'f', 'o', 'c', 'i');
foreach($aVarPrefixes as $sPrefix) {
$sFieldName = $sPrefix.ucfirst($sName);
if(isset($this->$sFieldName)) {
return $this->$sFieldName;
}
}
return null;
}
public function set($aNames, $mValue) {
$aVarPrefixes = array('a', 's', 'm', 'b', 'f', 'o', 'c', 'i');
if(is_string($aNames) && strpos($aNames, '*') !== false) {
$aNames = array(str_replace('*', 'Before', $aNames), str_replace('*', 'Between', $aNames), str_replace('*', 'After', $aNames));
} else if(!is_array($aNames)) {
$aNames = array($aNames);
}
foreach($aVarPrefixes as $sPrefix) {
$bDidReplace = false;
foreach($aNames as $sName) {
$sFieldName = $sPrefix.ucfirst($sName);
if(isset($this->$sFieldName)) {
$this->$sFieldName = $mValue;
$bDidReplace = true;
}
}
if($bDidReplace) {
return $this;
}
}
// Break the chain so the user knows this option is invalid
return false;
}
public function __call($sMethodName, $aArguments) {
if(strpos($sMethodName, 'set') === 0) {
return $this->set(substr($sMethodName, 3), $aArguments[0]);
} else if(strpos($sMethodName, 'get') === 0) {
return $this->get(substr($sMethodName, 3));
} else if(method_exists('\\Sabberworm\\CSS\\OutputFormatter', $sMethodName)) {
return call_user_func_array(array($this->getFormatter(), $sMethodName), $aArguments);
} else {
throw new \Exception('Unknown OutputFormat method called: '.$sMethodName);
}
}
public function indentWithTabs($iNumber = 1) {
return $this->setIndentation(str_repeat("\t", $iNumber));
}
public function indentWithSpaces($iNumber = 2) {
return $this->setIndentation(str_repeat(" ", $iNumber));
}
public function nextLevel() {
if($this->oNextLevelFormat === null) {
$this->oNextLevelFormat = clone $this;
$this->oNextLevelFormat->iIndentationLevel++;
$this->oNextLevelFormat->oFormatter = null;
}
return $this->oNextLevelFormat;
}
public function beLenient() {
$this->bIgnoreExceptions = true;
}
public function getFormatter() {
if($this->oFormatter === null) {
$this->oFormatter = new OutputFormatter($this);
}
return $this->oFormatter;
}
public function level() {
return $this->iIndentationLevel;
}
/**
* Create format.
*
* @return OutputFormat Format.
*/
public static function create() {
return new OutputFormat();
}
/**
* Create compact format.
*
* @return OutputFormat Format.
*/
public static function createCompact() {
$format = self::create();
$format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator('');
return $format;
}
/**
* Create pretty format.
*
* @return OutputFormat Format.
*/
public static function createPretty() {
$format = self::create();
$format->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
return $format;
}
}
class OutputFormatter {
private $oFormat;
public function __construct(OutputFormat $oFormat) {
$this->oFormat = $oFormat;
}
public function space($sName, $sType = null) {
$sSpaceString = $this->oFormat->get("Space$sName");
// If $sSpaceString is an array, we have multple values configured depending on the type of object the space applies to
if(is_array($sSpaceString)) {
if($sType !== null && isset($sSpaceString[$sType])) {
$sSpaceString = $sSpaceString[$sType];
} else {
$sSpaceString = reset($sSpaceString);
}
}
return $this->prepareSpace($sSpaceString);
}
public function spaceAfterRuleName() {
return $this->space('AfterRuleName');
}
public function spaceBeforeRules() {
return $this->space('BeforeRules');
}
public function spaceAfterRules() {
return $this->space('AfterRules');
}
public function spaceBetweenRules() {
return $this->space('BetweenRules');
}
public function spaceBeforeBlocks() {
return $this->space('BeforeBlocks');
}
public function spaceAfterBlocks() {
return $this->space('AfterBlocks');
}
public function spaceBetweenBlocks() {
return $this->space('BetweenBlocks');
}
public function spaceBeforeSelectorSeparator() {
return $this->space('BeforeSelectorSeparator');
}
public function spaceAfterSelectorSeparator() {
return $this->space('AfterSelectorSeparator');
}
public function spaceBeforeListArgumentSeparator($sSeparator) {
return $this->space('BeforeListArgumentSeparator', $sSeparator);
}
public function spaceAfterListArgumentSeparator($sSeparator) {
return $this->space('AfterListArgumentSeparator', $sSeparator);
}
public function spaceBeforeOpeningBrace() {
return $this->space('BeforeOpeningBrace');
}
/**
* Runs the given code, either swallowing or passing exceptions, depending on the bIgnoreExceptions setting.
*/
public function safely($cCode) {
if($this->oFormat->get('IgnoreExceptions')) {
// If output exceptions are ignored, run the code with exception guards
try {
return $cCode();
} catch (OutputException $e) {
return null;
} //Do nothing
} else {
// Run the code as-is
return $cCode();
}
}
/**
* Clone of the implode function but calls ->render with the current output format instead of __toString()
*/
public function implode($sSeparator, $aValues, $bIncreaseLevel = false) {
$sResult = '';
$oFormat = $this->oFormat;
if($bIncreaseLevel) {
$oFormat = $oFormat->nextLevel();
}
$bIsFirst = true;
foreach($aValues as $mValue) {
if($bIsFirst) {
$bIsFirst = false;
} else {
$sResult .= $sSeparator;
}
if($mValue instanceof \Sabberworm\CSS\Renderable) {
$sResult .= $mValue->render($oFormat);
} else {
$sResult .= $mValue;
}
}
return $sResult;
}
public function removeLastSemicolon($sString) {
if($this->oFormat->get('SemicolonAfterLastRule')) {
return $sString;
}
$sString = explode(';', $sString);
if(count($sString) < 2) {
return $sString[0];
}
$sLast = array_pop($sString);
$sNextToLast = array_pop($sString);
array_push($sString, $sNextToLast.$sLast);
return implode(';', $sString);
}
private function prepareSpace($sSpaceString) {
return str_replace("\n", "\n".$this->indent(), $sSpaceString);
}
private function indent() {
return str_repeat($this->oFormat->sIndentation, $this->oFormat->level());
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Sabberworm\CSS;
use Sabberworm\CSS\CSSList\Document;
use Sabberworm\CSS\Parsing\ParserState;
/**
* Parser class parses CSS from text into a data structure.
*/
class Parser {
private $oParserState;
/**
* Parser constructor.
* Note that that iLineNo starts from 1 and not 0
*
* @param $sText
* @param Settings|null $oParserSettings
* @param int $iLineNo
*/
public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) {
if ($oParserSettings === null) {
$oParserSettings = Settings::create();
}
$this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo);
}
public function setCharset($sCharset) {
$this->oParserState->setCharset($sCharset);
}
public function getCharset() {
$this->oParserState->getCharset();
}
public function parse() {
return Document::parse($this->oParserState);
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace Sabberworm\CSS\Parsing;
/**
* Thrown if the CSS parsers attempts to print something invalid
*/
class OutputException extends SourceException {
public function __construct($sMessage, $iLineNo = 0) {
parent::__construct($sMessage, $iLineNo);
}
}

View File

@ -0,0 +1,310 @@
<?php
namespace Sabberworm\CSS\Parsing;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Settings;
class ParserState {
private $oParserSettings;
private $sText;
private $aText;
private $iCurrentPosition;
private $sCharset;
private $iLength;
private $iLineNo;
public function __construct($sText, Settings $oParserSettings, $iLineNo = 1) {
$this->oParserSettings = $oParserSettings;
$this->sText = $sText;
$this->iCurrentPosition = 0;
$this->iLineNo = $iLineNo;
$this->setCharset($this->oParserSettings->sDefaultCharset);
}
public function setCharset($sCharset) {
$this->sCharset = $sCharset;
$this->aText = $this->strsplit($this->sText);
$this->iLength = count($this->aText);
}
public function getCharset() {
$this->oParserHelper->getCharset();
return $this->sCharset;
}
public function currentLine() {
return $this->iLineNo;
}
public function getSettings() {
return $this->oParserSettings;
}
public function parseIdentifier($bIgnoreCase = true) {
$sResult = $this->parseCharacter(true);
if ($sResult === null) {
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
}
$sCharacter = null;
while (($sCharacter = $this->parseCharacter(true)) !== null) {
$sResult .= $sCharacter;
}
if ($bIgnoreCase) {
$sResult = $this->strtolower($sResult);
}
return $sResult;
}
public function parseCharacter($bIsForIdentifier) {
if ($this->peek() === '\\') {
if ($bIsForIdentifier && $this->oParserSettings->bLenientParsing && ($this->comes('\0') || $this->comes('\9'))) {
// Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
return null;
}
$this->consume('\\');
if ($this->comes('\n') || $this->comes('\r')) {
return '';
}
if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
return $this->consume(1);
}
$sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
if ($this->strlen($sUnicode) < 6) {
//Consume whitespace after incomplete unicode escape
if (preg_match('/\\s/isSu', $this->peek())) {
if ($this->comes('\r\n')) {
$this->consume(2);
} else {
$this->consume(1);
}
}
}
$iUnicode = intval($sUnicode, 16);
$sUtf32 = "";
for ($i = 0; $i < 4; ++$i) {
$sUtf32 .= chr($iUnicode & 0xff);
$iUnicode = $iUnicode >> 8;
}
return iconv('utf-32le', $this->sCharset, $sUtf32);
}
if ($bIsForIdentifier) {
$peek = ord($this->peek());
// Ranges: a-z A-Z 0-9 - _
if (($peek >= 97 && $peek <= 122) ||
($peek >= 65 && $peek <= 90) ||
($peek >= 48 && $peek <= 57) ||
($peek === 45) ||
($peek === 95) ||
($peek > 0xa1)) {
return $this->consume(1);
}
} else {
return $this->consume(1);
}
return null;
}
public function consumeWhiteSpace() {
$comments = array();
do {
while (preg_match('/\\s/isSu', $this->peek()) === 1) {
$this->consume(1);
}
if($this->oParserSettings->bLenientParsing) {
try {
$oComment = $this->consumeComment();
} catch(UnexpectedTokenException $e) {
// When we cant find the end of a comment, we assume the document is finished.
$this->iCurrentPosition = $this->iLength;
return;
}
} else {
$oComment = $this->consumeComment();
}
if ($oComment !== false) {
$comments[] = $oComment;
}
} while($oComment !== false);
return $comments;
}
public function comes($sString, $bCaseInsensitive = false) {
$sPeek = $this->peek(strlen($sString));
return ($sPeek == '')
? false
: $this->streql($sPeek, $sString, $bCaseInsensitive);
}
public function peek($iLength = 1, $iOffset = 0) {
$iOffset += $this->iCurrentPosition;
if ($iOffset >= $this->iLength) {
return '';
}
return $this->substr($iOffset, $iLength);
}
public function consume($mValue = 1) {
if (is_string($mValue)) {
$iLineCount = substr_count($mValue, "\n");
$iLength = $this->strlen($mValue);
if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo);
}
$this->iLineNo += $iLineCount;
$this->iCurrentPosition += $this->strlen($mValue);
return $mValue;
} else {
if ($this->iCurrentPosition + $mValue > $this->iLength) {
throw new UnexpectedTokenException($mValue, $this->peek(5), 'count', $this->iLineNo);
}
$sResult = $this->substr($this->iCurrentPosition, $mValue);
$iLineCount = substr_count($sResult, "\n");
$this->iLineNo += $iLineCount;
$this->iCurrentPosition += $mValue;
return $sResult;
}
}
public function consumeExpression($mExpression, $iMaxLength = null) {
$aMatches = null;
$sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
return $this->consume($aMatches[0][0]);
}
throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
}
/**
* @return false|Comment
*/
public function consumeComment() {
$mComment = false;
if ($this->comes('/*')) {
$iLineNo = $this->iLineNo;
$this->consume(1);
$mComment = '';
while (($char = $this->consume(1)) !== '') {
$mComment .= $char;
if ($this->comes('*/')) {
$this->consume(2);
break;
}
}
}
if ($mComment !== false) {
// We skip the * which was included in the comment.
return new Comment(substr($mComment, 1), $iLineNo);
}
return $mComment;
}
public function isEnd() {
return $this->iCurrentPosition >= $this->iLength;
}
public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = array()) {
$aEnd = is_array($aEnd) ? $aEnd : array($aEnd);
$out = '';
$start = $this->iCurrentPosition;
while (($char = $this->consume(1)) !== '') {
if (in_array($char, $aEnd)) {
if ($bIncludeEnd) {
$out .= $char;
} elseif (!$consumeEnd) {
$this->iCurrentPosition -= $this->strlen($char);
}
return $out;
}
$out .= $char;
if ($comment = $this->consumeComment()) {
$comments[] = $comment;
}
}
$this->iCurrentPosition = $start;
throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo);
}
private function inputLeft() {
return $this->substr($this->iCurrentPosition, -1);
}
public function streql($sString1, $sString2, $bCaseInsensitive = true) {
if($bCaseInsensitive) {
return $this->strtolower($sString1) === $this->strtolower($sString2);
} else {
return $sString1 === $sString2;
}
}
public function backtrack($iAmount) {
$this->iCurrentPosition -= $iAmount;
}
public function strlen($sString) {
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strlen($sString, $this->sCharset);
} else {
return strlen($sString);
}
}
private function substr($iStart, $iLength) {
if ($iLength < 0) {
$iLength = $this->iLength - $iStart + $iLength;
}
if ($iStart + $iLength > $this->iLength) {
$iLength = $this->iLength - $iStart;
}
$sResult = '';
while ($iLength > 0) {
$sResult .= $this->aText[$iStart];
$iStart++;
$iLength--;
}
return $sResult;
}
private function strtolower($sString) {
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strtolower($sString, $this->sCharset);
} else {
return strtolower($sString);
}
}
private function strsplit($sString) {
if ($this->oParserSettings->bMultibyteSupport) {
if ($this->streql($this->sCharset, 'utf-8')) {
return preg_split('//u', $sString, null, PREG_SPLIT_NO_EMPTY);
} else {
$iLength = mb_strlen($sString, $this->sCharset);
$aResult = array();
for ($i = 0; $i < $iLength; ++$i) {
$aResult[] = mb_substr($sString, $i, 1, $this->sCharset);
}
return $aResult;
}
} else {
if($sString === '') {
return array();
} else {
return str_split($sString);
}
}
}
private function strpos($sString, $sNeedle, $iOffset) {
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
} else {
return strpos($sString, $sNeedle, $iOffset);
}
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Sabberworm\CSS\Parsing;
class SourceException extends \Exception {
private $iLineNo;
public function __construct($sMessage, $iLineNo = 0) {
$this->iLineNo = $iLineNo;
if (!empty($iLineNo)) {
$sMessage .= " [line no: $iLineNo]";
}
parent::__construct($sMessage);
}
public function getLineNo() {
return $this->iLineNo;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Sabberworm\CSS\Parsing;
/**
* Thrown if the CSS parsers encounters a token it did not expect
*/
class UnexpectedTokenException extends SourceException {
private $sExpected;
private $sFound;
// Possible values: literal, identifier, count, expression, search
private $sMatchType;
public function __construct($sExpected, $sFound, $sMatchType = 'literal', $iLineNo = 0) {
$this->sExpected = $sExpected;
$this->sFound = $sFound;
$this->sMatchType = $sMatchType;
$sMessage = "Token “{$sExpected}” ({$sMatchType}) not found. Got “{$sFound}”.";
if($this->sMatchType === 'search') {
$sMessage = "Search for “{$sExpected}” returned no results. Context: “{$sFound}”.";
} else if($this->sMatchType === 'count') {
$sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”.";
} else if($this->sMatchType === 'identifier') {
$sMessage = "Identifier expected. Got “{$sFound}";
} else if($this->sMatchType === 'custom') {
$sMessage = trim("$sExpected $sFound");
}
parent::__construct($sMessage, $iLineNo);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\Comment\Commentable;
interface AtRule extends Renderable, Commentable {
// Since there are more set rules than block rules, were whitelisting the block rules and have anything else be treated as a set rule.
const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
// …and more font-specific ones (to be used inside font-feature-values)
const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation';
public function atRuleName();
public function atRuleArgs();
}

View File

@ -0,0 +1,75 @@
<?php
namespace Sabberworm\CSS\Property;
/**
* CSSNamespace represents an @namespace rule.
*/
class CSSNamespace implements AtRule {
private $mUrl;
private $sPrefix;
private $iLineNo;
protected $aComments;
public function __construct($mUrl, $sPrefix = null, $iLineNo = 0) {
$this->mUrl = $mUrl;
$this->sPrefix = $sPrefix;
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return '@namespace '.($this->sPrefix === null ? '' : $this->sPrefix.' ').$this->mUrl->render($oOutputFormat).';';
}
public function getUrl() {
return $this->mUrl;
}
public function getPrefix() {
return $this->sPrefix;
}
public function setUrl($mUrl) {
$this->mUrl = $mUrl;
}
public function setPrefix($sPrefix) {
$this->sPrefix = $sPrefix;
}
public function atRuleName() {
return 'namespace';
}
public function atRuleArgs() {
$aResult = array($this->mUrl);
if($this->sPrefix) {
array_unshift($aResult, $this->sPrefix);
}
return $aResult;
}
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
public function getComments() {
return $this->aComments;
}
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace Sabberworm\CSS\Property;
/**
* Class representing an @charset rule.
* The following restrictions apply:
* • May not be found in any CSSList other than the Document.
* • May only appear at the very top of a Documents contents.
* • Must not appear more than once.
*/
class Charset implements AtRule {
private $sCharset;
protected $iLineNo;
protected $aComment;
public function __construct($sCharset, $iLineNo = 0) {
$this->sCharset = $sCharset;
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
public function setCharset($sCharset) {
$this->sCharset = $sCharset;
}
public function getCharset() {
return $this->sCharset;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return "@charset {$this->sCharset->render($oOutputFormat)};";
}
public function atRuleName() {
return 'charset';
}
public function atRuleArgs() {
return $this->sCharset;
}
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
public function getComments() {
return $this->aComments;
}
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Value\URL;
/**
* Class representing an @import rule.
*/
class Import implements AtRule {
private $oLocation;
private $sMediaQuery;
protected $iLineNo;
protected $aComments;
public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0) {
$this->oLocation = $oLocation;
$this->sMediaQuery = $sMediaQuery;
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
public function setLocation($oLocation) {
$this->oLocation = $oLocation;
}
public function getLocation() {
return $this->oLocation;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return "@import ".$this->oLocation->render($oOutputFormat).($this->sMediaQuery === null ? '' : ' '.$this->sMediaQuery).';';
}
public function atRuleName() {
return 'import';
}
public function atRuleArgs() {
$aResult = array($this->oLocation);
if($this->sMediaQuery) {
array_push($aResult, $this->sMediaQuery);
}
return $aResult;
}
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
public function getComments() {
return $this->aComments;
}
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace Sabberworm\CSS\Property;
/**
* Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this class.
*/
class Selector {
//Regexes for specificity calculations
const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/
(\.[\w]+) # classes
|
\[(\w+) # attributes
|
(\:( # pseudo classes
link|visited|active
|hover|focus
|lang
|target
|enabled|disabled|checked|indeterminate
|root
|nth-child|nth-last-child|nth-of-type|nth-last-of-type
|first-child|last-child|first-of-type|last-of-type
|only-child|only-of-type
|empty|contains
))
/ix';
const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/
((^|[\s\+\>\~]+)[\w]+ # elements
|
\:{1,2}( # pseudo-elements
after|before|first-letter|first-line|selection
))
/ix';
private $sSelector;
private $iSpecificity;
public function __construct($sSelector, $bCalculateSpecificity = false) {
$this->setSelector($sSelector);
if ($bCalculateSpecificity) {
$this->getSpecificity();
}
}
public function getSelector() {
return $this->sSelector;
}
public function setSelector($sSelector) {
$this->sSelector = trim($sSelector);
$this->iSpecificity = null;
}
public function __toString() {
return $this->getSelector();
}
public function getSpecificity() {
if ($this->iSpecificity === null) {
$a = 0;
/// @todo should exclude \# as well as "#"
$aMatches = null;
$b = substr_count($this->sSelector, '#');
$c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $this->sSelector, $aMatches);
$d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $this->sSelector, $aMatches);
$this->iSpecificity = ($a * 1000) + ($b * 100) + ($c * 10) + $d;
}
return $this->iSpecificity;
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Sabberworm\CSS;
interface Renderable {
public function __toString();
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat);
public function getLineNo();
}

View File

@ -0,0 +1,236 @@
<?php
namespace Sabberworm\CSS\Rule;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\Value\RuleValueList;
use Sabberworm\CSS\Value\Value;
/**
* RuleSets contains Rule objects which always have a key and a value.
* In CSS, Rules are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];”
*/
class Rule implements Renderable, Commentable {
private $sRule;
private $mValue;
private $bIsImportant;
private $aIeHack;
protected $iLineNo;
protected $aComments;
public function __construct($sRule, $iLineNo = 0) {
$this->sRule = $sRule;
$this->mValue = null;
$this->bIsImportant = false;
$this->aIeHack = array();
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
public static function parse(ParserState $oParserState) {
$aComments = $oParserState->consumeWhiteSpace();
$oRule = new Rule($oParserState->parseIdentifier(), $oParserState->currentLine());
$oRule->setComments($aComments);
$oRule->addComments($oParserState->consumeWhiteSpace());
$oParserState->consume(':');
$oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule()));
$oRule->setValue($oValue);
if ($oParserState->getSettings()->bLenientParsing) {
while ($oParserState->comes('\\')) {
$oParserState->consume('\\');
$oRule->addIeHack($oParserState->consume());
$oParserState->consumeWhiteSpace();
}
}
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('!')) {
$oParserState->consume('!');
$oParserState->consumeWhiteSpace();
$oParserState->consume('important');
$oRule->setIsImportant(true);
}
$oParserState->consumeWhiteSpace();
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
$oParserState->consumeWhiteSpace();
return $oRule;
}
private static function listDelimiterForRule($sRule) {
if (preg_match('/^font($|-)/', $sRule)) {
return array(',', '/', ' ');
}
return array(',', ' ', '/');
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
public function setRule($sRule) {
$this->sRule = $sRule;
}
public function getRule() {
return $this->sRule;
}
public function getValue() {
return $this->mValue;
}
public function setValue($mValue) {
$this->mValue = $mValue;
}
/**
* @deprecated Old-Style 2-dimensional array given. Retained for (some) backwards-compatibility. Use setValue() instead and wrapp the value inside a RuleValueList if necessary.
*/
public function setValues($aSpaceSeparatedValues) {
$oSpaceSeparatedList = null;
if (count($aSpaceSeparatedValues) > 1) {
$oSpaceSeparatedList = new RuleValueList(' ', $this->iLineNo);
}
foreach ($aSpaceSeparatedValues as $aCommaSeparatedValues) {
$oCommaSeparatedList = null;
if (count($aCommaSeparatedValues) > 1) {
$oCommaSeparatedList = new RuleValueList(',', $this->iLineNo);
}
foreach ($aCommaSeparatedValues as $mValue) {
if (!$oSpaceSeparatedList && !$oCommaSeparatedList) {
$this->mValue = $mValue;
return $mValue;
}
if ($oCommaSeparatedList) {
$oCommaSeparatedList->addListComponent($mValue);
} else {
$oSpaceSeparatedList->addListComponent($mValue);
}
}
if (!$oSpaceSeparatedList) {
$this->mValue = $oCommaSeparatedList;
return $oCommaSeparatedList;
} else {
$oSpaceSeparatedList->addListComponent($oCommaSeparatedList);
}
}
$this->mValue = $oSpaceSeparatedList;
return $oSpaceSeparatedList;
}
/**
* @deprecated Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility. Use getValue() instead and check for the existance of a (nested set of) ValueList object(s).
*/
public function getValues() {
if (!$this->mValue instanceof RuleValueList) {
return array(array($this->mValue));
}
if ($this->mValue->getListSeparator() === ',') {
return array($this->mValue->getListComponents());
}
$aResult = array();
foreach ($this->mValue->getListComponents() as $mValue) {
if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') {
$aResult[] = array($mValue);
continue;
}
if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) {
$aResult[] = array();
}
foreach ($mValue->getListComponents() as $mValue) {
$aResult[count($aResult) - 1][] = $mValue;
}
}
return $aResult;
}
/**
* Adds a value to the existing value. Value will be appended if a RuleValueList exists of the given type. Otherwise, the existing value will be wrapped by one.
*/
public function addValue($mValue, $sType = ' ') {
if (!is_array($mValue)) {
$mValue = array($mValue);
}
if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) {
$mCurrentValue = $this->mValue;
$this->mValue = new RuleValueList($sType, $this->iLineNo);
if ($mCurrentValue) {
$this->mValue->addListComponent($mCurrentValue);
}
}
foreach ($mValue as $mValueItem) {
$this->mValue->addListComponent($mValueItem);
}
}
public function addIeHack($iModifier) {
$this->aIeHack[] = $iModifier;
}
public function setIeHack(array $aModifiers) {
$this->aIeHack = $aModifiers;
}
public function getIeHack() {
return $this->aIeHack;
}
public function setIsImportant($bIsImportant) {
$this->bIsImportant = $bIsImportant;
}
public function getIsImportant() {
return $this->bIsImportant;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sResult = "{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}";
if ($this->mValue instanceof Value) { //Can also be a ValueList
$sResult .= $this->mValue->render($oOutputFormat);
} else {
$sResult .= $this->mValue;
}
if (!empty($this->aIeHack)) {
$sResult .= ' \\' . implode('\\', $this->aIeHack);
}
if ($this->bIsImportant) {
$sResult .= ' !important';
}
$sResult .= ';';
return $sResult;
}
/**
* @param array $aComments Array of comments.
*/
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array
*/
public function getComments() {
return $this->aComments;
}
/**
* @param array $aComments Array containing Comment objects.
*/
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace Sabberworm\CSS\RuleSet;
use Sabberworm\CSS\Property\AtRule;
/**
* A RuleSet constructed by an unknown @-rule. @font-face rules are rendered into AtRuleSet objects.
*/
class AtRuleSet extends RuleSet implements AtRule {
private $sType;
private $sArgs;
public function __construct($sType, $sArgs = '', $iLineNo = 0) {
parent::__construct($iLineNo);
$this->sType = $sType;
$this->sArgs = $sArgs;
}
public function atRuleName() {
return $this->sType;
}
public function atRuleArgs() {
return $this->sArgs;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sArgs = $this->sArgs;
if($sArgs) {
$sArgs = ' ' . $sArgs;
}
$sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult .= '}';
return $sResult;
}
}

View File

@ -0,0 +1,686 @@
<?php
namespace Sabberworm\CSS\RuleSet;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\OutputException;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Rule\Rule;
use Sabberworm\CSS\Value\RuleValueList;
use Sabberworm\CSS\Value\Value;
use Sabberworm\CSS\Value\Size;
use Sabberworm\CSS\Value\Color;
use Sabberworm\CSS\Value\URL;
/**
* Declaration blocks are the parts of a css file which denote the rules belonging to a selector.
* Declaration blocks usually appear directly inside a Document or another CSSList (mostly a MediaQuery).
*/
class DeclarationBlock extends RuleSet {
private $aSelectors;
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
$this->aSelectors = array();
}
public static function parse(ParserState $oParserState) {
$aComments = array();
$oResult = new DeclarationBlock($oParserState->currentLine());
$oResult->setSelector($oParserState->consumeUntil('{', false, true, $aComments));
$oResult->setComments($aComments);
RuleSet::parseRuleSet($oParserState, $oResult);
return $oResult;
}
public function setSelectors($mSelector) {
if (is_array($mSelector)) {
$this->aSelectors = $mSelector;
} else {
list( $sSelectors, $aPlaceholders ) = $this->addSelectorExpressionPlaceholders( $mSelector );
if ( empty( $aPlaceholders ) ) {
$this->aSelectors = explode(',', $sSelectors);
} else {
$aSearches = array_keys( $aPlaceholders );
$aReplaces = array_values( $aPlaceholders );
$this->aSelectors = array_map(
function( $sSelector ) use ( $aSearches, $aReplaces ) {
return str_replace( $aSearches, $aReplaces, $sSelector );
},
explode(',', $sSelectors)
);
}
}
foreach ($this->aSelectors as $iKey => $mSelector) {
if (!($mSelector instanceof Selector)) {
$this->aSelectors[$iKey] = new Selector($mSelector);
}
}
}
/**
* Add placeholders for parenthetical/bracketed expressions in selectors which may contain commas that break exploding.
*
* This prevents a single selector like `.widget:not(.foo, .bar)` from erroneously getting parsed in setSelectors as
* two selectors `.widget:not(.foo` and `.bar)`.
*
* @param string $sSelectors Selectors.
* @return array First array value is the selectors with placeholders, and second value is the array of placeholders mapped to the original expressions.
*/
private function addSelectorExpressionPlaceholders( $sSelectors ) {
$iOffset = 0;
$aPlaceholders = array();
while ( preg_match( '/\(|\[/', $sSelectors, $aMatches, PREG_OFFSET_CAPTURE, $iOffset ) ) {
$sMatchString = $aMatches[0][0];
$iMatchOffset = $aMatches[0][1];
$iStyleLength = strlen( $sSelectors );
$iOpenParens = 1;
$iStartOffset = $iMatchOffset + strlen( $sMatchString );
$iFinalOffset = $iStartOffset;
for ( ; $iFinalOffset < $iStyleLength; $iFinalOffset++ ) {
if ( '(' === $sSelectors[ $iFinalOffset ] || '[' === $sSelectors[ $iFinalOffset ] ) {
$iOpenParens++;
} elseif ( ')' === $sSelectors[ $iFinalOffset ] || ']' === $sSelectors[ $iFinalOffset ] ) {
$iOpenParens--;
}
// Found the end of the expression, so replace it with a placeholder.
if ( 0 === $iOpenParens ) {
$sMatchedExpr = substr( $sSelectors, $iMatchOffset, $iFinalOffset - $iMatchOffset + 1 );
$sPlaceholder = sprintf( '{placeholder:%d}', count( $aPlaceholders ) + 1 );
$aPlaceholders[ $sPlaceholder ] = $sMatchedExpr;
// Update the CSS to replace the matched calc() with the placeholder function.
$sSelectors = substr( $sSelectors, 0, $iMatchOffset ) . $sPlaceholder . substr( $sSelectors, $iFinalOffset + 1 );
// Update offset based on difference of length of placeholder vs original matched calc().
$iFinalOffset += strlen( $sPlaceholder ) - strlen( $sMatchedExpr );
break;
}
}
// Start matching at the next byte after the match.
$iOffset = $iFinalOffset + 1;
}
return array( $sSelectors, $aPlaceholders );
}
// remove one of the selector of the block
public function removeSelector($mSelector) {
if($mSelector instanceof Selector) {
$mSelector = $mSelector->getSelector();
}
foreach($this->aSelectors as $iKey => $oSelector) {
if($oSelector->getSelector() === $mSelector) {
unset($this->aSelectors[$iKey]);
return true;
}
}
return false;
}
/**
* @deprecated use getSelectors()
*/
public function getSelector() {
return $this->getSelectors();
}
/**
* @deprecated use setSelectors()
*/
public function setSelector($mSelector) {
$this->setSelectors($mSelector);
}
/**
* Get selectors.
*
* @return Selector[] Selectors.
*/
public function getSelectors() {
return $this->aSelectors;
}
/**
* Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
* */
public function expandShorthands() {
// border must be expanded before dimensions
$this->expandBorderShorthand();
$this->expandDimensionsShorthand();
$this->expandFontShorthand();
$this->expandBackgroundShorthand();
$this->expandListStyleShorthand();
}
/**
* Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
* */
public function createShorthands() {
$this->createBackgroundShorthand();
$this->createDimensionsShorthand();
// border must be shortened after dimensions
$this->createBorderShorthand();
$this->createFontShorthand();
$this->createListStyleShorthand();
}
/**
* Split shorthand border declarations (e.g. <tt>border: 1px red;</tt>)
* Additional splitting happens in expandDimensionsShorthand
* Multiple borders are not yet supported as of 3
* */
public function expandBorderShorthand() {
$aBorderRules = array(
'border', 'border-left', 'border-right', 'border-top', 'border-bottom'
);
$aBorderSizes = array(
'thin', 'medium', 'thick'
);
$aRules = $this->getRulesAssoc();
foreach ($aBorderRules as $sBorderRule) {
if (!isset($aRules[$sBorderRule]))
continue;
$oRule = $aRules[$sBorderRule];
$mRuleValue = $oRule->getValue();
$aValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
foreach ($aValues as $mValue) {
if ($mValue instanceof Value) {
$mNewValue = clone $mValue;
} else {
$mNewValue = $mValue;
}
if ($mValue instanceof Size) {
$sNewRuleName = $sBorderRule . "-width";
} else if ($mValue instanceof Color) {
$sNewRuleName = $sBorderRule . "-color";
} else {
if (in_array($mValue, $aBorderSizes)) {
$sNewRuleName = $sBorderRule . "-width";
} else/* if(in_array($mValue, $aBorderStyles)) */ {
$sNewRuleName = $sBorderRule . "-style";
}
}
$oNewRule = new Rule($sNewRuleName, $this->iLineNo);
$oNewRule->setIsImportant($oRule->getIsImportant());
$oNewRule->addValue(array($mNewValue));
$this->addRule($oNewRule);
}
$this->removeRule($sBorderRule);
}
}
/**
* Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)
* into their constituent parts.
* Handles margin, padding, border-color, border-style and border-width.
* */
public function expandDimensionsShorthand() {
$aExpansions = array(
'margin' => 'margin-%s',
'padding' => 'padding-%s',
'border-color' => 'border-%s-color',
'border-style' => 'border-%s-style',
'border-width' => 'border-%s-width'
);
$aRules = $this->getRulesAssoc();
foreach ($aExpansions as $sProperty => $sExpanded) {
if (!isset($aRules[$sProperty]))
continue;
$oRule = $aRules[$sProperty];
$mRuleValue = $oRule->getValue();
$aValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
$top = $right = $bottom = $left = null;
switch (count($aValues)) {
case 1:
$top = $right = $bottom = $left = $aValues[0];
break;
case 2:
$top = $bottom = $aValues[0];
$left = $right = $aValues[1];
break;
case 3:
$top = $aValues[0];
$left = $right = $aValues[1];
$bottom = $aValues[2];
break;
case 4:
$top = $aValues[0];
$right = $aValues[1];
$bottom = $aValues[2];
$left = $aValues[3];
break;
}
foreach (array('top', 'right', 'bottom', 'left') as $sPosition) {
$oNewRule = new Rule(sprintf($sExpanded, $sPosition), $this->iLineNo);
$oNewRule->setIsImportant($oRule->getIsImportant());
$oNewRule->addValue(${$sPosition});
$this->addRule($oNewRule);
}
$this->removeRule($sProperty);
}
}
/**
* Convert shorthand font declarations
* (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)
* into their constituent parts.
* */
public function expandFontShorthand() {
$aRules = $this->getRulesAssoc();
if (!isset($aRules['font']))
return;
$oRule = $aRules['font'];
// reset properties to 'normal' per http://www.w3.org/TR/21/fonts.html#font-shorthand
$aFontProperties = array(
'font-style' => 'normal',
'font-variant' => 'normal',
'font-weight' => 'normal',
'font-size' => 'normal',
'line-height' => 'normal'
);
$mRuleValue = $oRule->getValue();
$aValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
foreach ($aValues as $mValue) {
if (!$mValue instanceof Value) {
$mValue = mb_strtolower($mValue);
}
if (in_array($mValue, array('normal', 'inherit'))) {
foreach (array('font-style', 'font-weight', 'font-variant') as $sProperty) {
if (!isset($aFontProperties[$sProperty])) {
$aFontProperties[$sProperty] = $mValue;
}
}
} else if (in_array($mValue, array('italic', 'oblique'))) {
$aFontProperties['font-style'] = $mValue;
} else if ($mValue == 'small-caps') {
$aFontProperties['font-variant'] = $mValue;
} else if (
in_array($mValue, array('bold', 'bolder', 'lighter'))
|| ($mValue instanceof Size
&& in_array($mValue->getSize(), range(100, 900, 100)))
) {
$aFontProperties['font-weight'] = $mValue;
} else if ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') {
list($oSize, $oHeight) = $mValue->getListComponents();
$aFontProperties['font-size'] = $oSize;
$aFontProperties['line-height'] = $oHeight;
} else if ($mValue instanceof Size && $mValue->getUnit() !== null) {
$aFontProperties['font-size'] = $mValue;
} else {
$aFontProperties['font-family'] = $mValue;
}
}
foreach ($aFontProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $this->iLineNo);
$oNewRule->addValue($mValue);
$oNewRule->setIsImportant($oRule->getIsImportant());
$this->addRule($oNewRule);
}
$this->removeRule('font');
}
/*
* Convert shorthand background declarations
* (e.g. <tt>background: url("chess.png") gray 50% repeat fixed;</tt>)
* into their constituent parts.
* @see http://www.w3.org/TR/21/colors.html#propdef-background
* */
public function expandBackgroundShorthand() {
$aRules = $this->getRulesAssoc();
if (!isset($aRules['background']))
return;
$oRule = $aRules['background'];
$aBgProperties = array(
'background-color' => array('transparent'), 'background-image' => array('none'),
'background-repeat' => array('repeat'), 'background-attachment' => array('scroll'),
'background-position' => array(new Size(0, '%', null, false, $this->iLineNo), new Size(0, '%', null, false, $this->iLineNo))
);
$mRuleValue = $oRule->getValue();
$aValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
if (count($aValues) == 1 && $aValues[0] == 'inherit') {
foreach ($aBgProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $this->iLineNo);
$oNewRule->addValue('inherit');
$oNewRule->setIsImportant($oRule->getIsImportant());
$this->addRule($oNewRule);
}
$this->removeRule('background');
return;
}
$iNumBgPos = 0;
foreach ($aValues as $mValue) {
if (!$mValue instanceof Value) {
$mValue = mb_strtolower($mValue);
}
if ($mValue instanceof URL) {
$aBgProperties['background-image'] = $mValue;
} else if ($mValue instanceof Color) {
$aBgProperties['background-color'] = $mValue;
} else if (in_array($mValue, array('scroll', 'fixed'))) {
$aBgProperties['background-attachment'] = $mValue;
} else if (in_array($mValue, array('repeat', 'no-repeat', 'repeat-x', 'repeat-y'))) {
$aBgProperties['background-repeat'] = $mValue;
} else if (in_array($mValue, array('left', 'center', 'right', 'top', 'bottom'))
|| $mValue instanceof Size
) {
if ($iNumBgPos == 0) {
$aBgProperties['background-position'][0] = $mValue;
$aBgProperties['background-position'][1] = 'center';
} else {
$aBgProperties['background-position'][$iNumBgPos] = $mValue;
}
$iNumBgPos++;
}
}
foreach ($aBgProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $this->iLineNo);
$oNewRule->setIsImportant($oRule->getIsImportant());
$oNewRule->addValue($mValue);
$this->addRule($oNewRule);
}
$this->removeRule('background');
}
public function expandListStyleShorthand() {
$aListProperties = array(
'list-style-type' => 'disc',
'list-style-position' => 'outside',
'list-style-image' => 'none'
);
$aListStyleTypes = array(
'none', 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal',
'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin',
'upper-alpha', 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic',
'hiragana', 'hira-gana-iroha', 'katakana-iroha', 'katakana'
);
$aListStylePositions = array(
'inside', 'outside'
);
$aRules = $this->getRulesAssoc();
if (!isset($aRules['list-style']))
return;
$oRule = $aRules['list-style'];
$mRuleValue = $oRule->getValue();
$aValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
if (count($aValues) == 1 && $aValues[0] == 'inherit') {
foreach ($aListProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $this->iLineNo);
$oNewRule->addValue('inherit');
$oNewRule->setIsImportant($oRule->getIsImportant());
$this->addRule($oNewRule);
}
$this->removeRule('list-style');
return;
}
foreach ($aValues as $mValue) {
if (!$mValue instanceof Value) {
$mValue = mb_strtolower($mValue);
}
if ($mValue instanceof Url) {
$aListProperties['list-style-image'] = $mValue;
} else if (in_array($mValue, $aListStyleTypes)) {
$aListProperties['list-style-types'] = $mValue;
} else if (in_array($mValue, $aListStylePositions)) {
$aListProperties['list-style-position'] = $mValue;
}
}
foreach ($aListProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $this->iLineNo);
$oNewRule->setIsImportant($oRule->getIsImportant());
$oNewRule->addValue($mValue);
$this->addRule($oNewRule);
}
$this->removeRule('list-style');
}
public function createShorthandProperties(array $aProperties, $sShorthand) {
$aRules = $this->getRulesAssoc();
$aNewValues = array();
foreach ($aProperties as $sProperty) {
if (!isset($aRules[$sProperty]))
continue;
$oRule = $aRules[$sProperty];
if (!$oRule->getIsImportant()) {
$mRuleValue = $oRule->getValue();
$aValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
foreach ($aValues as $mValue) {
$aNewValues[] = $mValue;
}
$this->removeRule($sProperty);
}
}
if (count($aNewValues)) {
$oNewRule = new Rule($sShorthand, $this->iLineNo);
foreach ($aNewValues as $mValue) {
$oNewRule->addValue($mValue);
}
$this->addRule($oNewRule);
}
}
public function createBackgroundShorthand() {
$aProperties = array(
'background-color', 'background-image', 'background-repeat',
'background-position', 'background-attachment'
);
$this->createShorthandProperties($aProperties, 'background');
}
public function createListStyleShorthand() {
$aProperties = array(
'list-style-type', 'list-style-position', 'list-style-image'
);
$this->createShorthandProperties($aProperties, 'list-style');
}
/**
* Combine border-color, border-style and border-width into border
* Should be run after create_dimensions_shorthand!
* */
public function createBorderShorthand() {
$aProperties = array(
'border-width', 'border-style', 'border-color'
);
$this->createShorthandProperties($aProperties, 'border');
}
/*
* Looks for long format CSS dimensional properties
* (margin, padding, border-color, border-style and border-width)
* and converts them into shorthand CSS properties.
* */
public function createDimensionsShorthand() {
$aPositions = array('top', 'right', 'bottom', 'left');
$aExpansions = array(
'margin' => 'margin-%s',
'padding' => 'padding-%s',
'border-color' => 'border-%s-color',
'border-style' => 'border-%s-style',
'border-width' => 'border-%s-width'
);
$aRules = $this->getRulesAssoc();
foreach ($aExpansions as $sProperty => $sExpanded) {
$aFoldable = array();
foreach ($aRules as $sRuleName => $oRule) {
foreach ($aPositions as $sPosition) {
if ($sRuleName == sprintf($sExpanded, $sPosition)) {
$aFoldable[$sRuleName] = $oRule;
}
}
}
// All four dimensions must be present
if (count($aFoldable) == 4) {
$aValues = array();
foreach ($aPositions as $sPosition) {
$oRule = $aRules[sprintf($sExpanded, $sPosition)];
$mRuleValue = $oRule->getValue();
$aRuleValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aRuleValues[] = $mRuleValue;
} else {
$aRuleValues = $mRuleValue->getListComponents();
}
$aValues[$sPosition] = $aRuleValues;
}
$oNewRule = new Rule($sProperty, $this->iLineNo);
if ((string) $aValues['left'][0] == (string) $aValues['right'][0]) {
if ((string) $aValues['top'][0] == (string) $aValues['bottom'][0]) {
if ((string) $aValues['top'][0] == (string) $aValues['left'][0]) {
// All 4 sides are equal
$oNewRule->addValue($aValues['top']);
} else {
// Top and bottom are equal, left and right are equal
$oNewRule->addValue($aValues['top']);
$oNewRule->addValue($aValues['left']);
}
} else {
// Only left and right are equal
$oNewRule->addValue($aValues['top']);
$oNewRule->addValue($aValues['left']);
$oNewRule->addValue($aValues['bottom']);
}
} else {
// No sides are equal
$oNewRule->addValue($aValues['top']);
$oNewRule->addValue($aValues['left']);
$oNewRule->addValue($aValues['bottom']);
$oNewRule->addValue($aValues['right']);
}
$this->addRule($oNewRule);
foreach ($aPositions as $sPosition) {
$this->removeRule(sprintf($sExpanded, $sPosition));
}
}
}
}
/**
* Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and
* tries to convert them into a shorthand CSS <tt>font</tt> property.
* At least font-size AND font-family must be present in order to create a shorthand declaration.
* */
public function createFontShorthand() {
$aFontProperties = array(
'font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family'
);
$aRules = $this->getRulesAssoc();
if (!isset($aRules['font-size']) || !isset($aRules['font-family'])) {
return;
}
$oNewRule = new Rule('font', $this->iLineNo);
foreach (array('font-style', 'font-variant', 'font-weight') as $sProperty) {
if (isset($aRules[$sProperty])) {
$oRule = $aRules[$sProperty];
$mRuleValue = $oRule->getValue();
$aValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
if ($aValues[0] !== 'normal') {
$oNewRule->addValue($aValues[0]);
}
}
}
// Get the font-size value
$oRule = $aRules['font-size'];
$mRuleValue = $oRule->getValue();
$aFSValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aFSValues[] = $mRuleValue;
} else {
$aFSValues = $mRuleValue->getListComponents();
}
// But wait to know if we have line-height to add it
if (isset($aRules['line-height'])) {
$oRule = $aRules['line-height'];
$mRuleValue = $oRule->getValue();
$aLHValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aLHValues[] = $mRuleValue;
} else {
$aLHValues = $mRuleValue->getListComponents();
}
if ($aLHValues[0] !== 'normal') {
$val = new RuleValueList('/', $this->iLineNo);
$val->addListComponent($aFSValues[0]);
$val->addListComponent($aLHValues[0]);
$oNewRule->addValue($val);
}
} else {
$oNewRule->addValue($aFSValues[0]);
}
$oRule = $aRules['font-family'];
$mRuleValue = $oRule->getValue();
$aFFValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aFFValues[] = $mRuleValue;
} else {
$aFFValues = $mRuleValue->getListComponents();
}
$oFFValue = new RuleValueList(',', $this->iLineNo);
$oFFValue->setListComponents($aFFValues);
$oNewRule->addValue($oFFValue);
$this->addRule($oNewRule);
foreach ($aFontProperties as $sProperty) {
$this->removeRule($sProperty);
}
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
if(count($this->aSelectors) === 0) {
// If all the selectors have been removed, this declaration block becomes invalid
throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo);
}
$sResult = $oOutputFormat->sBeforeDeclarationBlock;
$sResult .= $oOutputFormat->implode($oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors);
$sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors;
$sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{';
$sResult .= parent::render($oOutputFormat);
$sResult .= '}';
$sResult .= $oOutputFormat->sAfterDeclarationBlock;
return $sResult;
}
}

View File

@ -0,0 +1,212 @@
<?php
namespace Sabberworm\CSS\RuleSet;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\Rule\Rule;
/**
* RuleSet is a generic superclass denoting rules. The typical example for rule sets are declaration block.
* However, unknown At-Rules (like @font-face) are also rule sets.
*/
abstract class RuleSet implements Renderable, Commentable {
private $aRules;
protected $iLineNo;
protected $aComments;
public function __construct($iLineNo = 0) {
$this->aRules = array();
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet) {
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
while (!$oParserState->comes('}')) {
$oRule = null;
if($oParserState->getSettings()->bLenientParsing) {
try {
$oRule = Rule::parse($oParserState);
} catch (UnexpectedTokenException $e) {
try {
$sConsume = $oParserState->consumeUntil(array("\n", ";", '}'), true);
// We need to “unfind” the matches to the end of the ruleSet as this will be matched later
if($oParserState->streql(substr($sConsume, -1), '}')) {
$oParserState->backtrack(1);
} else {
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
}
} catch (UnexpectedTokenException $e) {
// Weve reached the end of the document. Just close the RuleSet.
return;
}
}
} else {
$oRule = Rule::parse($oParserState);
}
if($oRule) {
$oRuleSet->addRule($oRule);
}
}
$oParserState->consume('}');
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
public function addRule(Rule $oRule, Rule $oSibling = null) {
$sRule = $oRule->getRule();
if(!isset($this->aRules[$sRule])) {
$this->aRules[$sRule] = array();
}
$iPosition = count($this->aRules[$sRule]);
if ($oSibling !== null) {
$iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true);
if ($iSiblingPos !== false) {
$iPosition = $iSiblingPos;
}
}
array_splice($this->aRules[$sRule], $iPosition, 0, array($oRule));
}
/**
* Returns all rules matching the given rule name
* @param (null|string|Rule) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()).
* @example $oRuleSet->getRules('font-') //returns an array of all rules either beginning with font- or matching font.
* @example $oRuleSet->getRules('font') //returns array(0 => $oRule, …) or array().
* @return Rule[] Rules.
*/
public function getRules($mRule = null) {
if ($mRule instanceof Rule) {
$mRule = $mRule->getRule();
}
$aResult = array();
foreach($this->aRules as $sName => $aRules) {
// Either no search rule is given or the search rule matches the found rule exactly or the search rule ends in “-” and the found rule starts with the search rule.
if(!$mRule || $sName === $mRule || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))) {
$aResult = array_merge($aResult, $aRules);
}
}
return $aResult;
}
/**
* Override all the rules of this set.
* @param Rule[] $aRules The rules to override with.
*/
public function setRules(array $aRules) {
$this->aRules = array();
foreach ($aRules as $rule) {
$this->addRule($rule);
}
}
/**
* Returns all rules matching the given pattern and returns them in an associative array with the rules name as keys. This method exists mainly for backwards-compatibility and is really only partially useful.
* @param (string) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()).
* Note: This method loses some information: Calling this (with an argument of 'background-') on a declaration block like { background-color: green; background-color; rgba(0, 127, 0, 0.7); } will only yield an associative array containing the rgba-valued rule while @link{getRules()} would yield an indexed array containing both.
* @return Rule[] Rules.
*/
public function getRulesAssoc($mRule = null) {
$aResult = array();
foreach($this->getRules($mRule) as $oRule) {
$aResult[$oRule->getRule()] = $oRule;
}
return $aResult;
}
/**
* Remove a rule from this RuleSet. This accepts all the possible values that @link{getRules()} accepts. If given a Rule, it will only remove this particular rule (by identity). If given a name, it will remove all rules by that name. Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would remove all rules with the same name. To get the old behvaiour, use removeRule($oRule->getRule()).
* @param (null|string|Rule) $mRule pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash, all rules starting with the pattern are removed as well as one matching the pattern with the dash excluded. Passing a Rule behaves matches by identity.
*/
public function removeRule($mRule) {
if($mRule instanceof Rule) {
$sRule = $mRule->getRule();
if(!isset($this->aRules[$sRule])) {
return;
}
foreach($this->aRules[$sRule] as $iKey => $oRule) {
if($oRule === $mRule) {
unset($this->aRules[$sRule][$iKey]);
}
}
} else {
foreach($this->aRules as $sName => $aRules) {
// Either no search rule is given or the search rule matches the found rule exactly or the search rule ends in “-” and the found rule starts with the search rule or equals it (without the trailing dash).
if(!$mRule || $sName === $mRule || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))) {
unset($this->aRules[$sName]);
}
}
}
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sResult = '';
$bIsFirst = true;
foreach ($this->aRules as $aRules) {
foreach($aRules as $oRule) {
$sRendered = $oOutputFormat->safely(function() use ($oRule, $oOutputFormat) {
return $oRule->render($oOutputFormat->nextLevel());
});
if($sRendered === null) {
continue;
}
if($bIsFirst) {
$bIsFirst = false;
$sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules();
} else {
$sResult .= $oOutputFormat->nextLevel()->spaceBetweenRules();
}
$sResult .= $sRendered;
}
}
if(!$bIsFirst) {
// Had some output
$sResult .= $oOutputFormat->spaceAfterRules();
}
return $oOutputFormat->removeLastSemicolon($sResult);
}
/**
* @param array $aComments Array of comments.
*/
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array
*/
public function getComments() {
return $this->aComments;
}
/**
* @param array $aComments Array containing Comment objects.
*/
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Sabberworm\CSS;
use Sabberworm\CSS\Rule\Rule;
/**
* Parser settings class.
*
* Configure parser behaviour here.
*/
class Settings {
/**
* Multi-byte string support. If true (mbstring extension must be enabled), will use (slower) mb_strlen, mb_convert_case, mb_substr and mb_strpos functions. Otherwise, the normal (ASCII-Only) functions will be used.
*/
public $bMultibyteSupport;
/**
* The default charset for the CSS if no `@charset` rule is found. Defaults to utf-8.
*/
public $sDefaultCharset = 'utf-8';
/**
* Lenient parsing. When used (which is true by default), the parser will not choke on unexpected tokens but simply ignore them.
*/
public $bLenientParsing = true;
private function __construct() {
$this->bMultibyteSupport = extension_loaded('mbstring');
}
public static function create() {
return new Settings();
}
public function withMultibyteSupport($bMultibyteSupport = true) {
$this->bMultibyteSupport = $bMultibyteSupport;
return $this;
}
public function withDefaultCharset($sDefaultCharset) {
$this->sDefaultCharset = $sDefaultCharset;
return $this;
}
public function withLenientParsing($bLenientParsing = true) {
$this->bLenientParsing = $bLenientParsing;
return $this;
}
public function beStrict() {
return $this->withLenientParsing(false);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Sabberworm\CSS\Value;
class CSSFunction extends ValueList {
protected $sName;
public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0) {
if($aArguments instanceof RuleValueList) {
$sSeparator = $aArguments->getListSeparator();
$aArguments = $aArguments->getListComponents();
}
$this->sName = $sName;
$this->iLineNo = $iLineNo;
parent::__construct($aArguments, $sSeparator, $iLineNo);
}
public function getName() {
return $this->sName;
}
public function setName($sName) {
$this->sName = $sName;
}
public function getArguments() {
return $this->aComponents;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$aArguments = parent::render($oOutputFormat);
return "{$this->sName}({$aArguments})";
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
class CSSString extends PrimitiveValue {
private $sString;
public function __construct($sString, $iLineNo = 0) {
$this->sString = $sString;
parent::__construct($iLineNo);
}
public static function parse(ParserState $oParserState) {
$sBegin = $oParserState->peek();
$sQuote = null;
if ($sBegin === "'") {
$sQuote = "'";
} else if ($sBegin === '"') {
$sQuote = '"';
}
if ($sQuote !== null) {
$oParserState->consume($sQuote);
}
$sResult = "";
$sContent = null;
if ($sQuote === null) {
// Unquoted strings end in whitespace or with braces, brackets, parentheses
while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) {
$sResult .= $oParserState->parseCharacter(false);
}
} else {
while (!$oParserState->comes($sQuote)) {
$sContent = $oParserState->parseCharacter(false);
if ($sContent === null) {
throw new SourceException("Non-well-formed quoted string {$oParserState->peek(3)}", $oParserState->currentLine());
}
$sResult .= $sContent;
}
$oParserState->consume($sQuote);
}
return new CSSString($sResult, $oParserState->currentLine());
}
public function setString($sString) {
$this->sString = $sString;
}
public function getString() {
return $this->sString;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sString = addslashes($this->sString);
$sString = str_replace("\n", '\A', $sString);
return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType();
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class CalcFunction extends CSSFunction {
const T_OPERAND = 1;
const T_OPERATOR = 2;
public static function parse(ParserState $oParserState) {
$aOperators = array('+', '-', '*', '/');
$sFunction = trim($oParserState->consumeUntil('(', false, true));
$oCalcList = new CalcRuleValueList($oParserState->currentLine());
$oList = new RuleValueList(',', $oParserState->currentLine());
$iNestingLevel = 0;
$iLastComponentType = NULL;
while(!$oParserState->comes(')') || $iNestingLevel > 0) {
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('(')) {
$iNestingLevel++;
$oCalcList->addListComponent($oParserState->consume(1));
continue;
} else if ($oParserState->comes(')')) {
$iNestingLevel--;
$oCalcList->addListComponent($oParserState->consume(1));
continue;
}
if ($iLastComponentType != CalcFunction::T_OPERAND) {
$oVal = Value::parsePrimitiveValue($oParserState);
$oCalcList->addListComponent($oVal);
$iLastComponentType = CalcFunction::T_OPERAND;
} else {
if (in_array($oParserState->peek(), $aOperators)) {
if (($oParserState->comes('-') || $oParserState->comes('+'))) {
if ($oParserState->peek(1, -1) != ' ' || !($oParserState->comes('- ') || $oParserState->comes('+ '))) {
throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine());
}
}
$oCalcList->addListComponent($oParserState->consume(1));
$iLastComponentType = CalcFunction::T_OPERATOR;
} else {
throw new UnexpectedTokenException(
sprintf(
'Next token was expected to be an operand of type %s. Instead "%s" was found.',
implode(', ', $aOperators),
$oVal
),
'',
'custom',
$oParserState->currentLine()
);
}
}
}
$oList->addListComponent($oCalcList);
$oParserState->consume(')');
return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Sabberworm\CSS\Value;
class CalcRuleValueList extends RuleValueList {
public function __construct($iLineNo = 0) {
parent::__construct(array(), ',', $iLineNo);
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return $oOutputFormat->implode(' ', $this->aComponents);
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
class Color extends CSSFunction {
public function __construct($aColor, $iLineNo = 0) {
parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo);
}
public static function parse(ParserState $oParserState) {
$aColor = array();
if ($oParserState->comes('#')) {
$oParserState->consume('#');
$sValue = $oParserState->parseIdentifier(false);
if ($oParserState->strlen($sValue) === 3) {
$sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2];
} else if ($oParserState->strlen($sValue) === 4) {
$sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3] . $sValue[3];
}
if ($oParserState->strlen($sValue) === 8) {
$aColor = array(
'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()),
'a' => new Size(round(self::mapRange(intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2), null, true, $oParserState->currentLine())
);
} else {
$aColor = array(
'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine())
);
}
} else {
$sColorMode = $oParserState->parseIdentifier(true);
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
$iLength = $oParserState->strlen($sColorMode);
for ($i = 0; $i < $iLength; ++$i) {
$oParserState->consumeWhiteSpace();
$aColor[$sColorMode[$i]] = Size::parse($oParserState, true);
$oParserState->consumeWhiteSpace();
if ($i < ($iLength - 1)) {
$oParserState->consume(',');
}
}
$oParserState->consume(')');
}
return new Color($aColor, $oParserState->currentLine());
}
private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) {
$fFromRange = $fFromMax - $fFromMin;
$fToRange = $fToMax - $fToMin;
$fMultiplier = $fToRange / $fFromRange;
$fNewVal = $fVal - $fFromMin;
$fNewVal *= $fMultiplier;
return $fNewVal + $fToMin;
}
public function getColor() {
return $this->aComponents;
}
public function setColor($aColor) {
$this->setName(implode('', array_keys($aColor)));
$this->aComponents = $aColor;
}
public function getColorDescription() {
return $this->getName();
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
// Shorthand RGB color values
if($oOutputFormat->getRGBHashNotation() && implode('', array_keys($this->aComponents)) === 'rgb') {
$sResult = sprintf(
'%02x%02x%02x',
$this->aComponents['r']->getSize(),
$this->aComponents['g']->getSize(),
$this->aComponents['b']->getSize()
);
return '#'.(($sResult[0] == $sResult[1]) && ($sResult[2] == $sResult[3]) && ($sResult[4] == $sResult[5]) ? "$sResult[0]$sResult[2]$sResult[4]" : $sResult);
}
return parent::render($oOutputFormat);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class LineName extends ValueList {
public function __construct($aComponents = array(), $iLineNo = 0) {
parent::__construct($aComponents, ' ', $iLineNo);
}
public static function parse(ParserState $oParserState) {
$oParserState->consume('[');
$oParserState->consumeWhiteSpace();
$aNames = array();
do {
if($oParserState->getSettings()->bLenientParsing) {
try {
$aNames[] = $oParserState->parseIdentifier();
} catch(UnexpectedTokenException $e) {}
} else {
$aNames[] = $oParserState->parseIdentifier();
}
$oParserState->consumeWhiteSpace();
} while (!$oParserState->comes(']'));
$oParserState->consume(']');
return new LineName($aNames, $oParserState->currentLine());
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return '[' . parent::render(\Sabberworm\CSS\OutputFormat::createCompact()) . ']';
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Sabberworm\CSS\Value;
abstract class PrimitiveValue extends Value {
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Sabberworm\CSS\Value;
class RuleValueList extends ValueList {
public function __construct($sSeparator = ',', $iLineNo = 0) {
parent::__construct(array(), $sSeparator, $iLineNo);
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
class Size extends PrimitiveValue {
const ABSOLUTE_SIZE_UNITS = 'px/cm/mm/mozmm/in/pt/pc/vh/vw/vm/vmin/vmax/rem'; //vh/vw/vm(ax)/vmin/rem are absolute insofar as they dont scale to the immediate parent (only the viewport)
const RELATIVE_SIZE_UNITS = '%/em/ex/ch/fr';
const NON_SIZE_UNITS = 'deg/grad/rad/s/ms/turns/Hz/kHz';
private static $SIZE_UNITS = null;
private $fSize;
private $sUnit;
private $bIsColorComponent;
public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0) {
parent::__construct($iLineNo);
$this->fSize = floatval($fSize);
$this->sUnit = $sUnit;
$this->bIsColorComponent = $bIsColorComponent;
}
public static function parse(ParserState $oParserState, $bIsColorComponent = false) {
$sSize = '';
if ($oParserState->comes('-')) {
$sSize .= $oParserState->consume('-');
}
while (is_numeric($oParserState->peek()) || $oParserState->comes('.')) {
if ($oParserState->comes('.')) {
$sSize .= $oParserState->consume('.');
} else {
$sSize .= $oParserState->consume(1);
}
}
$sUnit = null;
$aSizeUnits = self::getSizeUnits();
foreach($aSizeUnits as $iLength => &$aValues) {
$sKey = strtolower($oParserState->peek($iLength));
if(array_key_exists($sKey, $aValues)) {
if (($sUnit = $aValues[$sKey]) !== null) {
$oParserState->consume($iLength);
break;
}
}
}
return new Size(floatval($sSize), $sUnit, $bIsColorComponent, $oParserState->currentLine());
}
private static function getSizeUnits() {
if(self::$SIZE_UNITS === null) {
self::$SIZE_UNITS = array();
foreach (explode('/', Size::ABSOLUTE_SIZE_UNITS.'/'.Size::RELATIVE_SIZE_UNITS.'/'.Size::NON_SIZE_UNITS) as $val) {
$iSize = strlen($val);
if(!isset(self::$SIZE_UNITS[$iSize])) {
self::$SIZE_UNITS[$iSize] = array();
}
self::$SIZE_UNITS[$iSize][strtolower($val)] = $val;
}
// FIXME: Should we not order the longest units first?
ksort(self::$SIZE_UNITS, SORT_NUMERIC);
}
return self::$SIZE_UNITS;
}
public function setUnit($sUnit) {
$this->sUnit = $sUnit;
}
public function getUnit() {
return $this->sUnit;
}
public function setSize($fSize) {
$this->fSize = floatval($fSize);
}
public function getSize() {
return $this->fSize;
}
public function isColorComponent() {
return $this->bIsColorComponent;
}
/**
* Returns whether the number stored in this Size really represents a size (as in a length of something on screen).
* @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object.
*/
public function isSize() {
if (in_array($this->sUnit, explode('/', self::NON_SIZE_UNITS))) {
return false;
}
return !$this->isColorComponent();
}
public function isRelative() {
if (in_array($this->sUnit, explode('/', self::RELATIVE_SIZE_UNITS))) {
return true;
}
if ($this->sUnit === null && $this->fSize != 0) {
return true;
}
return false;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$l = localeconv();
$sPoint = preg_quote($l['decimal_point'], '/');
return preg_replace(array("/$sPoint/", "/^(-?)0\./"), array('.', '$1.'), $this->fSize) . ($this->sUnit === null ? '' : $this->sUnit);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
class URL extends PrimitiveValue {
private $oURL;
public function __construct(CSSString $oURL, $iLineNo = 0) {
parent::__construct($iLineNo);
$this->oURL = $oURL;
}
public static function parse(ParserState $oParserState) {
$bUseUrl = $oParserState->comes('url', true);
if ($bUseUrl) {
$oParserState->consume('url');
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
}
$oParserState->consumeWhiteSpace();
$oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());
if ($bUseUrl) {
$oParserState->consumeWhiteSpace();
$oParserState->consume(')');
}
return $oResult;
}
public function setURL(CSSString $oURL) {
$this->oURL = $oURL;
}
public function getURL() {
return $this->oURL;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return "url({$this->oURL->render($oOutputFormat)})";
}
}

View File

@ -0,0 +1,131 @@
<?php
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Renderable;
abstract class Value implements Renderable {
protected $iLineNo;
public function __construct($iLineNo = 0) {
$this->iLineNo = $iLineNo;
}
public static function parseValue(ParserState $oParserState, $aListDelimiters = array()) {
$aStack = array();
$oParserState->consumeWhiteSpace();
//Build a list of delimiters and parsed values
while (!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') || $oParserState->comes(')') || $oParserState->comes('\\'))) {
if (count($aStack) > 0) {
$bFoundDelimiter = false;
foreach ($aListDelimiters as $sDelimiter) {
if ($oParserState->comes($sDelimiter)) {
array_push($aStack, $oParserState->consume($sDelimiter));
$oParserState->consumeWhiteSpace();
$bFoundDelimiter = true;
break;
}
}
if (!$bFoundDelimiter) {
//Whitespace was the list delimiter
array_push($aStack, ' ');
}
}
array_push($aStack, self::parsePrimitiveValue($oParserState));
$oParserState->consumeWhiteSpace();
}
//Convert the list to list objects
foreach ($aListDelimiters as $sDelimiter) {
if (count($aStack) === 1) {
return $aStack[0];
}
$iStartPosition = null;
while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) {
$iLength = 2; //Number of elements to be joined
for ($i = $iStartPosition + 2; $i < count($aStack); $i+=2, ++$iLength) {
if ($sDelimiter !== $aStack[$i]) {
break;
}
}
$oList = new RuleValueList($sDelimiter, $oParserState->currentLine());
for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) {
$oList->addListComponent($aStack[$i]);
}
array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, array($oList));
}
}
if (!isset($aStack[0])) {
throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine());
}
return $aStack[0];
}
public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) {
$sResult = $oParserState->parseIdentifier($bIgnoreCase);
if ($oParserState->comes('(')) {
$oParserState->consume('(');
$aArguments = Value::parseValue($oParserState, array('=', ' ', ','));
$sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine());
$oParserState->consume(')');
}
return $sResult;
}
public static function parsePrimitiveValue(ParserState $oParserState) {
$oValue = null;
$oParserState->consumeWhiteSpace();
if (is_numeric($oParserState->peek()) || ($oParserState->comes('-.') && is_numeric($oParserState->peek(1, 2))) || (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1)))) {
$oValue = Size::parse($oParserState);
} else if ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) {
$oValue = Color::parse($oParserState);
} else if ($oParserState->comes('url', true)) {
$oValue = URL::parse($oParserState);
} else if ($oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true) || $oParserState->comes('-moz-calc', true)) {
$oValue = CalcFunction::parse($oParserState);
} else if ($oParserState->comes("'") || $oParserState->comes('"')) {
$oValue = CSSString::parse($oParserState);
} else if ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) {
$oValue = self::parseMicrosoftFilter($oParserState);
} else if ($oParserState->comes("[")) {
$oValue = LineName::parse($oParserState);
} else if ($oParserState->comes("U+")) {
$oValue = self::parseUnicodeRangeValue($oParserState);
} else {
$oValue = self::parseIdentifierOrFunction($oParserState);
}
$oParserState->consumeWhiteSpace();
return $oValue;
}
private static function parseMicrosoftFilter(ParserState $oParserState) {
$sFunction = $oParserState->consumeUntil('(', false, true);
$aArguments = Value::parseValue($oParserState, array(',', '='));
return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine());
}
private static function parseUnicodeRangeValue(ParserState $oParserState) {
$iCodepointMaxLenth = 6; // Code points outside BMP can use up to six digits
$sRange = "";
$oParserState->consume("U+");
do {
if ($oParserState->comes('-')) $iCodepointMaxLenth = 13; // Max length is 2 six digit code points + the dash(-) between them
$sRange .= $oParserState->consume(1);
} while (strlen($sRange) < $iCodepointMaxLenth && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek()));
return "U+{$sRange}";
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
//Methods are commented out because re-declaring them here is a fatal error in PHP < 5.3.9
//public abstract function __toString();
//public abstract function render(\Sabberworm\CSS\OutputFormat $oOutputFormat);
}

View File

@ -0,0 +1,47 @@
<?php
namespace Sabberworm\CSS\Value;
abstract class ValueList extends Value {
protected $aComponents;
protected $sSeparator;
public function __construct($aComponents = array(), $sSeparator = ',', $iLineNo = 0) {
parent::__construct($iLineNo);
if (!is_array($aComponents)) {
$aComponents = array($aComponents);
}
$this->aComponents = $aComponents;
$this->sSeparator = $sSeparator;
}
public function addListComponent($mComponent) {
$this->aComponents[] = $mComponent;
}
public function getListComponents() {
return $this->aComponents;
}
public function setListComponents($aComponents) {
$this->aComponents = $aComponents;
}
public function getListSeparator() {
return $this->sSeparator;
}
public function setListSeparator($sSeparator) {
$this->sSeparator = $sSeparator;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return $oOutputFormat->implode($oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), $this->aComponents);
}
}

View File

@ -0,0 +1,8 @@
<?php namespace WillWashburn\Stream\Exception;
/**
* Class StreamBufferTooSmallException
*
* @package WillWashburn\Stream\Exception
*/
class StreamBufferTooSmallException extends \Exception {}

View File

@ -0,0 +1,78 @@
<?php namespace WillWashburn\Stream;
use WillWashburn\Stream\Exception\StreamBufferTooSmallException;
/**
* Class Stream
*
* @package FasterImage
*/
class Stream implements StreamableInterface
{
/**
* The string that we have downloaded so far
*/
protected $stream_string = '';
/**
* The pointer in the string
*
* @var int
*/
protected $strpos = 0;
/**
* Get characters from the string but don't move the pointer
*
* @param $characters
*
* @return string
* @throws StreamBufferTooSmallException
*/
public function peek($characters)
{
if ( strlen($this->stream_string) < $this->strpos + $characters ) {
throw new StreamBufferTooSmallException('Not enough of the stream available.');
}
return substr($this->stream_string, $this->strpos, $characters);
}
/**
* Get Characters from the string
*
* @param $characters
*
* @return string
* @throws StreamBufferTooSmallException
*/
public function read($characters)
{
$result = $this->peek($characters);
$this->strpos += $characters;
return $result;
}
/**
* Resets the pointer to the 0 position
*
* @return mixed
*/
public function resetPointer()
{
$this->strpos = 0;
}
/**
* Append to the stream string
*
* @param $string
*/
public function write($string)
{
$this->stream_string .= $string;
}
}

View File

@ -0,0 +1,39 @@
<?php namespace WillWashburn\Stream;
/**
* Interface StreamableInterface
*
* @package FasterImage
*/
interface StreamableInterface {
/**
* Append to the stream string
*
* @param $string
*/
public function write($string);
/**
* Get Characters from the string
*
* @param $characters
*/
public function read($characters);
/**
* Get characters from the string but don't move the pointer
*
* @param $characters
*
* @return mixed
*/
public function peek($characters);
/**
* Resets the pointer to the 0 position
* @return mixed
*/
public function resetPointer();
}