/lib/CssCrush/DeclarationList.php
PHP | 598 lines | 431 code | 106 blank | 61 comment | 72 complexity | 85a314a5f1423101453c073855e65838 MD5 | raw file
- <?php
- /**
- *
- * Declaration lists.
- *
- */
- namespace CssCrush;
- class DeclarationList extends Iterator
- {
- public $flattened = true;
- public $processed = false;
- protected $rule;
- public $properties = [];
- public $canonicalProperties = [];
- // Declarations hash table for inter-rule this() referencing.
- public $data = [];
- // Declarations hash table for external query() referencing.
- public $queryData = [];
- public function __construct($declarationsString, Rule $rule)
- {
- parent::__construct();
- $this->rule = $rule;
- $pairs = DeclarationList::parse($declarationsString);
- foreach ($pairs as $index => $pair) {
- list($prop, $value) = $pair;
- // Directives.
- if ($prop === 'extends') {
- $this->rule->addExtendSelectors($value);
- unset($pairs[$index]);
- }
- elseif ($prop === 'name') {
- if (! $this->rule->name) {
- $this->rule->name = $value;
- }
- unset($pairs[$index]);
- }
- }
- // Build declaration list.
- foreach ($pairs as $index => &$pair) {
- list($prop, $value) = $pair;
- if (trim($value) !== '') {
- if ($prop === 'mixin') {
- $this->flattened = false;
- $this->store[] = $pair;
- }
- else {
- // Only store to $this->data if the value does not itself make a
- // this() call to avoid circular references.
- if (! preg_match(Regex::$patt->thisFunction, $value)) {
- $this->data[strtolower($prop)] = $value;
- }
- $this->add($prop, $value, $index);
- }
- }
- }
- }
- public function add($property, $value, $contextIndex = 0)
- {
- $declaration = new Declaration($property, $value, $contextIndex);
- if ($declaration->valid) {
- $this->index($declaration);
- $this->store[] = $declaration;
- return $declaration;
- }
- return false;
- }
- public function reset(array $declaration_stack)
- {
- $this->store = $declaration_stack;
- $this->updateIndex();
- }
- public function index($declaration)
- {
- $property = $declaration->property;
- if (isset($this->properties[$property])) {
- $this->properties[$property]++;
- }
- else {
- $this->properties[$property] = 1;
- }
- $this->canonicalProperties[$declaration->canonicalProperty] = true;
- }
- public function updateIndex()
- {
- $this->properties = [];
- $this->canonicalProperties = [];
- foreach ($this->store as $declaration) {
- $this->index($declaration);
- }
- }
- public function propertyCount($property)
- {
- return isset($this->properties[$property]) ? $this->properties[$property] : 0;
- }
- public function join($glue = ';')
- {
- return implode($glue, $this->store);
- }
- /*
- Aliasing.
- */
- public function aliasProperties($vendor_context = null)
- {
- $aliased_properties =& Crush::$process->aliases['properties'];
- // Bail early if nothing doing.
- if (! array_intersect_key($aliased_properties, $this->properties)) {
- return;
- }
- $stack = [];
- $rule_updated = false;
- $regex = Regex::$patt;
- foreach ($this->store as $declaration) {
- // Check declaration against vendor context.
- if ($vendor_context && $declaration->vendor && $declaration->vendor !== $vendor_context) {
- continue;
- }
- if ($declaration->skip) {
- $stack[] = $declaration;
- continue;
- }
- // Shim in aliased properties.
- if (isset($aliased_properties[$declaration->property])) {
- foreach ($aliased_properties[$declaration->property] as $prop_alias) {
- // If an aliased version already exists do not create one.
- if ($this->propertyCount($prop_alias)) {
- continue;
- }
- // Get property alias vendor.
- preg_match($regex->vendorPrefix, $prop_alias, $alias_vendor);
- // Check against vendor context.
- if ($vendor_context && $alias_vendor && $alias_vendor[1] !== $vendor_context) {
- continue;
- }
- // Create the aliased declaration.
- $copy = clone $declaration;
- $copy->property = $prop_alias;
- // Set the aliased declaration vendor property.
- $copy->vendor = null;
- if ($alias_vendor) {
- $copy->vendor = $alias_vendor[1];
- }
- $stack[] = $copy;
- $rule_updated = true;
- }
- }
- // Un-aliased property or a property alias that has been manually set.
- $stack[] = $declaration;
- }
- // Re-assign if any updates have been made.
- if ($rule_updated) {
- $this->reset($stack);
- }
- }
- public function aliasFunctions($vendor_context = null)
- {
- $function_aliases =& Crush::$process->aliases['functions'];
- $function_alias_groups =& Crush::$process->aliases['function_groups'];
- // The new modified set of declarations.
- $new_set = [];
- $rule_updated = false;
- // Shim in aliased functions.
- foreach ($this->store as $declaration) {
- // No functions, bail.
- if (! $declaration->functions || $declaration->skip) {
- $new_set[] = $declaration;
- continue;
- }
- // Get list of functions used in declaration that are alias-able, bail if none.
- $intersect = array_intersect_key($declaration->functions, $function_aliases);
- if (! $intersect) {
- $new_set[] = $declaration;
- continue;
- }
- // Keep record of which groups have been applied.
- $processed_groups = [];
- foreach (array_keys($intersect) as $fn_name) {
- // Store for all the duplicated declarations.
- $prefixed_copies = [];
- // Grouped function aliases.
- if ($function_aliases[$fn_name][0] === '.') {
- $group_id = $function_aliases[$fn_name];
- // If this group has been applied we can skip over.
- if (isset($processed_groups[$group_id])) {
- continue;
- }
- // Mark group as applied.
- $processed_groups[$group_id] = true;
- $groups =& $function_alias_groups[$group_id];
- foreach ($groups as $group_key => $replacements) {
- // If the declaration is vendor specific only create aliases for the same vendor.
- if (
- ($declaration->vendor && $group_key !== $declaration->vendor) ||
- ($vendor_context && $group_key !== $vendor_context)
- ) {
- continue;
- }
- $copy = clone $declaration;
- // Make swaps.
- $copy->value = preg_replace(
- $replacements['find'],
- $replacements['replace'],
- $copy->value
- );
- $prefixed_copies[] = $copy;
- $rule_updated = true;
- }
- // Post fixes.
- if (isset(PostAliasFix::$functions[$group_id])) {
- call_user_func(PostAliasFix::$functions[$group_id], $prefixed_copies, $group_id);
- }
- }
- // Single function aliases.
- else {
- foreach ($function_aliases[$fn_name] as $fn_alias) {
- // If the declaration is vendor specific only create aliases for the same vendor.
- if ($declaration->vendor) {
- preg_match(Regex::$patt->vendorPrefix, $fn_alias, $m);
- if (
- $m[1] !== $declaration->vendor ||
- ($vendor_context && $m[1] !== $vendor_context)
- ) {
- continue;
- }
- }
- $copy = clone $declaration;
- // Make swaps.
- $copy->value = preg_replace(
- Regex::make("~{{ LB }}$fn_name(?=\()~iS"),
- $fn_alias,
- $copy->value
- );
- $prefixed_copies[] = $copy;
- $rule_updated = true;
- }
- // Post fixes.
- if (isset(PostAliasFix::$functions[$fn_name])) {
- call_user_func(PostAliasFix::$functions[$fn_name], $prefixed_copies, $fn_name);
- }
- }
- $new_set = array_merge($new_set, $prefixed_copies);
- }
- $new_set[] = $declaration;
- }
- // Re-assign if any updates have been made.
- if ($rule_updated) {
- $this->reset($new_set);
- }
- }
- public function aliasDeclarations($vendor_context = null)
- {
- $declaration_aliases =& Crush::$process->aliases['declarations'];
- // First test for the existence of any aliased properties.
- if (! ($intersect = array_intersect_key($declaration_aliases, $this->properties))) {
- return;
- }
- $intersect = array_flip(array_keys($intersect));
- $new_set = [];
- $rule_updated = false;
- foreach ($this->store as $declaration) {
- // Check the current declaration property is actually aliased.
- if (isset($intersect[$declaration->property]) && ! $declaration->skip) {
- // Iterate on the current declaration property for value matches.
- foreach ($declaration_aliases[$declaration->property] as $value_match => $replacements) {
- // Create new alias declaration if the property and value match.
- if ($declaration->value === $value_match) {
- foreach ($replacements as $values) {
- // Check the vendor against context.
- if ($vendor_context && $vendor_context !== $values[2]) {
- continue;
- }
- // If the replacement property is null use the original declaration property.
- $new = new Declaration(
- ! empty($values[0]) ? $values[0] : $declaration->property,
- $values[1]
- );
- $new->important = $declaration->important;
- $new_set[] = $new;
- $rule_updated = true;
- }
- }
- }
- }
- $new_set[] = $declaration;
- }
- // Re-assign if any updates have been made.
- if ($rule_updated) {
- $this->reset($new_set);
- }
- }
- public static function parse($str, $options = [])
- {
- $str = Util::stripCommentTokens($str);
- $lines = preg_split('~\s*;\s*~', $str, null, PREG_SPLIT_NO_EMPTY);
- $options += [
- 'keyed' => false,
- 'ignore_directives' => false,
- 'lowercase_keys' => false,
- 'context' => null,
- 'flatten' => false,
- 'apply_hooks' => false,
- ];
- $pairs = [];
- foreach ($lines as $line) {
- if (! $options['ignore_directives'] && preg_match(Regex::$patt->ruleDirective, $line, $m)) {
- if (! empty($m[1])) {
- $property = 'mixin';
- }
- elseif (! empty($m[2])) {
- $property = 'extends';
- }
- else {
- $property = 'name';
- }
- $value = trim(substr($line, strlen($m[0])));
- }
- elseif (($colon_pos = strpos($line, ':')) !== false) {
- $property = trim(substr($line, 0, $colon_pos));
- $value = trim(substr($line, $colon_pos + 1));
- if ($options['lowercase_keys']) {
- $property = strtolower($property);
- }
- if ($options['apply_hooks']) {
- Crush::$process->emit('declaration_preprocess', [
- 'property' => &$property,
- 'value' => &$value,
- ]);
- }
- }
- else {
- continue;
- }
- if ($property === '' || $value === '') {
- continue;
- }
- if ($property === 'mixin' && $options['flatten']) {
- $pairs = Mixin::merge($pairs, $value, [
- 'keyed' => $options['keyed'],
- 'context' => $options['context'],
- ]);
- }
- elseif ($options['keyed']) {
- $pairs[$property] = $value;
- }
- else {
- $pairs[] = [$property, $value];
- }
- }
- return $pairs;
- }
- public function flatten()
- {
- if ($this->flattened) {
- return;
- }
- $newSet = [];
- foreach ($this->store as $declaration) {
- if (is_array($declaration) && $declaration[0] === 'mixin') {
- foreach (Mixin::merge([], $declaration[1], ['context' => $this->rule]) as $mixable) {
- if ($mixable instanceof Declaration) {
- $clone = clone $mixable;
- $clone->index = count($newSet);
- $newSet[] = $clone;
- }
- elseif ($mixable[0] === 'extends') {
- $this->rule->addExtendSelectors($mixable[1]);
- }
- else {
- $newSet[] = new Declaration($mixable[0], $mixable[1], count($newSet));
- }
- }
- }
- else {
- $declaration->index = count($newSet);
- $newSet[] = $declaration;
- }
- }
- $this->reset($newSet);
- $this->flattened = true;
- }
- public function process()
- {
- if ($this->processed) {
- return;
- }
- foreach ($this->store as $index => $declaration) {
- // Execute functions, store as data etc.
- $declaration->process($this->rule);
- // Drop declaration if value is now empty.
- if (! $declaration->valid) {
- unset($this->store[$index]);
- }
- }
- // data is done with, reclaim memory.
- unset($this->data);
- $this->processed = true;
- }
- public function expandData($dataset, $property)
- {
- // Expand shorthand properties to make them available
- // as data for this() and query().
- static $expandables = [
- 'margin-top' => 'margin',
- 'margin-right' => 'margin',
- 'margin-bottom' => 'margin',
- 'margin-left' => 'margin',
- 'padding-top' => 'padding',
- 'padding-right' => 'padding',
- 'padding-bottom' => 'padding',
- 'padding-left' => 'padding',
- 'border-top-width' => 'border-width',
- 'border-right-width' => 'border-width',
- 'border-bottom-width' => 'border-width',
- 'border-left-width' => 'border-width',
- 'border-top-left-radius' => 'border-radius',
- 'border-top-right-radius' => 'border-radius',
- 'border-bottom-right-radius' => 'border-radius',
- 'border-bottom-left-radius' => 'border-radius',
- 'border-top-color' => 'border-color',
- 'border-right-color' => 'border-color',
- 'border-bottom-color' => 'border-color',
- 'border-left-color' => 'border-color',
- ];
- $dataset =& $this->{$dataset};
- $property_group = isset($expandables[$property]) ? $expandables[$property] : null;
- // Bail if property non-expandable or already set.
- if (! $property_group || isset($dataset[$property]) || ! isset($dataset[$property_group])) {
- return;
- }
- // Get the expandable property value.
- $value = $dataset[$property_group];
- // Top-Right-Bottom-Left "trbl" expandable properties.
- $trbl_fmt = null;
- switch ($property_group) {
- case 'margin':
- $trbl_fmt = 'margin-%s';
- break;
- case 'padding':
- $trbl_fmt = 'padding-%s';
- break;
- case 'border-width':
- $trbl_fmt = 'border-%s-width';
- break;
- case 'border-radius':
- $trbl_fmt = 'border-%s-radius';
- break;
- case 'border-color':
- $trbl_fmt = 'border-%s-color';
- break;
- }
- if ($trbl_fmt) {
- $parts = explode(' ', $value);
- $placeholders = [];
- // 4 values.
- if (isset($parts[3])) {
- $placeholders = $parts;
- }
- // 3 values.
- elseif (isset($parts[2])) {
- $placeholders = [$parts[0], $parts[1], $parts[2], $parts[1]];
- }
- // 2 values.
- elseif (isset($parts[1])) {
- $placeholders = [$parts[0], $parts[1], $parts[0], $parts[1]];
- }
- // 1 value.
- else {
- $placeholders = array_pad($placeholders, 4, $parts[0]);
- }
- // Set positional variants.
- if ($property_group === 'border-radius') {
- $positions = [
- 'top-left',
- 'top-right',
- 'bottom-right',
- 'bottom-left',
- ];
- }
- else {
- $positions = [
- 'top',
- 'right',
- 'bottom',
- 'left',
- ];
- }
- foreach ($positions as $index => $position) {
- $prop = sprintf($trbl_fmt, $position);
- $dataset += [$prop => $placeholders[$index]];
- }
- }
- }
- }