Files
dokuwiki-plugins/lib/plugins/odt/ODT/css/cssimportnew.php
Trevor Batley bce7dd054a add contents
2025-10-09 15:04:29 +11:00

1442 lines
51 KiB
PHP

<?php
/**
* Class for importing and using CSS (new version).
* Partly uses code from the old version, e.g. css_declaration.
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author LarsDW223
*/
/**
* Class css_attribute_selector.
* Simple storage class to save exactly one CSS attribute selector.
*
* @package CSS\CSSAttributeSelector
*/
class css_attribute_selector {
/** var The namespace to which this attribute selector belongs */
protected $namespaze = NULL;
/** var The attribute name */
protected $attribute = NULL;
/** var The attribute selector operator */
protected $operator = NULL;
/** var The attribute selector value */
protected $value = NULL;
/**
* Construct the selector from $attribute_string.
*
* @param string $attribute_string String containing the selector
*/
public function __construct($attribute_string) {
$attribute_string = trim ($attribute_string, '[] ');
$found = strpos ($attribute_string, '|');
if ($found !== false &&
$attribute_string [$found+1] == '=') {
$found = strpos ($attribute_string, '|', $found+1);
}
if ($found !== false) {
if ($found > 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 ($this->operator == NULL) {
// 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) &&
$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 ($selector [$size-1]['selector'] == NULL) {
// 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 ($selector [$size-2]['combinator'] == NULL) {
// 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 ($parent === NULL) {
return false;
}
$start_search = $parent;
$is_match = $simple->matches_entry ($parent);
if ($is_match == true) {
// Found match. Stop this search.
break;
}
}while ($parent !== NULL);
// 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 ($parent === NULL) {
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 ($sibling === NULL) {
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 ($sibling === NULL) {
return false;
}
$start_search = $sibling;
if ($simple->matches_entry ($sibling) == true) {
// Found match. Stop this search.
break;
}
}while ($sibling !== NULL);
// Did we find anything?
if ($sibling === NULL) {
// 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 ($selector [$index]['combinator'] !== NULL ) {
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 ( $processed !== NULL ) {
$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 ($element == NULL) {
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 ($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 ($properties [$key] != NULL) {
return $properties [$key];
}
$parentsParent = $parent->iECSSM_getParent();
if ($parentsParent != NULL) {
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 ($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 ($parent != NULL) {
$font_size = $this->getParentsValue('font-size', $parent);
}
if ($font_size != NULL) {
// 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 ($properties ['font-size'] != NULL || $properties ['line-height'] != NULL) {
if ($properties ['font-size'] != NULL) {
$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 ($properties ['line-height'] != NULL) {
$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 ($dest [$key] == NULL || $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 ($parent != NULL) {
$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 ($properties == NULL) {
$properties = array();
$this->getPropertiesForElement ($properties, $parents [$index], $units, false);
$parents [$index]->setProperties ($properties);
}
if ($properties ['calculated'] == NULL) {
$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);
}
}
}