<?php
class CssCrush_Rule implements IteratorAggregate {
public $vendorContext = null;
public $properties = array();
public $selectors = null;
public $parens = array();
public $declarations = array();
public $comments = array();
public function __construct ( $selector_string = null, $declarations_string ) {
$regex = CssCrush::$regex;
// Parse the selectors chunk
if ( !empty( $selector_string ) ) {
$selector_adjustments = array(
// 'hocus' and 'pocus' pseudo class shorthand
'!:hocus([^a-z0-9_-])!' => ':any(:hover,:focus)$1',
'!:pocus([^a-z0-9_-])!' => ':any(:hover,:focus,:active)$1',
// Reduce double colon syntax for backwards compatability
'!::(after|before|first-letter|first-line)!' => ':$1',
);
$selector_string = preg_replace(
array_keys( $selector_adjustments ), array_values( $selector_adjustments ), $selector_string );
$selectors_match = CssCrush::splitDelimList( $selector_string, ',' );
$this->parens += $selectors_match->matches;
// Remove and store comments that sit above the first selector
// remove all comments between the other selectors
preg_match_all( $regex->token->comment, $selectors_match->list[0], $m );
$this->comments = $m[0];
foreach ( $selectors_match->list as &$selector ) {
$selector = preg_replace( $regex->token->comment, '', $selector );
$selector = trim( $selector );
}
$this->selectors = $selectors_match->list;
}
// Parse the declarations chunk
$declarations_match = CssCrush::splitDelimList( $declarations_string, ';' );
$this->parens += $declarations_match->matches;
// Parse declarations in to property/value pairs
foreach ( $declarations_match->list as $declaration ) {
// Strip comments around the property
$declaration = preg_replace( $regex->token->comment, '', $declaration );
// Store the property
$colonPos = strpos( $declaration, ':' );
if ( $colonPos === false ) {
// If there is no colon it's malformed
continue;
}
else {
$prop = trim( substr( $declaration, 0, $colonPos ) );
// Store the property name
$this->addProperty( $prop );
}
// Extract the value part of the declaration
$value = substr( $declaration, $colonPos + 1 );
$value = $value !== false ? trim( $value ) : $value;
if ( $value === false or $value === '' ) {
// We'll ignore declarations with empty values
continue;
}
// If are parenthesised expressions in the value
// Search for any custom functions so we can apply them
if ( count( $declarations_match->matches ) ) {
CssCrush::$storage->tmpParens = $declarations_match->matches;
$value = preg_replace_callback( $regex->function->custom, array( 'CssCrush_Function', 'css_fn' ), $value );
}
// Store the property family
// Store the vendor id, if one is present
if ( preg_match( $regex->vendorPrefix, $prop, $vendor ) ) {
$family = $vendor[2];
$vendor = $vendor[1];
}
else {
$vendor = null;
$family = $prop;
}
// Create an index of all functions in the current declaration
if ( preg_match_all( $regex->function->match, $value, $functions ) > 0 ) {
$out = array();
foreach ( $functions[2] as $index => $fn_name ) {
$out[] = $fn_name;
}
$functions = array_unique( $out );
}
else {
$functions = array();
}
// Store the declaration
$_declaration = (object) array(
'property' => $prop,
'family' => $family,
'vendor' => $vendor,
'functions' => $functions,
'value' => $value,
);
$this->declarations[] = $_declaration;
}
}
public function addPropertyAliases () {
$regex = CssCrush::$regex;
$aliasedProperties =& CssCrush::$aliases[ 'properties' ];
// First test for the existence of any aliased properties
$intersect = array_intersect( array_keys( $aliasedProperties ), array_keys( $this->properties ) );
if ( empty( $intersect ) ) {
return;
}
// Shim in aliased properties
$new_set = array();
foreach ( $this->declarations as $declaration ) {
$prop = $declaration->property;
if ( isset( $aliasedProperties[ $prop ] ) ) {
// There are aliases for the current property
foreach ( $aliasedProperties[ $prop ] as $prop_alias ) {
if ( $this->propertyCount( $prop_alias ) ) {
continue;
}
// If the aliased property hasn't been set manually, we create it
$copy = clone $declaration;
$copy->family = $copy->property;
$copy->property = $prop_alias;
// Remembering to set the vendor property
$copy->vendor = null;
// Increment the property count
$this->addProperty( $prop_alias );
if ( preg_match( $regex->vendorPrefix, $prop_alias, $vendor ) ) {
$copy->vendor = $vendor[1];
}
$new_set[] = $copy;
}
}
// Un-aliased property or a property alias that has been manually set
$new_set[] = $declaration;
}
// Re-assign
$this->declarations = $new_set;
}
public function addFunctionAliases () {
$function_aliases =& CssCrush::$aliases[ 'functions' ];
$aliased_functions = array_keys( $function_aliases );
if ( empty( $aliased_functions ) ) {
return;
}
$new_set = array();
// Keep track of the function aliases we apply and to which property 'family'
// they belong, so we can avoid un-unecessary duplications
$used_fn_aliases = array();
// Shim in aliased functions
foreach ( $this->declarations as $declaration ) {
// No functions, skip
if ( empty( $declaration->functions ) ) {
$new_set[] = $declaration;
continue;
}
// Get list of functions used in declaration that are alias-able, if none skip
$intersect = array_intersect( $declaration->functions, $aliased_functions );
if ( empty( $intersect ) ) {
$new_set[] = $declaration;
continue;
}
// CssCrush::log($intersect);
// Loop the aliasable functions
foreach ( $intersect as $fn_name ) {
if ( $declaration->vendor ) {
// If the property is vendor prefixed we use the vendor prefixed version
// of the function if it exists.
// Else we just skip and use the unprefixed version
$fn_search = "-{$declaration->vendor}-$fn_name";
if ( in_array( $fn_search, $function_aliases[ $fn_name ] ) ) {
$declaration->value = preg_replace(
'!(^| |,)' . $fn_name . '!',
'${1}' . $fn_search,
$declaration->value
);
$used_fn_aliases[ $declaration->family ][] = $fn_search;
}
}
else {
// Duplicate the rule for each alias
foreach ( $function_aliases[ $fn_name ] as $fn_alias ) {
if (
isset( $used_fn_aliases[ $declaration->family ] ) and
in_array( $fn_alias, $used_fn_aliases[ $declaration->family ] )
) {
// If the function alias has already been applied in a vendor property
// for the same declaration property assume all is good
continue;
}
$copy = clone $declaration;
$copy->value = preg_replace(
'!(^| |,)' . $fn_name . '!',
'${1}' . $fn_alias,
$copy->value
);
$new_set[] = $copy;
// Increment the property count
$this->addProperty( $copy->property );
}
}
}
$new_set[] = $declaration;
}
// Re-assign
$this->declarations = $new_set;
}
public function addValueAliases () {
$aliasedValues =& CssCrush::$aliases[ 'values' ];
// First test for the existence of any aliased properties
$intersect = array_intersect( array_keys( $aliasedValues ), array_keys( $this->properties ) );
if ( empty( $intersect ) ) {
return;
}
$new_set = array();
foreach ( $this->declarations as $declaration ) {
foreach ( $aliasedValues as $value_prop => $value_aliases ) {
if ( $this->propertyCount( $value_prop ) < 1 ) {
continue;
}
foreach ( $value_aliases as $value => $aliases ) {
if ( $declaration->value === $value ) {
foreach ( $aliases as $alias ) {
$copy = clone $declaration;
$copy->value = $alias;
$new_set[] = $copy;
}
}
}
}
$new_set[] = $declaration;
}
// Re-assign
$this->declarations = $new_set;
}
public function applyMacros () {
foreach ( CssCrush::$macros as $fn ) {
call_user_func( $fn, $this );
}
}
public function expandSelectors () {
$new_set = array();
$reg_comma = '!\s*,\s*!';
foreach ( $this->selectors as $selector ) {
$pos = strpos( $selector, ':any___' );
if ( $pos !== false ) {
// Contains an :any statement so we expand
$chain = array( '' );
do {
if ( $pos === 0 ) {
preg_match( '!:any(___p\d+___)!', $selector, $m );
// Parse the arguments
$expression = trim( $this->parens[ $m[1] ], '()' );
$parts = preg_split( $reg_comma, $expression, null, PREG_SPLIT_NO_EMPTY );
$tmp = array();
foreach ( $chain as $rowCopy ) {
foreach ( $parts as $part ) {
$tmp[] = $rowCopy . $part;
}
}
$chain = $tmp;
$selector = substr( $selector, strlen( $m[0] ) );
}
else {
foreach ( $chain as &$row ) {
$row .= substr( $selector, 0, $pos );
}
$selector = substr( $selector, $pos );
}
} while ( ( $pos = strpos( $selector, ':any___' ) ) !== false );
// Finish off
foreach ( $chain as &$row ) {
$new_set[] = $row . $selector;
}
}
else {
// Nothing special
$new_set[] = $selector;
}
}
$this->selectors = $new_set;
}
############
# IteratorAggregate
public function getIterator () {
return new ArrayIterator( $this->declarations );
}
############
# Rule API
public function propertyCount ( $prop ) {
if ( array_key_exists( $prop, $this->properties ) ) {
return $this->properties[ $prop ];
}
return 0;
}
// Add property to the rule index keeping track of the count
public function addProperty ( $prop ) {
if ( isset( $this->properties[ $prop ] ) ) {
$this->properties[ $prop ]++;
}
else {
$this->properties[ $prop ] = 1;
}
}
public function createDeclaration ( $property, $value, $options = array() ) {
$_declaration = array(
'property' => $property,
'family' => null,
'vendor' => null,
'value' => $value,
);
$this->addProperty( $property );
return (object) array_merge( $_declaration, $options );
}
// Get a declaration value without paren tokens
public function getDeclarationValue ( $declaration ) {
$paren_keys = array_keys( $this->parens );
$paren_values = array_values( $this->parens );
return str_replace( $paren_keys, $paren_values, $declaration->value );
}
}