0) { $this->namespaze = substr ($attribute_string, 0, $found); } $attribute_string = substr ($attribute_string, $found + 1); } $found = strpos ($attribute_string, '='); if ($found === false) { $this->attribute = $attribute_string; } else { if (ctype_alpha($attribute_string [$found-1])) { $this->attribute = substr($attribute_string, 0, $found); $this->operator = '='; $this->value = substr($attribute_string, $found + 1); } else { $this->attribute = substr($attribute_string, 0, $found - 1); $this->operator = $attribute_string [$found-1].$attribute_string [$found]; $this->value = substr($attribute_string, $found + 1); } $this->value = trim ($this->value, '"'); } } /** * The function checks if this atrribute selector matches the * attributes given in $attributes as key - value pairs. * * @param string $attributes String containing the selector * @return boolean */ public function matches (array $attributes=NULL) { if (!isset($this->operator)) { // Attribute should be present return isset($attributes) && array_key_exists($this->attribute, $attributes); } else { switch ($this->operator) { case '=': // Attribute should have exactly the value $this->value if ($attributes [$this->attribute] == $this->value) { return true; } else { return false; } break; case '~=': // Attribute value should contain the word $this->value $words = preg_split ('/\s/', $attributes [$this->attribute]); if (array_search($this->value, $words) !== false) { return true; } else { return false; } break; case '|=': // Attribute value should contain the word $this->value // or a word starting with $this->value.'-' $with_hypen = $this->value.'-'; $length = strlen ($with_hypen); if ($attributes [$this->attribute] == $this->value || strncmp($attributes [$this->attribute], $with_hypen, $length) == 0) { return true; } break; case '^=': // Attribute value should contain // a word starting with $this->value $length = strlen ($this->value); if (strncmp($attributes [$this->attribute], $this->value, $length) == 0) { return true; } break; case '$=': // Attribute value should contain // a word ending with $this->value $length = -1 * strlen ($this->value); if (substr($attributes [$this->attribute], $length) == $this->value) { return true; } break; case '*=': // Attribute value should include $this->value if (strpos($attributes [$this->attribute], $this->value) !== false) { return true; } break; } } return false; } /** * The function returns a string representation of this attribute * selector (only for debugging purpose). * * @return string */ public function toString () { $returnstring = '['; if (!empty($this->namespaze)) { $returnstring .= $this->namespaze.'|'; } $returnstring .= $this->attribute.$this->operator.$this->value; $returnstring .= ']'; return $returnstring; } } /** * Class css_simple_selector * Simple storage class to save a simple CSS selector. * * @package CSS\CSSSimpleSelector */ class css_simple_selector { /** var Element name/Type of this simple selector */ protected $type = NULL; /** var Pseudo element which this selector matches */ protected $pseudo_element = NULL; /** var Id which this selector matches */ protected $id = NULL; /** var Classes which this selector matches */ protected $classes = array(); /** var Pseudo classes which this selector matches */ protected $pseudo_classes = array(); /** var Attributes which this selector matches */ protected $attributes = array(); /** var Specificity of this selector */ protected $specificity = 0; /** * Internal function that checks if $sign is a sign that * separates/identifies the different parts of an simple selector. * * @param character $sign */ protected function isSpecialSign ($sign) { switch ($sign) { case '.': case '[': case '#': case ':': return true; } return false; } /** * Construct the simple selector from $simple_selector_string. * * @param string $simple_selector_string String containing the selector */ public function __construct($simple_selector_string) { $pos = 0; $simple_selector_string = trim ($simple_selector_string); $max = strlen ($simple_selector_string); if ($max == 0) { $this->type = '*'; return; } $a = 0; $b = 0; $c = 0; $content = ''; $first_sign = ''; $first = true; $pseudo_element = false; while ($pos < $max) { $sign = $simple_selector_string [$pos]; if ($this->isSpecialSign ($sign)) { if ($pos == 0) { $first_sign = $sign; } else { // Found the end. if (empty($first_sign)) { // Element name/type $this->type = $content; if ($content != '*') { $c++; } } else if ($first_sign == '.') { // Class $this->classes[] = $content; $b++; } else if ($first_sign == '#') { // ID $this->id = $content; $a++; } else if ($first_sign == ':') { //if ($next_sign != ':') { if (!$pseudo_element) { // Pseudo class $this->pseudo_classes[] = $content; $b++; } else { // Pseudo element $this->pseudo_element = $content; $c++; } } else if ($first_sign == '[') { $this->attributes [] = new css_attribute_selector($content); $b++; } $first_sign = $sign; $next_sign = $simple_selector_string [$pos+1]; if ($first_sign == ':' && $next_sign == ':') { $pseudo_element = true; $pos++; } else { $pseudo_element = false; } $content = ''; } } else { $content .= $sign; } $pos++; } // If $content is not empty then parse it if (!empty($content)) { if (empty($first_sign)) { // Element name/type $this->type = $content; if ($content != '*') { $c++; } } else if ($first_sign == '.') { // Class $this->classes[] = $content; $b++; } else if ($first_sign == '#') { // ID $this->id = $content; $a++; } else if ($first_sign == ':') { if ($next_sign != ':') { // Pseudo class $this->pseudo_classes[] = $content; $b++; } else { // Pseudo element $this->pseudo_element = $content; $c++; } } else if ($first_sign == '[') { $this->attributes [] = new css_attribute_selector($content); $b++; } } // Calculate specificity $this->specificity = $a * 100 + $b *10 + $c; } /** * The functions checks wheter this simple selector matches the given * $element or not. $element must support the interface iElementCSSMatchable * to enable this class to do the CSS selector matching. * * @param iElementCSSMatchable $element Element to check * @return boolean */ public function matches_entry (iElementCSSMatchable $element) { $element_attrs = $element->iECSSM_getAttributes(); // Match type/element if (!empty($this->type) && $this->type != '*' && $this->type != $element->iECSSM_getName()) { return false; } // Match class(es) if (count($this->classes) > 0) { if (empty($element_attrs ['class'])) { return false; } $comp = explode (' ', $element_attrs ['class']); foreach ($this->classes as $search) { if (array_search($search, $comp) === false) { return false; } } } // Match id if (!empty($this->id) && !empty($element_attrs ['id']) && $this->id != $element_attrs ['id']) { return false; } // Match attributes foreach ($this->attributes as $attr_sel) { if ($attr_sel->matches ($element_attrs) === false) { return false; } } // Match pseudo class(es) if (count($this->pseudo_classes) > 0) { foreach ($this->pseudo_classes as $search) { if ($element->iECSSM_has_pseudo_class($search) == false) { return false; } } } // Match pseudo element if (!empty($this->pseudo_element)) { if ($element->iECSSM_has_pseudo_element($this->pseudo_element) == false) { return false; } } return true; } /** * The function returns a string representation of this simple * selector (only for debugging purpose). * * @return string */ public function toString () { $returnstring = ''; if (!empty($this->type)) { $returnstring .= $this->type; } if (!empty($this->id)) { $returnstring .= '#'.$this->id; } foreach ($this->classes as $class) { $returnstring .= '.'.$class; } foreach ($this->attributes as $attr_sel) { $returnstring .= $attr_sel->toString(); } return $returnstring; } /** * Return the specificity of this simple selector. * * @return integer */ public function getSpecificity () { return $this->specificity; } } /** * Class css_selector. * Storage class to save a complete CSS selector. * The class can also store multiple selectors, e.g. like 'h1 , h2, h3 {...}' * * @package CSS\CSSSelector */ class css_selector { /** var Known combinators */ static protected $combinators = ' ,>+~'; /** var Brackets */ static protected $brackets = '[]'; /** var String from which this selector was created */ protected $selector_string = NULL; /** var Array with parsed selector(s) */ protected $selectors_parsed = array(); /** var Specificity of this selector */ protected $specificity = array(); /** * Construct the selector from $selector_string. * * @param string $selector_string String containing the selector */ public function __construct($selector_string) { $selector_string = str_replace("\n", '', $selector_string); $this->selector_string = trim($selector_string); $pos = 0; $max = strlen($this->selector_string); $current = ''; $selector = array(); $specificity = 0; $size = 0; $in_brackets = false; $separators = self::$combinators.self::$brackets; while ($pos < $max) { $sign = $this->selector_string [$pos]; $result = strpos ($separators, $sign); if ($sign == '[') { $in_brackets = true; } if ($result === false || $in_brackets == true) { // No combinator $current .= $sign; $pos++; if ($sign == ']') { $in_brackets = false; } } else { // Parse current $selector [$size]['selector'] = new css_simple_selector($current); $specificity += $selector [$size]['selector']->getSpecificity(); $size++; $current = ''; $combinator = $sign; $pos++; while ($pos < $max) { $sign = $this->selector_string[$pos]; if (strpos (self::$combinators, $sign) === false) { break; } $combinator .= $sign; $pos++; } if (ctype_space($combinator)) { $selector [$size]['combinator'] = ' '; $size++; } else { $combinator = trim ($combinator, ' '); if ($combinator != ',') { $selector [$size]['combinator'] = $combinator[0]; $size++; } else { $this->selectors_parsed [] = $selector; $this->specificity [] = $specificity; $selector = array(); $size = 0; $specificity = 0; } } } } if (!empty($current)) { $selector [$size]['selector'] = new css_simple_selector($current); $specificity += $selector [$size]['selector']->getSpecificity(); $this->selectors_parsed [] = $selector; $this->specificity [] = $specificity; } } /** * The function checks if the combined simple selectors in $selector * match $element or not. $element must support the interface iElementCSSMatchable * to enable this class to do the CSS selector matching. * * @param array $selector Internal selector array * @param iElementCSSMatchable $element Element to check * @return boolean */ protected function selector_matches (array $selector, iElementCSSMatchable $element) { $combinator = ''; $found = 0; $size = count($selector); if ($size == 0 ) { return false; } // First entry should be a selector if (!isset($selector [$size-1]['selector'])) { // No! (Error) return false; } // Start comparison with the current element $simple = $selector [$size-1]['selector']; if ($simple->matches_entry ($element) == false) { // If the current open element does not match then there is no match return false; } if ($size == 1) { // We are finished already return true; } // Next entry should be a combinator if (!isset($selector [$size-2]['combinator'])) { // No! (Error) return false; } $combinator = $selector [$size-2]['combinator']; $start_search = $element; for ($index = $size-3 ; $index >= 0 ; $index--) { // If we get here but start_search is already negative then there are // selectors left but no more subjects/element to match. if ($start_search < 0) { return false; } if (empty($selector [$index]['combinator'])) { $simple = $selector [$index]['selector']; switch ($combinator) { case ' ': // Find any parent, parent's parent... that matches our simple selector do { $parent = $start_search->iECSSM_getParent(); if (!isset($parent)) { return false; } $start_search = $parent; $is_match = $simple->matches_entry ($parent); if ($is_match == true) { // Found match. Stop this search. break; } }while (isset($parent)); // Did we find anything? if (!$is_match) { // No. return false; } $start_search = $parent; break; case '>': // Check if we have a parent and if it matches our simple selector $parent = $start_search->iECSSM_getParent(); if (!isset($parent)) { return false; } if ($simple->matches_entry ($parent) == false) { // No match. return false; } $start_search = $parent; break; case '+': // Immediate preceding sibling must match our simple selector $sibling = $start_search->iECSSM_getPrecedingSibling(); if (!isset($sibling)) { return false; } if ($simple->matches_entry ($sibling) == false) { // No match. return false; } $start_search = $sibling; break; case '~': // One of the preceding siblings must match our simple selector do { $sibling = $start_search->iECSSM_getPrecedingSibling(); if (!isset($sibling)) { return false; } $start_search = $sibling; if ($simple->matches_entry ($sibling) == true) { // Found match. Stop this search. break; } }while (isset($sibling)); // Did we find anything? if (!isset($sibling)) { // No. return false; } $start_search = $sibling; break; // We won't get the combinator ',' here cause that is // handled at construction time by creating an array of selectors //case ',': // break; } } else { $combinator = $selector [$index]['combinator']; } } // If we get here then everything matches! return true; } /** * The functions checks wheter any selector stored in this object * match the given $element or not. $element must support the interface * iElementCSSMatchable to enable this class to do the CSS selector matching. * * @param iElementCSSMatchable $element Element to check * @param integer $specificity Specificity of matching selector * @return boolean */ public function matches (iElementCSSMatchable $element, &$specificity) { $size = count ($this->selectors_parsed); $match = false; $specificity = 0; for ($index = 0 ; $index < $size ; $index++) { if ($this->selector_matches ($this->selectors_parsed [$index], $element) == true) { if ($this->specificity [$index] > $specificity) { $specificity = $this->specificity [$index]; } $match = true; } } return $match; } /** * The function returns a string representation of this * selector (only for debugging purpose). * * @return string */ public function toString () { $returnstring = ''; $max = count($this->selectors_parsed); $index_parsed = 0; foreach ($this->selectors_parsed as $selector) { $size = count($selector); for ($index = 0 ; $index < $size ; $index++) { if ( isset($selector [$index]['combinator']) ) { if ($selector [$index]['combinator'] == ' ') { $returnstring .= ' '; } else { $returnstring .= ' '.$selector [$index]['combinator'].' '; } } else { $simple = $selector [$index]['selector']; $returnstring .= $simple->toString(); if ($index < $size-1) { $returnstring .= ' '; } } } $index_parsed++; if ($index_parsed < $max) { $returnstring .= ','; } } return $returnstring; } } /** * Class css_rule_new. * * @package CSS\CSSRuleNew */ class css_rule_new { /** @var Media selector to which this rule belongs */ protected $media = NULL; /** @var Selector string from which this rule was created */ protected $selector = NULL; /** @var Array of css_declaration objects */ protected $declarations = array (); /** * Construct rule from strings $selector and $decls. * * @param string $selector String containing the selector * @param string $decls String containing the declarations * @param string|null $media String containing the media selector */ public function __construct($selector, $decls, $media = NULL) { $this->media = trim ($media); //print ("\nNew rule: ".$media."\n"); //Debuging // Create/parse selector $this->selector = new css_selector ($selector); $decls = trim ($decls, '{}'); // Parse declarations $pos = 0; $end = strlen ($decls); while ( $pos < $end ) { $colon = strpos ($decls, ':', $pos); if ( $colon === false ) { break; } $semi = strpos ($decls, ';', $colon + 1); if ( $semi === false ) { break; } $property = substr ($decls, $pos, $colon - $pos); $property = trim($property); $value = substr ($decls, $colon + 1, $semi - ($colon + 1)); $value = trim ($value); $values = preg_split ('/\s+/', $value); $value = ''; foreach ($values as $part) { if ( $part != '!important' ) { $value .= ' '.$part; } } $value = trim($value); // Create new declaration $declaration = new css_declaration ($property, $value); $this->declarations [] = $declaration; // Handle CSS shorthands, e.g. 'border' if ( $declaration->isShorthand () === true ) { $declaration->explode ($this->declarations); } $pos = $semi + 1; } } /** * The function returns a string representation of this * rule (only for debugging purpose). * * @return string */ public function toString () { $returnString = ''; $returnString .= "Media= \"".$this->media."\"\n"; $returnString .= $this->selector->toString().' '; $returnString .= "{\n"; foreach ($this->declarations as $declaration) { $returnString .= $declaration->getProperty ().':'.$declaration->getValue ().";\n"; } $returnString .= "}\n"; return $returnString; } /** * The functions checks wheter this rule matches the given $element * or not. $element must support the interface iElementCSSMatchable * to enable this class to do the CSS selector matching. * * @param iElementCSSMatchable $element Element to check * @param integer $specificity Specificity of matching selector * @param string $media Media selector to match * @return boolean */ public function matches(iElementCSSMatchable $element, &$specificity, $media = NULL) { $media = trim($media); if ( !empty($this->media) && $media !== $this->media ) { // Wrong media //print ("\nNo-Match ".$this->media."==".$media); //Debuging return false; } // The rules does match if the selector does match $result = $this->selector->matches($element, $specificity); return $result; } /** * The function returns the value of property $name or null if a * property with that name does not exist in this rule. * * @param string $name The property name * @return string|null */ public function getProperty ($name) { foreach ($this->declarations as $declaration) { if ( $name == $declaration->getProperty () ) { return $declaration->getValue (); } } return NULL; } /** * The function stores all properties of this rule in the array * $values as key - value pairs, e.g. $values ['color'] = 'red'; * * @param array $values Array for property storage * @return null */ public function getProperties (&$values) { foreach ($this->declarations as $declaration) { $property = $declaration->getProperty (); $value = $declaration->getValue (); $values [$property] = $value; } return NULL; } /** * The function calls $callback for each property stored in this * rule containing a length value. The return value of $callback * is saved as the new property value. * * @param callable $callback */ public function adjustLengthValues ($callback) { foreach ($this->declarations as $declaration) { $declaration->adjustLengthValues ($callback, $this); } } /** * The function calls $callback for each property stored in this * rule containing a URL reference. The return value of $callback * is saved as the new property value. * * @param callable $callback */ public function replaceURLPrefixes ($callback) { foreach ($this->declarations as $declaration) { $declaration->replaceURLPrefixes ($callback); } } } /** * Class cssimportnew * * @package CSS\CSSImportNew */ class cssimportnew { /** var Imported raw CSS code */ protected $raw; /** @var Array of css_rule_new */ protected $rules = array (); /** @var Actually set media selector */ protected $media = NULL; /** * Import CSS code from string $contents. * Returns true on success or false if any error occured during CSS parsing. * * @param string $contents * @return boolean */ function importFromString($contents) { $this->deleteComments ($contents); return $this->importFromStringInternal ($contents); } /** * Delete comments in $contents. All comments are overwritten with spaces. * The '&' is required. DO NOT DELETE!!! * * @param $contents */ protected function deleteComments (&$contents) { // Delete all comments first $pos = 0; $max = strlen ($contents); $in_comment = false; while ( $pos < $max ) { if ( ($pos+1) < $max && $contents [$pos] == '/' && $contents [$pos+1] == '*' ) { $in_comment = true; $contents [$pos] = ' '; $contents [$pos+1] = ' '; $pos += 2; continue; } if ( ($pos+1) < $max && $contents [$pos] == '*' && $contents [$pos+1] == '/' && $in_comment === true ) { $in_comment = false; $contents [$pos] = ' '; $contents [$pos+1] = ' '; $pos += 2; continue; } if ( $in_comment === true ) { $contents [$pos] = ' '; } $pos++; } } /** * Set the media selector to use for CSS matching to $media. * * @param string $media */ public function setMedia($media) { $this->media = $media; } /** * Return the actually set media selector. * * @return string */ public function getMedia() { return $this->media; } /** * Internal function that imports CSS code from string $contents. * (The function is calling itself recursively) * * @param string $contents * @param string|null $media Actually valid media selector * @param integer $processed Position to which $contents were parsed * @return bool */ protected function importFromStringInternal($contents, $media = NULL, &$processed = NULL) { // Find all CSS rules $pos = 0; $max = strlen ($contents); while ( $pos < $max ) { $bracket_open = strpos ($contents, '{', $pos); if ( $bracket_open === false ) { return false; } $bracket_close = strpos ($contents, '}', $pos); if ( $bracket_close === false ) { return false; } // If this is a nested call we might hit a closing } for the media section // which was the reason for this function call. In this case break and return. if ( $bracket_close < $bracket_open ) { $pos = $bracket_close + 1; break; } // Get the part before the open bracket and the last closing bracket // (or the start of the string). $before_open_bracket = substr ($contents, $pos, $bracket_open - $pos); // Is it a @something rule? $before_open_bracket = trim ($before_open_bracket); $at_rule_pos = stripos($before_open_bracket, '@'); if ( $at_rule_pos !== false ) { $at_rule_end = stripos($before_open_bracket, ' '); // Yes, decode content as normal rules with @something ... { ... } $at_rule_name = substr ($before_open_bracket, $at_rule_pos, $at_rule_end - $at_rule_pos); if ($at_rule_name == '@media') { $at_rule_name = substr ($before_open_bracket, $at_rule_end); } $contents_in_media = substr ($contents, $bracket_open + 1); $nested_processed = 0; $result = $this->importFromStringInternal ($contents_in_media, $at_rule_name, $nested_processed); if ( $result !== true ) { // Stop parsing on error. return false; } unset ($at_rule_name); $pos = $bracket_open + 1 + $nested_processed; } else { // No, decode rule the normal way selector { ... } // The selector is stored in $before_open_bracket $decls = substr ($contents, $bracket_open + 1, $bracket_close - $bracket_open); $this->rules [] = new css_rule_new ($before_open_bracket, $decls, $media); $pos = $bracket_close + 1; } } if ( isset($processed) ) { $processed = $pos; } return true; } /** * Import CSS code from file filename. * Returns true on success or false if any error occured during CSS parsing. * * @param string $filename * @return boolean */ function importFromFile($filename) { // Try to read in the file content if ( empty($filename) ) { return false; } $handle = fopen($filename, "rb"); if ( $handle === false ) { return false; } $contents = fread($handle, filesize($filename)); fclose($handle); if ( $contents === false ) { return false; } return $this->importFromString ($contents); } /** * Return the original CSS code that was imported. * * @return string */ public function getRaw () { return $this->raw; } /** * Get the value of CSS property for element $element. * If $element is not matched by any rule or the rule(s) matching * do not contain the property $name then null is returned. * * @param string $name Name of queried property * @param iElementCSSMatchable $element Element to match * @return string|null */ public function getPropertyForElement ($name, iElementCSSMatchable $element) { if ( empty ($name) ) { return NULL; } $value = NULL; $highest = 0; foreach ($this->rules as $rule) { $matched = $rule->matches($element, $specificity, $this->media); if ( $matched !== false ) { $current = $rule->getProperty ($name); // Only accept the property value if the current specificity of the matched // rule/selector is higher or equal than the highest one. if ( !empty ($current) && $specificity >= $highest) { $highest = $specificity; $value = $current; } } } return $value; } /** * Get all properties for element $element and store them in $dest. * Properties are stored as key -value pairs, e.g. $dest ['color'] = 'red'; * If $element is not matched by any rule then array $dest will be * empty (if it was empty before the call!). * * @param array $dest Property storage * @param iElementCSSMatchable $element Element to match * @param ODTUnits $units ODTUnits object for conversion * @param boolean $inherit Enable/disable inheritance * @return string|null */ public function getPropertiesForElement (&$dest, iElementCSSMatchable $element, ODTUnits $units, $inherit=true) { if (!isset($element)) { return; } $highest = array(); $temp = array(); foreach ($this->rules as $rule) { $matched = $rule->matches ($element, $specificity, $this->media); if ( $matched !== false ) { $current = array(); $rule->getProperties ($current); // Only accept a property value if the current specificity of the matched // rule/selector is higher or equal than the highest one. foreach ($current as $property => $value) { if (isset($highest [$property]) && $specificity >= $highest [$property]) { $highest [$property] = $specificity; $temp [$property] = $value; } } } } // Add inline style properties if present (always have highest specificity): // Create rule with selector '*' (doesn't matter) and inline style declarations $attributes = $element->iECSSM_getAttributes(); if (!empty($attributes ['style'])) { $rule = new css_rule ('*', $attributes ['style']); $rule->getProperties ($temp); } if ($inherit) { // Now calculate absolute values and inherit values from parents $this->calculateAndInherit ($temp, $element, $units); unset($temp ['calculated']); } $dest = $temp; } /** * Get the value of CSS property for element $parent. If $parent has * no match for the property with name $key then return the value of * the property for $parent's parents. * * @param string $key Name of queried property * @param iElementCSSMatchable $parent Element to match * @return string|null */ protected function getParentsValue($key, iElementCSSMatchable $parent) { $properties = $parent->getProperties (); if (isset($properties [$key])) { return $properties [$key]; } $parentsParent = $parent->iECSSM_getParent(); if (isset($parentsParent)) { return $this->getParentsValue($key, $parentsParent); } return NULL; } /** * The function calculates the absolute values for the relative * property values of element $element and store them in $properties. * * @param array $properties Property storage * @param iElementCSSMatchable $element Element to match * @param ODTUnits $units ODTUnits object for conversion */ protected function calculate (array &$properties, iElementCSSMatchable $element, ODTUnits $units) { if (isset($properties ['calculated']) && $properties ['calculated'] == '1') { // Already done return; } $properties ['calculated'] = '1'; $parent = $element->iECSSM_getParent(); // First get absolute font-size in points for // conversion of relative units if (isset($parent)) { $font_size = $this->getParentsValue('font-size', $parent); } if (isset($font_size)) { // Use the parents value // (It is assumed that the value is already calculated to an absolute // value. That's why the loops in calculateAndInherit() must run backwards $base_font_size_in_pt = $units->getDigits($font_size); } else { // If there is no parent value use global setting $base_font_size_in_pt = $units->getPixelPerEm ().'px'; $base_font_size_in_pt = $units->toPoints($base_font_size_in_pt, 'y'); $base_font_size_in_pt = $units->getDigits($base_font_size_in_pt); } // Do we have font-size or line-height set? if (isset($properties ['font-size']) || isset($properties ['line-height'])) { if (isset($properties ['font-size'])) { $font_size_unit = $units->stripDigits($properties ['font-size']); $font_size_digits = $units->getDigits($properties ['font-size']); if ($font_size_unit == '%' || $font_size_unit == 'em') { $base_font_size_in_pt = $units->getAbsoluteValue ($properties ['font-size'], $base_font_size_in_pt); $properties ['font-size'] = $base_font_size_in_pt.'pt'; } elseif ($font_size_unit != 'pt') { $properties ['font-size'] = $units->toPoints($properties ['font-size'], 'y'); $base_font_size_in_pt = $units->getDigits($properties ['font-size']); } else { $base_font_size_in_pt = $units->getDigits($properties ['font-size']); } } // Convert relative line-heights to absolute if (isset($properties ['line-height'])) { $line_height_unit = $units->stripDigits($properties ['line-height']); $line_height_digits = $units->getDigits($properties ['line-height']); if ($line_height_unit == '%') { $properties ['line-height'] = (($line_height_digits * $base_font_size_in_pt)/100).'pt'; } elseif (empty($line_height_unit)) { $properties ['line-height'] = ($line_height_digits * $base_font_size_in_pt).'pt'; } } } // Calculate all other absolute values // (NOT 'width' as it depends on the encapsulating element, // and not 'font-size' and 'line-height' => already done above foreach ($properties as $key => $value) { switch ($key) { case 'width': case 'font-size': case 'line-height': // Do nothing. break; case 'margin': case 'margin-left': case 'margin-right': case 'margin-top': case 'margin-bottom': // Do nothing. // We do not know the size of the surrounding element. break; default: // Convert '%' or 'em' value based on determined font-size $unit = $units->stripDigits($value); if ($unit == '%' || $unit == 'em') { $value = $units->getAbsoluteValue ($value, $base_font_size_in_pt); $properties [$key] = $value.'pt'; } break; } } $element->setProperties($properties); } /** * The function inherits all properties of the $parents into array * $dest. $parents is an array of elements (iElementCSSMatchable). * * @param array $dest Property storage * @param array $parents Parents to inherit from */ protected function inherit (array &$dest, array $parents) { // Inherit properties of all parents // (MUST be done backwards!) $max = count ($parents); foreach ($parents as $parent) { $properties = $parent->getProperties (); foreach ($properties as $key => $value) { if ($dest [$key] == 'inherit') { $dest [$key] = $value; } else { if (strncmp($key, 'background', strlen('background')) == 0) { // The property may not be inherited continue; } if (strncmp($key, 'border', strlen('border')) == 0) { // The property may not be inherited continue; } if (strncmp($key, 'padding', strlen('padding')) == 0) { // The property may not be inherited continue; } if (strncmp($key, 'margin', strlen('margin')) == 0) { // The property may not be inherited continue; } if (strncmp($key, 'outline', strlen('outline')) == 0) { // The property may not be inherited continue; } if (strncmp($key, 'counter', strlen('counter')) == 0) { // The property may not be inherited continue; } if (strncmp($key, 'page-break', strlen('page-break')) == 0) { // The property may not be inherited continue; } if (strncmp($key, 'cue', strlen('cue')) == 0) { // The property may not be inherited continue; } if (strncmp($key, 'pause', strlen('pause')) == 0) { // The property may not be inherited continue; } if (strpos($key, 'width') !== false) { // The property may not be inherited continue; } if (strpos($key, 'height') !== false) { // The property may not be inherited continue; } switch ($key) { case 'text-decoration': case 'text-shadow': case 'display': case 'table-layout': case 'vertical-align': case 'visibility': case 'position': case 'top': case 'right': case 'bottom': case 'left': case 'float': case 'clear': case 'z-index': case 'unicode-bidi': case 'overflow': case 'clip': case 'visibility': case 'content': case 'marker-offset': case 'play-during': // The property may not be inherited break; default: if (!isset($dest [$key]) || $dest [$key] == 'inherit') { $dest [$key] = $value; } break; } } } } } /** * Main function performing calculation and inheritance for element * $element. Properties are stored in $dest. * * @param array $dest Property storage * @param array $element Element to match * @param ODTUnits $units ODTUnits object for conversion */ protected function calculateAndInherit (array &$dest, iElementCSSMatchable $element, ODTUnits $units) { $parents = array(); $parent = $element->iECSSM_getParent(); while (isset($parent)) { $parents [] = $parent; $parent = $parent->iECSSM_getParent(); } // Determine properties of all parents if not done yet // and calculate absolute values // (MUST be done backwards!) $max = count ($parents); for ($index = $max-1 ; $index >= 0 ; $index--) { $properties = $parents [$index]->getProperties (); if (!isset($properties)) { $properties = array(); $this->getPropertiesForElement ($properties, $parents [$index], $units, false); $parents [$index]->setProperties ($properties); } if (!isset($properties ['calculated'])) { $this->calculate($properties, $parents [$index], $units); } } // Calculate our own absolute values $this->calculate($dest, $element, $units); // Inherit values from our parents $this->inherit($dest, $parents); } /** * Return a string representation of all imported rules. * (String can be large) * * @return string */ public function rulesToString () { $returnString = ''; foreach ($this->rules as $rule) { $returnString .= $rule->toString (); } return $returnString; } /** * The function strips the 'url(...)' part from an URL reference * and puts a $replacement path in front of the rest. * * @param string $URL Original URL reference * @param string $replacement Replacement path to set * @return string */ public static function replaceURLPrefix ($URL, $replacement) { if ( !empty ($URL) && !empty ($replacement) ) { // Replace 'url(...)' with $replacement $URL = substr ($URL, 3); $URL = trim ($URL, '()'); $URL = $replacement.$URL; } return $URL; } /** * The function calls $callback for each imported property * containing a length value. The return value of $callback * is saved as the new property value. * * @param callable $callback */ public function adjustLengthValues ($callback) { foreach ($this->rules as $rule) { $rule->adjustLengthValues ($callback); } } /** * The function calls $callback for each property imported * containing a URL reference. The return value of $callback * is saved as the new property value. * * @param callable $callback */ public function replaceURLPrefixes ($callback) { foreach ($this->rules as $rule) { $rule->replaceURLPrefixes ($callback); } } }