1<?php
2/**
3 * Helper class to read in a CSS style
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     LarsDW223
7 */
8
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) die();
11
12require_once DOKU_PLUGIN . 'odt/helper/csscolors.php';
13
14/**
15 * Abstract class to define kind of enum for the CSS value types.
16 * Actually only used by adjustLengthValues().
17 */
18abstract class CSSValueType
19{
20    const Other               = 0;
21    const LengthValueXAxis    = 1;
22    const LengthValueYAxis    = 2;
23    const StrokeOrBorderWidth = 3;
24    // etc.
25}
26
27/**
28 * Class css_declaration
29 *
30 * @package CSS\css_declaration
31 */
32class css_declaration {
33    protected static $css_units = array ('em', 'ex', '%', 'px', 'cm', 'mm', 'in', 'pt',
34                                         'pc', 'ch', 'rem', 'vh', 'vw', 'vmin', 'vmax');
35    protected $property;
36    protected $value;
37
38    /**
39     * Create a new declaration (property:value).
40     *
41     * @param string $property The property name of the declaration
42     * @param string $value    The assigned value
43     */
44    public function __construct($property, $value) {
45        $this->property = $property;
46        $this->value = trim($value, ';');
47    }
48
49    /**
50     * Get the property name of this declaration.
51     *
52     * @return string
53     */
54    public function getProperty () {
55        return $this->property;
56    }
57
58    /**
59     * Get the value assigned to the property of this declaration.
60     *
61     * @return string
62     */
63    public function getValue () {
64        return $this->value;
65    }
66
67    /**
68     * @param css_declaration[] $decls
69     */
70    public function explode (&$decls) {
71        if ( empty ($this->property) ) {
72            return;
73        }
74
75        switch ($this->property) {
76            case 'background':
77                $this->explodeBackgroundShorthand ($decls);
78            break;
79            case 'font':
80                $this->explodeFontShorthand ($decls);
81            break;
82            case 'padding':
83                $this->explodePaddingShorthand ($decls);
84            break;
85            case 'margin':
86                $this->explodeMarginShorthand ($decls);
87            break;
88            case 'border':
89                $this->explodeBorderShorthand ($decls);
90            break;
91            case 'list-style':
92                $this->explodeListStyleShorthand ($decls);
93            break;
94            case 'flex':
95                $this->explodeFlexShorthand ($decls);
96            break;
97            case 'transition':
98                $this->explodeTransitionShorthand ($decls);
99            break;
100            case 'outline':
101                $this->explodeOutlineShorthand ($decls);
102            break;
103            case 'animation':
104                $this->explodeAnimationShorthand ($decls);
105            break;
106            case 'border-bottom':
107                $this->explodeBorderBottomShorthand ($decls);
108            break;
109            case 'columns':
110                $this->explodeColumnsShorthand ($decls);
111            break;
112            case 'column-rule':
113                $this->explodeColumnRuleShorthand ($decls);
114            break;
115
116            //FIXME: Implement all the shorthands missing
117            //case ...
118        }
119    }
120
121    /**
122     * @return bool
123     */
124    public function isShorthand () {
125        switch ($this->property) {
126            case 'background':
127            case 'font':
128            case 'padding':
129            case 'margin':
130            case 'border':
131            case 'list-style':
132            case 'flex':
133            case 'transition':
134            case 'outline':
135            case 'animation':
136            case 'border-bottom':
137            case 'columns':
138            case 'column-rule':
139                return true;
140            break;
141
142            //FIXME: Implement all the shorthands missing
143            //case ...
144        }
145        return false;
146    }
147
148    /**
149     * @param css_declaration[] $decls
150     */
151    protected function explodeBackgroundShorthand (&$decls) {
152        if ( $this->property == 'background' ) {
153            $values = preg_split ('/\s+/', $this->value);
154            $index = 0;
155            if ($index < count($values)) {
156                $color_done = true;
157                $value = $values [$index];
158                if ($value [0] == '#' || csscolors::isKnownColorName($value)) {
159                    $decls [] = new css_declaration ('background-color', $value);
160                    $index++;
161                } else {
162                    switch ($value) {
163                        case 'transparent':
164                        case 'inherit':
165                        case 'initial':
166                            $decls [] = new css_declaration ('background-color', $value);
167                            $index++;
168                        break;
169                    }
170                }
171            }
172            if ($index < count($values)) {
173                $decls [] = new css_declaration ('background-image', $values [$index]);
174                $index++;
175            }
176            if ($index < count($values)) {
177                $decls [] = new css_declaration ('background-repeat', $values [$index]);
178                $index++;
179            }
180            if ($index < count($values)) {
181                $decls [] = new css_declaration ('background-attachment', $values [$index]);
182                $index++;
183            }
184            if ($index < count($values)) {
185                $decls [] = new css_declaration ('background-position', $values [$index]);
186                $index++;
187            }
188        }
189    }
190
191    /**
192     * @param css_declaration[] $decls
193     */
194    protected function explodeFontShorthand (&$decls, $setDefaults=false) {
195        if ( $this->property == 'font' ) {
196            $values = preg_split ('/\s+/', $this->value);
197
198            $font_style_set = false;
199            $font_variant_set = false;
200            $font_weight_set = false;
201            $font_size_set = false;
202            $font_family = '';
203
204            foreach ($values as $value) {
205                if ( $font_style_set === false ) {
206                    $default = false;
207                    switch ($value) {
208                        case 'normal':
209                        case 'italic':
210                        case 'oblique':
211                        case 'initial':
212                        case 'inherit':
213                            $decls [] = new css_declaration ('font-style', $value);
214                        break;
215                        default:
216                            $default = true;
217                            if ($setDefaults) {
218                                $decls [] = new css_declaration ('font-style', 'normal');
219                            }
220                        break;
221                    }
222                    $font_style_set = true;
223                    if ( $default === false ) {
224                        continue;
225                    }
226                }
227                if ( $font_variant_set === false ) {
228                    $default = false;
229                    switch ($value) {
230                        case 'normal':
231                        case 'small-caps':
232                        case 'initial':
233                        case 'inherit':
234                            $decls [] = new css_declaration ('font-variant', $value);
235                        break;
236                        default:
237                            $default = true;
238                            if ($setDefaults) {
239                                $decls [] = new css_declaration ('font-variant', 'normal');
240                            }
241                        break;
242                    }
243                    $font_variant_set = true;
244                    if ( $default === false ) {
245                        continue;
246                    }
247                }
248                if ( $font_weight_set === false ) {
249                    $default = false;
250                    switch ($value) {
251                        case 'normal':
252                        case 'bold':
253                        case 'bolder':
254                        case 'lighter':
255                        case '100':
256                        case '200':
257                        case '300':
258                        case '400':
259                        case '500':
260                        case '600':
261                        case '700':
262                        case '800':
263                        case '900':
264                        case 'initial':
265                        case 'inherit':
266                            $decls [] = new css_declaration ('font-weight', $value);
267                        break;
268                        default:
269                            $default = true;
270                            if ($setDefaults) {
271                                $decls [] = new css_declaration ('font-weight', 'normal');
272                            }
273                        break;
274                    }
275                    $font_weight_set = true;
276                    if ( $default === false ) {
277                        continue;
278                    }
279                }
280                if ( $font_size_set === false ) {
281                    $default = false;
282                    $params = explode ('/', $value);
283                    switch ($params [0]) {
284                        case 'medium':
285                        case 'xx-small':
286                        case 'x-small':
287                        case 'small':
288                        case 'large':
289                        case 'x-large':
290                        case 'xx-large':
291                        case 'smaller':
292                        case 'larger':
293                        case 'initial':
294                        case 'inherit':
295                            $decls [] = new css_declaration ('font-size', $params [0]);
296                        break;
297                        default:
298                            $found = false;
299                            foreach (self::$css_units as $css_unit) {
300                                if ( strpos ($value, $css_unit) !== false ) {
301                                    $decls [] = new css_declaration ('font-size', $params [0]);
302                                    $found = true;
303                                    break;
304                                }
305                            }
306                            if ( $found === false ) {
307                                $default = true;
308                                if ($setDefaults) {
309                                    $decls [] = new css_declaration ('font-size', 'medium');
310                                }
311                            }
312                        break;
313                    }
314                    if ( !empty($params [1]) ) {
315                        $decls [] = new css_declaration ('line-height', $params [1]);
316                    } else {
317                        if ($setDefaults) {
318                            $decls [] = new css_declaration ('line-height', 'normal');
319                        }
320                    }
321                    $font_size_set = true;
322                    if ( $default === false ) {
323                        continue;
324                    }
325                }
326
327                // All other properties are found.
328                // The rest is assumed to be a font-family.
329                if (empty ($font_family)) {
330                    $font_family = $value;
331                } else {
332                    $font_family .= ' '.$value;
333                }
334            }
335            if (!empty ($font_family)) {
336                $decls [] = new css_declaration ('font-family', $font_family);
337            }
338        }
339    }
340
341    /**
342     * @param css_declaration[] $decls
343     */
344    protected function explodePaddingShorthand (&$decls) {
345        if ( $this->property == 'padding' ) {
346            $values = preg_split ('/\s+/', $this->value);
347            switch (count($values)) {
348                case 4:
349                    $decls [] = new css_declaration ('padding-top', $values [0]);
350                    $decls [] = new css_declaration ('padding-right', $values [1]);
351                    $decls [] = new css_declaration ('padding-bottom', $values [2]);
352                    $decls [] = new css_declaration ('padding-left', $values [3]);
353                break;
354                case 3:
355                    $decls [] = new css_declaration ('padding-top', $values [0]);
356                    $decls [] = new css_declaration ('padding-right', $values [1]);
357                    $decls [] = new css_declaration ('padding-left', $values [1]);
358                    $decls [] = new css_declaration ('padding-bottom', $values [2]);
359                break;
360                case 2:
361                    $decls [] = new css_declaration ('padding-top', $values [0]);
362                    $decls [] = new css_declaration ('padding-bottom', $values [0]);
363                    $decls [] = new css_declaration ('padding-right', $values [1]);
364                    $decls [] = new css_declaration ('padding-left', $values [1]);
365                break;
366                case 1:
367                    $decls [] = new css_declaration ('padding-top', $values [0]);
368                    $decls [] = new css_declaration ('padding-bottom', $values [0]);
369                    $decls [] = new css_declaration ('padding-right', $values [0]);
370                    $decls [] = new css_declaration ('padding-left', $values [0]);
371                break;
372            }
373        }
374    }
375
376    /**
377     * @param css_declaration[] $decls
378     */
379    protected function explodeMarginShorthand (&$decls) {
380        if ( $this->property == 'margin' ) {
381            $values = preg_split ('/\s+/', $this->value);
382            switch (count($values)) {
383                case 4:
384                    $decls [] = new css_declaration ('margin-top', $values [0]);
385                    $decls [] = new css_declaration ('margin-right', $values [1]);
386                    $decls [] = new css_declaration ('margin-bottom', $values [2]);
387                    $decls [] = new css_declaration ('margin-left', $values [3]);
388                break;
389                case 3:
390                    $decls [] = new css_declaration ('margin-top', $values [0]);
391                    $decls [] = new css_declaration ('margin-right', $values [1]);
392                    $decls [] = new css_declaration ('margin-left', $values [1]);
393                    $decls [] = new css_declaration ('margin-bottom', $values [2]);
394                break;
395                case 2:
396                    $decls [] = new css_declaration ('margin-top', $values [0]);
397                    $decls [] = new css_declaration ('margin-bottom', $values [0]);
398                    $decls [] = new css_declaration ('margin-right', $values [1]);
399                    $decls [] = new css_declaration ('margin-left', $values [1]);
400                break;
401                case 1:
402                    $decls [] = new css_declaration ('margin-top', $values [0]);
403                    $decls [] = new css_declaration ('margin-bottom', $values [0]);
404                    $decls [] = new css_declaration ('margin-right', $values [0]);
405                    $decls [] = new css_declaration ('margin-left', $values [0]);
406                break;
407            }
408        }
409    }
410
411    /**
412     * @param css_declaration[] $decls
413     */
414    protected function explodeBorderShorthand (&$decls) {
415        $border_sides = array ('border-left', 'border-right', 'border-top', 'border-bottom');
416        if ( $this->property == 'border' ) {
417            $values = preg_split ('/\s+/', $this->value);
418            $index = 0;
419            $border_width_set = false;
420            $border_style_set = false;
421            $border_color_set = false;
422            while ( $index < 3 ) {
423                if ( $border_width_set === false ) {
424                    switch ($values [$index]) {
425                        case 'thin':
426                        case 'medium':
427                        case 'thick':
428                            $decls [] = new css_declaration ('border-width', $values [$index]);
429                            foreach ($border_sides as $border_side) {
430                                $decls [] = new css_declaration ($border_side.'-width', $values [$index]);
431                            }
432                        break;
433                        default:
434                            if ( strpos ($values [$index], 'px') !== false ) {
435                                $decls [] = new css_declaration ('border-width', $values [$index]);
436                                foreach ($border_sides as $border_side) {
437                                    $decls [] = new css_declaration ($border_side.'-width', $values [$index]);
438                                }
439                            } else {
440                                // There is no default value? So leave it unset.
441                            }
442                        break;
443                    }
444                    $border_width_set = true;
445                    $index++;
446                    continue;
447                }
448                if ( $border_style_set === false ) {
449                    switch ($values [$index]) {
450                        case 'none':
451                        case 'dotted':
452                        case 'dashed':
453                        case 'solid':
454                        case 'double':
455                        case 'groove':
456                        case 'ridge':
457                        case 'inset':
458                        case 'outset':
459                            $decls [] = new css_declaration ('border-style', $values [$index]);
460                            foreach ($border_sides as $border_side) {
461                                $decls [] = new css_declaration ($border_side.'-style', $values [$index]);
462                            }
463                        break;
464                        default:
465                            $decls [] = new css_declaration ('border-style', 'none');
466                            foreach ($border_sides as $border_side) {
467                                $decls [] = new css_declaration ($border_side.'-style', 'none');
468                            }
469                        break;
470                    }
471                    $border_style_set = true;
472                    $index++;
473                    continue;
474                }
475                if ( $border_color_set === false ) {
476                    $decls [] = new css_declaration ('border-color', $values [$index]);
477                    foreach ($border_sides as $border_side) {
478                        $decls [] = new css_declaration ($border_side.'-color', $values [$index]);
479                    }
480
481                    // This is the last value.
482                    break;
483                }
484            }
485            foreach ($border_sides as $border_side) {
486                $decls [] = new css_declaration ($border_side, $values [0].' '.$values [1].' '.$values [2]);
487            }
488        }
489    }
490
491    /**
492     * @param css_declaration[] $decls
493     */
494    protected function explodeListStyleShorthand (&$decls) {
495        if ( $this->property == 'list-style' ) {
496            $values = preg_split ('/\s+/', $this->value);
497
498            $list_style_type_set = false;
499            $list_style_position_set = false;
500            $list_style_image_set = false;
501            foreach ($values as $value) {
502                if ( $list_style_type_set === false ) {
503                    $default = false;
504                    switch ($value) {
505                        case 'disc':
506                        case 'armenian':
507                        case 'circle':
508                        case 'cjk-ideographic':
509                        case 'decimal':
510                        case 'decimal-leading-zero':
511                        case 'georgian':
512                        case 'hebrew':
513                        case 'hiragana':
514                        case 'hiragana-iroha':
515                        case 'katakana':
516                        case 'katakana-iroha':
517                        case 'lower-alpha':
518                        case 'lower-greek':
519                        case 'lower-latin':
520                        case 'lower-roman':
521                        case 'none':
522                        case 'square':
523                        case 'upper-alpha':
524                        case 'upper-latin':
525                        case 'upper-roman':
526                        case 'initial':
527                        case 'inherit':
528                            $decls [] = new css_declaration ('list-style-type', $value);
529                        break;
530                        default:
531                            $default = true;
532                            $decls [] = new css_declaration ('list-style-type', 'disc');
533                        break;
534                    }
535                    $list_style_type_set = true;
536                    if ( $default === false ) {
537                        continue;
538                    }
539                }
540                if ( $list_style_position_set === false ) {
541                    $default = false;
542                    switch ($value) {
543                        case 'inside':
544                        case 'outside':
545                        case 'initial':
546                        case 'inherit':
547                            $decls [] = new css_declaration ('list-style-position', $value);
548                        break;
549                        default:
550                            $default = true;
551                            $decls [] = new css_declaration ('list-style-position', 'outside');
552                        break;
553                    }
554                    $list_style_position_set = true;
555                    if ( $default === false ) {
556                        continue;
557                    }
558                }
559                if ( $list_style_image_set === false ) {
560                    $decls [] = new css_declaration ('list-style-image', $value);
561                    $list_style_image_set = true;
562                }
563            }
564            if ( $list_style_image_set === false ) {
565                $decls [] = new css_declaration ('list-style-image', 'none');
566            }
567        }
568    }
569
570    /**
571     * @param css_declaration[] $decls
572     */
573    protected function explodeFlexShorthand (&$decls) {
574        if ( $this->property == 'flex' ) {
575            $values = preg_split ('/\s+/', $this->value);
576            if ( count($values) > 0 ) {
577                $decls [] = new css_declaration ('flex-grow', $values [0]);
578            }
579            if ( count($values) > 1 ) {
580                $decls [] = new css_declaration ('flex-shrink', $values [1]);
581            }
582            if ( count($values) > 2 ) {
583                $decls [] = new css_declaration ('flex-basis', $values [2]);
584            }
585        }
586    }
587
588    /**
589     * @param css_declaration[] $decls
590     */
591    protected function explodeTransitionShorthand (&$decls) {
592        if ( $this->property == 'transition' ) {
593            $values = preg_split ('/\s+/', $this->value);
594            if ( count($values) > 0 ) {
595                $decls [] = new css_declaration ('transition-property', $values [0]);
596            }
597            if ( count($values) > 1 ) {
598                $decls [] = new css_declaration ('transition-duration', $values [1]);
599            }
600            if ( count($values) > 2 ) {
601                $decls [] = new css_declaration ('transition-timing-function', $values [2]);
602            }
603            if ( count($values) > 3 ) {
604                $decls [] = new css_declaration ('transition-delay', $values [3]);
605            }
606        }
607    }
608
609    /**
610     * @param css_declaration[] $decls
611     */
612    protected function explodeOutlineShorthand (&$decls) {
613        if ( $this->property == 'outline' ) {
614            $values = preg_split ('/\s+/', $this->value);
615
616            $outline_color_set = false;
617            $outline_style_set = false;
618            $outline_width_set = false;
619            foreach ($values as $value) {
620                if ( $outline_color_set === false ) {
621                    $decls [] = new css_declaration ('outline-color', $value);
622                    $outline_color_set = true;
623                    continue;
624                }
625                if ( $outline_style_set === false ) {
626                    $default = false;
627                    switch ($value) {
628                        case 'none':
629                        case 'hidden':
630                        case 'dotted':
631                        case 'dashed':
632                        case 'solid':
633                        case 'double':
634                        case 'groove':
635                        case 'ridge':
636                        case 'inset':
637                        case 'outset':
638                        case 'initial':
639                        case 'inherit':
640                            $decls [] = new css_declaration ('outline-style', $value);
641                        break;
642                        default:
643                            $default = true;
644                            $decls [] = new css_declaration ('outline-style', 'none');
645                        break;
646                    }
647                    $outline_style_set = true;
648                    if ( $default === false ) {
649                        continue;
650                    }
651                }
652                if ( $outline_width_set === false ) {
653                    $default = false;
654                    switch ($value) {
655                        case 'medium':
656                        case 'thin':
657                        case 'thick':
658                        case 'initial':
659                        case 'inherit':
660                            $decls [] = new css_declaration ('outline-width', $value);
661                        break;
662                        default:
663                            $found = false;
664                            foreach (self::$css_units as $css_unit) {
665                                if ( strpos ($value, $css_unit) !== false ) {
666                                    $decls [] = new css_declaration ('outline-width', $value);
667                                    $found = true;
668                                    break;
669                                }
670                            }
671                            if ( $found === false ) {
672                                $default = true;
673                                $decls [] = new css_declaration ('outline-width', 'medium');
674                            }
675                        break;
676                    }
677                    $outline_width_set = true;
678                    if ( $default === false ) {
679                        continue;
680                    }
681                }
682            }
683        }
684    }
685
686    /**
687     * @param css_declaration[] $decls
688     */
689    protected function explodeAnimationShorthand (&$decls) {
690        if ( $this->property == 'animation' ) {
691            $values = preg_split ('/\s+/', $this->value);
692            if ( count($values) > 0 ) {
693                $decls [] = new css_declaration ('animation-name', $values [0]);
694            }
695            if ( count($values) > 1 ) {
696                $decls [] = new css_declaration ('animation-duration', $values [1]);
697            }
698            if ( count($values) > 2 ) {
699                $decls [] = new css_declaration ('animation-timing-function', $values [2]);
700            }
701            if ( count($values) > 3 ) {
702                $decls [] = new css_declaration ('animation-delay', $values [3]);
703            }
704            if ( count($values) > 4 ) {
705                $decls [] = new css_declaration ('animation-iteration-count', $values [4]);
706            }
707            if ( count($values) > 5 ) {
708                $decls [] = new css_declaration ('animation-direction', $values [5]);
709            }
710            if ( count($values) > 6 ) {
711                $decls [] = new css_declaration ('animation-fill-mode', $values [6]);
712            }
713            if ( count($values) > 7 ) {
714                $decls [] = new css_declaration ('animation-play-state', $values [7]);
715            }
716        }
717    }
718
719    /**
720     * @param css_declaration[] $decls
721     */
722    protected function explodeBorderBottomShorthand (&$decls) {
723        if ( $this->property == 'border-bottom' ) {
724            $values = preg_split ('/\s+/', $this->value);
725
726            $border_bottom_width_set = false;
727            $border_bottom_style_set = false;
728            $border_bottom_color_set = false;
729            foreach ($values as $value) {
730                if ( $border_bottom_width_set === false ) {
731                    $default = false;
732                    switch ($value) {
733                        case 'medium':
734                        case 'thin':
735                        case 'thick':
736                        case 'initial':
737                        case 'inherit':
738                            $decls [] = new css_declaration ('border-bottom-width', $value);
739                        break;
740                        default:
741                            $found = false;
742                            foreach (self::$css_units as $css_unit) {
743                                if ( strpos ($value, $css_unit) !== false ) {
744                                    $decls [] = new css_declaration ('border-bottom-width', $value);
745                                    $found = true;
746                                    break;
747                                }
748                            }
749                            if ( $found === false ) {
750                                $default = true;
751                                $decls [] = new css_declaration ('border-bottom-width', 'medium');
752                            }
753                        break;
754                    }
755                    $border_bottom_width_set = true;
756                    if ( $default === false ) {
757                        continue;
758                    }
759                }
760                if ( $border_bottom_style_set === false ) {
761                    $default = false;
762                    switch ($value) {
763                        case 'none':
764                        case 'hidden':
765                        case 'dotted':
766                        case 'dashed':
767                        case 'solid':
768                        case 'double':
769                        case 'groove':
770                        case 'ridge':
771                        case 'inset':
772                        case 'outset':
773                        case 'initial':
774                        case 'inherit':
775                            $decls [] = new css_declaration ('border-bottom-style', $value);
776                        break;
777                        default:
778                            $default = true;
779                            $decls [] = new css_declaration ('border-bottom-style', 'none');
780                        break;
781                    }
782                    $border_bottom_style_set = true;
783                    if ( $default === false ) {
784                        continue;
785                    }
786                }
787                if ( $border_bottom_color_set === false ) {
788                    $decls [] = new css_declaration ('border-bottom-color', $value);
789                    $border_bottom_color_set = true;
790                    continue;
791                }
792            }
793        }
794    }
795
796    /**
797     * @param css_declaration[] $decls
798     */
799    protected function explodeColumnsShorthand (&$decls) {
800        if ( $this->property == 'columns' ) {
801            $values = preg_split ('/\s+/', $this->value);
802            if ( count($values) == 1 && $values [0] == 'auto' ) {
803                $decls [] = new css_declaration ('column-width', 'auto');
804                $decls [] = new css_declaration ('column-count', 'auto');
805                return;
806            }
807            if ( count($values) > 0 ) {
808                $decls [] = new css_declaration ('column-width', $values [0]);
809            }
810            if ( count($values) > 1 ) {
811                $decls [] = new css_declaration ('column-count', $values [1]);
812            }
813        }
814    }
815
816    /**
817     * @param css_declaration[] $decls
818     */
819    protected function explodeColumnRuleShorthand (&$decls) {
820        if ( $this->property == 'column-rule' ) {
821            $values = preg_split ('/\s+/', $this->value);
822            if ( count($values) > 0 ) {
823                $decls [] = new css_declaration ('column-rule-width', $values [0]);
824            }
825            if ( count($values) > 1 ) {
826                $decls [] = new css_declaration ('column-rule-style', $values [1]);
827            }
828            if ( count($values) > 2 ) {
829                $decls [] = new css_declaration ('column-rule-color', $values [2]);
830            }
831        }
832    }
833
834    /**
835     * @param $callback
836     */
837    public function adjustLengthValues ($callback, $rule=NULL) {
838        switch ($this->property) {
839            case 'border-width':
840            case 'outline-width':
841            case 'border-bottom-width':
842            case 'column-rule-width':
843                $this->value =
844                    call_user_func($callback, $this->property, $this->value, CSSValueType::StrokeOrBorderWidth, $rule);
845            break;
846
847            case 'margin-left':
848            case 'margin-right':
849            case 'padding-left':
850            case 'padding-right':
851            case 'width':
852            case 'column-width':
853                $this->value =
854                    call_user_func($callback, $this->property, $this->value, CSSValueType::LengthValueXAxis, $rule);
855            break;
856
857            case 'margin-top':
858            case 'margin-bottom':
859            case 'padding-top':
860            case 'padding-bottom':
861            case 'min-height':
862            case 'height':
863            case 'line-height':
864                $this->value =
865                    call_user_func($callback, $this->property, $this->value, CSSValueType::LengthValueYAxis, $rule);
866            break;
867
868            case 'border':
869            case 'border-left':
870            case 'border-right':
871            case 'border-top':
872            case 'border-bottom':
873                $this->adjustLengthValuesBorder ($callback, $rule);
874            break;
875
876            // FIXME: Shorthands are currently not processed.
877            // Every Shorthand would need an extra function which knows if it has any length values.
878            // Just like the explode...Shorthand functions.
879        }
880    }
881
882    /**
883     * @param $callback
884     */
885    protected function adjustLengthValuesBorder ($callback, $rule=NULL) {
886        switch ($this->property) {
887            case 'border':
888            case 'border-left':
889            case 'border-right':
890            case 'border-top':
891            case 'border-bottom':
892                $values = preg_split ('/\s+/', $this->value);
893                $width =
894                    call_user_func($callback, $this->property, $values [0], CSSValueType::StrokeOrBorderWidth, $rule);
895                $this->value = $width . ' ' . $values [1] . ' ' . $values [2];
896            break;
897        }
898    }
899
900    /**
901     * @param $callback
902     */
903    public function replaceURLPrefixes ($callback) {
904        if (strncmp($this->value, 'url(', 4) == 0) {
905            $url = substr($this->value, 4, -1);
906            $this->value = call_user_func($callback, $this->property, $this->value, $url);
907        }
908    }
909}
910
911/**
912 * Class css_rule
913 *
914 * @package CSS\css_rule
915 */
916class css_rule {
917    protected $media = NULL;
918    protected $selectors = array ();
919    /** @var css_declaration[]  */
920    protected $declarations = array ();
921
922    /**
923     * @param $selector
924     * @param $decls
925     * @param null $media
926     */
927    public function __construct($selector, $decls, $media = NULL) {
928
929        $this->media = trim ($media);
930        //print ("\nNew rule: ".$media."\n"); //Debuging
931
932        $this->selectors = explode (' ', $selector);
933
934        $decls = trim ($decls, '{}');
935
936        // Parse declarations
937        $pos = 0;
938        $end = strlen ($decls);
939        while ( $pos < $end ) {
940            $colon = strpos ($decls, ':', $pos);
941            if ( $colon === false ) {
942                break;
943            }
944            $semi = strpos ($decls, ';', $colon + 1);
945            if ( $semi === false ) {
946                break;
947            }
948
949            $property = substr ($decls, $pos, $colon - $pos);
950            $property = trim($property);
951
952            $value = substr ($decls, $colon + 1, $semi - ($colon + 1));
953            $value = trim ($value);
954            $values = preg_split ('/\s+/', $value);
955            $value = '';
956            foreach ($values as $part) {
957                if ( $part != '!important' ) {
958                    $value .= ' '.$part;
959                }
960            }
961            $value = trim($value);
962
963            // Create new declaration
964            $declaration = new css_declaration ($property, $value);
965            $this->declarations [] = $declaration;
966
967            // Handle CSS shorthands, e.g. 'border'
968            if ( $declaration->isShorthand () === true ) {
969                $declaration->explode ($this->declarations);
970            }
971
972            $pos = $semi + 1;
973        }
974    }
975
976    /**
977     * @return string
978     */
979    public function toString () {
980        $returnString = '';
981        $returnString .= "Media= \"".$this->media."\"\n";
982        foreach ($this->selectors as $selector) {
983            $returnString .= $selector.' ';
984        }
985        $returnString .= "{\n";
986        foreach ($this->declarations as $declaration) {
987            $returnString .= '  '.$declaration->getProperty ().':'.$declaration->getValue ().";\n";
988        }
989        $returnString .= "}\n";
990        return $returnString;
991    }
992
993    /**
994     * @param $element
995     * @param $classString
996     * @param null $media
997     * @return bool|int
998     */
999    public function matches ($element, $classString, $media = NULL, $cssId=NULL) {
1000
1001        $media = trim ($media);
1002        if ( !empty($this->media) && $media != $this->media ) {
1003            // Wrong media
1004            //print ("\nNo-Match ".$this->media."==".$media); //Debuging
1005            return false;
1006        }
1007
1008        $matches = 0;
1009        $classes = explode (' ', $classString);
1010
1011        foreach ($this->selectors as $selector) {
1012            if ( !empty($classString) ) {
1013                foreach ($classes as $class) {
1014                    if ( $selector [0] == '.' && $selector == '.'.$class ) {
1015                        $matches++;
1016                        break;
1017                    } else if ( $selector [0] == '#' && $selector == '#'.$cssId ) {
1018                        $matches++;
1019                        break;
1020                    } else if ( $selector == $element || $selector == $element.'.'.$class ) {
1021                        $matches++;
1022                        break;
1023                    }
1024                }
1025            } else {
1026                if ( $selector [0] == '#' && $selector == '#'.$cssId ) {
1027                    $matches++;
1028                } else if ( $selector == $element ) {
1029                    $matches++;
1030                }
1031            }
1032        }
1033
1034        // We only got a match if all selectors were matched
1035        if ( $matches == count($this->selectors) ) {
1036            // Return the number of matched selectors
1037            // This enables the caller to choose the most specific rule
1038            return $matches;
1039        }
1040
1041        return false;
1042    }
1043
1044    /**
1045     * @param $name
1046     * @return null
1047     */
1048    public function getProperty ($name) {
1049        foreach ($this->declarations as $declaration) {
1050            if ( $name == $declaration->getProperty () ) {
1051                return $declaration->getValue ();
1052            }
1053        }
1054        return NULL;
1055    }
1056
1057    /**
1058     * @param $values
1059     * @return null
1060     */
1061    public function getProperties (&$values) {
1062        foreach ($this->declarations as $declaration) {
1063            $property = $declaration->getProperty ();
1064            $value = $declaration->getValue ();
1065            $values [$property] = $value;
1066        }
1067        return NULL;
1068    }
1069
1070    /**
1071     * @param $callback
1072     */
1073    public function adjustLengthValues ($callback) {
1074        foreach ($this->declarations as $declaration) {
1075            $declaration->adjustLengthValues ($callback);
1076        }
1077    }
1078}
1079
1080/**
1081 * Class helper_plugin_odt_cssimport
1082 *
1083 * @package helper\cssimport
1084 */
1085class helper_plugin_odt_cssimport extends DokuWiki_Plugin {
1086    protected $replacements = array();
1087    protected $raw;
1088    /** @var css_rule[]  */
1089    protected $rules = array ();
1090
1091    /**
1092     * Imports CSS from a file.
1093     * @deprecated since 3015-05-23, use importFromFile
1094     *
1095     * @param $filename
1096     */
1097    function importFrom($filename) {
1098        dbg_deprecated('importFromFile');
1099        $this->importFromFile($filename);
1100    }
1101
1102    /**
1103     * @param $contents
1104     * @return bool
1105     */
1106    function importFromString($contents) {
1107        $this->deleteComments ($contents);
1108        return $this->importFromStringInternal ($contents);
1109    }
1110
1111    /**
1112     * Delete comments in $contents. All comments are overwritten with spaces.
1113     * The '&' is required. DO NOT DELETE!!!
1114     * @param $contents
1115     */
1116    protected function deleteComments (&$contents) {
1117        // Delete all comments first
1118        $pos = 0;
1119        $max = strlen ($contents);
1120        $in_comment = false;
1121        while ( $pos < $max ) {
1122            if ( ($pos+1) < $max &&
1123                 $contents [$pos] == '/' &&
1124                 $contents [$pos+1] == '*' ) {
1125                $in_comment = true;
1126
1127                $contents [$pos] = ' ';
1128                $contents [$pos+1] = ' ';
1129                $pos += 2;
1130                continue;
1131            }
1132            if ( ($pos+1) < $max &&
1133                 $contents [$pos] == '*' &&
1134                 $contents [$pos+1] == '/' &&
1135                 $in_comment === true ) {
1136                $in_comment = false;
1137
1138                $contents [$pos] = ' ';
1139                $contents [$pos+1] = ' ';
1140                $pos += 2;
1141                continue;
1142            }
1143            if ( $in_comment === true ) {
1144                $contents [$pos] = ' ';
1145            }
1146            $pos++;
1147        }
1148    }
1149
1150    /**
1151     * @param $contents
1152     * @param null $media
1153     * @return bool
1154     */
1155    protected function importFromStringInternal($contents, $media = NULL, &$processed = NULL) {
1156        // Find all CSS rules
1157        $pos = 0;
1158        $max = strlen ($contents);
1159        while ( $pos < $max ) {
1160            $bracket_open = strpos ($contents, '{', $pos);
1161            if ( $bracket_open === false ) {
1162                return false;
1163            }
1164            $bracket_close = strpos ($contents, '}', $pos);
1165            if ( $bracket_close === false ) {
1166                return false;
1167            }
1168
1169            // If this is a nested call we might hit a closing } for the media section
1170            // which was the reason for this function call. In this case break and return.
1171            if ( $bracket_close < $bracket_open ) {
1172                $pos = $bracket_close + 1;
1173                break;
1174            }
1175
1176            // Get the part before the open bracket and the last closing bracket
1177            // (or the start of the string).
1178            $before_open_bracket = substr ($contents, $pos, $bracket_open - $pos);
1179
1180            // Is it a @media rule?
1181            $before_open_bracket = trim ($before_open_bracket);
1182            $mediapos = stripos($before_open_bracket, '@media');
1183            if ( $mediapos !== false ) {
1184
1185                // Yes, decode content as normal rules with @media ... { ... }
1186                //$new_media = substr_replace ($before_open_bracket, NULL, $mediapos, strlen ('@media'));
1187                $new_media = substr ($before_open_bracket, $mediapos + strlen ('@media'));
1188                $contents_in_media = substr ($contents, $bracket_open + 1);
1189
1190                $nested_processed = 0;
1191                $result = $this->importFromStringInternal ($contents_in_media, $new_media, $nested_processed);
1192                if ( $result !== true ) {
1193                    // Stop parsing on error.
1194                    return false;
1195                }
1196                unset ($new_media);
1197                $pos = $bracket_open + 1 + $nested_processed;
1198            } else {
1199
1200                // No, decode rule the normal way selector { ... }
1201                $selectors = explode (',', $before_open_bracket);
1202
1203                $decls = substr ($contents, $bracket_open + 1, $bracket_close - $bracket_open);
1204
1205                // Create a own, new rule for every selector
1206                foreach ( $selectors as $selector ) {
1207                    $selector = trim ($selector);
1208                    $this->rules [] = new css_rule ($selector, $decls, $media);
1209                }
1210
1211                $pos = $bracket_close + 1;
1212            }
1213        }
1214        if ( $processed !== NULL ) {
1215            $processed = $pos;
1216        }
1217        return true;
1218    }
1219
1220    /**
1221     * @param $filename
1222     * @return bool|void
1223     */
1224    function importFromFile($filename) {
1225        // Try to read in the file content
1226        if ( empty($filename) ) {
1227            return false;
1228        }
1229
1230        $handle = fopen($filename, "rb");
1231        if ( $handle === false ) {
1232            return false;
1233        }
1234
1235        $contents = fread($handle, filesize($filename));
1236        fclose($handle);
1237        if ( $contents === false ) {
1238            return false;
1239        }
1240
1241        return $this->importFromString ($contents);
1242    }
1243
1244    /**
1245     * @param $filename
1246     * @return bool
1247     */
1248    function loadReplacements($filename) {
1249        // Try to read in the file content
1250        if ( empty($filename) ) {
1251            return false;
1252        }
1253
1254        $handle = fopen($filename, "rb");
1255        if ( $handle === false ) {
1256            return false;
1257        }
1258
1259        $filesize = filesize($filename);
1260        $contents = fread($handle, $filesize);
1261        fclose($handle);
1262        if ( $contents === false ) {
1263            return false;
1264        }
1265
1266        // Delete all comments first
1267        $contents = preg_replace ('/;.*/', ' ', $contents);
1268
1269        // Find the start of the replacements section
1270        $rep_start = strpos ($contents, '[replacements]');
1271        if ( $rep_start === false ) {
1272            return false;
1273        }
1274        $rep_start += strlen ('[replacements]');
1275
1276        // Find the end of the replacements section
1277        // (The end is either the next section or the end of file)
1278        $rep_end = strpos ($contents, '[', $rep_start);
1279        if ( $rep_end === false ) {
1280            $rep_end = $filesize - 1;
1281        }
1282
1283        // Find all replacment definitions
1284        $defs = substr ($contents, $rep_start, $rep_end - $rep_start);
1285        $defs_end = strlen ($defs);
1286
1287        $def_pos = 0;
1288        while ( $def_pos < $defs_end ) {
1289            $linestart = strpos ($defs, "\n", $def_pos);
1290            if ( $linestart === false ) {
1291                break;
1292            }
1293            $linestart += strlen ("\n");
1294
1295            $lineend = strpos ($defs, "\n", $linestart);
1296            if ( $lineend === false ) {
1297                $lineend = $defs_end;
1298            }
1299
1300            $equal_sign = strpos ($defs, '=', $linestart);
1301            if ( $equal_sign === false || $equal_sign > $lineend ) {
1302                $def_pos = $linestart;
1303                continue;
1304            }
1305
1306            $quote_start = strpos ($defs, '"', $equal_sign + 1);
1307            if ( $quote_start === false || $quote_start > $lineend ) {
1308                $def_pos = $linestart;
1309                continue;
1310            }
1311
1312            $quote_end = strpos ($defs, '"', $quote_start + 1);
1313            if ( $quote_end === false || $quote_start > $lineend) {
1314                $def_pos = $linestart;
1315                continue;
1316            }
1317            if ( $quote_end - $quote_start < 2 ) {
1318                $def_pos = $linestart;
1319                continue;
1320            }
1321
1322            $replacement = substr ($defs, $linestart, $equal_sign - $linestart);
1323            $value = substr ($defs, $quote_start + 1, $quote_end - ($quote_start + 1));
1324            $replacement = trim($replacement);
1325            $value = trim($value);
1326
1327            $this->replacements [$replacement] = $value;
1328
1329            $def_pos = $lineend;
1330        }
1331
1332        return true;
1333    }
1334
1335    /**
1336     * @return mixed
1337     */
1338    public function getRaw () {
1339        return $this->raw;
1340    }
1341
1342    /**
1343     * @param $name
1344     * @return mixed
1345     */
1346    public function getReplacement ($name) {
1347        return $this->replacements [$name];
1348    }
1349
1350    /**
1351     * @param $element
1352     * @param $classString
1353     * @param $name
1354     * @param null $media
1355     * @return null
1356     */
1357    public function getPropertyForElement ($element, $classString, $name, $media = NULL) {
1358        if ( empty ($name) ) {
1359            return NULL;
1360        }
1361
1362        $value = NULL;
1363        foreach ($this->rules as $rule) {
1364            $matched = $rule->matches ($element, $classString, $media);
1365            if ( $matched !== false ) {
1366                $current = $rule->getProperty ($name);
1367                if ( !empty ($current) ) {
1368                    $value = $current;
1369                }
1370            }
1371        }
1372
1373        return $value;
1374    }
1375
1376    /**
1377     * @param $classString
1378     * @param $name
1379     * @return null
1380     */
1381    public function getProperty ($classString, $name) {
1382        if ( empty ($classString) || empty ($name) ) {
1383            return NULL;
1384        }
1385
1386        $value = $this->getPropertyForElement (NULL, $classString, $name);
1387        return $value;
1388    }
1389
1390    /**
1391     * @param $dest
1392     * @param $element
1393     * @param $classString
1394     * @param null $media
1395     */
1396    public function getPropertiesForElement (&$dest, $element, $classString, $media = NULL, $cssId=NULL) {
1397        if ( empty ($element) && empty ($classString) && empty ($cssId) ) {
1398            return;
1399        }
1400
1401        foreach ($this->rules as $rule) {
1402            $matched = $rule->matches ($element, $classString, $media, $cssId);
1403            if ( $matched !== false ) {
1404                $rule->getProperties ($dest);
1405            }
1406        }
1407    }
1408
1409    /**
1410     * @param $value
1411     * @param int $emValue
1412     * @return string
1413     */
1414    public function adjustValueForODT ($value, $emValue = 0) {
1415        // ODT specific function. Shouldn't be used anymore.
1416        // Call the ODT renderer's function instead.
1417        dbg_deprecated('renderer_plugin_odt_page::adjustValueForODT');
1418
1419        $values = preg_split ('/\s+/', $value);
1420        $value = '';
1421        foreach ($values as $part) {
1422            // Replace it if necessary
1423            $part = trim($part);
1424            $rep = $this->getReplacement($part);
1425            if ( !empty ($rep) ) {
1426                $part = $rep;
1427            }
1428            $length = strlen ($part);
1429
1430            // If it is a short color value (#xxx) then convert it to long value (#xxxxxx)
1431            // (ODT does not support the short form)
1432            if ( $part [0] == '#' && $length == 4 ) {
1433                $part = '#'.$part [1].$part [1].$part [2].$part [2].$part [3].$part [3];
1434            } else {
1435                // If it is a CSS color name, get it's real color value
1436                /** @var helper_plugin_odt_csscolors $odt_colors */
1437                $odt_colors = plugin_load('helper', 'odt_csscolors');
1438                $color = $odt_colors->getColorValue ($part);
1439                if ( $part == 'black' || $color != '#000000' ) {
1440                    $part = $color;
1441                }
1442            }
1443
1444            if ( $length > 2 && $part [$length-2] == 'e' && $part [$length-1] == 'm' ) {
1445                $number = substr ($part, 0, $length-2);
1446                if ( is_numeric ($number) && !empty ($emValue) ) {
1447                    $part = ($number * $emValue).'pt';
1448                }
1449            }
1450
1451            // Replace px with pt (px does not seem to be supported by ODT)
1452            if ( $length > 2 && $part [$length-2] == 'p' && $part [$length-1] == 'x' ) {
1453                $part [$length-1] = 't';
1454            }
1455
1456            $value .= ' '.$part;
1457        }
1458        $value = trim($value);
1459
1460        return $value;
1461    }
1462
1463    /**
1464     * @return string
1465     */
1466    public function rulesToString () {
1467        $returnString = '';
1468        foreach ($this->rules as $rule) {
1469            $returnString .= $rule->toString ();
1470        }
1471        return $returnString;
1472    }
1473
1474    /**
1475     * @param $URL
1476     * @param $replacement
1477     * @return string
1478     */
1479    public function replaceURLPrefix ($URL, $replacement) {
1480        if ( !empty ($URL) && !empty ($replacement) ) {
1481            // Replace 'url(...)' with $replacement
1482            $URL = substr ($URL, 3);
1483            $URL = trim ($URL, '()');
1484            $URL = $replacement.$URL;
1485        }
1486        return $URL;
1487    }
1488
1489    /**
1490     * @param $callback
1491     */
1492    public function adjustLengthValues ($callback) {
1493        foreach ($this->rules as $rule) {
1494            $rule->adjustLengthValues ($callback);
1495        }
1496    }
1497}
1498
1499