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