<?php
/**
*
* CSS rule API
*
*/
class csscrush_rule implements IteratorAggregate, Countable {
public $vendorContext;
public $isNested;
public $label;
public $properties = array();
public $selectorList = array();
// The comments associated with the rule
public $comments = array();
// Arugments passed in via 'extend' property
public $extendArgs = array();
public $_declarations = array();
// A table for storing the declarations as data for this() referencing
public $localData = array();
// A table for storing the declarations as data for external query() referencing
public $data = array();
public function declarationCheckin ( $prop, $value, &$pairs ) {
if ( $prop !== '' && $value !== '' ) {
// First resolve query() calls that reference earlier rules
if ( preg_match( csscrush_regex::$patt->queryFunction, $value ) ) {
csscrush_function::executeCustomFunctions( $value,
csscrush_regex::$patt->queryFunction, array(
'query' => array( $this, 'cssQueryFunction' ),
), $prop );
}
if ( strpos( $prop, 'data-' ) === 0 ) {
// If it's with data prefix, we don't want to print it
// Just remove the prefix
$prop = substr( $prop, strlen( 'data-' ) );
// On first pass we want to store data properties on $this->data,
// as well as on local
$this->data[ $prop ] = $value;
}
else {
// Add to the stack
$pairs[] = array( $prop, $value );
}
// Set on $this->localData
$this->localData[ $prop ] = $value;
// Unset on data tables if the value has a this() call:
// - Restriction to avoid circular references
if ( preg_match( csscrush_regex::$patt->thisFunction, $value ) ) {
unset( $this->localData[ $prop ] );
unset( $this->data[ $prop ] );
}
}
}
public function __construct ( $selector_string = null, $declarations_string ) {
$regex = csscrush_regex::$patt;
$this->label = csscrush::tokenLabelCreate( 'r' );
// Parse the selectors chunk
if ( ! empty( $selector_string ) ) {
$selectors_match = csscrush_util::splitDelimList( $selector_string, ',' );
// Remove and store comments that sit above the first selector
// remove all comments between the other selectors
if ( strpos( $selectors_match->list[0], '___c' ) !== false ) {
preg_match_all( $regex->commentToken, $selectors_match->list[0], $m );
$this->comments = $m[0];
}
// Strip any other comments then create selector instances
foreach ( $selectors_match->list as $selector ) {
$selector = trim( csscrush_util::stripComments( $selector ) );
// If the selector matches an absract directive
if ( preg_match( $regex->abstract, $selector, $m ) ) {
$abstract_name = $m[1];
// Link the rule to the abstract name and skip forward to declaration parsing
csscrush::$process->abstracts[ $abstract_name ] = $this;
break;
}
$this->addSelector( new csscrush_selector( $selector ) );
// Store selector relationships
// - This happens twice; on first pass for mixins, second pass is for inheritance
$this->indexSelectors();
}
}
// Parse the declarations chunk
// Need to split safely as there are semi-colons in data-uris
$declarations_match = csscrush_util::splitDelimList( $declarations_string, ';', true );
// First create a simple array of all properties and value pairs in raw state
$pairs = array();
// Split declarations in to property/value pairs
foreach ( $declarations_match->list as $declaration ) {
// Strip comments around the property
$declaration = csscrush_util::stripComments( $declaration );
// Extract the property part of the declaration
$colonPos = strpos( $declaration, ':' );
if ( $colonPos === false ) {
continue; // If there's no colon it's malformed
}
$prop = trim( substr( $declaration, 0, $colonPos ) );
// Extract the value part of the declaration
$value = substr( $declaration, $colonPos + 1 );
$value = $value !== false ? trim( $value ) : $value;
if ( $prop === 'mixin' ) {
// Mixins are a special case
if ( $mixin_declarations = csscrush_mixin::parseValue( $value ) ) {
// Add mixin declarations to the stack
while ( $mixin_declaration = array_shift( $mixin_declarations ) ) {
$this->declarationCheckin(
$mixin_declaration['property'], $mixin_declaration['value'], $pairs );
}
}
}
elseif ( $prop === 'extends' ) {
// Extends are also a special case
$this->setExtendSelectors( $value );
}
else {
$this->declarationCheckin( $prop, $value, $pairs );
}
}
// Bind declaration objects on the rule
foreach ( $pairs as $pair ) {
list( $prop, $value ) = $pair;
// Resolve self references, aka this()
csscrush_function::executeCustomFunctions( $value,
csscrush_regex::$patt->thisFunction, array(
'this' => array( $this, 'cssThisFunction' ),
), $prop );
if ( trim( $value ) !== '' ) {
// Add declaration and update the data table
$this->data[ $prop ] = $value;
$this->addDeclaration( $prop, $value );
}
}
// localData no longer required
$this->localData = null;
}
public function __set ( $name, $value ) {
if ( $name === 'declarations' ) {
$this->_declarations = $value;
// Update the table of properties
$this->updatePropertyTable();
}
}
public function __get ( $name ) {
if ( $name === 'declarations' ) {
return $this->_declarations;
}
}
public function cssThisFunction ( $input, $fn_name ) {
$args = csscrush_function::parseArgsSimple( $input );
if ( isset( $this->localData[ $args[0] ] ) ) {
return $this->localData[ $args[0] ];
}
elseif ( isset( $args[1] ) ) {
return $args[1];
}
else {
return '';
}
}
public function cssQueryFunction ( $input, $fn_name, $call_property ) {
$result = '';
$args = csscrush_function::parseArgs( $input );
if ( count( $args ) < 1 ) {
return $result;
}
$abstracts =& csscrush::$process->abstracts;
$mixins =& csscrush::$process->mixins;
$selectorRelationships =& csscrush::$process->selectorRelationships;
// Resolve arguments
$name = array_shift( $args );
$property = $call_property;
if ( isset( $args[0] ) ) {
if ( $args[0] !== 'default' ) {
$property = array_shift( $args );
}
else {
array_shift( $args );
}
}
$default = isset( $args[0] ) ? $args[0] : null;
// csscrush::log( array( $name, $property, $default ), 'query args' );
// Try to match a abstract rule first
if ( preg_match( csscrush_regex::$patt->name, $name ) ) {
// Search order: abstracts, mixins, rules
if ( isset( $abstracts[ $name ]->data[ $property ] ) ) {
$result = $abstracts[ $name ]->data[ $property ];
}
elseif ( isset( $mixins[ $name ]->data[ $property ] ) ) {
$result = $mixins[ $name ]->data[ $property ];
}
elseif ( isset( $selectorRelationships[ $name ]->data[ $property ] ) ) {
$result = $selectorRelationships[ $name ]->data[ $property ];
}
}
else {
// Look for a rule match
$name = csscrush_selector::makeReadableSelector( $name );
if ( isset( $selectorRelationships[ $name ]->data[ $property ] ) ) {
$result = $selectorRelationships[ $name ]->data[ $property ];
}
}
if ( $result === '' && ! is_null( $default ) ) {
$result = $default;
}
return $result;
}
public function updatePropertyTable () {
// Create a new table of properties
$new_properties_table = array();
foreach ( $this as $declaration ) {
$name = $declaration->property;
if ( isset( $new_properties_table[ $name ] ) ) {
$new_properties_table[ $name ]++;
}
else {
$new_properties_table[ $name ] = 1;
}
}
$this->properties = $new_properties_table;
}
public function addPropertyAliases () {
$regex = csscrush_regex::$patt;
$aliasedProperties =& csscrush::$config->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 (
! $declaration->skip &&
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::$config->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 (
$declaration->skip ||
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 ] ) &&
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::$config->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 ) {
if ( !$declaration->skip ) {
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 expandSelectors () {
$new_set = array();
$reg_comma = '!\s*,\s*!';
foreach ( $this->selectorList as $readableValue => $selector ) {
$pos = strpos( $selector->value, ':any___' );
if ( $pos !== false ) {
// Contains an :any statement so we expand
$chain = array( '' );
do {
if ( $pos === 0 ) {
preg_match( '!:any(___p\d+___)!', $selector->value, $m );
// Parse the arguments
$expression = trim( csscrush::$storage->tokens->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->value = substr( $selector->value, strlen( $m[0] ) );
}
else {
foreach ( $chain as &$row ) {
$row .= substr( $selector->value, 0, $pos );
}
$selector->value = substr( $selector->value, $pos );
}
} while ( ( $pos = strpos( $selector->value, ':any___' ) ) !== false );
// Finish off
foreach ( $chain as &$row ) {
// Not creating a named rule association with this expanded selector
$new_set[] = new csscrush_selector( $row . $selector->value );
}
// Store the unexpanded selector to selectorRelationships
csscrush::$process->selectorRelationships[ $readableValue ] = $this;
}
else {
// Nothing to expand
$new_set[ $readableValue ] = $selector;
}
} // foreach
$this->selectorList = $new_set;
}
public function indexSelectors () {
foreach ( $this->selectorList as $selector ) {
csscrush::$process->selectorRelationships[ $selector->readableValue ] = $this;
}
}
public function setExtendSelectors ( $raw_value ) {
$abstracts =& csscrush::$process->abstracts;
$selectorRelationships =& csscrush::$process->selectorRelationships;
// Pass extra argument to trim the returned list
$args = csscrush_util::splitDelimList( $raw_value, ',', true, true );
// Reset if called earlier, last call wins by intention
$this->extendArgs = array();
foreach ( $args->list as $arg ) {
$this->extendArgs[] = new csscrush_extendArg( $arg );
}
}
public function applyExtendables () {
if ( ! $this->extendArgs ) {
return;
}
$abstracts =& csscrush::$process->abstracts;
$selectorRelationships =& csscrush::$process->selectorRelationships;
// Filter the extendArgs list to usable references
foreach ( $this->extendArgs as $key => $extend_arg ) {
$name = $extend_arg->name;
if ( isset( $abstracts[ $name ] ) ) {
$parent_rule = $abstracts[ $name ];
$extend_arg->pointer = $parent_rule;
}
elseif ( isset( $selectorRelationships[ $name ] ) ) {
$parent_rule = $selectorRelationships[ $name ];
$extend_arg->pointer = $parent_rule;
}
else {
// Unusable, so unset it
unset( $this->extendArgs[ $key ] );
}
}
// Create a stack of all parent rule args
$parent_extend_args = array();
foreach ( $this->extendArgs as $extend_arg ) {
$parent_extend_args = array_merge( $parent_extend_args, $extend_arg->pointer->extendArgs );
}
// Merge this rule's extendArgs with parent extendArgs
$this->extendArgs = array_merge( $this->extendArgs, $parent_extend_args );
// Filter now?
// Add this rule's selectors to all extendArgs
foreach ( $this->extendArgs as $extend_arg ) {
$ancestor = $extend_arg->pointer;
$extend_selectors = $this->selectorList;
// If there is a pseudo class extension create a new set accordingly
if ( $extend_arg->pseudo ) {
$extend_selectors = array();
foreach ( $this->selectorList as $readable => $selector ) {
$new_selector = clone $selector;
$new_readable = $new_selector->appendPseudo( $extend_arg->pseudo );
$extend_selectors[ $new_readable ] = $new_selector;
}
}
$ancestor->addSelectors( $extend_selectors );
}
}
public function addSelector ( $selector ) {
$this->selectorList[ $selector->readableValue ] = $selector;
}
public function addSelectors ( $list ) {
$this->selectorList = array_merge( $this->selectorList, $list );
}
############
# IteratorAggregate
public function getIterator () {
return new ArrayIterator( $this->declarations );
}
############
# Countable
public function count() {
return count( $this->_declarations );
}
############
# Rule API
public function propertyCount ( $prop ) {
if ( array_key_exists( $prop, $this->properties ) ) {
return $this->properties[ $prop ];
}
return 0;
}
public function addProperty ( $prop ) {
if ( isset( $this->properties[ $prop ] ) ) {
$this->properties[ $prop ]++;
}
else {
$this->properties[ $prop ] = 1;
}
}
public function addDeclaration ( $prop, $value ) {
// Create declaration, add to the stack if it's valid
$declaration = new csscrush_declaration( $prop, $value );
if ( $declaration->isValid ) {
// Manually increment the property name since we're directly updating the _declarations list
$this->addProperty( $prop );
$this->_declarations[] = $declaration;
return $declaration;
}
return false;
}
public static function get ( $token ) {
if ( isset( csscrush::$storage->tokens->rules[ $token ] ) ) {
return csscrush::$storage->tokens->rules[ $token ];
}
return null;
}
}
/**
*
* Declaration objects
*
*/
class csscrush_declaration {
public $property;
public $family;
public $vendor;
public $functions;
public $value;
public $skip;
public $important;
public $parenTokens;
public $isValid = true;
public function __construct ( $prop, $value ) {
$regex = csscrush_regex::$patt;
// Normalize input. Lowercase the property name
$prop = strtolower( trim( $prop ) );
$value = trim( $value );
// Check the input
if ( $prop === '' || $value === '' || $value === null ) {
$this->isValid = false;
return;
}
// Test for escape tilde
if ( $skip = strpos( $prop, '~' ) === 0 ) {
$prop = substr( $prop, 1 );
}
// 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;
}
// Check for !important keywords
if ( ( $important = strpos( $value, '!important' ) ) !== false ) {
$value = substr( $value, 0, $important );
$important = true;
}
// Ignore declarations with null css values
if ( $value === false || $value === '' ) {
$this->isValid = false;
return;
}
// Apply custom functions
if ( ! $skip ) {
csscrush_function::executeCustomFunctions( $value );
}
// Tokenize all remaining paren pairs
$match_obj = csscrush_util::matchAllBrackets( $value );
$this->parenTokens = $match_obj->matches;
$value = $match_obj->string;
// Create an index of all regular functions in the value
if ( preg_match_all( $regex->function, $value, $functions ) > 0 ) {
$out = array();
foreach ( $functions[2] as $index => $fn_name ) {
$out[] = $fn_name;
}
$functions = array_unique( $out );
}
else {
$functions = array();
}
$this->property = $prop;
$this->family = $family;
$this->vendor = $vendor;
$this->functions = $functions;
$this->value = $value;
$this->skip = $skip;
$this->important = $important;
}
public function getFullValue () {
return csscrush_util::tokenReplace( $this->value, $this->parenTokens );
}
}
/**
*
* Selector objects
*
*/
class csscrush_selector {
public $value;
public $readableValue;
public $allowPrefix = true;
public static function makeReadableSelector ( $selector_string ) {
// Quick test for paren tokens
if ( strpos( $selector_string, '___p' ) !== false ) {
$selector_string = csscrush_util::tokenReplaceAll( $selector_string, 'parens' );
}
// Create space around combinators, then normalize whitespace
$selector_string = preg_replace( '!([>+~])!', ' $1 ', $selector_string );
$selector_string = csscrush_util::normalizeWhiteSpace( $selector_string );
// Quick test for string tokens
if ( strpos( $selector_string, '___s' ) !== false ) {
$selector_string = csscrush_util::tokenReplaceAll( $selector_string, 'strings' );
}
// Quick test for double-colons for backwards compat
if ( strpos( $selector_string, '::' ) !== false ) {
$selector_string = preg_replace( '!::(after|before|first-(?:letter|line))!', ':$1', $selector_string );
}
return $selector_string;
}
public function __construct ( $raw_selector, $associated_rule = null ) {
if ( strpos( $raw_selector, '^' ) === 0 ) {
$raw_selector = ltrim( $raw_selector, "^ \n\r\t" );
$this->allowPrefix = false;
}
$this->readableValue = self::makeReadableSelector( $raw_selector );
$this->value = $raw_selector;
}
public function __toString () {
return $this->readableValue;
}
public function appendPseudo ( $pseudo ) {
// Check to avoid doubling-up
if ( ! csscrush_util::strEndsWith( $this->readableValue, $pseudo ) ) {
$this->readableValue .= $pseudo;
$this->value .= $pseudo;
}
return $this->readableValue;
}
}
/**
*
* Extend argument objects
*
*/
class csscrush_extendArg {
public $pointer;
public $name;
public $pseudo;
public function __construct ( $name ) {
$this->name = $name;
if ( ! preg_match( csscrush_regex::$patt->name, $this->name ) ) {
// Not a regular name: Some kind of selector so normalize it for later comparison
$this->name = csscrush_selector::makeReadableSelector( $this->name );
// If applying the pseudo on output store
if ( substr( $this->name, -1 ) === '!' ) {
$this->name = rtrim( $this->name, ' !' );
if ( preg_match( '!\:\:?[\w-]+$!', $this->name, $m ) ) {
$this->pseudo = $m[0];
}
}
}
}
}