1<?php 2 3/** 4 * Plugin Columns: Syntax & rendering 5 * 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 * @author Mykola Ostrovskyy <dwpforge@gmail.com> 8 * Based on plugin by Michael Arlt <michael.arlt [at] sk-schwanstetten [dot] de> 9 */ 10 11/* Must be run within Dokuwiki */ 12if(!defined('DOKU_INC')) die(); 13 14if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); 15require_once(DOKU_PLUGIN . 'syntax.php'); 16 17class syntax_plugin_columns extends DokuWiki_Syntax_Plugin { 18 19 private $mode; 20 private $lexerSyntax; 21 private $syntax; 22 private $xhtmlRenderer; 23 private $odtRenderer; 24 25 /** 26 * Constructor 27 */ 28 public function __construct() { 29 $this->mode = substr(get_class($this), 7); 30 31 $columns = $this->getColumnsTagName(); 32 $newColumn = $this->getNewColumnTagName(); 33 if ($this->getConf('wrapnewcol') == 1) { 34 $newColumnLexer = '<' . $newColumn . '(?:>|\s.*?>)'; 35 $newColumnHandler = '<' . $newColumn . '(.*?)>'; 36 } 37 else { 38 $newColumnLexer = $newColumn; 39 $newColumnHandler = $newColumn; 40 } 41 $enterLexer = '<' . $columns . '(?:>|\s.*?>)'; 42 $enterHandler = '<' . $columns . '(.*?)>'; 43 $exit = '<\/' . $columns . '>'; 44 45 $this->lexerSyntax['enter'] = $enterLexer; 46 $this->lexerSyntax['newcol'] = $newColumnLexer; 47 $this->lexerSyntax['exit'] = $exit; 48 49 $this->syntax[DOKU_LEXER_ENTER] = '/' . $enterHandler . '/'; 50 $this->syntax[DOKU_LEXER_MATCHED] = '/' . $newColumnHandler . '/'; 51 $this->syntax[DOKU_LEXER_EXIT] = '/' . $exit . '/'; 52 } 53 54 /** 55 * What kind of syntax are we? 56 */ 57 public function getType() { 58 return 'substition'; 59 } 60 61 public function getPType() { 62 return 'block'; 63 } 64 65 /** 66 * Where to sort in? 67 */ 68 public function getSort() { 69 return 65; 70 } 71 72 public function connectTo($mode) { 73 $this->Lexer->addSpecialPattern($this->lexerSyntax['enter'], $mode, $this->mode); 74 $this->Lexer->addSpecialPattern($this->lexerSyntax['newcol'], $mode, $this->mode); 75 $this->Lexer->addSpecialPattern($this->lexerSyntax['exit'], $mode, $this->mode); 76 } 77 78 /** 79 * Handle the match 80 */ 81 public function handle($match, $state, $pos, Doku_Handler $handler) { 82 foreach ($this->syntax as $state => $pattern) { 83 if (preg_match($pattern, $match, $data) == 1) { 84 break; 85 } 86 } 87 switch ($state) { 88 case DOKU_LEXER_ENTER: 89 case DOKU_LEXER_MATCHED: 90 return array($state, preg_split('/\s+/', $data[1], -1, PREG_SPLIT_NO_EMPTY)); 91 92 case DOKU_LEXER_EXIT: 93 return array($state, array()); 94 } 95 return false; 96 } 97 98 /** 99 * Create output 100 */ 101 public function render($mode, Doku_Renderer $renderer, $data) { 102 $columnsRenderer = $this->getRenderer($mode, $renderer); 103 104 if ($columnsRenderer != NULL) { 105 $columnsRenderer->render($data[0], $renderer, $data[1]); 106 return true; 107 } 108 return false; 109 } 110 111 /** 112 * 113 */ 114 private function getRenderer($mode, Doku_Renderer $renderer) { 115 switch ($mode) { 116 case 'xhtml': 117 if ($this->xhtmlRenderer == NULL) { 118 $this->xhtmlRenderer = new columns_renderer_xhtml(); 119 } 120 return $this->xhtmlRenderer; 121 122 case 'odt': 123 if ($this->odtRenderer == NULL) { 124 if (method_exists($renderer, 'getODTPropertiesFromElement')) { 125 $this->odtRenderer = new columns_renderer_odt_v2(); 126 } 127 else { 128 $this->odtRenderer = new columns_renderer_odt_v1(); 129 } 130 } 131 return $this->odtRenderer; 132 } 133 134 return NULL; 135 } 136 137 /** 138 * Returns columns tag 139 */ 140 private function getColumnsTagName() { 141 $tag = $this->getConf('kwcolumns'); 142 if ($tag == '') { 143 $tag = $this->getLang('kwcolumns'); 144 } 145 return $tag; 146 } 147 148 /** 149 * Returns new column tag 150 */ 151 private function getNewColumnTagName() { 152 $tag = $this->getConf('kwnewcol'); 153 if ($tag == '') { 154 $tag = $this->getLang('kwnewcol'); 155 } 156 return $tag; 157 } 158} 159 160/** 161 * Base class for columns rendering. 162 */ 163abstract class columns_renderer { 164 /** 165 * 166 */ 167 public function render($state, Doku_Renderer $renderer, $attribute) { 168 switch ($state) { 169 case DOKU_LEXER_ENTER: 170 $this->render_enter($renderer, $attribute); 171 break; 172 173 case DOKU_LEXER_MATCHED: 174 $this->render_matched($renderer, $attribute); 175 break; 176 177 case DOKU_LEXER_EXIT: 178 $this->render_exit($renderer, $attribute); 179 break; 180 } 181 } 182 183 abstract protected function render_enter(Doku_Renderer $renderer, $attribute); 184 abstract protected function render_matched(Doku_Renderer $renderer, $attribute); 185 abstract protected function render_exit(Doku_Renderer $renderer, $attribute); 186 187 /** 188 * 189 */ 190 protected function getAttribute($attribute, $name) { 191 $result = ''; 192 if (array_key_exists($name, $attribute)) { 193 $result = $attribute[$name]; 194 } 195 return $result; 196 } 197 198 /** 199 * 200 */ 201 protected function getStyle($attribute, $attributeName, $styleName = '') { 202 $result = $this->getAttribute($attribute, $attributeName); 203 if ($result != '') { 204 if ($styleName == '') { 205 $styleName = $attributeName; 206 } 207 $result = $styleName . ':' . $result . ';'; 208 } 209 return $result; 210 } 211} 212 213/** 214 * Class columns_renderer_xhtml 215 * @author LarsDW223 216 */ 217class columns_renderer_xhtml extends columns_renderer { 218 /** 219 * 220 */ 221 public function render($state, Doku_Renderer $renderer, $attribute) { 222 parent::render($state, $renderer, $attribute); 223 224 if ($state == 987 && method_exists($renderer, 'finishSectionEdit')) { 225 $renderer->finishSectionEdit($attribute); 226 } 227 } 228 229 /** 230 * 231 */ 232 protected function render_enter(Doku_Renderer $renderer, $attribute) { 233 $renderer->doc .= $this->renderTable($attribute) . DOKU_LF; 234 $renderer->doc .= '<tr>' . $this->renderTd($attribute) . DOKU_LF; 235 } 236 237 /** 238 * 239 */ 240 protected function render_matched(Doku_Renderer $renderer, $attribute) { 241 $renderer->doc .= '</td>' . $this->renderTd($attribute) . DOKU_LF; 242 } 243 244 /** 245 * 246 */ 247 protected function render_exit(Doku_Renderer $renderer, $attribute) { 248 $renderer->doc .= '</td></tr></table>' . DOKU_LF; 249 } 250 251 /** 252 * 253 */ 254 private function renderTable($attribute) { 255 $width = $this->getAttribute($attribute, 'table-width'); 256 if ($width != '') { 257 return '<table class="columns-plugin" style="width:' . $width . '">'; 258 } 259 else { 260 return '<table class="columns-plugin">'; 261 } 262 } 263 264 /** 265 * 266 */ 267 private function renderTd($attribute) { 268 $class[] = 'columns-plugin'; 269 $class[] = $this->getAttribute($attribute, 'class'); 270 $class[] = $this->getAttribute($attribute, 'text-align'); 271 $html = '<td class="' . implode(' ', array_filter($class)) . '"'; 272 $style = $this->getStyle($attribute, 'column-width', 'width'); 273 $style .= $this->getStyle($attribute, 'vertical-align'); 274 if ($style != '') { 275 $html .= ' style="' . $style . '"'; 276 } 277 return $html . '>'; 278 } 279} 280 281/** 282 * Class columns_renderer_odt_v1 283 */ 284class columns_renderer_odt_v1 extends columns_renderer { 285 /** 286 * 287 */ 288 protected function render_enter(Doku_Renderer $renderer, $attribute) { 289 $this->addOdtTableStyle($renderer, $attribute); 290 $this->addOdtColumnStyles($renderer, $attribute); 291 $this->renderOdtTableEnter($renderer, $attribute); 292 $this->renderOdtColumnEnter($renderer, $attribute); 293 } 294 295 /** 296 * 297 */ 298 protected function render_matched(Doku_Renderer $renderer, $attribute) { 299 $this->addOdtColumnStyles($renderer, $attribute); 300 $this->renderOdtColumnExit($renderer); 301 $this->renderOdtColumnEnter($renderer, $attribute); 302 } 303 304 /** 305 * 306 */ 307 protected function render_exit(Doku_Renderer $renderer, $attribute) { 308 $this->renderOdtColumnExit($renderer); 309 $this->renderOdtTableExit($renderer); 310 } 311 312 /** 313 * 314 */ 315 private function addOdtTableStyle(Doku_Renderer $renderer, $attribute) { 316 $styleName = $this->getOdtTableStyleName($this->getAttribute($attribute, 'block-id')); 317 $style = '<style:style style:name="' . $styleName . '" style:family="table">'; 318 $style .= '<style:table-properties'; 319 $width = $this->getAttribute($attribute, 'table-width'); 320 321 if (($width != '') && ($width != '100%')) { 322 $metrics = $this->getOdtMetrics($renderer->autostyles); 323 $style .= ' style:width="' . $this->getOdtAbsoluteWidth($metrics, $width) . '"'; 324 } 325 $align = ($width == '100%') ? 'margins' : 'left'; 326 $style .= ' table:align="' . $align . '"/>'; 327 $style .= '</style:style>'; 328 329 $renderer->autostyles[$styleName] = $style; 330 } 331 332 /** 333 * 334 */ 335 private function addOdtColumnStyles(Doku_Renderer $renderer, $attribute) { 336 $blockId = $this->getAttribute($attribute, 'block-id'); 337 $columnId = $this->getAttribute($attribute, 'column-id'); 338 $styleName = $this->getOdtTableStyleName($blockId, $columnId); 339 340 $style = '<style:style style:name="' . $styleName . '" style:family="table-column">'; 341 $style .= '<style:table-column-properties'; 342 $width = $this->getAttribute($attribute, 'column-width'); 343 344 if ($width != '') { 345 $metrics = $this->getOdtMetrics($renderer->autostyles); 346 $style .= ' style:column-width="' . $this->getOdtAbsoluteWidth($metrics, $width) . '"'; 347 } 348 $style .= '/>'; 349 $style .= '</style:style>'; 350 351 $renderer->autostyles[$styleName] = $style; 352 353 $styleName = $this->getOdtTableStyleName($blockId, $columnId, 1); 354 355 $style = '<style:style style:name="' . $styleName . '" style:family="table-cell">'; 356 $style .= '<style:table-cell-properties'; 357 $style .= ' fo:border="none"'; 358 $style .= ' fo:padding-top="0cm"'; 359 $style .= ' fo:padding-bottom="0cm"'; 360 361 switch ($this->getAttribute($attribute, 'class')) { 362 case 'first': 363 $style .= ' fo:padding-left="0cm"'; 364 $style .= ' fo:padding-right="0.4cm"'; 365 break; 366 367 case 'last': 368 $style .= ' fo:padding-left="0.4cm"'; 369 $style .= ' fo:padding-right="0cm"'; 370 break; 371 } 372 373 /* There seems to be no easy way to control horizontal alignment of text within 374 the column as fo:text-align aplies to individual paragraphs. */ 375 //TODO: $this->getAttribute($attribute, 'text-align'); 376 377 $align = $this->getAttribute($attribute, 'vertical-align'); 378 if ($align != '') { 379 $style .= ' style:vertical-align="' . $align . '"'; 380 } 381 else { 382 $style .= ' style:vertical-align="top"'; 383 } 384 385 $style .= '/>'; 386 $style .= '</style:style>'; 387 388 $renderer->autostyles[$styleName] = $style; 389 } 390 391 /** 392 * 393 */ 394 private function renderOdtTableEnter(Doku_Renderer $renderer, $attribute) { 395 $columns = $this->getAttribute($attribute, 'columns'); 396 $blockId = $this->getAttribute($attribute, 'block-id'); 397 $styleName = $this->getOdtTableStyleName($blockId); 398 399 $renderer->doc .= '<table:table table:style-name="' . $styleName . '">'; 400 for ($c = 0; $c < $columns; $c++) { 401 $styleName = $this->getOdtTableStyleName($blockId, $c + 1); 402 $renderer->doc .= '<table:table-column table:style-name="' . $styleName . '" />'; 403 } 404 $renderer->doc .= '<table:table-row>'; 405 } 406 407 /** 408 * 409 */ 410 private function renderOdtColumnEnter(Doku_Renderer $renderer, $attribute) { 411 $blockId = $this->getAttribute($attribute, 'block-id'); 412 $columnId = $this->getAttribute($attribute, 'column-id'); 413 $styleName = $this->getOdtTableStyleName($blockId, $columnId, 1); 414 $renderer->doc .= '<table:table-cell table:style-name="' . $styleName . '" office:value-type="string">'; 415 } 416 417 /** 418 * 419 */ 420 private function renderOdtColumnExit(Doku_Renderer $renderer) { 421 $renderer->doc .= '</table:table-cell>'; 422 } 423 424 /** 425 * 426 */ 427 private function renderOdtTableExit(Doku_Renderer $renderer) { 428 $renderer->doc .= '</table:table-row>'; 429 $renderer->doc .= '</table:table>'; 430 } 431 432 /** 433 * Convert relative units to absolute 434 */ 435 private function getOdtAbsoluteWidth($metrics, $width) { 436 if (preg_match('/([\d\.]+)(.+)/', $width, $match) == 1) { 437 switch ($match[2]) { 438 case '%': 439 /* Won't work for nested column blocks */ 440 $width = ($match[1] / 100 * $metrics['page-width']) . $metrics['page-width-units']; 441 break; 442 case 'em': 443 /* Rough estimate */ 444 $width = ($match[1] * 0.8 * $metrics['font-size']) . $metrics['font-size-units']; 445 break; 446 } 447 } 448 return $width; 449 } 450 451 /** 452 * 453 */ 454 private function getOdtTableStyleName($blockId, $columnId = 0, $cell = 0) { 455 $result = 'ColumnsBlock' . $blockId; 456 if ($columnId != 0) { 457 if ($columnId <= 26) { 458 $result .= '.' . chr(ord('A') + $columnId - 1); 459 } 460 else { 461 /* To unlikey to handle it properly */ 462 $result .= '.a'; 463 } 464 if ($cell != 0) { 465 $result .= $cell; 466 } 467 } 468 return $result; 469 } 470 471 /** 472 * 473 */ 474 private function getOdtMetrics($autoStyle) { 475 $result = array(); 476 if (array_key_exists('pm1', $autoStyle)) { 477 $style = $autoStyle['pm1']; 478 if (preg_match('/fo:page-width="([\d\.]+)(.+?)"/', $style, $match) == 1) { 479 $result['page-width'] = floatval($match[1]); 480 $result['page-width-units'] = $match[2]; 481 $units = $match[2]; 482 483 if (preg_match('/fo:margin-left="([\d\.]+)(.+?)"/', $style, $match) == 1) { 484 // TODO: Unit conversion 485 if ($match[2] == $units) { 486 $result['page-width'] -= floatval($match[1]); 487 } 488 } 489 if (preg_match('/fo:margin-right="([\d\.]+)(.+?)"/', $style, $match) == 1) { 490 if ($match[2] == $units) { 491 $result['page-width'] -= floatval($match[1]); 492 } 493 } 494 } 495 } 496 if (!array_key_exists('page-width', $result)) { 497 $result['page-width'] = 17; 498 $result['page-width-units'] = 'cm'; 499 } 500 501 /* There seems to be no easy way to get default font size apart from loading styles.xml. */ 502 $styles = io_readFile(DOKU_PLUGIN . 'odt/styles.xml'); 503 if (preg_match('/<style:default-style style:family="paragraph">(.+?)<\/style:default-style>/s', $styles, $match) == 1) { 504 if (preg_match('/<style:text-properties(.+?)>/', $match[1], $match) == 1) { 505 if (preg_match('/fo:font-size="([\d\.]+)(.+?)"/', $match[1], $match) == 1) { 506 $result['font-size'] = floatval($match[1]); 507 $result['font-size-units'] = $match[2]; 508 } 509 } 510 } 511 if (!array_key_exists('font-size', $result)) { 512 $result['font-size'] = 12; 513 $result['font-size-units'] = 'pt'; 514 } 515 return $result; 516 } 517} 518 519/** 520 * Class columns_renderer_odt_v2 521 * @author LarsDW223 522 */ 523class columns_renderer_odt_v2 extends columns_renderer { 524 /** 525 * 526 */ 527 protected function render_enter(Doku_Renderer $renderer, $attribute) { 528 $this->renderOdtTableEnter($renderer, $attribute); 529 $this->renderOdtColumnEnter($renderer, $attribute); 530 } 531 532 /** 533 * 534 */ 535 protected function render_matched(Doku_Renderer $renderer, $attribute) { 536 $this->renderOdtColumnExit($renderer); 537 $this->renderOdtColumnEnter($renderer, $attribute); 538 } 539 540 /** 541 * 542 */ 543 protected function render_exit(Doku_Renderer $renderer, $attribute) { 544 $this->renderOdtColumnExit($renderer); 545 $this->renderOdtTableExit($renderer); 546 } 547 548 /** 549 * 550 */ 551 private function renderOdtTableEnter(Doku_Renderer $renderer, $attribute) { 552 $properties = array(); 553 $properties ['width'] = $this->getAttribute($attribute, 'table-width'); 554 $properties ['align'] = 'left'; 555 $renderer->_odtTableOpenUseProperties ($properties); 556 $renderer->tablerow_open(); 557 } 558 559 /** 560 * 561 */ 562 private function renderOdtColumnEnter(Doku_Renderer $renderer, $attribute) { 563 $properties = array(); 564 $properties ['width'] = $this->getAttribute($attribute, 'column-width'); 565 $properties ['border'] = 'none'; 566 $properties ['padding-top'] = '0cm'; 567 $properties ['padding-bottom'] = '0cm'; 568 switch ($this->getAttribute($attribute, 'class')) { 569 case 'first': 570 $properties ['padding-left'] = '0cm'; 571 $properties ['padding-right'] = '0.4cm'; 572 break; 573 574 case 'last': 575 $properties ['padding-left'] = '0.4cm'; 576 $properties ['padding-right'] = '0cm'; 577 break; 578 } 579 $align = $this->getAttribute($attribute, 'vertical-align'); 580 if ($align != '') { 581 $properties ['vertical-align'] = $align; 582 } 583 else { 584 $properties ['vertical-align'] = 'top'; 585 } 586 $align = $this->getAttribute($attribute, 'text-align'); 587 if ($align != '') { 588 $properties ['text-align'] = $align; 589 } 590 else { 591 $properties ['text-align'] = 'left'; 592 } 593 594 $renderer->_odtTableCellOpenUseProperties($properties); 595 } 596 597 /** 598 * 599 */ 600 private function renderOdtColumnExit(Doku_Renderer $renderer) { 601 $renderer->tablecell_close(); 602 } 603 604 /** 605 * 606 */ 607 private function renderOdtTableExit(Doku_Renderer $renderer) { 608 $renderer->tablerow_close(); 609 $renderer->table_close(); 610 } 611} 612