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