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 (!isset($this->operator)) {
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            !empty($element_attrs ['id']) &&
333            $this->id != $element_attrs ['id']) {
334            return false;
335        }
336
337        // Match attributes
338        foreach ($this->attributes as $attr_sel) {
339            if ($attr_sel->matches ($element_attrs) === false) {
340                return false;
341            }
342        }
343
344        // Match pseudo class(es)
345        if (count($this->pseudo_classes) > 0) {
346            foreach ($this->pseudo_classes as $search) {
347                if ($element->iECSSM_has_pseudo_class($search) == false) {
348                    return false;
349                }
350            }
351        }
352
353        // Match pseudo element
354        if (!empty($this->pseudo_element)) {
355            if ($element->iECSSM_has_pseudo_element($this->pseudo_element) == false) {
356                return false;
357            }
358        }
359
360        return true;
361    }
362
363    /**
364     * The function returns a string representation of this simple
365     * selector (only for debugging purpose).
366     *
367     * @return   string
368     */
369    public function toString () {
370        $returnstring = '';
371        if (!empty($this->type)) {
372            $returnstring .= $this->type;
373        }
374        if (!empty($this->id)) {
375            $returnstring .= '#'.$this->id;
376        }
377        foreach ($this->classes as $class) {
378            $returnstring .= '.'.$class;
379        }
380        foreach ($this->attributes as $attr_sel) {
381            $returnstring .= $attr_sel->toString();
382        }
383        return $returnstring;
384    }
385
386    /**
387     * Return the specificity of this simple selector.
388     *
389     * @return   integer
390     */
391    public function getSpecificity () {
392        return $this->specificity;
393    }
394}
395
396/**
397 * Class css_selector.
398 * Storage class to save a complete CSS selector.
399 * The class can also store multiple selectors, e.g. like 'h1 , h2, h3 {...}'
400 *
401 * @package CSS\CSSSelector
402 */
403class css_selector {
404    /** var Known combinators */
405    static protected $combinators = ' ,>+~';
406    /** var Brackets */
407    static protected $brackets = '[]';
408    /** var String from which this selector was created */
409    protected $selector_string = NULL;
410    /** var Array with parsed selector(s) */
411    protected $selectors_parsed = array();
412    /** var Specificity of this selector */
413    protected $specificity = array();
414
415    /**
416     * Construct the selector from $selector_string.
417     *
418     * @param    string $selector_string String containing the selector
419     */
420    public function __construct($selector_string) {
421        $selector_string = str_replace("\n", '', $selector_string);
422        $this->selector_string = trim($selector_string);
423
424        $pos = 0;
425        $max = strlen($this->selector_string);
426        $current = '';
427        $selector = array();
428        $specificity = 0;
429        $size = 0;
430        $in_brackets = false;
431        $separators = self::$combinators.self::$brackets;
432        while ($pos < $max) {
433            $sign = $this->selector_string [$pos];
434            $result = strpos ($separators, $sign);
435            if ($sign == '[') {
436                $in_brackets = true;
437            }
438            if ($result === false || $in_brackets == true) {
439                // No combinator
440                $current .= $sign;
441                $pos++;
442
443                if ($sign == ']') {
444                    $in_brackets = false;
445                }
446            } else {
447                // Parse current
448                $selector [$size]['selector'] = new css_simple_selector($current);
449                $specificity += $selector [$size]['selector']->getSpecificity();
450                $size++;
451                $current = '';
452
453                $combinator = $sign;
454                $pos++;
455                while ($pos < $max) {
456                    $sign = $this->selector_string[$pos];
457                    if (strpos (self::$combinators, $sign) === false) {
458                        break;
459                    }
460                    $combinator .= $sign;
461                    $pos++;
462                }
463                if (ctype_space($combinator)) {
464                    $selector [$size]['combinator'] = ' ';
465                    $size++;
466                } else {
467                    $combinator = trim ($combinator, ' ');
468                    if ($combinator != ',') {
469                        $selector [$size]['combinator'] = $combinator[0];
470                        $size++;
471                    } else {
472                        $this->selectors_parsed [] = $selector;
473                        $this->specificity [] = $specificity;
474                        $selector = array();
475                        $size = 0;
476                        $specificity = 0;
477                    }
478                }
479            }
480        }
481        if (!empty($current)) {
482            $selector [$size]['selector'] = new css_simple_selector($current);
483            $specificity += $selector [$size]['selector']->getSpecificity();
484            $this->selectors_parsed [] = $selector;
485            $this->specificity [] = $specificity;
486        }
487    }
488
489    /**
490     * The function checks if the combined simple selectors in $selector
491     * match $element or not. $element must support the interface iElementCSSMatchable
492     * to enable this class to do the CSS selector matching.
493     *
494     * @param    array                $selector Internal selector array
495     * @param    iElementCSSMatchable $element  Element to check
496     * @return   boolean
497     */
498    protected function selector_matches (array $selector, iElementCSSMatchable $element) {
499        $combinator = '';
500        $found = 0;
501        $size = count($selector);
502        if ($size == 0 ) {
503            return false;
504        }
505
506        // First entry should be a selector
507        if (!isset($selector [$size-1]['selector'])) {
508            // No! (Error)
509            return false;
510        }
511
512        // Start comparison with the current element
513        $simple = $selector [$size-1]['selector'];
514        if ($simple->matches_entry ($element) == false) {
515            // If the current open element does not match then there is no match
516            return false;
517        }
518        if ($size == 1) {
519            // We are finished already
520            return true;
521        }
522
523        // Next entry should be a combinator
524        if (!isset($selector [$size-2]['combinator'])) {
525            // No! (Error)
526            return false;
527        }
528        $combinator = $selector [$size-2]['combinator'];
529
530        $start_search = $element;
531        for ($index = $size-3 ; $index >= 0 ; $index--) {
532            // If we get here but start_search is already negative then there are
533            // selectors left but no more subjects/element to match.
534            if ($start_search < 0) {
535                return false;
536            }
537            if (empty($selector [$index]['combinator'])) {
538                $simple = $selector [$index]['selector'];
539                switch ($combinator) {
540                    case ' ':
541                        // Find any parent, parent's parent... that matches our simple selector
542                        do {
543                            $parent = $start_search->iECSSM_getParent();
544                            if (!isset($parent)) {
545                                return false;
546                            }
547                            $start_search = $parent;
548                            $is_match = $simple->matches_entry ($parent);
549                            if ($is_match == true) {
550                                // Found match. Stop this search.
551                                break;
552                            }
553                        }while (isset($parent));
554
555                        // Did we find anything?
556                        if (!$is_match) {
557                            // No.
558                            return false;
559                        }
560                        $start_search = $parent;
561                        break;
562
563                    case '>':
564                        // Check if we have a parent and if it matches our simple selector
565                        $parent = $start_search->iECSSM_getParent();
566                        if (!isset($parent)) {
567                            return false;
568                        }
569                        if ($simple->matches_entry ($parent) == false) {
570                            // No match.
571                            return false;
572                        }
573                        $start_search = $parent;
574                        break;
575
576                    case '+':
577                        // Immediate preceding sibling must match our simple selector
578                        $sibling = $start_search->iECSSM_getPrecedingSibling();
579                        if (!isset($sibling)) {
580                            return false;
581                        }
582                        if ($simple->matches_entry ($sibling) == false) {
583                            // No match.
584                            return false;
585                        }
586                        $start_search = $sibling;
587                        break;
588
589                    case '~':
590                        // One of the preceding siblings must match our simple selector
591                        do {
592                            $sibling = $start_search->iECSSM_getPrecedingSibling();
593                            if (!isset($sibling)) {
594                                return false;
595                            }
596                            $start_search = $sibling;
597                            if ($simple->matches_entry ($sibling) == true) {
598                                // Found match. Stop this search.
599                                break;
600                            }
601                        }while (isset($sibling));
602
603                        // Did we find anything?
604                        if (!isset($sibling)) {
605                            // No.
606                            return false;
607                        }
608                        $start_search = $sibling;
609                        break;
610
611                    // We won't get the combinator ',' here cause that is
612                    // handled at construction time by creating an array of selectors
613                    //case ',':
614                    //    break;
615                }
616            } else {
617                $combinator = $selector [$index]['combinator'];
618            }
619        }
620
621        // If we get here then everything matches!
622        return true;
623    }
624
625    /**
626     * The functions checks wheter any selector stored in this object
627     * match the given $element or not. $element must support the interface
628     * iElementCSSMatchable to enable this class to do the CSS selector matching.
629     *
630     * @param    iElementCSSMatchable $element     Element to check
631     * @param    integer              $specificity Specificity of matching selector
632     * @return   boolean
633     */
634    public function matches (iElementCSSMatchable $element, &$specificity) {
635        $size = count ($this->selectors_parsed);
636        $match = false;
637        $specificity = 0;
638        for ($index = 0 ; $index < $size ; $index++) {
639            if ($this->selector_matches ($this->selectors_parsed [$index], $element) == true) {
640                if ($this->specificity [$index] > $specificity) {
641                    $specificity = $this->specificity [$index];
642                }
643                $match = true;
644            }
645        }
646        return $match;
647    }
648
649    /**
650     * The function returns a string representation of this
651     * selector (only for debugging purpose).
652     *
653     * @return   string
654     */
655    public function toString () {
656        $returnstring = '';
657        $max = count($this->selectors_parsed);
658        $index_parsed = 0;
659        foreach ($this->selectors_parsed as $selector) {
660            $size = count($selector);
661            for ($index = 0 ; $index < $size ; $index++) {
662                if ( isset($selector [$index]['combinator']) ) {
663                    if ($selector [$index]['combinator'] == ' ') {
664                        $returnstring .= ' ';
665                    } else {
666                        $returnstring .= ' '.$selector [$index]['combinator'].' ';
667                    }
668                } else {
669                    $simple = $selector [$index]['selector'];
670                    $returnstring .= $simple->toString();
671                    if ($index < $size-1) {
672                        $returnstring .= ' ';
673                    }
674                }
675            }
676            $index_parsed++;
677            if ($index_parsed < $max) {
678                $returnstring .= ',';
679            }
680        }
681        return $returnstring;
682    }
683}
684
685/**
686 * Class css_rule_new.
687 *
688 * @package CSS\CSSRuleNew
689 */
690class css_rule_new {
691    /** @var Media selector to which this rule belongs */
692    protected $media = NULL;
693    /** @var Selector string from which this rule was created */
694    protected $selector = NULL;
695    /** @var Array of css_declaration objects */
696    protected $declarations = array ();
697
698    /**
699     * Construct rule from strings $selector and $decls.
700     *
701     * @param    string      $selector String containing the selector
702     * @param    string      $decls    String containing the declarations
703     * @param    string|null $media    String containing the media selector
704     */
705    public function __construct($selector, $decls, $media = NULL) {
706
707        $this->media = trim ($media);
708        //print ("\nNew rule: ".$media."\n"); //Debuging
709
710        // Create/parse selector
711        $this->selector = new css_selector ($selector);
712
713        $decls = trim ($decls, '{}');
714
715        // Parse declarations
716        $pos = 0;
717        $end = strlen ($decls);
718        while ( $pos < $end ) {
719            $colon = strpos ($decls, ':', $pos);
720            if ( $colon === false ) {
721                break;
722            }
723            $semi = strpos ($decls, ';', $colon + 1);
724            if ( $semi === false ) {
725                break;
726            }
727
728            $property = substr ($decls, $pos, $colon - $pos);
729            $property = trim($property);
730
731            $value = substr ($decls, $colon + 1, $semi - ($colon + 1));
732            $value = trim ($value);
733            $values = preg_split ('/\s+/', $value);
734            $value = '';
735            foreach ($values as $part) {
736                if ( $part != '!important' ) {
737                    $value .= ' '.$part;
738                }
739            }
740            $value = trim($value);
741
742            // Create new declaration
743            $declaration = new css_declaration ($property, $value);
744            $this->declarations [] = $declaration;
745
746            // Handle CSS shorthands, e.g. 'border'
747            if ( $declaration->isShorthand () === true ) {
748                $declaration->explode ($this->declarations);
749            }
750
751            $pos = $semi + 1;
752        }
753    }
754
755    /**
756     * The function returns a string representation of this
757     * rule (only for debugging purpose).
758     *
759     * @return   string
760     */
761    public function toString () {
762        $returnString = '';
763        $returnString .= "Media= \"".$this->media."\"\n";
764        $returnString .= $this->selector->toString().' ';
765        $returnString .= "{\n";
766        foreach ($this->declarations as $declaration) {
767            $returnString .= $declaration->getProperty ().':'.$declaration->getValue ().";\n";
768        }
769        $returnString .= "}\n";
770        return $returnString;
771    }
772
773    /**
774     * The functions checks wheter this rule matches the given $element
775     * or not. $element must support the interface iElementCSSMatchable
776     * to enable this class to do the CSS selector matching.
777     *
778     * @param    iElementCSSMatchable $element     Element to check
779     * @param    integer              $specificity Specificity of matching selector
780     * @param    string               $media       Media selector to match
781     * @return   boolean
782     */
783    public function matches(iElementCSSMatchable $element, &$specificity, $media = NULL) {
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 ( isset($processed) ) {
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 (!isset($element)) {
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 (isset($highest [$property]) && $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 (isset($properties [$key])) {
1144            return $properties [$key];
1145        }
1146
1147        $parentsParent = $parent->iECSSM_getParent();
1148        if (isset($parentsParent)) {
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 (isset($properties ['calculated']) && $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 (isset($parent)) {
1175            $font_size = $this->getParentsValue('font-size', $parent);
1176        }
1177        if (isset($font_size)) {
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 (isset($properties ['font-size']) || isset($properties ['line-height'])) {
1191            if (isset($properties ['font-size'])) {
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 (isset($properties ['line-height'])) {
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 (!isset($dest [$key]) || $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 (isset($parent)) {
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 (!isset($properties)) {
1369                $properties = array();
1370                $this->getPropertiesForElement ($properties, $parents [$index], $units, false);
1371                $parents [$index]->setProperties ($properties);
1372            }
1373            if (!isset($properties ['calculated'])) {
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