1<?php
2/**
3 * Class for importing and using CSS (new version).
4 * Partly uses code from the old version, e.g. css_declaration.
5 *
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author     LarsDW223
8 */
9
10/**
11 * Class css_attribute_selector.
12 * Simple storage class to save exactly one CSS attribute selector.
13 *
14 * @package CSS\CSSAttributeSelector
15 */
16class css_attribute_selector {
17    /** var The namespace to which this attribute selector belongs */
18    protected $namespaze = NULL;
19    /** var The attribute name */
20    protected $attribute = NULL;
21    /** var The attribute selector operator */
22    protected $operator = NULL;
23    /** var The attribute selector value */
24    protected $value = NULL;
25
26    /**
27     * Construct the selector from $attribute_string.
28     *
29     * @param    string $attribute_string String containing the selector
30     */
31    public function __construct($attribute_string) {
32        $attribute_string = trim ($attribute_string, '[] ');
33        $found = strpos ($attribute_string, '|');
34        if ($found !== false &&
35            $attribute_string [$found+1] == '=') {
36            $found = strpos ($attribute_string, '|', $found+1);
37        }
38        if ($found !== false) {
39            if ($found > 0) {
40                $this->namespaze = substr ($attribute_string, 0, $found);
41            }
42            $attribute_string = substr ($attribute_string, $found + 1);
43        }
44        $found = strpos ($attribute_string, '=');
45        if ($found === false) {
46            $this->attribute = $attribute_string;
47        } else {
48            if (ctype_alpha($attribute_string [$found-1])) {
49                $this->attribute = substr($attribute_string, 0, $found);
50                $this->operator = '=';
51                $this->value = substr($attribute_string, $found + 1);
52            } else {
53                $this->attribute = substr($attribute_string, 0, $found - 1);
54                $this->operator = $attribute_string [$found-1].$attribute_string [$found];
55                $this->value = substr($attribute_string, $found + 1);
56            }
57            $this->value = trim ($this->value, '"');
58        }
59    }
60
61    /**
62     * The function checks if this atrribute selector matches the
63     * attributes given in $attributes as key - value pairs.
64     *
65     * @param    string $attributes String containing the selector
66     * @return   boolean
67     */
68    public function matches (array $attributes=NULL) {
69        if ($this->operator == NULL) {
70            // Attribute should be present
71            return isset($attributes) && array_key_exists($this->attribute, $attributes);
72        } else {
73            switch ($this->operator) {
74                case '=':
75                    // Attribute should have exactly the value $this->value
76                    if ($attributes [$this->attribute] == $this->value) {
77                        return true;
78                    } else {
79                        return false;
80                    }
81                    break;
82
83                case '~=':
84                    // Attribute value should contain the word $this->value
85                    $words = preg_split ('/\s/', $attributes [$this->attribute]);
86                    if (array_search($this->value, $words) !== false) {
87                        return true;
88                    } else {
89                        return false;
90                    }
91                    break;
92
93                case '|=':
94                    // Attribute value should contain the word $this->value
95                    // or a word starting with $this->value.'-'
96                    $with_hypen = $this->value.'-';
97                    $length = strlen ($with_hypen);
98                    if ($attributes [$this->attribute] == $this->value ||
99                        strncmp($attributes [$this->attribute], $with_hypen, $length) == 0) {
100                        return true;
101                    }
102                    break;
103
104                case '^=':
105                    // Attribute value should contain
106                    // a word starting with $this->value
107                    $length = strlen ($this->value);
108                    if (strncmp($attributes [$this->attribute], $this->value, $length) == 0) {
109                        return true;
110                    }
111                    break;
112
113                case '$=':
114                    // Attribute value should contain
115                    // a word ending with $this->value
116                    $length = -1 * strlen ($this->value);
117                    if (substr($attributes [$this->attribute], $length) == $this->value) {
118                        return true;
119                    }
120                    break;
121
122                case '*=':
123                    // Attribute value should include $this->value
124                    if (strpos($attributes [$this->attribute], $this->value) !== false) {
125                        return true;
126                    }
127                    break;
128            }
129        }
130        return false;
131    }
132
133    /**
134     * The function returns a string representation of this attribute
135     * selector (only for debugging purpose).
136     *
137     * @return   string
138     */
139    public function toString () {
140        $returnstring = '[';
141        if (!empty($this->namespaze)) {
142            $returnstring .= $this->namespaze.'|';
143        }
144        $returnstring .= $this->attribute.$this->operator.$this->value;
145        $returnstring .= ']';
146        return $returnstring;
147    }
148}
149
150/**
151 * Class css_simple_selector
152 * Simple storage class to save a simple CSS selector.
153 *
154 * @package CSS\CSSSimpleSelector
155 */
156class css_simple_selector {
157    /** var Element name/Type of this simple selector */
158    protected $type = NULL;
159    /** var Pseudo element which this selector matches */
160    protected $pseudo_element = NULL;
161    /** var Id which this selector matches */
162    protected $id = NULL;
163    /** var Classes which this selector matches */
164    protected $classes = array();
165    /** var Pseudo classes which this selector matches */
166    protected $pseudo_classes = array();
167    /** var Attributes which this selector matches */
168    protected $attributes = array();
169    /** var Specificity of this selector */
170    protected $specificity = 0;
171
172    /**
173     * Internal function that checks if $sign is a sign that
174     * separates/identifies the different parts of an simple selector.
175     *
176     * @param character $sign
177     */
178    protected function isSpecialSign ($sign) {
179        switch ($sign) {
180            case '.':
181            case '[':
182            case '#':
183            case ':':
184                return true;
185        }
186        return false;
187    }
188
189    /**
190     * Construct the simple selector from $simple_selector_string.
191     *
192     * @param    string $simple_selector_string String containing the selector
193     */
194    public function __construct($simple_selector_string) {
195        $pos = 0;
196        $simple_selector_string = trim ($simple_selector_string);
197        $max = strlen ($simple_selector_string);
198        if ($max == 0) {
199            $this->type = '*';
200            return;
201        }
202
203        $a = 0;
204        $b = 0;
205        $c = 0;
206
207        $content = '';
208        $first_sign = '';
209        $first = true;
210        $pseudo_element = false;
211        while ($pos < $max) {
212            $sign = $simple_selector_string [$pos];
213            if ($this->isSpecialSign ($sign)) {
214                if ($pos == 0) {
215                    $first_sign = $sign;
216                } else {
217                    // Found the end.
218                    if (empty($first_sign)) {
219                        // Element name/type
220                        $this->type = $content;
221                        if ($content != '*') {
222                            $c++;
223                        }
224                    } else if ($first_sign == '.') {
225                        // Class
226                        $this->classes[] = $content;
227                        $b++;
228                    } else if ($first_sign == '#') {
229                        // ID
230                        $this->id = $content;
231                        $a++;
232                    } else if ($first_sign == ':') {
233                        //if ($next_sign != ':') {
234                        if (!$pseudo_element) {
235                            // Pseudo class
236                            $this->pseudo_classes[] = $content;
237                            $b++;
238                        } else {
239                            // Pseudo element
240                            $this->pseudo_element = $content;
241                            $c++;
242                        }
243                    } else if ($first_sign == '[') {
244                        $this->attributes [] = new css_attribute_selector($content);
245                        $b++;
246                    }
247                    $first_sign = $sign;
248                    $next_sign = $simple_selector_string [$pos+1];
249                    if ($first_sign == ':' && $next_sign == ':') {
250                        $pseudo_element = true;
251                        $pos++;
252                    } else {
253                        $pseudo_element = false;
254                    }
255                    $content = '';
256                }
257            } else {
258                $content .= $sign;
259            }
260            $pos++;
261        }
262
263        // If $content is not empty then parse it
264        if (!empty($content)) {
265            if (empty($first_sign)) {
266                // Element name/type
267                $this->type = $content;
268                if ($content != '*') {
269                    $c++;
270                }
271            } else if ($first_sign == '.') {
272                // Class
273                $this->classes[] = $content;
274                $b++;
275            } else if ($first_sign == '#') {
276                // ID
277                $this->id = $content;
278                $a++;
279            } else if ($first_sign == ':') {
280                if ($next_sign != ':') {
281                    // Pseudo class
282                    $this->pseudo_classes[] = $content;
283                    $b++;
284                } else {
285                    // Pseudo element
286                    $this->pseudo_element = $content;
287                    $c++;
288                }
289            } else if ($first_sign == '[') {
290                $this->attributes [] = new css_attribute_selector($content);
291                $b++;
292            }
293        }
294
295        // Calculate specificity
296        $this->specificity = $a * 100 + $b *10 + $c;
297    }
298
299    /**
300     * The functions checks wheter this simple selector matches the given
301     * $element or not. $element must support the interface iElementCSSMatchable
302     * to enable this class to do the CSS selector matching.
303     *
304     * @param    iElementCSSMatchable $element Element to check
305     * @return   boolean
306     */
307    public function matches_entry (iElementCSSMatchable $element) {
308        $element_attrs = $element->iECSSM_getAttributes();
309
310        // Match type/element
311        if (!empty($this->type) &&
312            $this->type != '*' &&
313            $this->type != $element->iECSSM_getName()) {
314            return false;
315        }
316
317        // Match class(es)
318        if (count($this->classes) > 0) {
319            if (empty($element_attrs ['class'])) {
320                return false;
321            }
322            $comp = explode (' ', $element_attrs ['class']);
323            foreach ($this->classes as $search) {
324                if (array_search($search, $comp) === false) {
325                    return false;
326                }
327            }
328        }
329
330        // Match id
331        if (!empty($this->id) &&
332            $this->id != $element_attrs ['id']) {
333            return false;
334        }
335
336        // Match attributes
337        foreach ($this->attributes as $attr_sel) {
338            if ($attr_sel->matches ($element_attrs) === false) {
339                return false;
340            }
341        }
342
343        // Match pseudo class(es)
344        if (count($this->pseudo_classes) > 0) {
345            foreach ($this->pseudo_classes as $search) {
346                if ($element->iECSSM_has_pseudo_class($search) == false) {
347                    return false;
348                }
349            }
350        }
351
352        // Match pseudo element
353        if (!empty($this->pseudo_element)) {
354            if ($element->iECSSM_has_pseudo_element($this->pseudo_element) == false) {
355                return false;
356            }
357        }
358
359        return true;
360    }
361
362    /**
363     * The function returns a string representation of this simple
364     * selector (only for debugging purpose).
365     *
366     * @return   string
367     */
368    public function toString () {
369        $returnstring = '';
370        if (!empty($this->type)) {
371            $returnstring .= $this->type;
372        }
373        if (!empty($this->id)) {
374            $returnstring .= '#'.$this->id;
375        }
376        foreach ($this->classes as $class) {
377            $returnstring .= '.'.$class;
378        }
379        foreach ($this->attributes as $attr_sel) {
380            $returnstring .= $attr_sel->toString();
381        }
382        return $returnstring;
383    }
384
385    /**
386     * Return the specificity of this simple selector.
387     *
388     * @return   integer
389     */
390    public function getSpecificity () {
391        return $this->specificity;
392    }
393}
394
395/**
396 * Class css_selector.
397 * Storage class to save a complete CSS selector.
398 * The class can also store multiple selectors, e.g. like 'h1 , h2, h3 {...}'
399 *
400 * @package CSS\CSSSelector
401 */
402class css_selector {
403    /** var Known combinators */
404    static protected $combinators = ' ,>+~';
405    /** var Brackets */
406    static protected $brackets = '[]';
407    /** var String from which this selector was created */
408    protected $selector_string = NULL;
409    /** var Array with parsed selector(s) */
410    protected $selectors_parsed = array();
411    /** var Specificity of this selector */
412    protected $specificity = array();
413
414    /**
415     * Construct the selector from $selector_string.
416     *
417     * @param    string $selector_string String containing the selector
418     */
419    public function __construct($selector_string) {
420        $selector_string = str_replace("\n", '', $selector_string);
421        $this->selector_string = trim($selector_string);
422
423        $pos = 0;
424        $max = strlen($this->selector_string);
425        $current = '';
426        $selector = array();
427        $specificity = 0;
428        $size = 0;
429        $in_brackets = false;
430        $separators = self::$combinators.self::$brackets;
431        while ($pos < $max) {
432            $sign = $this->selector_string [$pos];
433            $result = strpos ($separators, $sign);
434            if ($sign == '[') {
435                $in_brackets = true;
436            }
437            if ($result === false || $in_brackets == true) {
438                // No combinator
439                $current .= $sign;
440                $pos++;
441
442                if ($sign == ']') {
443                    $in_brackets = false;
444                }
445            } else {
446                // Parse current
447                $selector [$size]['selector'] = new css_simple_selector($current);
448                $specificity += $selector [$size]['selector']->getSpecificity();
449                $size++;
450                $current = '';
451
452                $combinator = $sign;
453                $pos++;
454                while ($pos < $max) {
455                    $sign = $this->selector_string[$pos];
456                    if (strpos (self::$combinators, $sign) === false) {
457                        break;
458                    }
459                    $combinator .= $sign;
460                    $pos++;
461                }
462                if (ctype_space($combinator)) {
463                    $selector [$size]['combinator'] = ' ';
464                    $size++;
465                } else {
466                    $combinator = trim ($combinator, ' ');
467                    if ($combinator != ',') {
468                        $selector [$size]['combinator'] = $combinator[0];
469                        $size++;
470                    } else {
471                        $this->selectors_parsed [] = $selector;
472                        $this->specificity [] = $specificity;
473                        $selector = array();
474                        $size = 0;
475                        $specificity = 0;
476                    }
477                }
478            }
479        }
480        if (!empty($current)) {
481            $selector [$size]['selector'] = new css_simple_selector($current);
482            $specificity += $selector [$size]['selector']->getSpecificity();
483            $this->selectors_parsed [] = $selector;
484            $this->specificity [] = $specificity;
485        }
486    }
487
488    /**
489     * The function checks if the combined simple selectors in $selector
490     * match $element or not. $element must support the interface iElementCSSMatchable
491     * to enable this class to do the CSS selector matching.
492     *
493     * @param    array                $selector Internal selector array
494     * @param    iElementCSSMatchable $element  Element to check
495     * @return   boolean
496     */
497    protected function selector_matches (array $selector, iElementCSSMatchable $element) {
498        $combinator = '';
499        $found = 0;
500        $size = count($selector);
501        if ($size == 0 ) {
502            return false;
503        }
504
505        // First entry should be a selector
506        if ($selector [$size-1]['selector'] == NULL) {
507            // No! (Error)
508            return false;
509        }
510
511        // Start comparison with the current element
512        $simple = $selector [$size-1]['selector'];
513        if ($simple->matches_entry ($element) == false) {
514            // If the current open element does not match then there is no match
515            return false;
516        }
517        if ($size == 1) {
518            // We are finished already
519            return true;
520        }
521
522        // Next entry should be a combinator
523        if ($selector [$size-2]['combinator'] == NULL) {
524            // No! (Error)
525            return false;
526        }
527        $combinator = $selector [$size-2]['combinator'];
528
529        $start_search = $element;
530        for ($index = $size-3 ; $index >= 0 ; $index--) {
531            // If we get here but start_search is already negative then there are
532            // selectors left but no more subjects/element to match.
533            if ($start_search < 0) {
534                return false;
535            }
536            if (empty($selector [$index]['combinator'])) {
537                $simple = $selector [$index]['selector'];
538                switch ($combinator) {
539                    case ' ':
540                        // Find any parent, parent's parent... that matches our simple selector
541                        do {
542                            $parent = $start_search->iECSSM_getParent();
543                            if ($parent === NULL) {
544                                return false;
545                            }
546                            $start_search = $parent;
547                            $is_match = $simple->matches_entry ($parent);
548                            if ($is_match == true) {
549                                // Found match. Stop this search.
550                                break;
551                            }
552                        }while ($parent !== NULL);
553
554                        // Did we find anything?
555                        if (!$is_match) {
556                            // No.
557                            return false;
558                        }
559                        $start_search = $parent;
560                        break;
561
562                    case '>':
563                        // Check if we have a parent and if it matches our simple selector
564                        $parent = $start_search->iECSSM_getParent();
565                        if ($parent === NULL) {
566                            return false;
567                        }
568                        if ($simple->matches_entry ($parent) == false) {
569                            // No match.
570                            return false;
571                        }
572                        $start_search = $parent;
573                        break;
574
575                    case '+':
576                        // Immediate preceding sibling must match our simple selector
577                        $sibling = $start_search->iECSSM_getPrecedingSibling();
578                        if ($sibling === NULL) {
579                            return false;
580                        }
581                        if ($simple->matches_entry ($sibling) == false) {
582                            // No match.
583                            return false;
584                        }
585                        $start_search = $sibling;
586                        break;
587
588                    case '~':
589                        // One of the preceding siblings must match our simple selector
590                        do {
591                            $sibling = $start_search->iECSSM_getPrecedingSibling();
592                            if ($sibling === NULL) {
593                                return false;
594                            }
595                            $start_search = $sibling;
596                            if ($simple->matches_entry ($sibling) == true) {
597                                // Found match. Stop this search.
598                                break;
599                            }
600                        }while ($sibling !== NULL);
601
602                        // Did we find anything?
603                        if ($sibling === NULL) {
604                            // No.
605                            return false;
606                        }
607                        $start_search = $sibling;
608                        break;
609
610                    // We won't get the combinator ',' here cause that is
611                    // handled at construction time by creating an array of selectors
612                    //case ',':
613                    //    break;
614                }
615            } else {
616                $combinator = $selector [$index]['combinator'];
617            }
618        }
619
620        // If we get here then everything matches!
621        return true;
622    }
623
624    /**
625     * The functions checks wheter any selector stored in this object
626     * match the given $element or not. $element must support the interface
627     * iElementCSSMatchable to enable this class to do the CSS selector matching.
628     *
629     * @param    iElementCSSMatchable $element     Element to check
630     * @param    integer              $specificity Specificity of matching selector
631     * @return   boolean
632     */
633    public function matches (iElementCSSMatchable $element, &$specificity) {
634        $size = count ($this->selectors_parsed);
635        $match = false;
636        $specificity = 0;
637        for ($index = 0 ; $index < $size ; $index++) {
638            if ($this->selector_matches ($this->selectors_parsed [$index], $element) == true) {
639                if ($this->specificity [$index] > $specificity) {
640                    $specificity = $this->specificity [$index];
641                }
642                $match = true;
643            }
644        }
645        return $match;
646    }
647
648    /**
649     * The function returns a string representation of this
650     * selector (only for debugging purpose).
651     *
652     * @return   string
653     */
654    public function toString () {
655        $returnstring = '';
656        $max = count($this->selectors_parsed);
657        $index_parsed = 0;
658        foreach ($this->selectors_parsed as $selector) {
659            $size = count($selector);
660            for ($index = 0 ; $index < $size ; $index++) {
661                if ($selector [$index]['combinator'] !== NULL ) {
662                    if ($selector [$index]['combinator'] == ' ') {
663                        $returnstring .= ' ';
664                    } else {
665                        $returnstring .= ' '.$selector [$index]['combinator'].' ';
666                    }
667                } else {
668                    $simple = $selector [$index]['selector'];
669                    $returnstring .= $simple->toString();
670                    if ($index < $size-1) {
671                        $returnstring .= ' ';
672                    }
673                }
674            }
675            $index_parsed++;
676            if ($index_parsed < $max) {
677                $returnstring .= ',';
678            }
679        }
680        return $returnstring;
681    }
682}
683
684/**
685 * Class css_rule_new.
686 *
687 * @package CSS\CSSRuleNew
688 */
689class css_rule_new {
690    /** @var Media selector to which this rule belongs */
691    protected $media = NULL;
692    /** @var Selector string from which this rule was created */
693    protected $selector = NULL;
694    /** @var Array of css_declaration objects */
695    protected $declarations = array ();
696
697    /**
698     * Construct rule from strings $selector and $decls.
699     *
700     * @param    string      $selector String containing the selector
701     * @param    string      $decls    String containing the declarations
702     * @param    string|null $media    String containing the media selector
703     */
704    public function __construct($selector, $decls, $media = NULL) {
705
706        $this->media = trim ($media);
707        //print ("\nNew rule: ".$media."\n"); //Debuging
708
709        // Create/parse selector
710        $this->selector = new css_selector ($selector);
711
712        $decls = trim ($decls, '{}');
713
714        // Parse declarations
715        $pos = 0;
716        $end = strlen ($decls);
717        while ( $pos < $end ) {
718            $colon = strpos ($decls, ':', $pos);
719            if ( $colon === false ) {
720                break;
721            }
722            $semi = strpos ($decls, ';', $colon + 1);
723            if ( $semi === false ) {
724                break;
725            }
726
727            $property = substr ($decls, $pos, $colon - $pos);
728            $property = trim($property);
729
730            $value = substr ($decls, $colon + 1, $semi - ($colon + 1));
731            $value = trim ($value);
732            $values = preg_split ('/\s+/', $value);
733            $value = '';
734            foreach ($values as $part) {
735                if ( $part != '!important' ) {
736                    $value .= ' '.$part;
737                }
738            }
739            $value = trim($value);
740
741            // Create new declaration
742            $declaration = new css_declaration ($property, $value);
743            $this->declarations [] = $declaration;
744
745            // Handle CSS shorthands, e.g. 'border'
746            if ( $declaration->isShorthand () === true ) {
747                $declaration->explode ($this->declarations);
748            }
749
750            $pos = $semi + 1;
751        }
752    }
753
754    /**
755     * The function returns a string representation of this
756     * rule (only for debugging purpose).
757     *
758     * @return   string
759     */
760    public function toString () {
761        $returnString = '';
762        $returnString .= "Media= \"".$this->media."\"\n";
763        $returnString .= $this->selector->toString().' ';
764        $returnString .= "{\n";
765        foreach ($this->declarations as $declaration) {
766            $returnString .= $declaration->getProperty ().':'.$declaration->getValue ().";\n";
767        }
768        $returnString .= "}\n";
769        return $returnString;
770    }
771
772    /**
773     * The functions checks wheter this rule matches the given $element
774     * or not. $element must support the interface iElementCSSMatchable
775     * to enable this class to do the CSS selector matching.
776     *
777     * @param    iElementCSSMatchable $element     Element to check
778     * @param    integer              $specificity Specificity of matching selector
779     * @param    string               $media       Media selector to match
780     * @return   boolean
781     */
782    public function matches (iElementCSSMatchable $element, &$specificity, $media = NULL) {
783
784        $media = trim ($media);
785        if ( !empty($this->media) && $media != $this->media ) {
786            // Wrong media
787            //print ("\nNo-Match ".$this->media."==".$media); //Debuging
788            return false;
789        }
790
791        // The rules does match if the selector does match
792        $result = $this->selector->matches($element, $specificity);
793
794        return $result;
795    }
796
797    /**
798     * The function returns the value of property $name or null if a
799     * property with that name does not exist in this rule.
800     *
801     * @param    string $name    The property name
802     * @return string|null
803     */
804    public function getProperty ($name) {
805        foreach ($this->declarations as $declaration) {
806            if ( $name == $declaration->getProperty () ) {
807                return $declaration->getValue ();
808            }
809        }
810        return NULL;
811    }
812
813    /**
814     * The function stores all properties of this rule in the array
815     * $values as key - value pairs, e.g. $values ['color'] = 'red';
816     *
817     * @param    array $values    Array for property storage
818     * @return null
819     */
820    public function getProperties (&$values) {
821        foreach ($this->declarations as $declaration) {
822            $property = $declaration->getProperty ();
823            $value = $declaration->getValue ();
824            $values [$property] = $value;
825        }
826        return NULL;
827    }
828
829    /**
830     * The function calls $callback for each property stored in this
831     * rule containing a length value. The return value of $callback
832     * is saved as the new property value.
833     *
834     * @param    callable $callback
835     */
836    public function adjustLengthValues ($callback) {
837        foreach ($this->declarations as $declaration) {
838            $declaration->adjustLengthValues ($callback, $this);
839        }
840    }
841
842    /**
843     * The function calls $callback for each property stored in this
844     * rule containing a URL reference. The return value of $callback
845     * is saved as the new property value.
846     *
847     * @param    callable $callback
848     */
849    public function replaceURLPrefixes ($callback) {
850        foreach ($this->declarations as $declaration) {
851            $declaration->replaceURLPrefixes ($callback);
852        }
853    }
854}
855
856/**
857 * Class cssimportnew
858 *
859 * @package CSS\CSSImportNew
860 */
861class cssimportnew {
862    /** var Imported raw CSS code */
863    protected $raw;
864    /** @var Array of css_rule_new  */
865    protected $rules = array ();
866    /** @var Actually set media selector */
867    protected $media = NULL;
868
869    /**
870     * Import CSS code from string $contents.
871     * Returns true on success or false if any error occured during CSS parsing.
872     *
873     * @param    string      $contents
874     * @return boolean
875     */
876    function importFromString($contents) {
877        $this->deleteComments ($contents);
878        return $this->importFromStringInternal ($contents);
879    }
880
881    /**
882     * Delete comments in $contents. All comments are overwritten with spaces.
883     * The '&' is required. DO NOT DELETE!!!
884     *
885     * @param $contents
886     */
887    protected function deleteComments (&$contents) {
888        // Delete all comments first
889        $pos = 0;
890        $max = strlen ($contents);
891        $in_comment = false;
892        while ( $pos < $max ) {
893            if ( ($pos+1) < $max &&
894                 $contents [$pos] == '/' &&
895                 $contents [$pos+1] == '*' ) {
896                $in_comment = true;
897
898                $contents [$pos] = ' ';
899                $contents [$pos+1] = ' ';
900                $pos += 2;
901                continue;
902            }
903            if ( ($pos+1) < $max &&
904                 $contents [$pos] == '*' &&
905                 $contents [$pos+1] == '/' &&
906                 $in_comment === true ) {
907                $in_comment = false;
908
909                $contents [$pos] = ' ';
910                $contents [$pos+1] = ' ';
911                $pos += 2;
912                continue;
913            }
914            if ( $in_comment === true ) {
915                $contents [$pos] = ' ';
916            }
917            $pos++;
918        }
919    }
920
921    /**
922     * Set the media selector to use for CSS matching to $media.
923     *
924     * @param    string      $media
925     */
926    public function setMedia ($media) {
927        $this->media = $media;
928    }
929
930    /**
931     * Return the actually set media selector.
932     *
933     * @return    string
934     */
935    public function getMedia () {
936        return $this->media;
937    }
938
939    /**
940     * Internal function that imports CSS code from string $contents.
941     * (The function is calling itself recursively)
942     *
943     * @param    string      $contents
944     * @param    string|null $media     Actually valid media selector
945     * @param    integer     $processed Position to which $contents were parsed
946     * @return bool
947     */
948    protected function importFromStringInternal($contents, $media = NULL, &$processed = NULL) {
949        // Find all CSS rules
950        $pos = 0;
951        $max = strlen ($contents);
952        while ( $pos < $max ) {
953            $bracket_open = strpos ($contents, '{', $pos);
954            if ( $bracket_open === false ) {
955                return false;
956            }
957            $bracket_close = strpos ($contents, '}', $pos);
958            if ( $bracket_close === false ) {
959                return false;
960            }
961
962            // If this is a nested call we might hit a closing } for the media section
963            // which was the reason for this function call. In this case break and return.
964            if ( $bracket_close < $bracket_open ) {
965                $pos = $bracket_close + 1;
966                break;
967            }
968
969            // Get the part before the open bracket and the last closing bracket
970            // (or the start of the string).
971            $before_open_bracket = substr ($contents, $pos, $bracket_open - $pos);
972
973            // Is it a @something rule?
974            $before_open_bracket = trim ($before_open_bracket);
975            $at_rule_pos = stripos($before_open_bracket, '@');
976            if ( $at_rule_pos !== false ) {
977                $at_rule_end = stripos($before_open_bracket, ' ');
978
979                // Yes, decode content as normal rules with @something ... { ... }
980                $at_rule_name = substr ($before_open_bracket, $at_rule_pos, $at_rule_end - $at_rule_pos);
981                if ($at_rule_name == '@media') {
982                    $at_rule_name = substr ($before_open_bracket, $at_rule_end);
983                }
984                $contents_in_media = substr ($contents, $bracket_open + 1);
985
986                $nested_processed = 0;
987                $result = $this->importFromStringInternal ($contents_in_media, $at_rule_name, $nested_processed);
988                if ( $result !== true ) {
989                    // Stop parsing on error.
990                    return false;
991                }
992                unset ($at_rule_name);
993                $pos = $bracket_open + 1 + $nested_processed;
994            } else {
995
996                // No, decode rule the normal way selector { ... }
997                // The selector is stored in $before_open_bracket
998                $decls = substr ($contents, $bracket_open + 1, $bracket_close - $bracket_open);
999                $this->rules [] = new css_rule_new ($before_open_bracket, $decls, $media);
1000
1001                $pos = $bracket_close + 1;
1002            }
1003        }
1004        if ( $processed !== NULL ) {
1005            $processed = $pos;
1006        }
1007        return true;
1008    }
1009
1010    /**
1011     * Import CSS code from file filename.
1012     * Returns true on success or false if any error occured during CSS parsing.
1013     *
1014     * @param    string      $filename
1015     * @return boolean
1016     */
1017    function importFromFile($filename) {
1018        // Try to read in the file content
1019        if ( empty($filename) ) {
1020            return false;
1021        }
1022
1023        $handle = fopen($filename, "rb");
1024        if ( $handle === false ) {
1025            return false;
1026        }
1027
1028        $contents = fread($handle, filesize($filename));
1029        fclose($handle);
1030        if ( $contents === false ) {
1031            return false;
1032        }
1033
1034        return $this->importFromString ($contents);
1035    }
1036
1037    /**
1038     * Return the original CSS code that was imported.
1039     *
1040     * @return string
1041     */
1042    public function getRaw () {
1043        return $this->raw;
1044    }
1045
1046    /**
1047     * Get the value of CSS property for element $element.
1048     * If $element is not matched by any rule or the rule(s) matching
1049     * do not contain the property $name then null is returned.
1050     *
1051     * @param    string               $name    Name of queried property
1052     * @param    iElementCSSMatchable $element Element to match
1053     * @return string|null
1054     */
1055    public function getPropertyForElement ($name, iElementCSSMatchable $element) {
1056        if ( empty ($name) ) {
1057            return NULL;
1058        }
1059
1060        $value = NULL;
1061        $highest = 0;
1062        foreach ($this->rules as $rule) {
1063            $matched = $rule->matches ($element, $specificity, $this->media);
1064            if ( $matched !== false ) {
1065                $current = $rule->getProperty ($name);
1066
1067                // Only accept the property value if the current specificity of the matched
1068                // rule/selector is higher or equal than the highest one.
1069                if ( !empty ($current) && $specificity >= $highest) {
1070                    $highest = $specificity;
1071                    $value = $current;
1072                }
1073            }
1074        }
1075
1076        return $value;
1077    }
1078
1079    /**
1080     * Get all properties for element $element and store them in $dest.
1081     * Properties are stored as key -value pairs, e.g. $dest ['color'] = 'red';
1082     * If $element is not matched by any rule then array $dest will be
1083     * empty (if it was empty before the call!).
1084     *
1085     * @param    array                $dest    Property storage
1086     * @param    iElementCSSMatchable $element Element to match
1087     * @param    ODTUnits             $units   ODTUnits object for conversion
1088     * @param    boolean              $inherit Enable/disable inheritance
1089     * @return string|null
1090     */
1091    public function getPropertiesForElement (&$dest, iElementCSSMatchable $element, ODTUnits $units, $inherit=true) {
1092        if ($element == NULL) {
1093            return;
1094        }
1095
1096        $highest = array();
1097        $temp = array();
1098        foreach ($this->rules as $rule) {
1099            $matched = $rule->matches ($element, $specificity, $this->media);
1100            if ( $matched !== false ) {
1101                $current = array();
1102                $rule->getProperties ($current);
1103
1104                // Only accept a property value if the current specificity of the matched
1105                // rule/selector is higher or equal than the highest one.
1106                foreach ($current as $property => $value) {
1107                    if ($specificity >= $highest [$property]) {
1108                        $highest [$property] = $specificity;
1109                        $temp [$property] = $value;
1110                    }
1111                }
1112            }
1113        }
1114
1115        // Add inline style properties if present (always have highest specificity):
1116        // Create rule with selector '*' (doesn't matter) and inline style declarations
1117        $attributes = $element->iECSSM_getAttributes();
1118        if (!empty($attributes ['style'])) {
1119            $rule = new css_rule ('*', $attributes ['style']);
1120            $rule->getProperties ($temp);
1121        }
1122
1123        if ($inherit) {
1124            // Now calculate absolute values and inherit values from parents
1125            $this->calculateAndInherit ($temp, $element, $units);
1126            unset($temp ['calculated']);
1127        }
1128
1129        $dest = $temp;
1130    }
1131
1132    /**
1133     * Get the value of CSS property for element $parent. If $parent has
1134     * no match for the property with name $key then return the value of
1135     * the property for $parent's parents.
1136     *
1137     * @param    string               $key    Name of queried property
1138     * @param    iElementCSSMatchable $parent Element to match
1139     * @return string|null
1140     */
1141    protected function getParentsValue($key, iElementCSSMatchable $parent) {
1142        $properties = $parent->getProperties ();
1143        if ($properties [$key] != NULL) {
1144            return $properties [$key];
1145        }
1146
1147        $parentsParent = $parent->iECSSM_getParent();
1148        if ($parentsParent != NULL) {
1149            return $this->getParentsValue($key, $parentsParent);
1150        }
1151
1152        return NULL;
1153    }
1154
1155    /**
1156     * The function calculates the absolute values for the relative
1157     * property values of element $element and store them in $properties.
1158     *
1159     * @param    array                $properties Property storage
1160     * @param    iElementCSSMatchable $element    Element to match
1161     * @param    ODTUnits             $units   ODTUnits object for conversion
1162     */
1163    protected function calculate (array &$properties, iElementCSSMatchable $element, ODTUnits $units) {
1164        if ($properties ['calculated'] == '1') {
1165            // Already done
1166            return;
1167        }
1168
1169        $properties ['calculated'] = '1';
1170        $parent = $element->iECSSM_getParent();
1171
1172        // First get absolute font-size in points for
1173        // conversion of relative units
1174        if ($parent != NULL) {
1175            $font_size = $this->getParentsValue('font-size', $parent);
1176        }
1177        if ($font_size != NULL) {
1178            // Use the parents value
1179            // (It is assumed that the value is already calculated to an absolute
1180            //  value. That's why the loops in calculateAndInherit() must run backwards
1181            $base_font_size_in_pt = $units->getDigits($font_size);
1182        } else {
1183            // If there is no parent value use global setting
1184            $base_font_size_in_pt = $units->getPixelPerEm ().'px';
1185            $base_font_size_in_pt = $units->toPoints($base_font_size_in_pt, 'y');
1186            $base_font_size_in_pt = $units->getDigits($base_font_size_in_pt);
1187        }
1188
1189        // Do we have font-size or line-height set?
1190        if ($properties ['font-size'] != NULL || $properties ['line-height'] != NULL) {
1191            if ($properties ['font-size'] != NULL) {
1192                $font_size_unit = $units->stripDigits($properties ['font-size']);
1193                $font_size_digits = $units->getDigits($properties ['font-size']);
1194                if ($font_size_unit == '%' || $font_size_unit == 'em') {
1195                    $base_font_size_in_pt = $units->getAbsoluteValue ($properties ['font-size'], $base_font_size_in_pt);
1196                    $properties ['font-size'] = $base_font_size_in_pt.'pt';
1197
1198                } elseif ($font_size_unit != 'pt') {
1199                    $properties ['font-size'] = $units->toPoints($properties ['font-size'], 'y');
1200                    $base_font_size_in_pt = $units->getDigits($properties ['font-size']);
1201                } else {
1202                    $base_font_size_in_pt = $units->getDigits($properties ['font-size']);
1203                }
1204            }
1205
1206            // Convert relative line-heights to absolute
1207            if ($properties ['line-height'] != NULL) {
1208                $line_height_unit = $units->stripDigits($properties ['line-height']);
1209                $line_height_digits = $units->getDigits($properties ['line-height']);
1210                if ($line_height_unit == '%') {
1211                    $properties ['line-height'] = (($line_height_digits * $base_font_size_in_pt)/100).'pt';
1212                } elseif (empty($line_height_unit)) {
1213                    $properties ['line-height'] = ($line_height_digits * $base_font_size_in_pt).'pt';
1214                }
1215            }
1216        }
1217
1218        // Calculate all other absolute values
1219        // (NOT 'width' as it depends on the encapsulating element,
1220        //  and not 'font-size' and 'line-height' => already done above
1221        foreach ($properties as $key => $value) {
1222            switch ($key) {
1223                case 'width':
1224                case 'font-size':
1225                case 'line-height':
1226                    // Do nothing.
1227                break;
1228                case 'margin':
1229                case 'margin-left':
1230                case 'margin-right':
1231                case 'margin-top':
1232                case 'margin-bottom':
1233                    // Do nothing.
1234                    // We do not know the size of the surrounding element.
1235                break;
1236                default:
1237                    // Convert '%' or 'em' value based on determined font-size
1238                    $unit = $units->stripDigits($value);
1239                    if ($unit == '%' || $unit == 'em') {
1240                        $value = $units->getAbsoluteValue ($value, $base_font_size_in_pt);
1241                        $properties [$key] = $value.'pt';
1242                    }
1243                break;
1244            }
1245        }
1246
1247        $element->setProperties($properties);
1248    }
1249
1250    /**
1251     * The function inherits all properties of the $parents into array
1252     * $dest. $parents is an array of elements (iElementCSSMatchable).
1253     *
1254     * @param    array $dest    Property storage
1255     * @param    array $parents Parents to inherit from
1256     */
1257    protected function inherit (array &$dest, array $parents) {
1258        // Inherit properties of all parents
1259        // (MUST be done backwards!)
1260        $max = count ($parents);
1261        foreach ($parents as $parent) {
1262            $properties = $parent->getProperties ();
1263            foreach ($properties as $key => $value) {
1264                if ($dest [$key] == 'inherit') {
1265                    $dest [$key] = $value;
1266                } else {
1267                    if (strncmp($key, 'background', strlen('background')) == 0) {
1268                        // The property may not be inherited
1269                        continue;
1270                    }
1271                    if (strncmp($key, 'border', strlen('border')) == 0) {
1272                        // The property may not be inherited
1273                        continue;
1274                    }
1275                    if (strncmp($key, 'padding', strlen('padding')) == 0) {
1276                        // The property may not be inherited
1277                        continue;
1278                    }
1279                    if (strncmp($key, 'margin', strlen('margin')) == 0) {
1280                        // The property may not be inherited
1281                        continue;
1282                    }
1283                    if (strncmp($key, 'outline', strlen('outline')) == 0) {
1284                        // The property may not be inherited
1285                        continue;
1286                    }
1287                    if (strncmp($key, 'counter', strlen('counter')) == 0) {
1288                        // The property may not be inherited
1289                        continue;
1290                    }
1291                    if (strncmp($key, 'page-break', strlen('page-break')) == 0) {
1292                        // The property may not be inherited
1293                        continue;
1294                    }
1295                    if (strncmp($key, 'cue', strlen('cue')) == 0) {
1296                        // The property may not be inherited
1297                        continue;
1298                    }
1299                    if (strncmp($key, 'pause', strlen('pause')) == 0) {
1300                        // The property may not be inherited
1301                        continue;
1302                    }
1303                    if (strpos($key, 'width') !== false) {
1304                        // The property may not be inherited
1305                        continue;
1306                    }
1307                    if (strpos($key, 'height') !== false) {
1308                        // The property may not be inherited
1309                        continue;
1310                    }
1311                    switch ($key) {
1312                        case 'text-decoration':
1313                        case 'text-shadow':
1314                        case 'display':
1315                        case 'table-layout':
1316                        case 'vertical-align':
1317                        case 'visibility':
1318                        case 'position':
1319                        case 'top':
1320                        case 'right':
1321                        case 'bottom':
1322                        case 'left':
1323                        case 'float':
1324                        case 'clear':
1325                        case 'z-index':
1326                        case 'unicode-bidi':
1327                        case 'overflow':
1328                        case 'clip':
1329                        case 'visibility':
1330                        case 'content':
1331                        case 'marker-offset':
1332                        case 'play-during':
1333                            // The property may not be inherited
1334                        break;
1335                        default:
1336                            if ($dest [$key] == NULL || $dest [$key] == 'inherit') {
1337                                $dest [$key] = $value;
1338                            }
1339                        break;
1340                    }
1341                }
1342            }
1343        }
1344    }
1345
1346    /**
1347     * Main function performing calculation and inheritance for element
1348     * $element. Properties are stored in $dest.
1349     *
1350     * @param    array $dest    Property storage
1351     * @param    array $element Element to match
1352     * @param    ODTUnits             $units   ODTUnits object for conversion
1353     */
1354    protected function calculateAndInherit (array &$dest, iElementCSSMatchable $element, ODTUnits $units) {
1355        $parents = array();
1356        $parent = $element->iECSSM_getParent();
1357        while ($parent != NULL) {
1358            $parents [] = $parent;
1359            $parent = $parent->iECSSM_getParent();
1360        }
1361
1362        // Determine properties of all parents if not done yet
1363        // and calculate absolute values
1364        // (MUST be done backwards!)
1365        $max = count ($parents);
1366        for ($index = $max-1 ; $index >= 0 ; $index--) {
1367            $properties = $parents [$index]->getProperties ();
1368            if ($properties == NULL) {
1369                $properties = array();
1370                $this->getPropertiesForElement ($properties, $parents [$index], $units, false);
1371                $parents [$index]->setProperties ($properties);
1372            }
1373            if ($properties ['calculated'] == NULL) {
1374                $this->calculate($properties, $parents [$index], $units);
1375            }
1376        }
1377
1378        // Calculate our own absolute values
1379        $this->calculate($dest, $element, $units);
1380
1381        // Inherit values from our parents
1382        $this->inherit($dest, $parents);
1383    }
1384
1385    /**
1386     * Return a string representation of all imported rules.
1387     * (String can be large)
1388     *
1389     * @return string
1390     */
1391    public function rulesToString () {
1392        $returnString = '';
1393        foreach ($this->rules as $rule) {
1394            $returnString .= $rule->toString ();
1395        }
1396        return $returnString;
1397    }
1398
1399    /**
1400     * The function strips the 'url(...)' part from an URL reference
1401     * and puts a $replacement path in front of the rest.
1402     *
1403     * @param    string $URL         Original URL reference
1404     * @param    string $replacement Replacement path to set
1405     * @return string
1406     */
1407    public static function replaceURLPrefix ($URL, $replacement) {
1408        if ( !empty ($URL) && !empty ($replacement) ) {
1409            // Replace 'url(...)' with $replacement
1410            $URL = substr ($URL, 3);
1411            $URL = trim ($URL, '()');
1412            $URL = $replacement.$URL;
1413        }
1414        return $URL;
1415    }
1416
1417    /**
1418     * The function calls $callback for each imported property
1419     * containing a length value. The return value of $callback
1420     * is saved as the new property value.
1421     *
1422     * @param    callable $callback
1423     */
1424    public function adjustLengthValues ($callback) {
1425        foreach ($this->rules as $rule) {
1426            $rule->adjustLengthValues ($callback);
1427        }
1428    }
1429
1430    /**
1431     * The function calls $callback for each property imported
1432     * containing a URL reference. The return value of $callback
1433     * is saved as the new property value.
1434     *
1435     * @param    callable $callback
1436     */
1437    public function replaceURLPrefixes ($callback) {
1438        foreach ($this->rules as $rule) {
1439            $rule->replaceURLPrefixes ($callback);
1440        }
1441    }
1442}
1443