<?php
/**
*
* Main script. Includes core public API
*
*/
class csscrush {
// Path information, global settings
public static $config;
// The path of this script
public static $location;
// Aliases from the aliases file
public static $aliases = array();
public static $aliasesRaw = array();
// Macro function names
public static $macros = array();
public static $COMPILE_SUFFIX = '.crush.css';
// Global variable storage
protected static $globalVars = array();
protected static $assetsLoaded = false;
// Properties available to each 'file' process
public static $storage;
public static $compileName;
public static $options;
protected static $tokenUID;
// Regular expressions
public static $regex = array(
'import' => '!
@import\s+ # import at-rule
(?:
url\(\s*([^\)]+)\s*\) # url function
| # or
([_s\d]+) # string token
)
\s*([^;]*);? # media argument
!x',
'variables' => '!@(?:variables|define)\s*([^\{]*)\{\s*(.*?)\s*\};?!s',
'atRule' => '!@([-a-z_]+)\s*([^\{]*)\{\s*(.*?)\s*\};?!s',
'comment' => '!/\*(.*?)\*/!s',
'string' => '!(\'|"|`)(?:\\1|[^\1])*?\1!',
// As an exception we treat @font-face and @page rules like standard rules
'rule' => '!
(\n(?:[^@{}]+|@(?:font-face|page)[^{]*)) # The selector
\{([^{}]*)\} # The declaration block
!x',
'token' => array(
'comment' => '!___c\d+___!',
'string' => '!___s\d+___!',
'rule' => '!___r\d+___!',
'paren' => '!___p\d+___!',
),
'function' => array(
'var' => '!(?:
([^a-z0-9_-])
var\(\s*([a-z0-9_-]+)\s*\)
|
\$\(\s*([a-z0-9_-]+)\s*\) # Dollar syntax
)!ix',
'match' => '!(^|[^a-z0-9_-])([a-z_-]+)(___p\d+___)!i',
),
'vendorPrefix' => '!^-([a-z]+)-([a-z-]+)!',
'absoluteUrl' => '!^https?://!',
);
// Init called once manually post class definition
public static function init ( $current_dir ) {
self::$location = $current_dir;
self::$config = $config = new stdClass;
$config->file = '.' . __CLASS__;
$config->data = null;
$config->path = null;
$config->baseDir = null;
$config->baseURL = null;
// Get normalized document root reference: no symlink, forward slashes, no trailing slashes
$docRoot = null;
if ( isset( $_SERVER[ 'DOCUMENT_ROOT' ] ) ) {
$docRoot = realpath( $_SERVER[ 'DOCUMENT_ROOT' ] );
}
else {
// Probably IIS
$scriptname = $_SERVER[ 'SCRIPT_NAME' ];
$fullpath = realpath( basename( $scriptname ) );
$docRoot = substr( $fullpath, 0, stripos( $fullpath, $scriptname ) );
}
$config->docRoot = csscrush_util::normalizeSystemPath( $docRoot );
// Casting to objects for ease of use
self::$regex = (object) self::$regex;
self::$regex->token = (object) self::$regex->token;
self::$regex->function = (object) self::$regex->function;
}
// Aliases and macros loader
protected static function loadAssets () {
// Find an aliases file in the root directory
// a local file will overrides the default
$aliases_file = csscrush_util::find( 'Aliases-local.ini', 'Aliases.ini' );
// Load aliases file if it exists
if ( $aliases_file ) {
if ( $result = @parse_ini_file( $aliases_file, true ) ) {
self::$aliasesRaw = $result;
// Value aliases require a little preprocessing
if ( isset( self::$aliasesRaw[ 'values' ] ) ) {
$store = array();
foreach ( self::$aliasesRaw[ 'values' ] as $prop_val => $aliases ) {
list( $prop, $value ) = array_map( 'trim', explode( ':', $prop_val ) );
$store[ $prop ][ $value ] = $aliases;
}
self::$aliasesRaw[ 'values' ] = $store;
}
}
else {
trigger_error( __METHOD__ . ": Aliases file could not be parsed.\n", E_USER_NOTICE );
}
}
else {
trigger_error( __METHOD__ . ": Aliases file not found.\n", E_USER_NOTICE );
}
// Find a plugins file in the root directory
// a local file will overrides the default
$plugins_file = csscrush_util::find( 'Plugins-local.ini', 'Plugins.ini' );
// Load plugins
if ( $plugins_file ) {
if ( $result = @parse_ini_file( $plugins_file ) ) {
foreach ( $result[ 'plugins' ] as $plugin_file ) {
$path = self::$location . "/plugins/$plugin_file";
if ( file_exists( $path ) ) {
require_once $path;
}
else {
trigger_error( __METHOD__ . ": Plugin file $plugin_file not found.\n", E_USER_NOTICE );
}
}
}
else {
trigger_error( __METHOD__ . ": Plugin file could not be parsed.\n", E_USER_NOTICE );
}
}
}
// Initialize config data, create config cache file if needed
protected static function loadCacheData () {
$config = self::$config;
if (
file_exists( $config->path ) and
$config->data and
$config->data[ 'originPath' ] == $config->path
) {
// Already loaded and config file exists in the current directory
return;
}
$configFileExists = file_exists( $config->path );
$configFileWritable = $configFileExists ? is_writable( $config->path ) : false;
if ( $configFileExists and $configFileWritable ) {
// Load from file
$config->data = unserialize( file_get_contents( $config->path ) );
}
else {
// Config file may exist but not be writable (may not be visible in some ftp situations?)
if ( $configFileExists ) {
if ( ! @unlink( $config->path ) ) {
trigger_error( __METHOD__ . ": Could not delete config data file.\n", E_USER_NOTICE );
}
}
// Create
self::log( 'Creating config data file' );
file_put_contents( $config->path, serialize( array() ) );
$config->data = array();
}
}
// Establish the hostfile directory and optionally test it's writable
protected static function setPath ( $new_dir, $write_test = true ) {
$config = self::$config;
$docRoot = $config->docRoot;
if ( strpos( $new_dir, $docRoot ) !== 0 ) {
// Not a system path
$new_dir = realpath( "$docRoot/$new_dir" );
}
$pathtest = true;
if ( ! file_exists( $new_dir ) ) {
trigger_error( __METHOD__ . ": directory '$new_dir' doesn't exist.\n", E_USER_WARNING );
$pathtest = false;
}
else if ( $write_test and ! is_writable( $new_dir ) ) {
self::log( 'Attempting to change permissions' );
if ( ! @chmod( $new_dir, 0755 ) ) {
trigger_error( __METHOD__ . ": directory '$new_dir' is unwritable.\n", E_USER_WARNING );
self::log( 'Unable to update permissions' );
$pathtest = false;
}
else {
self::log( 'Permissions updated' );
}
}
$config->path = "$new_dir/$config->file";
$config->baseDir = $new_dir;
$config->baseURL = substr( $new_dir, strlen( $docRoot ) );
return $pathtest;
}
#############
# Public API
/**
* Process host CSS file and return a new compiled file
*
* @param string $file URL or System path to the host CSS file
* @param mixed $options An array of options or null
* @return string The public path to the compiled file or an empty string
*/
public static function file ( $file, $options = null ) {
$config = self::$config;
// Reset for current process
self::reset();
// Since we're comparing strings, we need to iron out OS differences
$file = str_replace( '\\', '/', $file );
$docRoot = $config->docRoot;
$pathtest = true;
if ( strpos( $file, $docRoot ) === 0 ) {
// System path
$pathtest = self::setPath( dirname( $file ) );
}
else if ( strpos( $file, '/' ) === 0 ) {
// WWW root path
$pathtest = self::setPath( dirname( $docRoot . $file ) );
}
else {
// Relative path
$pathtest = self::setPath( dirname( dirname( __FILE__ ) . '/' . $file ) );
}
if ( ! $pathtest ) {
// Main directory not found or is not writable return an empty string
return '';
}
// Load the data of previously cached files to self::$config
self::loadCacheData();
// Get the merged options, stored to self::$options
$options = self::getOptions( $options );
// Get the hostfile object
$hostfile = self::getHostfile( $file );
// Compiled filename we're searching for
// This can be given as an option, uses the host-filename by default
$baseCompileName = basename( $hostfile->name, '.css' );
if ( !empty( $options[ 'output_file' ] ) ) {
$baseCompileName = basename( $options[ 'output_file' ], '.css' );
}
self::$compileName = $baseCompileName . self::$COMPILE_SUFFIX;
// If cache is enabled check for a valid compiled file
if ( $options[ 'cache' ] === true ) {
$validCompliledFile = self::validateCache( $hostfile );
if ( is_string( $validCompliledFile ) ) {
return $validCompliledFile;
}
}
// Collate hostfile and imports
$stream = csscrush_importer::hostfile( $hostfile );
// Compile
$stream = self::compile( $stream );
// Create file and return path. Return empty string on failure
if ( file_put_contents( "$config->baseDir/" . self::$compileName, $stream ) ) {
return "$config->baseURL/" . self::$compileName .
( $options[ 'versioning' ] ? '?' . time() : '' );
}
else {
return '';
}
}
/**
* Process host CSS file and return an HTML link tag with populated href
*
* @param string $file Absolute or relative path to the host CSS file
* @param mixed $options An array of options or null
* @param array $attributes An array of HTML attributes
* @return string HTML link tag or error message inside HTML comment
*/
public static function tag ( $file, $options = null, $attributes = array() ) {
$file = self::file( $file, $options );
if ( !empty( $file ) ) {
// On success return the tag with any custom attributes
$attributes[ 'rel' ] = "stylesheet";
$attributes[ 'href' ] = $file;
$attr_string = csscrush_util::htmlAttributes( $attributes );
return "<link$attr_string />\n";
}
else {
// Return an HTML comment with message on failure
$class = __CLASS__;
return "<!-- $class: File $file not found -->\n";
}
}
/**
* Process host CSS file and return CSS as text wrapped in html style tags
*
* @param string $file Absolute or relative path to the host CSS file
* @param mixed $options An array of options or null
* @param array $attributes An array of HTML attributes, set false to return CSS text without tag
* @return string HTML link tag or error message inside HTML comment
*/
public static function inline ( $file, $options = null, $attributes = array() ) {
$file = self::file( $file, $options );
if ( !empty( $file ) ) {
// On success fetch the CSS text
$content = file_get_contents( self::$config->baseDir . '/' . self::$compileName );
$tag_open = '';
$tag_close = '';
if ( is_array( $attributes ) ) {
$attr_string = csscrush_util::htmlAttributes( $attributes );
$tag_open = "<style$attr_string>";
$tag_close = '</style>';
}
return "$tag_open{$content}$tag_close\n";
}
else {
// Return an HTML comment with message on failure
$class = __CLASS__;
return "<!-- $class: File $file not found -->\n";
}
}
/**
* Compile a raw string of CSS string and return it
*
* @param string $string CSS text
* @param mixed $options An array of options or null
* @return string CSS text
*/
public static function string ( $string, $options = null ) {
// Reset for current process
self::reset();
self::getOptions( $options );
// Set the path context if one is given
if ( isset( $options[ 'import_context' ] ) && ! empty( $options[ 'import_context' ] ) ) {
self::setPath( $options[ 'import_context' ] );
}
// It's not associated with a real file so we create an 'empty' hostfile object
$hostfile = self::getHostfile();
// Set the string on the object
$hostfile->string = $string;
// Import files may be ignored
if ( isset( $options[ 'no_import' ] ) ) {
$hostfile->importIgnore = true;
}
// Collate imports
$stream = csscrush_importer::hostfile( $hostfile );
// Return compiled string
return self::compile( $stream );
}
/**
* Add variables globally
*
* @param mixed $var Assoc array of variable names and values, a php ini filename or null
*/
public static function globalVars ( $vars ) {
// Merge into the stack, overrides existing variables of the same name
if ( is_array( $vars ) ) {
self::$globalVars = array_merge( self::$globalVars, $vars );
}
// Test for a file. If it is attempt to parse it
elseif ( is_string( $vars ) and file_exists( $vars ) ) {
if ( $result = parse_ini_file( $vars ) ) {
self::$globalVars = array_merge( self::$globalVars, $result );
}
}
// Clear the stack if the argument is explicitly null
elseif ( is_null( $vars ) ) {
self::$globalVars = array();
}
}
/**
* Clear config file and compiled files for the specified directory
*
* @param string $dir System path to the directory
*/
public static function clearCache ( $dir = '' ) {
if ( empty( $dir ) ) {
$dir = dirname( __FILE__ );
}
else if ( !file_exists( $dir ) ) {
return;
}
$configPath = $dir . '/' . self::$config->file;
if ( file_exists( $configPath ) ) {
unlink( $configPath );
}
// Remove any compiled files
$suffix = self::$COMPILE_SUFFIX;
$suffixLength = strlen( $suffix );
foreach ( scandir( $dir ) as $file ) {
if (
strpos( $file, $suffix ) === strlen( $file ) - $suffixLength
) {
unlink( $dir . "/{$file}" );
}
}
}
#####################
# Developer related
public static $logging = false;
public static function log () {
if ( ! self::$logging ) {
return;
}
static $log = '';
$args = func_get_args();
if ( !count( $args ) ) {
// No arguments, return the log
return $log;
}
else {
$arg = $args[0];
}
if ( is_string( $arg ) ) {
$log .= $arg . '<hr>';
}
else {
$out = '<pre>';
ob_start();
print_r( $arg );
$out .= ob_get_clean();
$out .= '</pre>';
$log .= $out . '<hr>';
}
}
#####################
# Internal functions
protected static function getHostfile ( $file = false ) {
// May return a hostfile object associated with a real file
// Alternatively it may return a hostfile object with string input
$config = self::$config;
// Make basic information about the hostfile accessible
$hostfile = new stdClass;
$hostfile->name = $file ? basename( $file ) : null;
$hostfile->dir = $config->baseDir;
$hostfile->path = $file ? "$config->baseDir/$hostfile->name" : null;
if ( $file ) {
if ( !file_exists( $hostfile->path ) ) {
// If host file is not found return an empty string
trigger_error( __METHOD__ . ": File '$hostfile->name' not found.\n", E_USER_WARNING );
return '';
}
else {
// Capture the modified time
$hostfile->mtime = filemtime( $hostfile->path );
}
}
return $hostfile;
}
protected static function getBoilerplate () {
$file = csscrush_util::find( 'CssCrush-local.boilerplate', 'CssCrush.boilerplate' );
if ( ! $file or ! self::$options[ 'boilerplate' ] ) {
return '';
}
// Load the file
$boilerplate = file_get_contents( $file );
// Process any tags, currently only '{{datetime}}' is supported
if ( preg_match_all( '!\{\{([^}]+)\}\}!', $boilerplate, $boilerplate_matches ) ) {
$replacements = array();
foreach ( $boilerplate_matches[0] as $index => $tag ) {
if ( $boilerplate_matches[1][$index] === 'datetime' ) {
$replacements[] = @date( 'Y-m-d H:i:s O' );
}
else {
$replacements[] = '?';
}
}
$boilerplate = str_replace( $boilerplate_matches[0], $replacements, $boilerplate );
}
// Pretty print
$boilerplate = explode( PHP_EOL, $boilerplate );
$boilerplate = array_map( 'trim', $boilerplate );
$boilerplate = array_map( create_function( '$it', 'return !empty($it) ? " $it" : $it;' ), $boilerplate );
$boilerplate = implode( PHP_EOL . ' *', $boilerplate );
return <<<TPL
/*
*$boilerplate
*/
TPL;
}
protected static function getOptions ( $options ) {
// Create default options for those not set
$option_defaults = array(
// Minify. Set true for formatting and comments
'debug' => false,
// Append 'checksum' to output file name
'versioning' => true,
// Use the template boilerplate
'boilerplate' => true,
// Variables passed in at runtime
'vars' => array(),
// Enable/disable the cache
'cache' => true,
// Output file. Defaults the host-filename
'output_file' => null,
// Vendor target. Only apply prefixes for a specific vendor, set to 'none' for no prefixes
'vendor_target' => 'all',
// Whether to rewrite the url references inside imported files
// This will be 'true' by default eventually
'rewrite_import_urls' => false,
// Keeping track of global vars internally
'_globalVars' => self::$globalVars,
);
self::$options = is_array( $options ) ?
array_merge( $option_defaults, $options ) : $option_defaults;
return self::$options;
}
protected static function pruneAliases () {
// If a vendor target is given, we prune the aliases array
$vendor = self::$options[ 'vendor_target' ];
// For expicit 'none' argument turn off aliases
if ( 'none' === $vendor ) {
self::$aliases = null;
return;
}
// Default vendor argument, use all aliases as normal
if ( 'all' === $vendor ) {
return;
}
// Normalize vendor_target argument
$vendor = str_replace( '-', '', self::$options[ 'vendor_target' ] );
$vendor = "-$vendor-";
// Loop the aliases array, filter down to the target vendor
foreach ( self::$aliases as $group_name => $group_array ) {
// Property/value aliases are a special case
if ( 'values' === $group_name ) {
foreach ( $group_array as $property => $values ) {
$result = array();
foreach ( $values as $value => $prefix_values ) {
foreach ( $prefix_values as $prefix ) {
if ( strpos( $prefix, $vendor ) === 0 ) {
$result[] = $prefix;
}
}
}
self::$aliases[ 'values' ][ $property ][ $value ] = $result;
}
continue;
}
foreach ( $group_array as $alias_keyword => $prefix_array ) {
$result = array();
foreach ( $prefix_array as $prefix ) {
if ( strpos( $prefix, $vendor ) === 0 ) {
$result[] = $prefix;
}
}
// Prune the whole alias keyword if there is no result
if ( empty( $result ) ) {
unset( self::$aliases[ $group_name ][ $alias_keyword ] );
}
else {
self::$aliases[ $group_name ][ $alias_keyword ] = $result;
}
}
}
// self::log( self::$aliases );
}
protected static function calculateVariables () {
$regex = self::$regex;
// In-file variables override global variables
// Runtime variables override in-file variables
self::$storage->variables = array_merge(
self::$globalVars, self::$storage->variables );
if ( !empty( self::$options[ 'vars' ] ) ) {
self::$storage->variables = array_merge(
self::$storage->variables, self::$options[ 'vars' ] );
}
// Place variables referenced inside variables
// Excecute any custom functions
foreach ( self::$storage->variables as $name => &$value ) {
// Referenced variables
$value = preg_replace_callback(
$regex->function->var, array( 'self', 'cb_placeVariables' ), $value );
// Custom functions:
// Variable values can be escaped from function parsing with a double bang
if ( strpos( $value, '!!' ) === 0 ) {
$value = ltrim( $value, "!\t\r " );
}
else {
$value = csscrush_function::parseAndExecuteValue( $value );
}
}
}
protected static function placeVariables ( $stream ) {
$stream = preg_replace_callback(
self::$regex->function->var, array( 'self', 'cb_placeVariables' ), $stream );
// Place variables in any string tokens
foreach ( self::$storage->tokens->strings as $label => &$string ) {
if ( strpos( $string, '$' ) !== false ) {
$string = preg_replace_callback(
self::$regex->function->var, array( 'self', 'cb_placeVariables' ), $string );
}
}
return $stream;
}
protected static function reset () {
// Reset properties for current process
self::$tokenUID = 0;
self::$storage = new stdclass;
self::$storage->tokens = (object) array(
'strings' => array(),
'comments' => array(),
'rules' => array(),
'parens' => array(),
);
self::$storage->variables = array();
// Temporary storage
self::$storage->tmp = new stdclass;
}
protected static function compile ( $stream ) {
$regex = self::$regex;
$options = self::$options;
// Load in aliases and macros
if ( !self::$assetsLoaded ) {
self::loadAssets();
self::$assetsLoaded = true;
}
// Set aliases. May be pruned if a vendor target is set
self::$aliases = self::$aliasesRaw;
self::pruneAliases();
// Parse variables
$stream = self::extractVariables( $stream );
// Calculate the variable stack
self::calculateVariables();
self::log( self::$storage->variables );
// Place the variables
$stream = self::placeVariables( $stream );
// Normalize whitespace
$stream = csscrush_util::normalizeWhiteSpace( $stream );
// Adjust the stream so we can extract the rules cleanly
$map = array(
'@' => "\n@",
'}' => "}\n",
'{' => "{\n",
';' => ";\n",
);
$stream = "\n" . str_replace( array_keys( $map ), array_values( $map ), $stream );
// Rules
$stream = self::extractAndProcessRules( $stream );
// Alias at-rules (if there are any)
$stream = self::aliasAtRules( $stream );
// print it all back
$stream = self::display( $stream );
// Add in boilerplate
if ( $options[ 'boilerplate' ] ) {
$stream = self::getBoilerplate() . "\n$stream";
}
self::log( self::$config->data );
// Release memory
self::$storage = null;
return $stream;
}
protected static function display ( $stream ) {
$minify = !self::$options[ 'debug' ];
$regex = self::$regex;
if ( $minify ) {
$stream = preg_replace( $regex->token->comment, '', $stream );
}
else {
// Create newlines after tokens
$stream = preg_replace( '!([{}])!', "$1\n", $stream );
$stream = preg_replace( '!([@])!', "\n$1", $stream );
$stream = preg_replace( '!(___[a-z0-9]+___)!', "$1\n", $stream );
// Kill double spaces
$stream = ltrim( preg_replace( '!\n+!', "\n", $stream ) );
}
// Kill leading space
$stream = preg_replace( '!\n\s+!', "\n", $stream );
// Print out rules
$stream = preg_replace_callback( $regex->token->rule, array( 'self', 'cb_printRule' ), $stream );
// Insert parens
$paren_labels = array_keys( self::$storage->tokens->parens );
$paren_values = array_values( self::$storage->tokens->parens );
$stream = str_replace( $paren_labels, $paren_values, $stream );
if ( $minify ) {
$stream = self::minify( $stream );
}
else {
// Insert comments
$comment_labels = array_keys( self::$storage->tokens->comments );
$comment_values = array_values( self::$storage->tokens->comments );
foreach ( $comment_values as &$comment ) {
$comment = "$comment\n";
}
$stream = str_replace( $comment_labels, $comment_values, $stream );
// Normalize line breaks
$stream = preg_replace( '!\n{3,}!', "\n\n", $stream );
}
// Insert literals
$string_labels = array_keys( self::$storage->tokens->strings );
$string_values = array_values( self::$storage->tokens->strings );
$stream = str_replace( $string_labels, $string_values, $stream );
// I think we're done
return $stream;
}
protected static function validateCache ( $hostfile ) {
$config = self::$config;
// Search base directory for an existing compiled file
foreach ( scandir( $config->baseDir ) as $filename ) {
if ( self::$compileName != $filename ) {
continue;
}
// Cached file exists
self::log( 'Cached file exists' );
$existingfile = new stdClass;
$existingfile->name = $filename;
$existingfile->path = "$config->baseDir/$existingfile->name";
$existingfile->URL = "$config->baseURL/$existingfile->name";
// Start off with the host file then add imported files
$all_files = array( $hostfile->mtime );
if ( file_exists( $existingfile->path ) and isset( $config->data[ self::$compileName ] ) ) {
// File exists and has config
self::log( 'has config' );
foreach ( $config->data[ $existingfile->name ][ 'imports' ] as $import_file ) {
// Check if this is docroot relative or hostfile relative
$root = strpos( $import_file, '/' ) === 0 ? $config->docRoot : $config->baseDir;
$import_filepath = realpath( $root ) . "/{$import_file}";
if ( file_exists( $import_filepath ) ) {
$all_files[] = filemtime( $import_filepath );
}
else {
// File has been moved, remove old file and skip to compile
self::log( 'Import file has been moved, removing existing file' );
unlink( $existingfile->path );
return false;
}
}
$existing_options = $config->data[ $existingfile->name ][ 'options' ];
$existing_datesum = $config->data[ $existingfile->name ][ 'datem_sum' ];
if (
$existing_options == self::$options and
$existing_datesum == array_sum( $all_files )
) {
// Files have not been modified and config is the same: return the old file
self::log( "Files and options have not been modified, returning existing
file '$existingfile->URL'" );
return $existingfile->URL . ( self::$options[ 'versioning' ] !== false ? "?{$existing_datesum}" : '' );
}
else {
// Remove old file and continue making a new one...
self::log( 'Files or options have been modified, removing existing file' );
unlink( $existingfile->path );
}
}
else if ( file_exists( $existingfile->path ) ) {
// File exists but has no config
self::log( 'File exists but no config, removing existing file' );
unlink( $existingfile->path );
}
return false;
} // foreach
return false;
}
protected static function minify ( $str ) {
$replacements = array(
'!\n+| (\{)!' => '$1', // Trim whitespace
'!(^|[: \(,])0(\.\d+)!' => '$1$2', // Strip leading zeros on floats
'!(^|[: \(,])\.?0[a-zA-Z]{1,5}!i' => '${1}0', // Strip unnecessary units on zero values
'!(^|\:) *(0 0 0|0 0 0 0) *(;|\})!' => '${1}0${3}', // Collapse zero lists
'!(padding|margin) ?\: *0 0 *(;|\})!' => '${1}:0${2}', // Collapse zero lists continued
'!\s*([>~+=])\s*!' => '$1', // Clean-up around combinators
'!\#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3!i'
=> '#$1$2$3', // Compress hex codes
);
return preg_replace(
array_keys( $replacements ), array_values( $replacements ), $str );
}
protected static function aliasAtRules ( $stream ) {
if ( empty( self::$aliases[ 'at-rules' ] ) ) {
return $stream;
}
$aliases = self::$aliases[ 'at-rules' ];
foreach ( $aliases as $at_rule => $at_rule_aliases ) {
if (
strpos( $stream, "@$at_rule " ) === -1 or
strpos( $stream, "@$at_rule{" ) === -1
) {
// Nothing to see here
continue;
}
$scan_pos = 0;
// Find at-rules that we want to alias
while ( preg_match( "!@$at_rule" . '[\s{]!', $stream, $match, PREG_OFFSET_CAPTURE, $scan_pos ) ) {
// Store the match position
$block_start_pos = $match[0][1];
// Capture the curly bracketed block
$curly_match = csscrush_util::matchBrackets( $stream, $brackets = array( '{', '}' ), $block_start_pos );
if ( !$curly_match ) {
// Couldn't match the block
break;
}
// The end of the block
$block_end_pos = $curly_match->end;
// Build up string with aliased blocks for splicing
$original_block = substr( $stream, $block_start_pos, $block_end_pos - $block_start_pos );
$blocks = array();
foreach ( $at_rule_aliases as $alias ) {
// Copy original block, replacing at-rule with alias name
$copy_block = str_replace( "@$at_rule", "@$alias", $original_block );
// Aliases are nearly always prefixed, capture the current vendor name
preg_match( self::$regex->vendorPrefix, $alias, $vendor );
$vendor = $vendor ? $vendor[1] : null;
// Duplicate rules
if ( preg_match_all( self::$regex->token->rule, $copy_block, $copy_matches ) ) {
$originals = array();
$replacements = array();
foreach ( $copy_matches[0] as $copy_match ) {
// Clone the matched rule
$originals[] = $rule_label = $copy_match;
$cloneRule = clone self::$storage->tokens->rules[ $rule_label ];
// Set the vendor context
$cloneRule->vendorContext = $vendor;
// Filter out declarations that have different vendor context
$new_set = array();
foreach ( $cloneRule as $declaration ) {
if ( !$declaration->vendor or $declaration->vendor === $vendor ) {
$new_set[] = $declaration;
}
}
$cloneRule->declarations = $new_set;
// Store the clone
$replacements[] = $clone_rule_label = self::createTokenLabel( 'r' );
self::$storage->tokens->rules[ $clone_rule_label ] = $cloneRule;
}
// Finally replace the original labels with the cloned rule labels
$copy_block = str_replace( $originals, $replacements, $copy_block );
}
$blocks[] = $copy_block;
}
// The original version is always last in the list
$blocks[] = $original_block;
$blocks = implode( "\n", $blocks );
// Glue back together
$stream =
substr( $stream, 0, $block_start_pos ) .
$blocks .
substr( $stream, $block_end_pos );
// Move the regex pointer forward
$scan_pos = $block_start_pos + strlen( $blocks );
} // while
} // foreach
return $stream;
}
public static function createTokenLabel ( $prefix, $counter = null ) {
$counter = !is_null( $counter ) ? $counter : ++self::$tokenUID;
return "___$prefix{$counter}___";
}
#############################
# preg_replace callbacks
protected static function cb_extractStrings ( $match ) {
$label = csscrush::createTokenLabel( 's' );
csscrush::$storage->tokens->strings[ $label ] = $match[0];
return $label;
}
protected static function cb_restoreStrings ( $match ) {
return csscrush::$storage->tokens->strings[ $match[0] ];
}
protected static function cb_extractComments ( $match ) {
$comment = $match[0];
// Strip private comments
$private_comment_marker = '$!';
if ( strpos( $comment, '/*' . $private_comment_marker ) === 0 ) {
return '';
}
$label = self::createTokenLabel( 'c' );
self::$storage->tokens->comments[ $label ] = $comment;
return $label;
}
protected static function cb_restoreComments ( $match ) {
return self::$storage->tokens->comments[ $match[0] ];
}
protected static function cb_extractVariables ( $match ) {
$regex = self::$regex;
$block = $match[2];
// Strip comment markers
$block = preg_replace( $regex->token->comment, '', $block );
// Need to split safely as there are semi-colons in data-uris
$variables_match = csscrush_util::splitDelimList( $block, ';', true );
// Loop through the pairs, restore parens
foreach ( $variables_match->list as $var ) {
$colon = strpos( $var, ':' );
if ( $colon === -1 ) {
continue;
}
$name = trim( substr( $var, 0, $colon ) );
$value = trim( substr( $var, $colon + 1 ) );
self::$storage->variables[ trim( $name ) ] = $value;
}
return '';
}
protected static function cb_placeVariables ( $match ) {
$before_char = $match[1];
// Check for dollar shorthand
if ( empty( $match[2] ) and isset( $match[3] ) and strpos( $match[0], '$' ) !== false ) {
$variable_name = $match[3];
}
else {
$variable_name = $match[2];
}
if ( isset( self::$storage->variables[ $variable_name ] ) ) {
return $before_char . self::$storage->variables[ $variable_name ];
}
else {
return $before_char;
}
}
protected static function cb_extractAndProcessRules ( $match ) {
$rule = new stdClass;
$rule->selector_raw = $match[1];
$rule->declaration_raw = $match[2];
csscrush_hook::run( 'rule_preprocess', $rule );
$rule = new csscrush_rule( $rule->selector_raw, $rule->declaration_raw );
// Only store rules with declarations
if ( !empty( $rule->declarations ) ) {
csscrush_hook::run( 'rule_prealias', $rule );
if ( !empty( self::$aliases ) ) {
$rule->addPropertyAliases();
$rule->addFunctionAliases();
$rule->addValueAliases();
}
csscrush_hook::run( 'rule_postalias', $rule );
$rule->expandSelectors();
csscrush_hook::run( 'rule_postprocess', $rule );
$label = self::createTokenLabel( 'r' );
self::$storage->tokens->rules[ $label ] = $rule;
return $label . "\n";
}
else {
return '';
}
}
protected static function cb_restoreLiteral ( $match ) {
return self::$storage->tokens[ $match[0] ];
}
protected static function cb_printRule ( $match ) {
$minify = !self::$options[ 'debug' ];
$ruleLabel = $match[0];
if ( !isset( self::$storage->tokens->rules[ $ruleLabel ] ) ) {
return '';
}
$rule = self::$storage->tokens->rules[ $ruleLabel ];
// Build the selector
$selectors = implode( ',', $rule->selectors );
// Build the block
$block = array();
$colon = $minify ? ':' : ': ';
foreach ( $rule as $declaration ) {
$block[] = "{$declaration->property}$colon{$declaration->value}";
}
// Return whole rule
if ( $minify ) {
$block = implode( ';', $block );
return "$selectors{{$block}}";
}
else {
$block = implode( ";\n\t", $block );
// Include pre rule comments
$comments = implode( "\n", $rule->comments );
return "$comments\n$selectors {\n\t$block;\n\t}\n";
}
}
############
# Parsing methods
public static function extractAndProcessRules ( $stream ) {
return preg_replace_callback( self::$regex->rule, array( 'self', 'cb_extractAndProcessRules' ), $stream );
}
public static function extractVariables ( $stream ) {
return preg_replace_callback( self::$regex->variables, array( 'self', 'cb_extractVariables' ), $stream );
}
public static function extractComments ( $stream ) {
return preg_replace_callback( self::$regex->comment, array( 'self', 'cb_extractComments' ), $stream );
}
public static function extractStrings ( $stream ) {
return preg_replace_callback( self::$regex->string, array( 'self', 'cb_extractStrings' ), $stream );
}
}
#######################
# Procedural style API
function csscrush_file ( $file, $options = null ) {
return csscrush::file( $file, $options );
}
function csscrush_tag ( $file, $options = null, $attributes = array() ) {
return csscrush::tag( $file, $options, $attributes );
}
function csscrush_inline ( $file, $options = null, $attributes = array() ) {
return csscrush::inline( $file, $options, $attributes );
}
function csscrush_string ( $string, $options = null ) {
return csscrush::string( $string, $options );
}
function csscrush_globalvars ( $vars ) {
return csscrush::globalVars( $vars );
}
function csscrush_clearcache ( $dir = '' ) {
return csscrush::clearcache( $dir );
}