*/ require_once(DOKU_PLUGIN . 'columns/rewriter.php'); class action_plugin_columns extends DokuWiki_Action_Plugin { private $block; private $currentBlock; private $currentSectionLevel; private $sectionEdit; /** * Register callbacks */ public function register(Doku_Event_Handler $controller) { $controller->register_hook('PARSER_HANDLER_DONE', 'AFTER', $this, 'handle'); } /** * */ public function handle(&$event, $param) { $this->reset(); $this->buildLayout($event); $rewriter = new instruction_rewriter(); foreach ($this->block as $block) { $block->processAttributes($event); $rewriter->addCorrections($block->getCorrections()); } $rewriter->process($event->data->calls); } /** * Find all columns instructions and construct columns layout based on them */ private function buildLayout(&$event) { $calls = count($event->data->calls); for ($c = 0; $c < $calls; $c++) { $call =& $event->data->calls[$c]; switch ($call[0]) { case 'section_open': $this->currentSectionLevel = $call[1][0]; $this->currentBlock->openSection(); break; case 'section_close': $this->currentBlock->closeSection($c); break; case 'plugin': if ($call[1][0] == 'columns') { $this->handleColumns($c, $call[1][1][0], $this->detectSectionEdit($event->data->calls, $c)); } break; } } } /** * Reset internal state */ private function reset() { $this->block = array(); $this->block[0] = new columns_root_block(); $this->currentBlock = $this->block[0]; $this->currentSectionLevel = 0; $this->sectionEdit = array(); } /** * */ private function detectSectionEdit($call, $start) { $result = null; $calls = count($call); for ($c = $start + 1; $c < $calls; $c++) { switch ($call[$c][0]) { case 'section_close': case 'p_open': case 'p_close': /* Skip these instructions */ break; case 'header': if (end($this->sectionEdit) != $c) { $this->sectionEdit[] = $c; $result = $call[$c][2]; } break 2; case 'plugin': if ($call[$c][1][0] == 'columns') { break; } else { break 2; } default: break 2; } } return $result; } /** * */ private function handleColumns($callIndex, $state, $sectionEdit) { switch ($state) { case DOKU_LEXER_ENTER: $this->currentBlock = new columns_block(count($this->block), $this->currentBlock); $this->currentBlock->addColumn($callIndex, $this->currentSectionLevel); $this->currentBlock->startSection($sectionEdit); $this->block[] = $this->currentBlock; break; case DOKU_LEXER_MATCHED: $this->currentBlock->addColumn($callIndex, $this->currentSectionLevel); $this->currentBlock->startSection($sectionEdit); break; case DOKU_LEXER_EXIT: $this->currentBlock->endSection($sectionEdit); $this->currentBlock->close($callIndex); $this->currentBlock = $this->currentBlock->getParent(); break; } } } class columns_root_block { private $sectionLevel; private $call; /** * Constructor */ public function __construct() { $this->sectionLevel = 0; $this->call = array(); } /** * */ public function getParent() { return $this; } /** * Collect stray tags */ public function addColumn($callIndex, $sectionLevel) { $this->call[] = $callIndex; } /** * */ public function openSection() { $this->sectionLevel++; } /** * */ public function closeSection($callIndex) { if ($this->sectionLevel > 0) { $this->sectionLevel--; } else { $this->call[] = $callIndex; } } /** * */ public function startSection($callInfo) { } /** * */ public function endSection($callInfo) { } /** * Collect stray tags */ public function close($callIndex) { $this->call[] = $callIndex; } /** * */ public function processAttributes(&$event) { } /** * Delete all captured tags */ public function getCorrections() { $correction = array(); foreach ($this->call as $call) { $correction[] = new instruction_rewriter_delete($call); } return $correction; } } class columns_block { private $id; private $parent; private $column; private $currentColumn; private $closed; /** * Constructor */ public function __construct($id, $parent) { $this->id = $id; $this->parent = $parent; $this->column = array(); $this->currentColumn = null; $this->closed = false; } /** * */ public function getParent() { return $this->parent; } /** * */ public function addColumn($callIndex, $sectionLevel) { if ($this->currentColumn != null) { $this->currentColumn->close($callIndex); } $this->currentColumn = new columns_column($callIndex, $sectionLevel); $this->column[] = $this->currentColumn; } /** * */ public function openSection() { $this->currentColumn->openSection(); } /** * */ public function closeSection($callIndex) { $this->currentColumn->closeSection($callIndex); } /** * */ public function startSection($callInfo) { $this->currentColumn->startSection($callInfo); } /** * */ public function endSection($callInfo) { $this->currentColumn->endSection($callInfo); } /** * */ public function close($callIndex) { $this->currentColumn->close($callIndex); $this->closed = true; } /** * Convert raw attributes and layout information into column attributes */ public function processAttributes(&$event) { $columns = count($this->column); for ($c = 0; $c < $columns; $c++) { $call =& $event->data->calls[$this->column[$c]->getOpenCall()]; if ($c == 0) { $this->loadBlockAttributes($call[1][1][1]); $this->column[0]->addAttribute('columns', $columns); $this->column[0]->addAttribute('class', 'first'); } else { $this->loadColumnAttributes($c, $call[1][1][1]); if ($c == ($columns - 1)) { $this->column[$c]->addAttribute('class', 'last'); } } $this->column[$c]->addAttribute('block-id', $this->id); $this->column[$c]->addAttribute('column-id', $c + 1); $call[1][1][1] = $this->column[$c]->getAttributes(); } } /** * Convert raw attributes into column attributes */ private function loadBlockAttributes($attribute) { $column = -1; $nextColumn = -1; foreach ($attribute as $a) { list($name, $temp) = $this->parseAttribute($a); if ($name == 'width') { if (($column == -1) && array_key_exists('column-width', $temp)) { $this->column[0]->addAttribute('table-width', $temp['column-width']); } $nextColumn = $column + 1; } if (($column >= 0) && ($column < count($this->column))) { $this->column[$column]->addAttributes($temp); } $column = $nextColumn; } } /** * Convert raw attributes into column attributes */ private function loadColumnAttributes($column, $attribute) { foreach ($attribute as $a) { list($name, $temp) = $this->parseAttribute($a); $this->column[$column]->addAttributes($temp); } } /** * */ private function parseAttribute($attribute) { static $syntax = array( '/^left|right|center|justify$/' => 'text-align', '/^top|middle|bottom$/' => 'vertical-align', '/^[lrcjtmb]{1,2}$/' => 'align', '/^continue|\.{3}$/' => 'continue', '/^(\*?)((?:-|(?:\d+\.?|\d*\.\d+)(?:%|em|px|cm|mm|in|pt)))(\*?)$/' => 'width' ); $result = array(); $attributeName = ''; foreach ($syntax as $pattern => $name) { if (preg_match($pattern, $attribute, $match) == 1) { $attributeName = $name; break; } } switch ($attributeName) { case 'text-align': case 'vertical-align': $result[$attributeName] = $match[0]; break; case 'align': $result = $this->parseAlignAttribute($match[0]); break; case 'continue': $result[$attributeName] = 'on'; break; case 'width': $result = $this->parseWidthAttribute($match); break; } return array($attributeName, $result); } /** * */ private function parseAlignAttribute($syntax) { $result = array(); $align1 = $this->getAlignStyle($syntax[0]); if (strlen($syntax) == 2) { $align2 = $this->getAlignStyle($syntax[1]); if ($align1 != $align2) { $result[$align1] = $this->getAlignment($syntax[0]); $result[$align2] = $this->getAlignment($syntax[1]); } } else { $result[$align1] = $this->getAlignment($syntax[0]); } return $result; } /** * */ private function getAlignStyle($align) { return preg_match('/[lrcj]/', $align) ? 'text-align' : 'vertical-align'; } /** * */ private function parseWidthAttribute($syntax) { $result = array(); if ($syntax[2] != '-') { $result['column-width'] = $syntax[2]; } $align = $syntax[1] . '-' . $syntax[3]; if ($align != '-') { $result['text-align'] = $this->getAlignment($align); } return $result; } /** * Returns column text alignment */ private function getAlignment($syntax) { static $align = array( 'l' => 'left', '-*' => 'left', 'r' => 'right', '*-' => 'right', 'c' => 'center', '*-*' => 'center', 'j' => 'justify', 't' => 'top', 'm' => 'middle', 'b' => 'bottom' ); if (array_key_exists($syntax, $align)) { return $align[$syntax]; } else { return ''; } } /** * Returns a list of corrections that have to be applied to the instruction array */ public function getCorrections() { if ($this->closed) { $correction = $this->fixSections(); } else { $correction = $this->deleteColumns(); } return $correction; } /** * Re-write section open/close instructions to produce valid HTML * See columns:design#section_fixing for details */ private function fixSections() { $correction = array(); foreach ($this->column as $column) { $correction = array_merge($correction, $column->getCorrections()); } return $correction; } /** * */ private function deleteColumns() { $correction = array(); foreach ($this->column as $column) { $correction[] = $column->delete(); } return $correction; } } class columns_attributes_bag { private $attribute; /** * Constructor */ public function __construct() { $this->attribute = array(); } /** * */ public function addAttribute($name, $value) { $this->attribute[$name] = $value; } /** * */ public function addAttributes($attribute) { if (is_array($attribute) && (count($attribute) > 0)) { $this->attribute = array_merge($this->attribute, $attribute); } } /** * */ public function getAttribute($name) { $result = ''; if (array_key_exists($name, $this->attribute)) { $result = $this->attribute[$name]; } return $result; } /** * */ public function getAttributes() { return $this->attribute; } } class columns_column extends columns_attributes_bag { private $open; private $close; private $sectionLevel; private $sectionOpen; private $sectionClose; private $sectionStart; private $sectionEnd; /** * Constructor */ public function __construct($open, $sectionLevel) { parent::__construct(); $this->open = $open; $this->close = -1; $this->sectionLevel = $sectionLevel; $this->sectionOpen = false; $this->sectionClose = -1; $this->sectionStart = null; $this->sectionEnd = null; } /** * */ public function getOpenCall() { return $this->open; } /** * */ public function openSection() { $this->sectionOpen = true; } /** * */ public function closeSection($callIndex) { if ($this->sectionClose == -1) { $this->sectionClose = $callIndex; } } /** * */ public function startSection($callInfo) { $this->sectionStart = $callInfo; } /** * */ public function endSection($callInfo) { $this->sectionEnd = $callInfo; } /** * */ public function close($callIndex) { $this->close = $callIndex; } /** * */ public function delete() { return new instruction_rewriter_delete($this->open); } /** * Re-write section open/close instructions to produce valid HTML * See columns:design#section_fixing for details */ public function getCorrections() { $result = array(); $deleteSectionClose = ($this->sectionClose != -1); $closeSection = $this->sectionOpen; if ($this->sectionStart != null) { $result = array_merge($result, $this->moveStartSectionEdit()); } if (($this->getAttribute('continue') == 'on') && ($this->sectionLevel > 0)) { $result[] = $this->openStartSection(); /* Ensure that this section will be properly closed */ $deleteSectionClose = false; $closeSection = true; } if ($deleteSectionClose) { /* Remove first section_close from the column to prevent in the middle of the column */ $result[] = new instruction_rewriter_delete($this->sectionClose); } if ($closeSection || ($this->sectionEnd != null)) { $result = array_merge($result, $this->closeLastSection($closeSection)); } return $result; } /** * Moves section_edit at the start of the column out of the column */ private function moveStartSectionEdit() { $result = array(); $result[0] = new instruction_rewriter_insert($this->open); $result[0]->addPluginCall('columns', array(987, $this->sectionStart - 1), DOKU_LEXER_MATCHED); return $result; } /** * Insert section_open at the start of the column */ private function openStartSection() { $insert = new instruction_rewriter_insert($this->open + 1); $insert->addCall('section_open', array($this->sectionLevel)); return $insert; } /** * Close last open section in the column */ private function closeLastSection($closeSection) { $result = array(); $result[0] = new instruction_rewriter_insert($this->close); if ($closeSection) { $result[0]->addCall('section_close', array()); } if ($this->sectionEnd != null) { $result[0]->addPluginCall('columns', array(987, $this->sectionEnd - 1), DOKU_LEXER_MATCHED); } return $result; } }