1<?php 2 3/** 4 * Plugin Columns: Layout parser 5 * 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 * @author Mykola Ostrovskyy <dwpforge@gmail.com> 8 */ 9 10/* Must be run within Dokuwiki */ 11if(!defined('DOKU_INC')) die(); 12 13if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); 14require_once(DOKU_PLUGIN . 'action.php'); 15require_once(DOKU_PLUGIN . 'columns/rewriter.php'); 16 17class action_plugin_columns extends DokuWiki_Action_Plugin { 18 19 private $block; 20 private $currentBlock; 21 private $currentSectionLevel; 22 private $sectionEdit; 23 24 /** 25 * Register callbacks 26 */ 27 public function register(Doku_Event_Handler $controller) { 28 $controller->register_hook('PARSER_HANDLER_DONE', 'AFTER', $this, 'handle'); 29 } 30 31 /** 32 * 33 */ 34 public function handle(&$event, $param) { 35 $this->reset(); 36 $this->buildLayout($event); 37 $rewriter = new instruction_rewriter(); 38 foreach ($this->block as $block) { 39 $block->processAttributes($event); 40 $rewriter->addCorrections($block->getCorrections()); 41 } 42 $rewriter->process($event->data->calls); 43 } 44 45 /** 46 * Find all columns instructions and construct columns layout based on them 47 */ 48 private function buildLayout(&$event) { 49 $calls = count($event->data->calls); 50 for ($c = 0; $c < $calls; $c++) { 51 $call =& $event->data->calls[$c]; 52 switch ($call[0]) { 53 case 'section_open': 54 $this->currentSectionLevel = $call[1][0]; 55 $this->currentBlock->openSection(); 56 break; 57 58 case 'section_close': 59 $this->currentBlock->closeSection($c); 60 break; 61 62 case 'plugin': 63 if ($call[1][0] == 'columns') { 64 $this->handleColumns($c, $call[1][1][0], $this->detectSectionEdit($event->data->calls, $c)); 65 } 66 break; 67 } 68 } 69 } 70 71 /** 72 * Reset internal state 73 */ 74 private function reset() { 75 $this->block = array(); 76 $this->block[0] = new columns_root_block(); 77 $this->currentBlock = $this->block[0]; 78 $this->currentSectionLevel = 0; 79 $this->sectionEdit = array(); 80 } 81 82 /** 83 * 84 */ 85 private function detectSectionEdit($call, $start) { 86 $result = null; 87 $calls = count($call); 88 for ($c = $start + 1; $c < $calls; $c++) { 89 switch ($call[$c][0]) { 90 case 'section_close': 91 case 'p_open': 92 case 'p_close': 93 /* Skip these instructions */ 94 break; 95 96 case 'header': 97 if (end($this->sectionEdit) != $c) { 98 $this->sectionEdit[] = $c; 99 $result = $call[$c][2]; 100 } 101 break 2; 102 103 case 'plugin': 104 if ($call[$c][1][0] == 'columns') { 105 break; 106 } else { 107 break 2; 108 } 109 110 default: 111 break 2; 112 } 113 } 114 return $result; 115 } 116 117 /** 118 * 119 */ 120 private function handleColumns($callIndex, $state, $sectionEdit) { 121 switch ($state) { 122 case DOKU_LEXER_ENTER: 123 $this->currentBlock = new columns_block(count($this->block), $this->currentBlock); 124 $this->currentBlock->addColumn($callIndex, $this->currentSectionLevel); 125 $this->currentBlock->startSection($sectionEdit); 126 $this->block[] = $this->currentBlock; 127 break; 128 129 case DOKU_LEXER_MATCHED: 130 $this->currentBlock->addColumn($callIndex, $this->currentSectionLevel); 131 $this->currentBlock->startSection($sectionEdit); 132 break; 133 134 case DOKU_LEXER_EXIT: 135 $this->currentBlock->endSection($sectionEdit); 136 $this->currentBlock->close($callIndex); 137 $this->currentBlock = $this->currentBlock->getParent(); 138 break; 139 } 140 } 141} 142 143class columns_root_block { 144 145 private $sectionLevel; 146 private $call; 147 148 /** 149 * Constructor 150 */ 151 public function __construct() { 152 $this->sectionLevel = 0; 153 $this->call = array(); 154 } 155 156 /** 157 * 158 */ 159 public function getParent() { 160 return $this; 161 } 162 163 /** 164 * Collect stray <newcolumn> tags 165 */ 166 public function addColumn($callIndex, $sectionLevel) { 167 $this->call[] = $callIndex; 168 } 169 170 /** 171 * 172 */ 173 public function openSection() { 174 $this->sectionLevel++; 175 } 176 177 /** 178 * 179 */ 180 public function closeSection($callIndex) { 181 if ($this->sectionLevel > 0) { 182 $this->sectionLevel--; 183 } 184 else { 185 $this->call[] = $callIndex; 186 } 187 } 188 189 /** 190 * 191 */ 192 public function startSection($callInfo) { 193 } 194 195 /** 196 * 197 */ 198 public function endSection($callInfo) { 199 } 200 201 /** 202 * Collect stray </colums> tags 203 */ 204 public function close($callIndex) { 205 $this->call[] = $callIndex; 206 } 207 208 /** 209 * 210 */ 211 public function processAttributes(&$event) { 212 } 213 214 /** 215 * Delete all captured tags 216 */ 217 public function getCorrections() { 218 $correction = array(); 219 foreach ($this->call as $call) { 220 $correction[] = new instruction_rewriter_delete($call); 221 } 222 return $correction; 223 } 224} 225 226class columns_block { 227 228 private $id; 229 private $parent; 230 private $column; 231 private $currentColumn; 232 private $closed; 233 234 /** 235 * Constructor 236 */ 237 public function __construct($id, $parent) { 238 $this->id = $id; 239 $this->parent = $parent; 240 $this->column = array(); 241 $this->currentColumn = null; 242 $this->closed = false; 243 } 244 245 /** 246 * 247 */ 248 public function getParent() { 249 return $this->parent; 250 } 251 252 /** 253 * 254 */ 255 public function addColumn($callIndex, $sectionLevel) { 256 if ($this->currentColumn != null) { 257 $this->currentColumn->close($callIndex); 258 } 259 $this->currentColumn = new columns_column($callIndex, $sectionLevel); 260 $this->column[] = $this->currentColumn; 261 } 262 263 /** 264 * 265 */ 266 public function openSection() { 267 $this->currentColumn->openSection(); 268 } 269 270 /** 271 * 272 */ 273 public function closeSection($callIndex) { 274 $this->currentColumn->closeSection($callIndex); 275 } 276 277 /** 278 * 279 */ 280 public function startSection($callInfo) { 281 $this->currentColumn->startSection($callInfo); 282 } 283 284 /** 285 * 286 */ 287 public function endSection($callInfo) { 288 $this->currentColumn->endSection($callInfo); 289 } 290 291 /** 292 * 293 */ 294 public function close($callIndex) { 295 $this->currentColumn->close($callIndex); 296 $this->closed = true; 297 } 298 299 /** 300 * Convert raw attributes and layout information into column attributes 301 */ 302 public function processAttributes(&$event) { 303 $columns = count($this->column); 304 for ($c = 0; $c < $columns; $c++) { 305 $call =& $event->data->calls[$this->column[$c]->getOpenCall()]; 306 if ($c == 0) { 307 $this->loadBlockAttributes($call[1][1][1]); 308 $this->column[0]->addAttribute('columns', $columns); 309 $this->column[0]->addAttribute('class', 'first'); 310 } 311 else { 312 $this->loadColumnAttributes($c, $call[1][1][1]); 313 if ($c == ($columns - 1)) { 314 $this->column[$c]->addAttribute('class', 'last'); 315 } 316 } 317 $this->column[$c]->addAttribute('block-id', $this->id); 318 $this->column[$c]->addAttribute('column-id', $c + 1); 319 $call[1][1][1] = $this->column[$c]->getAttributes(); 320 } 321 } 322 323 /** 324 * Convert raw attributes into column attributes 325 */ 326 private function loadBlockAttributes($attribute) { 327 $column = -1; 328 $nextColumn = -1; 329 foreach ($attribute as $a) { 330 list($name, $temp) = $this->parseAttribute($a); 331 if ($name == 'width') { 332 if (($column == -1) && array_key_exists('column-width', $temp)) { 333 $this->column[0]->addAttribute('table-width', $temp['column-width']); 334 } 335 $nextColumn = $column + 1; 336 } 337 if (($column >= 0) && ($column < count($this->column))) { 338 $this->column[$column]->addAttributes($temp); 339 } 340 $column = $nextColumn; 341 } 342 } 343 344 /** 345 * Convert raw attributes into column attributes 346 */ 347 private function loadColumnAttributes($column, $attribute) { 348 foreach ($attribute as $a) { 349 list($name, $temp) = $this->parseAttribute($a); 350 $this->column[$column]->addAttributes($temp); 351 } 352 } 353 354 /** 355 * 356 */ 357 private function parseAttribute($attribute) { 358 static $syntax = array( 359 '/^left|right|center|justify$/' => 'text-align', 360 '/^top|middle|bottom$/' => 'vertical-align', 361 '/^[lrcjtmb]{1,2}$/' => 'align', 362 '/^continue|\.{3}$/' => 'continue', 363 '/^(\*?)((?:-|(?:\d+\.?|\d*\.\d+)(?:%|em|px|cm|mm|in|pt)))(\*?)$/' => 'width' 364 ); 365 $result = array(); 366 $attributeName = ''; 367 foreach ($syntax as $pattern => $name) { 368 if (preg_match($pattern, $attribute, $match) == 1) { 369 $attributeName = $name; 370 break; 371 } 372 } 373 switch ($attributeName) { 374 case 'text-align': 375 case 'vertical-align': 376 $result[$attributeName] = $match[0]; 377 break; 378 379 case 'align': 380 $result = $this->parseAlignAttribute($match[0]); 381 break; 382 383 case 'continue': 384 $result[$attributeName] = 'on'; 385 break; 386 387 case 'width': 388 $result = $this->parseWidthAttribute($match); 389 break; 390 } 391 return array($attributeName, $result); 392 } 393 394 /** 395 * 396 */ 397 private function parseAlignAttribute($syntax) { 398 $result = array(); 399 $align1 = $this->getAlignStyle($syntax[0]); 400 if (strlen($syntax) == 2) { 401 $align2 = $this->getAlignStyle($syntax[1]); 402 if ($align1 != $align2) { 403 $result[$align1] = $this->getAlignment($syntax[0]); 404 $result[$align2] = $this->getAlignment($syntax[1]); 405 } 406 } 407 else { 408 $result[$align1] = $this->getAlignment($syntax[0]); 409 } 410 return $result; 411 } 412 413 /** 414 * 415 */ 416 private function getAlignStyle($align) { 417 return preg_match('/[lrcj]/', $align) ? 'text-align' : 'vertical-align'; 418 } 419 420 /** 421 * 422 */ 423 private function parseWidthAttribute($syntax) { 424 $result = array(); 425 if ($syntax[2] != '-') { 426 $result['column-width'] = $syntax[2]; 427 } 428 $align = $syntax[1] . '-' . $syntax[3]; 429 if ($align != '-') { 430 $result['text-align'] = $this->getAlignment($align); 431 } 432 return $result; 433 } 434 435 /** 436 * Returns column text alignment 437 */ 438 private function getAlignment($syntax) { 439 static $align = array( 440 'l' => 'left', '-*' => 'left', 441 'r' => 'right', '*-' => 'right', 442 'c' => 'center', '*-*' => 'center', 443 'j' => 'justify', 444 't' => 'top', 445 'm' => 'middle', 446 'b' => 'bottom' 447 ); 448 if (array_key_exists($syntax, $align)) { 449 return $align[$syntax]; 450 } 451 else { 452 return ''; 453 } 454 } 455 456 /** 457 * Returns a list of corrections that have to be applied to the instruction array 458 */ 459 public function getCorrections() { 460 if ($this->closed) { 461 $correction = $this->fixSections(); 462 } 463 else { 464 $correction = $this->deleteColumns(); 465 } 466 return $correction; 467 } 468 469 /** 470 * Re-write section open/close instructions to produce valid HTML 471 * See columns:design#section_fixing for details 472 */ 473 private function fixSections() { 474 $correction = array(); 475 foreach ($this->column as $column) { 476 $correction = array_merge($correction, $column->getCorrections()); 477 } 478 return $correction; 479 } 480 481 /** 482 * 483 */ 484 private function deleteColumns() { 485 $correction = array(); 486 foreach ($this->column as $column) { 487 $correction[] = $column->delete(); 488 } 489 return $correction; 490 } 491} 492 493class columns_attributes_bag { 494 495 private $attribute; 496 497 /** 498 * Constructor 499 */ 500 public function __construct() { 501 $this->attribute = array(); 502 } 503 504 /** 505 * 506 */ 507 public function addAttribute($name, $value) { 508 $this->attribute[$name] = $value; 509 } 510 511 /** 512 * 513 */ 514 public function addAttributes($attribute) { 515 if (is_array($attribute) && (count($attribute) > 0)) { 516 $this->attribute = array_merge($this->attribute, $attribute); 517 } 518 } 519 520 /** 521 * 522 */ 523 public function getAttribute($name) { 524 $result = ''; 525 if (array_key_exists($name, $this->attribute)) { 526 $result = $this->attribute[$name]; 527 } 528 return $result; 529 } 530 531 /** 532 * 533 */ 534 public function getAttributes() { 535 return $this->attribute; 536 } 537} 538 539class columns_column extends columns_attributes_bag { 540 541 private $open; 542 private $close; 543 private $sectionLevel; 544 private $sectionOpen; 545 private $sectionClose; 546 private $sectionStart; 547 private $sectionEnd; 548 549 /** 550 * Constructor 551 */ 552 public function __construct($open, $sectionLevel) { 553 parent::__construct(); 554 555 $this->open = $open; 556 $this->close = -1; 557 $this->sectionLevel = $sectionLevel; 558 $this->sectionOpen = false; 559 $this->sectionClose = -1; 560 $this->sectionStart = null; 561 $this->sectionEnd = null; 562 } 563 564 /** 565 * 566 */ 567 public function getOpenCall() { 568 return $this->open; 569 } 570 571 /** 572 * 573 */ 574 public function openSection() { 575 $this->sectionOpen = true; 576 } 577 578 /** 579 * 580 */ 581 public function closeSection($callIndex) { 582 if ($this->sectionClose == -1) { 583 $this->sectionClose = $callIndex; 584 } 585 } 586 587 /** 588 * 589 */ 590 public function startSection($callInfo) { 591 $this->sectionStart = $callInfo; 592 } 593 594 /** 595 * 596 */ 597 public function endSection($callInfo) { 598 $this->sectionEnd = $callInfo; 599 } 600 601 /** 602 * 603 */ 604 public function close($callIndex) { 605 $this->close = $callIndex; 606 } 607 608 /** 609 * 610 */ 611 public function delete() { 612 return new instruction_rewriter_delete($this->open); 613 } 614 615 /** 616 * Re-write section open/close instructions to produce valid HTML 617 * See columns:design#section_fixing for details 618 */ 619 public function getCorrections() { 620 $result = array(); 621 $deleteSectionClose = ($this->sectionClose != -1); 622 $closeSection = $this->sectionOpen; 623 if ($this->sectionStart != null) { 624 $result = array_merge($result, $this->moveStartSectionEdit()); 625 } 626 if (($this->getAttribute('continue') == 'on') && ($this->sectionLevel > 0)) { 627 $result[] = $this->openStartSection(); 628 /* Ensure that this section will be properly closed */ 629 $deleteSectionClose = false; 630 $closeSection = true; 631 } 632 if ($deleteSectionClose) { 633 /* Remove first section_close from the column to prevent </div> in the middle of the column */ 634 $result[] = new instruction_rewriter_delete($this->sectionClose); 635 } 636 if ($closeSection || ($this->sectionEnd != null)) { 637 $result = array_merge($result, $this->closeLastSection($closeSection)); 638 } 639 return $result; 640 } 641 642 /** 643 * Moves section_edit at the start of the column out of the column 644 */ 645 private function moveStartSectionEdit() { 646 $result = array(); 647 $result[0] = new instruction_rewriter_insert($this->open); 648 $result[0]->addPluginCall('columns', array(987, $this->sectionStart - 1), DOKU_LEXER_MATCHED); 649 return $result; 650 } 651 652 /** 653 * Insert section_open at the start of the column 654 */ 655 private function openStartSection() { 656 $insert = new instruction_rewriter_insert($this->open + 1); 657 $insert->addCall('section_open', array($this->sectionLevel)); 658 return $insert; 659 } 660 661 /** 662 * Close last open section in the column 663 */ 664 private function closeLastSection($closeSection) { 665 $result = array(); 666 $result[0] = new instruction_rewriter_insert($this->close); 667 if ($closeSection) { 668 $result[0]->addCall('section_close', array()); 669 } 670 if ($this->sectionEnd != null) { 671 $result[0]->addPluginCall('columns', array(987, $this->sectionEnd - 1), DOKU_LEXER_MATCHED); 672 } 673 return $result; 674 } 675} 676