1<?php 2/** 3 * ODTParagraphStyle: class for ODT paragraph styles. 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author LarsDW223 7 */ 8 9require_once DOKU_PLUGIN.'odt/ODT/XMLUtil.php'; 10require_once 'ODTStyle.php'; 11 12ODTStyleStyle::register('ODTParagraphStyle'); 13 14/** 15 * The ODTParagraphStyle class 16 */ 17class ODTParagraphStyle extends ODTStyleStyle 18{ 19 static $paragraph_fields = array( 20 'line-height' => array ('fo:line-height', 'paragraph', true), 21 'line-height-at-least' => array ('style:line-height-at-least', 'paragraph', true), 22 'line-spacing' => array ('style:line-spacing', 'paragraph', true), 23 'font-independent-line-spacing' => array ('style:font-independent-line-spacing', 'paragraph', true), 24 'text-align' => array ('fo:text-align', 'paragraph', true), 25 'text-align-last' => array ('fo:text-align-last', 'paragraph', true), 26 'justify-single-word' => array ('style:justify-single-word', 'paragraph', true), 27 'keep-together' => array ('fo:keep-together', 'paragraph', true), 28 'widows' => array ('fo:widows', 'paragraph', true), 29 'orphans' => array ('fo:orphans', 'paragraph', true), 30 'tab-stop-distance' => array ('style:tab-stop-distance', 'paragraph', true), 31 'hyphenation-keep' => array ('fo:hyphenation-keep', 'paragraph', true), 32 'hyphenation-ladder-count' => array ('fo:hyphenation-ladder-count', 'paragraph', true), 33 'register-true' => array ('style:register-true', 'paragraph', true), 34 'text-indent' => array ('fo:text-indent', 'paragraph', true), 35 'auto-text-indent' => array ('style:auto-text-indent', 'paragraph', true), 36 'margin' => array ('fo:margin', 'paragraph', true), 37 'margin-top' => array ('fo:margin-top', 'paragraph', true), 38 'margin-right' => array ('fo:margin-right', 'paragraph', true), 39 'margin-bottom' => array ('fo:margin-bottom', 'paragraph', true), 40 'margin-left' => array ('fo:margin-left', 'paragraph', true), 41 'break-before' => array ('fo:break-before', 'paragraph', true), 42 'break-after' => array ('fo:break-after', 'paragraph', true), 43 'background-color' => array ('fo:background-color', 'paragraph', true), 44 'border' => array ('fo:border', 'paragraph', true), 45 'border-top' => array ('fo:border-top', 'paragraph', true), 46 'border-right' => array ('fo:border-right', 'paragraph', true), 47 'border-bottom' => array ('fo:border-bottom', 'paragraph', true), 48 'border-left' => array ('fo:border-left', 'paragraph', true), 49 'border-line-width' => array ('style:border-line-width', 'paragraph', true), 50 'border-line-width-top' => array ('style:border-line-width-top', 'paragraph', true), 51 'border-line-width-bottom' => array ('style:border-line-width-bottom', 'paragraph', true), 52 'border-line-width-left' => array ('style:border-line-width-left', 'paragraph', true), 53 'border-line-width-right' => array ('style:border-line-width-right', 'paragraph', true), 54 'join-border' => array ('style:join-border', 'paragraph', true), 55 'padding' => array ('fo:padding', 'paragraph', true), 56 'padding-top' => array ('fo:padding-top', 'paragraph', true), 57 'padding-bottom' => array ('fo:padding-bottom', 'paragraph', true), 58 'padding-left' => array ('fo:padding-left', 'paragraph', true), 59 'padding-right' => array ('fo:padding-right', 'paragraph', true), 60 'shadow' => array ('style:shadow', 'paragraph', true), 61 'keep-with-next' => array ('fo:keep-with-next', 'paragraph', true), 62 'number-lines' => array ('text:number-lines', 'paragraph', true), 63 'line-number' => array ('text:line-number', 'paragraph', true), 64 'text-autospace' => array ('style:text-autospace', 'paragraph', true), 65 'punctuation-wrap' => array ('style:punctuation-wrap', 'paragraph', true), 66 'line-break' => array ('style:line-break', 'paragraph', true), 67 'vertical-align' => array ('style:vertical-align', 'paragraph', true), 68 'writing-mode' => array ('style:writing-mode', 'paragraph', true), 69 'writing-mode-automatic' => array ('style:writing-mode-automatic', 'paragraph', true), 70 'snap-to-layout-grid' => array ('style:snap-to-layout-grid', 'paragraph', true), 71 'page-number' => array ('style:page-number', 'paragraph', true), 72 'background-transparency' => array ('style:background-transparency', 'paragraph', true), 73 ); 74 75 // Additional fields for child element tab-stop. 76 static $tab_stop_fields = array( 77 'style-position' => array ('style:position', 'tab-stop', true), 78 'style-type' => array ('style:type', 'tab-stop', true), 79 'style-leader-type' => array ('style:leader-type', 'tab-stop', true), 80 'style-leader-style' => array ('style:leader-style', 'tab-stop', true), 81 'style-leader-width' => array ('style:leader-width', 'tab-stop', true), 82 'style-leader-color' => array ('style:leader-color', 'tab-stop', true), 83 'style-leader-text' => array ('style:leader-text', 'tab-stop', true), 84 ); 85 86 protected $style_properties = array(); 87 protected $text_properties = array(); 88 protected $tab_stops = array(); 89 90 /** 91 * Constructor. 92 */ 93 public function __construct() { 94 parent::__construct(); 95 } 96 97 /** 98 * Set style properties by importing values from a properties array. 99 * Properties might be disabled by setting them in $disabled. 100 * The style must have been previously created. 101 * 102 * @param $properties Properties to be imported 103 * @param $disabled Properties to be ignored 104 */ 105 public function importProperties($properties, $disabled=array()) { 106 foreach ($properties as $property => $value) { 107 if ($disabled [$property] == 0) { 108 $this->setProperty($property, $value); 109 } 110 } 111 } 112 113 /** 114 * Check if a style is a common style. 115 * 116 * @return bool Is common style 117 */ 118 public function mustBeCommonStyle() { 119 return false; 120 } 121 122 /** 123 * Get the style family of a style. 124 * 125 * @return string Style family 126 */ 127 static public function getFamily() { 128 return 'paragraph'; 129 } 130 131 /** 132 * Set a property. 133 * 134 * @param $property The name of the property to set 135 * @param $value New value to set 136 */ 137 public function setProperty($property, $value) { 138 $style_fields = ODTStyleStyle::getStyleProperties (); 139 if (array_key_exists ($property, $style_fields)) { 140 $this->setPropertyInternal 141 ($property, $style_fields [$property][0], $value, $style_fields [$property][1], $this->style_properties); 142 return; 143 } 144 // FIXME: currently with setProperty there always will only be one tab-stop. 145 // Maybe in the future supply a function "add tab stop" or something. 146 if (array_key_exists ($property, self::$tab_stop_fields)) { 147 if (!isset($this->tab_stops [0])) { 148 $this->tab_stops [0] = array(); 149 } 150 $this->setPropertyInternal 151 ($property, self::$tab_stop_fields [$property][0], $value, self::$tab_stop_fields [$property][1], $this->tab_stops[0]); 152 return; 153 } 154 // Compare with paragraph fields before text fields first! 155 // So, paragraph properties get precedence. 156 if (array_key_exists ($property, self::$paragraph_fields)) { 157 $this->setPropertyInternal 158 ($property, self::$paragraph_fields [$property][0], $value, self::$paragraph_fields [$property][1]); 159 return; 160 } 161 $text_fields = ODTTextStyle::getTextProperties (); 162 if (array_key_exists ($property, $text_fields)) { 163 $this->setPropertyInternal 164 ($property, $text_fields [$property][0], $value, $text_fields [$property][1], $this->text_properties); 165 return; 166 } 167 } 168 169 /** 170 * Get the value of a property. 171 * 172 * @param $property The property name 173 * @return string The current value of the property 174 */ 175 public function getProperty($property) { 176 $style_fields = ODTStyleStyle::getStyleProperties (); 177 if (array_key_exists ($property, $style_fields)) { 178 return $this->style_properties [$property]['value']; 179 } 180 $paragraph_fields = self::$paragraph_fields; 181 if (array_key_exists ($property, $paragraph_fields)) { 182 return parent::getProperty($property); 183 } 184 $text_fields = ODTTextStyle::getTextProperties (); 185 if (array_key_exists ($property, $text_fields)) { 186 return $this->text_properties [$property]['value']; 187 } 188 return parent::getProperty($property); 189 } 190 191 /** 192 * Create new style by importing ODT style definition. 193 * 194 * @param $xmlCode Style definition in ODT XML format 195 * @return ODTStyle New specific style 196 */ 197 static public function importODTStyle($xmlCode) { 198 $style = new ODTParagraphStyle(); 199 $attrs = 0; 200 201 $open = XMLUtil::getElementOpenTag('style:style', $xmlCode); 202 if (!empty($open)) { 203 $attrs += $style->importODTStyleInternal(ODTStyleStyle::getStyleProperties (), $open, $style->style_properties); 204 } else { 205 $open = XMLUtil::getElementOpenTag('style:default-style', $xmlCode); 206 if (!empty($open)) { 207 $style->setDefault(true); 208 $attrs += $style->importODTStyleInternal(ODTStyleStyle::getStyleProperties (), $open, $style->style_properties); 209 } 210 } 211 212 $open = XMLUtil::getElementOpenTag('style:paragraph-properties', $xmlCode); 213 if (!empty($open)) { 214 $attrs += $style->importODTStyleInternal(self::$paragraph_fields, $xmlCode); 215 } 216 217 $open = XMLUtil::getElementOpenTag('style:text-properties', $xmlCode); 218 if (!empty($open)) { 219 $attrs += $style->importODTStyleInternal(ODTTextStyle::getTextProperties (), $open, $style->text_properties); 220 } 221 222 // Get all tab-stops. 223 $tabs = XMLUtil::getElementContent('style:tab-stops', $xmlCode); 224 if (isset($tabs)) { 225 $max = strlen($tabs); 226 $pos = 0; 227 $index = 0; 228 $tab = XMLUtil::getElement('style:tab-stop', $tabs, $end); 229 $pos = $end; 230 while (isset($tab)) { 231 $style->tab_stops [$index] = array(); 232 $attrs += $style->importODTStyleInternal(self::$tab_stop_fields, $tab, $style->tab_stops [$index]); 233 $index++; 234 $tab = XMLUtil::getElement('style:tab-stop', substr ($tabs, $pos), $end); 235 $pos += $end; 236 } 237 } 238 239 // If style has no meaningfull content then throw it away 240 if ( $attrs == 0 ) { 241 return NULL; 242 } 243 244 return $style; 245 } 246 247 static public function getParagraphProperties () { 248 return self::$paragraph_fields; 249 } 250 251 /** 252 * Encode current style values in a string and return it. 253 * 254 * @return string ODT XML encoded style 255 */ 256 public function toString() { 257 $style = ''; 258 $para_props = ''; 259 $text_props = ''; 260 $tab_stops_props = ''; 261 262 // Get style contents 263 foreach ($this->style_properties as $property => $items) { 264 if ($items ['odt_property'] != 'style:family') { 265 $style .= $items ['odt_property'].'="'.$items ['value'].'" '; 266 } 267 } 268 $style .= 'style:family="'.$this->getFamily().'" '; 269 270 // Get paragraph properties ODT properties 271 foreach ($this->properties as $property => $items) { 272 $para_props .= $items ['odt_property'].'="'.$items ['value'].'" '; 273 } 274 275 // Get text properties 276 foreach ($this->text_properties as $property => $items) { 277 $text_props .= $items ['odt_property'].'="'.$items ['value'].'" '; 278 } 279 280 // Get tab-stops properties 281 for ($index = 0 ; $index < count($this->tab_stops) ; $index++) { 282 $tab_stops_props .= '<style:tab-stop '; 283 foreach ($this->tab_stops[$index] as $property => $items) { 284 $tab_stops_props .= $items ['odt_property'].'="'.$items ['value'].'" '; 285 } 286 $tab_stops_props .= '/>'; 287 } 288 289 // Build style. 290 if (!$this->isDefault()) { 291 $style = '<style:style '.$style.">\n"; 292 } else { 293 $style = '<style:default-style '.$style.">\n"; 294 } 295 if (!empty($para_props) || !empty($tab_stops_props)) { 296 if (empty($tab_stops_props)) { 297 $style .= '<style:paragraph-properties '.$para_props."/>\n"; 298 } else { 299 $style .= '<style:paragraph-properties '.$para_props.">\n"; 300 $style .= '<style:tab-stops>'."\n"; 301 $style .= $tab_stops_props."\n"; 302 $style .= '</style:tab-stops>'."\n"; 303 $style .= '</style:paragraph-properties>'."\n"; 304 } 305 } 306 if (!empty($text_props)) { 307 $style .= '<style:text-properties '.$text_props."/>\n"; 308 } 309 if (!$this->isDefault()) { 310 $style .= '</style:style>'."\n"; 311 } else { 312 $style .= '</style:default-style>'."\n"; 313 } 314 return $style; 315 } 316 317 /** 318 * This function creates a paragraph style using the style as set in the assoziative array $properties. 319 * The parameters in the array should be named as the CSS property names e.g. 'color' or 'background-color'. 320 * Properties which shall not be used in the style can be disabled by setting the value in disabled_props 321 * to 1 e.g. $disabled_props ['color'] = 1 would block the usage of the color property. 322 * 323 * The currently supported properties are: 324 * background-color, color, font-style, font-weight, font-size, border, font-family, font-variant, letter-spacing, 325 * vertical-align, line-height, background-image 326 * 327 * The function returns the name of the new style or NULL if all relevant properties are empty. 328 * 329 * @author LarsDW223 330 * @param $properties 331 * @param null $disabled_props 332 * @return ODTParagraphStyle or NULL 333 */ 334 public static function createParagraphStyle(array $properties, array $disabled_props = NULL, ODTDocument $doc=NULL){ 335 // Convert 'text-decoration'. 336 if ( $properties ['text-decoration'] == 'line-through' ) { 337 $properties ['text-line-through-style'] = 'solid'; 338 } 339 if ( $properties ['text-decoration'] == 'underline' ) { 340 $properties ['text-underline-style'] = 'solid'; 341 } 342 if ( $properties ['text-decoration'] == 'overline' ) { 343 $properties ['text-overline-style'] = 'solid'; 344 } 345 346 // If the property 'vertical-align' has the value 'sub' or 'super' 347 // then for ODT it needs to be converted to the corresponding 'text-position' property. 348 // Replace sub and super with text-position. 349 $valign = $properties ['vertical-align']; 350 if (!empty($valign)) { 351 if ( $valign == 'sub' ) { 352 $properties ['text-position'] = '-33% 100%'; 353 unset($properties ['vertical-align']); 354 } elseif ( $valign == 'super' ) { 355 $properties ['text-position'] = '33% 100%'; 356 unset($properties ['vertical-align']); 357 } 358 } 359 360 // Separate country from language 361 $lang = $properties ['lang']; 362 $country = $properties ['country']; 363 if ( !empty($lang) ) { 364 $parts = preg_split ('/-/', $lang); 365 $lang = $parts [0]; 366 $country = $parts [1]; 367 $properties ['country'] = trim($country); 368 $properties ['lang'] = trim($lang); 369 } 370 if (!empty($properties ['country'])) { 371 if (empty($properties ['country-asian'])) { 372 $properties ['country-asian'] = $properties ['country']; 373 } 374 if (empty($properties ['country-complex'])) { 375 $properties ['country-complex'] = $properties ['country']; 376 } 377 } 378 379 // Always set 'auto-text-indent = false' if 'text-indent' is set. 380 if (!empty($properties ['text-indent'])) { 381 $properties ['auto-text-indent'] = 'false'; 382 383 $length = strlen ($properties ['text-indent']); 384 if ( $length > 0 && $properties ['text-indent'] [$length-1] == '%' && isset($doc) ) { 385 // Percentage value needs to be converted to absolute value. 386 // ODT standard says that percentage value should work if used in a common style. 387 // This did not work with LibreOffice 4.4.3.2. 388 $value = trim ($properties ['text-indent'], '%'); 389 $properties ['text-indent'] = $doc->getAbsWidthMindMargins ($value).'cm'; 390 } 391 } 392 393 // Eventually create parent for font-size 394 $save = $disabled_props ['font-size']; 395 $odt_fo_size = ''; 396 if ( empty ($disabled_props ['font-size']) ) { 397 $odt_fo_size = $properties ['font-size']; 398 } 399 $parent = ''; 400 $length = strlen ($odt_fo_size); 401 if ( $length > 0 && $odt_fo_size [$length-1] == '%' && isset($doc)) { 402 // A font-size in percent is only supported in common style definitions, not in automatic 403 // styles. Create a common style and set it as parent for this automatic style. 404 $name = 'Size'.trim ($odt_fo_size, '%').'pc'; 405 $style_obj = ODTTextStyle::createSizeOnlyTextStyle ($name, $odt_fo_size); 406 $doc->addStyle($style_obj); 407 $parent = $style_obj->getProperty('style-name'); 408 if (!empty($parent)) { 409 $properties ['style-parent'] = $parent; 410 } 411 } 412 413 // Create style name (if not given). 414 $style_name = $properties ['style-name']; 415 if ( empty($style_name) ) { 416 $style_name = self::getNewStylename ('Paragraph'); 417 $properties ['style-name'] = $style_name; 418 } 419 420 // FIXME: fix missing tab stop handling... 421 //case 'tab-stop': 422 // $tab .= $params [$property]['name'].'="'.$value.'" '; 423 // $tab .= self::writeExtensionNames ($params [$property]['name'], $value); 424 // break; 425 426 // Create empty paragraph style. 427 $object = new ODTParagraphStyle(); 428 if (!isset($object)) { 429 return NULL; 430 } 431 432 // Import our properties 433 $object->importProperties($properties, $disabled_props); 434 435 // Restore $disabled_props 436 $disabled_props ['font-size'] = $save; 437 return $object; 438 } 439 440 /** 441 * Simple helper function for creating a paragrapg style for a pagebreak. 442 * 443 * @author LarsDW223 444 * 445 * @param string $parent Name of the parent style to set 446 * @param string $before Pagebreak before or after? 447 * @return ODTParagraphStyle 448 */ 449 public static function createPagebreakStyle($style_name, $parent=NULL,$before=true) { 450 $properties = array(); 451 $properties ['style-name'] = $style_name; 452 if ( !empty($parent) ) { 453 $properties ['style-parent'] = $parent; 454 } 455 if ($before == true ) { 456 $properties ['break-before'] = 'page'; 457 } else { 458 $properties ['break-after'] = 'page'; 459 } 460 return self::createParagraphStyle($properties); 461 } 462 463 /** 464 * Set a property. 465 * 466 * @param $property The name of the property to set 467 * @param $value New value to set 468 */ 469 public static function copyLayoutProperties(ODTParagraphStyle $source, ODTParagraphStyle $dest, array $disabled=NULL) { 470 // DO NOT COPY STYLE FIELDS/PROPERTIES 471 472 // Copy $tab_stop_fields 473 foreach (self::$tab_stop_fields as $property => $fields) { 474 $value = $source->getProperty($property); 475 if (isset($value) && $disabled [$property] == 0) { 476 $dest -> setProperty($property, $value); 477 } 478 } 479 480 // Copy $paragraph_fields 481 foreach (self::$paragraph_fields as $property => $fields) { 482 $value = $source->getProperty($property); 483 if (isset($value) && $disabled [$property] == 0) { 484 $dest -> setProperty($property, $value); 485 } 486 } 487 488 // Copy $text_fields 489 $text_fields = ODTTextStyle::getTextProperties (); 490 foreach ($text_fields as $property => $fields) { 491 $value = $source->getProperty($property); 492 if (isset($value) && $disabled [$property] == 0) { 493 $dest -> setProperty($property, $value); 494 } 495 } 496 } 497} 498