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                    if (!isset($values [$index])) $values [$index] = 'medium';
425                    switch ($values [$index]) {
426                        case 'thin':
427                        case 'medium':
428                        case 'thick':
429                            $decls [] = new css_declaration ('border-width', $values [$index]);
430                            foreach ($border_sides as $border_side) {
431                                $decls [] = new css_declaration ($border_side.'-width', $values [$index]);
432                            }
433                        break;
434                        default:
435                            if ( strpos ($values [$index], 'px') !== false ) {
436                                $decls [] = new css_declaration ('border-width', $values [$index]);
437                                foreach ($border_sides as $border_side) {
438                                    $decls [] = new css_declaration ($border_side.'-width', $values [$index]);
439                                }
440                            } else {
441                                // There is no default value? So leave it unset.
442                            }
443                        break;
444                    }
445                    $border_width_set = true;
446                    $index++;
447                    continue;
448                }
449                if ( $border_style_set === false ) {
450                    if (!isset($values [$index])) $values [$index] = 'none';
451                    switch ($values [$index]) {
452                        case 'none':
453                        case 'dotted':
454                        case 'dashed':
455                        case 'solid':
456                        case 'double':
457                        case 'groove':
458                        case 'ridge':
459                        case 'inset':
460                        case 'outset':
461                            $decls [] = new css_declaration ('border-style', $values [$index]);
462                            foreach ($border_sides as $border_side) {
463                                $decls [] = new css_declaration ($border_side.'-style', $values [$index]);
464                            }
465                        break;
466                        default:
467                            $decls [] = new css_declaration ('border-style', 'none');
468                            foreach ($border_sides as $border_side) {
469                                $decls [] = new css_declaration ($border_side.'-style', 'none');
470                            }
471                        break;
472                    }
473                    $border_style_set = true;
474                    $index++;
475                    continue;
476                }
477                if ( $border_color_set === false ) {
478                    if (!isset($values [$index])) $values [$index] = 'initial';
479                    $decls [] = new css_declaration ('border-color', $values [$index]);
480                    foreach ($border_sides as $border_side) {
481                        $decls [] = new css_declaration ($border_side.'-color', $values [$index]);
482                    }
483
484                    // This is the last value.
485                    break;
486                }
487            }
488            foreach ($border_sides as $border_side) {
489                $decls [] = new css_declaration ($border_side, $values [0].' '.$values [1].' '.$values [2]);
490            }
491        }
492    }
493
494    /**
495     * @param css_declaration[] $decls
496     */
497    protected function explodeListStyleShorthand (&$decls) {
498        if ( $this->property == 'list-style' ) {
499            $values = preg_split ('/\s+/', $this->value);
500
501            $list_style_type_set = false;
502            $list_style_position_set = false;
503            $list_style_image_set = false;
504            foreach ($values as $value) {
505                if ( $list_style_type_set === false ) {
506                    $default = false;
507                    switch ($value) {
508                        case 'disc':
509                        case 'armenian':
510                        case 'circle':
511                        case 'cjk-ideographic':
512                        case 'decimal':
513                        case 'decimal-leading-zero':
514                        case 'georgian':
515                        case 'hebrew':
516                        case 'hiragana':
517                        case 'hiragana-iroha':
518                        case 'katakana':
519                        case 'katakana-iroha':
520                        case 'lower-alpha':
521                        case 'lower-greek':
522                        case 'lower-latin':
523                        case 'lower-roman':
524                        case 'none':
525                        case 'square':
526                        case 'upper-alpha':
527                        case 'upper-latin':
528                        case 'upper-roman':
529                        case 'initial':
530                        case 'inherit':
531                            $decls [] = new css_declaration ('list-style-type', $value);
532                        break;
533                        default:
534                            $default = true;
535                            $decls [] = new css_declaration ('list-style-type', 'disc');
536                        break;
537                    }
538                    $list_style_type_set = true;
539                    if ( $default === false ) {
540                        continue;
541                    }
542                }
543                if ( $list_style_position_set === false ) {
544                    $default = false;
545                    switch ($value) {
546                        case 'inside':
547                        case 'outside':
548                        case 'initial':
549                        case 'inherit':
550                            $decls [] = new css_declaration ('list-style-position', $value);
551                        break;
552                        default:
553                            $default = true;
554                            $decls [] = new css_declaration ('list-style-position', 'outside');
555                        break;
556                    }
557                    $list_style_position_set = true;
558                    if ( $default === false ) {
559                        continue;
560                    }
561                }
562                if ( $list_style_image_set === false ) {
563                    $decls [] = new css_declaration ('list-style-image', $value);
564                    $list_style_image_set = true;
565                }
566            }
567            if ( $list_style_image_set === false ) {
568                $decls [] = new css_declaration ('list-style-image', 'none');
569            }
570        }
571    }
572
573    /**
574     * @param css_declaration[] $decls
575     */
576    protected function explodeFlexShorthand (&$decls) {
577        if ( $this->property == 'flex' ) {
578            $values = preg_split ('/\s+/', $this->value);
579            if ( count($values) > 0 ) {
580                $decls [] = new css_declaration ('flex-grow', $values [0]);
581            }
582            if ( count($values) > 1 ) {
583                $decls [] = new css_declaration ('flex-shrink', $values [1]);
584            }
585            if ( count($values) > 2 ) {
586                $decls [] = new css_declaration ('flex-basis', $values [2]);
587            }
588        }
589    }
590
591    /**
592     * @param css_declaration[] $decls
593     */
594    protected function explodeTransitionShorthand (&$decls) {
595        if ( $this->property == 'transition' ) {
596            $values = preg_split ('/\s+/', $this->value);
597            if ( count($values) > 0 ) {
598                $decls [] = new css_declaration ('transition-property', $values [0]);
599            }
600            if ( count($values) > 1 ) {
601                $decls [] = new css_declaration ('transition-duration', $values [1]);
602            }
603            if ( count($values) > 2 ) {
604                $decls [] = new css_declaration ('transition-timing-function', $values [2]);
605            }
606            if ( count($values) > 3 ) {
607                $decls [] = new css_declaration ('transition-delay', $values [3]);
608            }
609        }
610    }
611
612    /**
613     * @param css_declaration[] $decls
614     */
615    protected function explodeOutlineShorthand (&$decls) {
616        if ( $this->property == 'outline' ) {
617            $values = preg_split ('/\s+/', $this->value);
618
619            $outline_color_set = false;
620            $outline_style_set = false;
621            $outline_width_set = false;
622            foreach ($values as $value) {
623                if ( $outline_color_set === false ) {
624                    $decls [] = new css_declaration ('outline-color', $value);
625                    $outline_color_set = true;
626                    continue;
627                }
628                if ( $outline_style_set === false ) {
629                    $default = false;
630                    switch ($value) {
631                        case 'none':
632                        case 'hidden':
633                        case 'dotted':
634                        case 'dashed':
635                        case 'solid':
636                        case 'double':
637                        case 'groove':
638                        case 'ridge':
639                        case 'inset':
640                        case 'outset':
641                        case 'initial':
642                        case 'inherit':
643                            $decls [] = new css_declaration ('outline-style', $value);
644                        break;
645                        default:
646                            $default = true;
647                            $decls [] = new css_declaration ('outline-style', 'none');
648                        break;
649                    }
650                    $outline_style_set = true;
651                    if ( $default === false ) {
652                        continue;
653                    }
654                }
655                if ( $outline_width_set === false ) {
656                    $default = false;
657                    switch ($value) {
658                        case 'medium':
659                        case 'thin':
660                        case 'thick':
661                        case 'initial':
662                        case 'inherit':
663                            $decls [] = new css_declaration ('outline-width', $value);
664                        break;
665                        default:
666                            $found = false;
667                            foreach (self::$css_units as $css_unit) {
668                                if ( strpos ($value, $css_unit) !== false ) {
669                                    $decls [] = new css_declaration ('outline-width', $value);
670                                    $found = true;
671                                    break;
672                                }
673                            }
674                            if ( $found === false ) {
675                                $default = true;
676                                $decls [] = new css_declaration ('outline-width', 'medium');
677                            }
678                        break;
679                    }
680                    $outline_width_set = true;
681                    if ( $default === false ) {
682                        continue;
683                    }
684                }
685            }
686        }
687    }
688
689    /**
690     * @param css_declaration[] $decls
691     */
692    protected function explodeAnimationShorthand (&$decls) {
693        if ( $this->property == 'animation' ) {
694            $values = preg_split ('/\s+/', $this->value);
695            if ( count($values) > 0 ) {
696                $decls [] = new css_declaration ('animation-name', $values [0]);
697            }
698            if ( count($values) > 1 ) {
699                $decls [] = new css_declaration ('animation-duration', $values [1]);
700            }
701            if ( count($values) > 2 ) {
702                $decls [] = new css_declaration ('animation-timing-function', $values [2]);
703            }
704            if ( count($values) > 3 ) {
705                $decls [] = new css_declaration ('animation-delay', $values [3]);
706            }
707            if ( count($values) > 4 ) {
708                $decls [] = new css_declaration ('animation-iteration-count', $values [4]);
709            }
710            if ( count($values) > 5 ) {
711                $decls [] = new css_declaration ('animation-direction', $values [5]);
712            }
713            if ( count($values) > 6 ) {
714                $decls [] = new css_declaration ('animation-fill-mode', $values [6]);
715            }
716            if ( count($values) > 7 ) {
717                $decls [] = new css_declaration ('animation-play-state', $values [7]);
718            }
719        }
720    }
721
722    /**
723     * @param css_declaration[] $decls
724     */
725    protected function explodeBorderBottomShorthand (&$decls) {
726        if ( $this->property == 'border-bottom' ) {
727            $values = preg_split ('/\s+/', $this->value);
728
729            $border_bottom_width_set = false;
730            $border_bottom_style_set = false;
731            $border_bottom_color_set = false;
732            foreach ($values as $value) {
733                if ( $border_bottom_width_set === false ) {
734                    $default = false;
735                    switch ($value) {
736                        case 'medium':
737                        case 'thin':
738                        case 'thick':
739                        case 'initial':
740                        case 'inherit':
741                            $decls [] = new css_declaration ('border-bottom-width', $value);
742                        break;
743                        default:
744                            $found = false;
745                            foreach (self::$css_units as $css_unit) {
746                                if ( strpos ($value, $css_unit) !== false ) {
747                                    $decls [] = new css_declaration ('border-bottom-width', $value);
748                                    $found = true;
749                                    break;
750                                }
751                            }
752                            if ( $found === false ) {
753                                $default = true;
754                                $decls [] = new css_declaration ('border-bottom-width', 'medium');
755                            }
756                        break;
757                    }
758                    $border_bottom_width_set = true;
759                    if ( $default === false ) {
760                        continue;
761                    }
762                }
763                if ( $border_bottom_style_set === false ) {
764                    $default = false;
765                    switch ($value) {
766                        case 'none':
767                        case 'hidden':
768                        case 'dotted':
769                        case 'dashed':
770                        case 'solid':
771                        case 'double':
772                        case 'groove':
773                        case 'ridge':
774                        case 'inset':
775                        case 'outset':
776                        case 'initial':
777                        case 'inherit':
778                            $decls [] = new css_declaration ('border-bottom-style', $value);
779                        break;
780                        default:
781                            $default = true;
782                            $decls [] = new css_declaration ('border-bottom-style', 'none');
783                        break;
784                    }
785                    $border_bottom_style_set = true;
786                    if ( $default === false ) {
787                        continue;
788                    }
789                }
790                if ( $border_bottom_color_set === false ) {
791                    $decls [] = new css_declaration ('border-bottom-color', $value);
792                    $border_bottom_color_set = true;
793                    continue;
794                }
795            }
796        }
797    }
798
799    /**
800     * @param css_declaration[] $decls
801     */
802    protected function explodeColumnsShorthand (&$decls) {
803        if ( $this->property == 'columns' ) {
804            $values = preg_split ('/\s+/', $this->value);
805            if ( count($values) == 1 && $values [0] == 'auto' ) {
806                $decls [] = new css_declaration ('column-width', 'auto');
807                $decls [] = new css_declaration ('column-count', 'auto');
808                return;
809            }
810            if ( count($values) > 0 ) {
811                $decls [] = new css_declaration ('column-width', $values [0]);
812            }
813            if ( count($values) > 1 ) {
814                $decls [] = new css_declaration ('column-count', $values [1]);
815            }
816        }
817    }
818
819    /**
820     * @param css_declaration[] $decls
821     */
822    protected function explodeColumnRuleShorthand (&$decls) {
823        if ( $this->property == 'column-rule' ) {
824            $values = preg_split ('/\s+/', $this->value);
825            if ( count($values) > 0 ) {
826                $decls [] = new css_declaration ('column-rule-width', $values [0]);
827            }
828            if ( count($values) > 1 ) {
829                $decls [] = new css_declaration ('column-rule-style', $values [1]);
830            }
831            if ( count($values) > 2 ) {
832                $decls [] = new css_declaration ('column-rule-color', $values [2]);
833            }
834        }
835    }
836
837    /**
838     * @param $callback
839     */
840    public function adjustLengthValues ($callback, $rule=NULL) {
841        switch ($this->property) {
842            case 'border-width':
843            case 'outline-width':
844            case 'border-bottom-width':
845            case 'column-rule-width':
846                $this->value =
847                    call_user_func($callback, $this->property, $this->value, CSSValueType::StrokeOrBorderWidth, $rule);
848            break;
849
850            case 'margin-left':
851            case 'margin-right':
852            case 'padding-left':
853            case 'padding-right':
854            case 'width':
855            case 'column-width':
856                $this->value =
857                    call_user_func($callback, $this->property, $this->value, CSSValueType::LengthValueXAxis, $rule);
858            break;
859
860            case 'margin-top':
861            case 'margin-bottom':
862            case 'padding-top':
863            case 'padding-bottom':
864            case 'min-height':
865            case 'height':
866            case 'line-height':
867                $this->value =
868                    call_user_func($callback, $this->property, $this->value, CSSValueType::LengthValueYAxis, $rule);
869            break;
870
871            case 'border':
872            case 'border-left':
873            case 'border-right':
874            case 'border-top':
875            case 'border-bottom':
876                $this->adjustLengthValuesBorder ($callback, $rule);
877            break;
878
879            // FIXME: Shorthands are currently not processed.
880            // Every Shorthand would need an extra function which knows if it has any length values.
881            // Just like the explode...Shorthand functions.
882        }
883    }
884
885    /**
886     * @param $callback
887     */
888    protected function adjustLengthValuesBorder ($callback, $rule=NULL) {
889        switch ($this->property) {
890            case 'border':
891            case 'border-left':
892            case 'border-right':
893            case 'border-top':
894            case 'border-bottom':
895                $values = preg_split ('/\s+/', $this->value);
896                if (!isset($values [1])) $values [1] = 'none'; // border-style
897                if (!isset($values [2])) $values [2] = 'currentcolor'; // border-color
898                $width =
899                    call_user_func($callback, $this->property, $values [0], CSSValueType::StrokeOrBorderWidth, $rule);
900                $this->value = $width . ' ' . $values [1] . ' ' . $values [2];
901            break;
902        }
903    }
904
905    /**
906     * @param $callback
907     */
908    public function replaceURLPrefixes ($callback) {
909        if (strncmp($this->value, 'url(', 4) == 0) {
910            $url = substr($this->value, 4, -1);
911            $this->value = call_user_func($callback, $this->property, $this->value, $url);
912        }
913    }
914}
915
916/**
917 * Class css_rule
918 *
919 * @package CSS\css_rule
920 */
921class css_rule {
922    protected $media = NULL;
923    protected $selectors = array ();
924    /** @var css_declaration[]  */
925    protected $declarations = array ();
926
927    /**
928     * @param $selector
929     * @param $decls
930     * @param null $media
931     */
932    public function __construct($selector, $decls, $media = NULL) {
933
934        $this->media = trim ($media);
935        //print ("\nNew rule: ".$media."\n"); //Debuging
936
937        $this->selectors = explode (' ', $selector);
938
939        $decls = trim ($decls, '{}');
940
941        // Parse declarations
942        $pos = 0;
943        $end = strlen ($decls);
944        while ( $pos < $end ) {
945            $colon = strpos ($decls, ':', $pos);
946            if ( $colon === false ) {
947                break;
948            }
949            $semi = strpos ($decls, ';', $colon + 1);
950            if ( $semi === false ) {
951                break;
952            }
953
954            $property = substr ($decls, $pos, $colon - $pos);
955            $property = trim($property);
956
957            $value = substr ($decls, $colon + 1, $semi - ($colon + 1));
958            $value = trim ($value);
959            $values = preg_split ('/\s+/', $value);
960            $value = '';
961            foreach ($values as $part) {
962                if ( $part != '!important' ) {
963                    $value .= ' '.$part;
964                }
965            }
966            $value = trim($value);
967
968            // Create new declaration
969            $declaration = new css_declaration ($property, $value);
970            $this->declarations [] = $declaration;
971
972            // Handle CSS shorthands, e.g. 'border'
973            if ( $declaration->isShorthand () === true ) {
974                $declaration->explode ($this->declarations);
975            }
976
977            $pos = $semi + 1;
978        }
979    }
980
981    /**
982     * @return string
983     */
984    public function toString () {
985        $returnString = '';
986        $returnString .= "Media= \"".$this->media."\"\n";
987        foreach ($this->selectors as $selector) {
988            $returnString .= $selector.' ';
989        }
990        $returnString .= "{\n";
991        foreach ($this->declarations as $declaration) {
992            $returnString .= '  '.$declaration->getProperty ().':'.$declaration->getValue ().";\n";
993        }
994        $returnString .= "}\n";
995        return $returnString;
996    }
997
998    /**
999     * @param $element
1000     * @param $classString
1001     * @param null $media
1002     * @return bool|int
1003     */
1004    public function matches ($element, $classString, $media = NULL, $cssId=NULL) {
1005
1006        $media = trim ($media);
1007        if ( !empty($this->media) && $media != $this->media ) {
1008            // Wrong media
1009            //print ("\nNo-Match ".$this->media."==".$media); //Debuging
1010            return false;
1011        }
1012
1013        $matches = 0;
1014        $classes = explode (' ', $classString);
1015
1016        foreach ($this->selectors as $selector) {
1017            if ( !empty($classString) ) {
1018                foreach ($classes as $class) {
1019                    if ( $selector [0] == '.' && $selector == '.'.$class ) {
1020                        $matches++;
1021                        break;
1022                    } else if ( $selector [0] == '#' && $selector == '#'.$cssId ) {
1023                        $matches++;
1024                        break;
1025                    } else if ( $selector == $element || $selector == $element.'.'.$class ) {
1026                        $matches++;
1027                        break;
1028                    }
1029                }
1030            } else {
1031                if ( $selector [0] == '#' && $selector == '#'.$cssId ) {
1032                    $matches++;
1033                } else if ( $selector == $element ) {
1034                    $matches++;
1035                }
1036            }
1037        }
1038
1039        // We only got a match if all selectors were matched
1040        if ( $matches == count($this->selectors) ) {
1041            // Return the number of matched selectors
1042            // This enables the caller to choose the most specific rule
1043            return $matches;
1044        }
1045
1046        return false;
1047    }
1048
1049    /**
1050     * @param $name
1051     * @return null
1052     */
1053    public function getProperty ($name) {
1054        foreach ($this->declarations as $declaration) {
1055            if ( $name == $declaration->getProperty () ) {
1056                return $declaration->getValue ();
1057            }
1058        }
1059        return NULL;
1060    }
1061
1062    /**
1063     * @param $values
1064     * @return null
1065     */
1066    public function getProperties (&$values) {
1067        foreach ($this->declarations as $declaration) {
1068            $property = $declaration->getProperty ();
1069            $value = $declaration->getValue ();
1070            $values [$property] = $value;
1071        }
1072        return NULL;
1073    }
1074
1075    /**
1076     * @param $callback
1077     */
1078    public function adjustLengthValues ($callback) {
1079        foreach ($this->declarations as $declaration) {
1080            $declaration->adjustLengthValues ($callback);
1081        }
1082    }
1083}
1084
1085/**
1086 * Class helper_plugin_odt_cssimport
1087 *
1088 * @package helper\cssimport
1089 */
1090class helper_plugin_odt_cssimport extends DokuWiki_Plugin {
1091    protected $replacements = array();
1092    protected $raw;
1093    /** @var css_rule[]  */
1094    protected $rules = array ();
1095
1096    /**
1097     * Imports CSS from a file.
1098     * @deprecated since 3015-05-23, use importFromFile
1099     *
1100     * @param $filename
1101     */
1102    function importFrom($filename) {
1103        dbg_deprecated('importFromFile');
1104        $this->importFromFile($filename);
1105    }
1106
1107    /**
1108     * @param $contents
1109     * @return bool
1110     */
1111    function importFromString($contents) {
1112        $this->deleteComments ($contents);
1113        return $this->importFromStringInternal ($contents);
1114    }
1115
1116    /**
1117     * Delete comments in $contents. All comments are overwritten with spaces.
1118     * The '&' is required. DO NOT DELETE!!!
1119     * @param $contents
1120     */
1121    protected function deleteComments (&$contents) {
1122        // Delete all comments first
1123        $pos = 0;
1124        $max = strlen ($contents);
1125        $in_comment = false;
1126        while ( $pos < $max ) {
1127            if ( ($pos+1) < $max &&
1128                 $contents [$pos] == '/' &&
1129                 $contents [$pos+1] == '*' ) {
1130                $in_comment = true;
1131
1132                $contents [$pos] = ' ';
1133                $contents [$pos+1] = ' ';
1134                $pos += 2;
1135                continue;
1136            }
1137            if ( ($pos+1) < $max &&
1138                 $contents [$pos] == '*' &&
1139                 $contents [$pos+1] == '/' &&
1140                 $in_comment === true ) {
1141                $in_comment = false;
1142
1143                $contents [$pos] = ' ';
1144                $contents [$pos+1] = ' ';
1145                $pos += 2;
1146                continue;
1147            }
1148            if ( $in_comment === true ) {
1149                $contents [$pos] = ' ';
1150            }
1151            $pos++;
1152        }
1153    }
1154
1155    /**
1156     * @param $contents
1157     * @param null $media
1158     * @return bool
1159     */
1160    protected function importFromStringInternal($contents, $media = NULL, &$processed = NULL) {
1161        // Find all CSS rules
1162        $pos = 0;
1163        $max = strlen ($contents);
1164        while ( $pos < $max ) {
1165            $bracket_open = strpos ($contents, '{', $pos);
1166            if ( $bracket_open === false ) {
1167                return false;
1168            }
1169            $bracket_close = strpos ($contents, '}', $pos);
1170            if ( $bracket_close === false ) {
1171                return false;
1172            }
1173
1174            // If this is a nested call we might hit a closing } for the media section
1175            // which was the reason for this function call. In this case break and return.
1176            if ( $bracket_close < $bracket_open ) {
1177                $pos = $bracket_close + 1;
1178                break;
1179            }
1180
1181            // Get the part before the open bracket and the last closing bracket
1182            // (or the start of the string).
1183            $before_open_bracket = substr ($contents, $pos, $bracket_open - $pos);
1184
1185            // Is it a @media rule?
1186            $before_open_bracket = trim ($before_open_bracket);
1187            $mediapos = stripos($before_open_bracket, '@media');
1188            if ( $mediapos !== false ) {
1189
1190                // Yes, decode content as normal rules with @media ... { ... }
1191                //$new_media = substr_replace ($before_open_bracket, NULL, $mediapos, strlen ('@media'));
1192                $new_media = substr ($before_open_bracket, $mediapos + strlen ('@media'));
1193                $contents_in_media = substr ($contents, $bracket_open + 1);
1194
1195                $nested_processed = 0;
1196                $result = $this->importFromStringInternal ($contents_in_media, $new_media, $nested_processed);
1197                if ( $result !== true ) {
1198                    // Stop parsing on error.
1199                    return false;
1200                }
1201                unset ($new_media);
1202                $pos = $bracket_open + 1 + $nested_processed;
1203            } else {
1204
1205                // No, decode rule the normal way selector { ... }
1206                $selectors = explode (',', $before_open_bracket);
1207
1208                $decls = substr ($contents, $bracket_open + 1, $bracket_close - $bracket_open);
1209
1210                // Create a own, new rule for every selector
1211                foreach ( $selectors as $selector ) {
1212                    $selector = trim ($selector);
1213                    $this->rules [] = new css_rule ($selector, $decls, $media);
1214                }
1215
1216                $pos = $bracket_close + 1;
1217            }
1218        }
1219        if ( isset($processed) ) {
1220            $processed = $pos;
1221        }
1222        return true;
1223    }
1224
1225    /**
1226     * @param $filename
1227     * @return bool|void
1228     */
1229    function importFromFile($filename) {
1230        // Try to read in the file content
1231        if ( empty($filename) ) {
1232            return false;
1233        }
1234
1235        $handle = fopen($filename, "rb");
1236        if ( $handle === false ) {
1237            return false;
1238        }
1239
1240        $contents = fread($handle, filesize($filename));
1241        fclose($handle);
1242        if ( $contents === false ) {
1243            return false;
1244        }
1245
1246        return $this->importFromString ($contents);
1247    }
1248
1249    /**
1250     * @param $filename
1251     * @return bool
1252     */
1253    function loadReplacements($filename) {
1254        // Try to read in the file content
1255        if ( empty($filename) ) {
1256            return false;
1257        }
1258
1259        $handle = fopen($filename, "rb");
1260        if ( $handle === false ) {
1261            return false;
1262        }
1263
1264        $filesize = filesize($filename);
1265        $contents = fread($handle, $filesize);
1266        fclose($handle);
1267        if ( $contents === false ) {
1268            return false;
1269        }
1270
1271        // Delete all comments first
1272        $contents = preg_replace ('/;.*/', ' ', $contents);
1273
1274        // Find the start of the replacements section
1275        $rep_start = strpos ($contents, '[replacements]');
1276        if ( $rep_start === false ) {
1277            return false;
1278        }
1279        $rep_start += strlen ('[replacements]');
1280
1281        // Find the end of the replacements section
1282        // (The end is either the next section or the end of file)
1283        $rep_end = strpos ($contents, '[', $rep_start);
1284        if ( $rep_end === false ) {
1285            $rep_end = $filesize - 1;
1286        }
1287
1288        // Find all replacment definitions
1289        $defs = substr ($contents, $rep_start, $rep_end - $rep_start);
1290        $defs_end = strlen ($defs);
1291
1292        $def_pos = 0;
1293        while ( $def_pos < $defs_end ) {
1294            $linestart = strpos ($defs, "\n", $def_pos);
1295            if ( $linestart === false ) {
1296                break;
1297            }
1298            $linestart += strlen ("\n");
1299
1300            $lineend = strpos ($defs, "\n", $linestart);
1301            if ( $lineend === false ) {
1302                $lineend = $defs_end;
1303            }
1304
1305            $equal_sign = strpos ($defs, '=', $linestart);
1306            if ( $equal_sign === false || $equal_sign > $lineend ) {
1307                $def_pos = $linestart;
1308                continue;
1309            }
1310
1311            $quote_start = strpos ($defs, '"', $equal_sign + 1);
1312            if ( $quote_start === false || $quote_start > $lineend ) {
1313                $def_pos = $linestart;
1314                continue;
1315            }
1316
1317            $quote_end = strpos ($defs, '"', $quote_start + 1);
1318            if ( $quote_end === false || $quote_start > $lineend) {
1319                $def_pos = $linestart;
1320                continue;
1321            }
1322            if ( $quote_end - $quote_start < 2 ) {
1323                $def_pos = $linestart;
1324                continue;
1325            }
1326
1327            $replacement = substr ($defs, $linestart, $equal_sign - $linestart);
1328            $value = substr ($defs, $quote_start + 1, $quote_end - ($quote_start + 1));
1329            $replacement = trim($replacement);
1330            $value = trim($value);
1331
1332            $this->replacements [$replacement] = $value;
1333
1334            $def_pos = $lineend;
1335        }
1336
1337        return true;
1338    }
1339
1340    /**
1341     * @return mixed
1342     */
1343    public function getRaw () {
1344        return $this->raw;
1345    }
1346
1347    /**
1348     * @param $name
1349     * @return mixed
1350     */
1351    public function getReplacement ($name) {
1352        return $this->replacements [$name];
1353    }
1354
1355    /**
1356     * @param $element
1357     * @param $classString
1358     * @param $name
1359     * @param null $media
1360     * @return null
1361     */
1362    public function getPropertyForElement ($element, $classString, $name, $media = NULL) {
1363        if ( empty ($name) ) {
1364            return NULL;
1365        }
1366
1367        $value = NULL;
1368        foreach ($this->rules as $rule) {
1369            $matched = $rule->matches ($element, $classString, $media);
1370            if ( $matched !== false ) {
1371                $current = $rule->getProperty ($name);
1372                if ( !empty ($current) ) {
1373                    $value = $current;
1374                }
1375            }
1376        }
1377
1378        return $value;
1379    }
1380
1381    /**
1382     * @param $classString
1383     * @param $name
1384     * @return null
1385     */
1386    public function getProperty ($classString, $name) {
1387        if ( empty ($classString) || empty ($name) ) {
1388            return NULL;
1389        }
1390
1391        $value = $this->getPropertyForElement (NULL, $classString, $name);
1392        return $value;
1393    }
1394
1395    /**
1396     * @param $dest
1397     * @param $element
1398     * @param $classString
1399     * @param null $media
1400     */
1401    public function getPropertiesForElement (&$dest, $element, $classString, $media = NULL, $cssId=NULL) {
1402        if ( empty ($element) && empty ($classString) && empty ($cssId) ) {
1403            return;
1404        }
1405
1406        foreach ($this->rules as $rule) {
1407            $matched = $rule->matches ($element, $classString, $media, $cssId);
1408            if ( $matched !== false ) {
1409                $rule->getProperties ($dest);
1410            }
1411        }
1412    }
1413
1414    /**
1415     * @param $value
1416     * @param int $emValue
1417     * @return string
1418     */
1419    public function adjustValueForODT ($value, $emValue = 0) {
1420        // ODT specific function. Shouldn't be used anymore.
1421        // Call the ODT renderer's function instead.
1422        dbg_deprecated('renderer_plugin_odt_page::adjustValueForODT');
1423
1424        $values = preg_split ('/\s+/', $value);
1425        $value = '';
1426        foreach ($values as $part) {
1427            // Replace it if necessary
1428            $part = trim($part);
1429            $rep = $this->getReplacement($part);
1430            if ( !empty ($rep) ) {
1431                $part = $rep;
1432            }
1433            $length = strlen ($part);
1434
1435            // If it is a short color value (#xxx) then convert it to long value (#xxxxxx)
1436            // (ODT does not support the short form)
1437            if ( $part [0] == '#' && $length == 4 ) {
1438                $part = '#'.$part [1].$part [1].$part [2].$part [2].$part [3].$part [3];
1439            } else {
1440                // If it is a CSS color name, get it's real color value
1441                /** @var helper_plugin_odt_csscolors $odt_colors */
1442                $odt_colors = plugin_load('helper', 'odt_csscolors');
1443                $color = $odt_colors->getColorValue ($part);
1444                if ( $part == 'black' || $color != '#000000' ) {
1445                    $part = $color;
1446                }
1447            }
1448
1449            if ( $length > 2 && $part [$length-2] == 'e' && $part [$length-1] == 'm' ) {
1450                $number = substr ($part, 0, $length-2);
1451                if ( is_numeric ($number) && !empty ($emValue) ) {
1452                    $part = ($number * $emValue).'pt';
1453                }
1454            }
1455
1456            // Replace px with pt (px does not seem to be supported by ODT)
1457            if ( $length > 2 && $part [$length-2] == 'p' && $part [$length-1] == 'x' ) {
1458                $part [$length-1] = 't';
1459            }
1460
1461            $value .= ' '.$part;
1462        }
1463        $value = trim($value);
1464
1465        return $value;
1466    }
1467
1468    /**
1469     * @return string
1470     */
1471    public function rulesToString () {
1472        $returnString = '';
1473        foreach ($this->rules as $rule) {
1474            $returnString .= $rule->toString ();
1475        }
1476        return $returnString;
1477    }
1478
1479    /**
1480     * @param $URL
1481     * @param $replacement
1482     * @return string
1483     */
1484    public function replaceURLPrefix ($URL, $replacement) {
1485        if ( !empty ($URL) && !empty ($replacement) ) {
1486            // Replace 'url(...)' with $replacement
1487            $URL = substr ($URL, 3);
1488            $URL = trim ($URL, '()');
1489            $URL = $replacement.$URL;
1490        }
1491        return $URL;
1492    }
1493
1494    /**
1495     * @param $callback
1496     */
1497    public function adjustLengthValues ($callback) {
1498        foreach ($this->rules as $rule) {
1499            $rule->adjustLengthValues ($callback);
1500        }
1501    }
1502}
1503
1504