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