= 20070626. * Tested with DokuWiki versions 20090214, 20091225, 20110525a, 20121013, 20130510a, * 20131208, 20140505e, 20140929d, 20150810a, 20160626e, 20170219e, 20180422a, 20200729. * * Install to lib/plugins/diagram/syntax/main.php. * * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) * @author Nikita Melnichenko [http://nikita.melnichenko.name] * @copyright Copyright 2007-2021, Nikita Melnichenko * * Thanks for help to: * - Anika Henke */ // includes if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); require_once(DOKU_PLUGIN.'syntax.php'); /** * DokuWiki plugin Diagram, Main component. * * Constructs diagrams. */ class syntax_plugin_diagram_main extends DokuWiki_Syntax_Plugin { /** * Tag name in wiki text. * * @staticvar string */ var $tag_name = 'diagram'; /** * Splitter tag name in wiki text. * * Must be = syntax_plugin_diagram_splitter::$tag_name. * Copied for compability with PHP4. * * @staticvar string */ var $tag_name_splitter = '_diagram_'; /** * CSS classes used in the diagram. * * @staticvar array */ var $css_classes = array( /* spacers */ 'spacer-horizontal' => 'd-sh', 'spacer-vertical' => 'd-sv', /* block */ 'block' => 'd-b', /* connection borders */ 'border-right-solid' => 'd-brs', 'border-right-dashed' => 'd-brd', 'border-bottom-solid' => 'd-bbs', 'border-bottom-dashed' => 'd-bbd', /* arrow directions */ 'arrow-top' => 'd-at', 'arrow-right' => 'd-ar', 'arrow-bottom' => 'd-ab', 'arrow-left' => 'd-al', 'arrow-inside' => 'd-ai' ); /** * Get syntax type. * * @return string one of the mode types defined in $PARSER_MODES in parser.php */ function getType () { // containers are complex modes that can contain many other modes // plugin generates table, so type should be container return 'container'; } /** * Get paragraph type. * * Defines how this syntax is handled regarding paragraphs. This is important * for correct XHTML nesting. Should return one of the following: * - 'normal' - The plugin can be used inside paragraphs * - 'block' - Open paragraphs need to be closed before plugin output * - 'stack' - Special case. Plugin wraps other paragraphs. * * @return string */ function getPType () { // table cannot be put inside paragraphs return 'block'; } /** * Get position of plugin's mode in decision list. * * @return integer */ function getSort () { // position doesn't matter return 999; } /** * Connect pattern to lexer. * * @param string $mode */ function connectTo ($mode) { // parse all content in one shot $this->Lexer->addSpecialPattern('<'.$this->tag_name.'>.*?tag_name.'>', $mode, 'plugin_diagram_main'); } /** * Handle the match. * * @param string $match * @param integer $state one of lexer states defined in lexer.php * @param integer $pos position of first * @param Doku_Handler $handler * @return array data for rendering */ function handle ($match, $state, $pos, Doku_Handler $handler) { // strip tags $tag_name_len = strlen($this->tag_name); $content = substr($match, $tag_name_len + 2, strlen($match) - 2 * $tag_name_len - 5); // parse content using Splitter component $calls = p_get_instructions('<'.$this->tag_name_splitter.'>' .$content.'tag_name_splitter.'>'); // compose commands and abbreviations list($commands, $abbrs) = $this->_genCommandsAndAbbrs($calls); // compose internal specification of table $framework = $this->_genFramework($commands); return array( 'framework' => $framework, 'abbreviations' => $abbrs ); } /** * Create XHTML text. * * @param string $mode render mode; only 'xhtml' supported * @param Doku_Renderer $renderer * @param array $data data from handler * @return bool */ function render ($mode, Doku_Renderer $renderer, $data) { if ($mode != 'xhtml') return false; // add generated code to document $renderer->doc .= "\n".$this->_renderDiagram( $data['framework'], $data['abbreviations']); return true; } /** * Compose commands and abbreviations from wiki calls. * * Supported abbreviation parameters: * - border-color (CSS property) * - background-color (CSS property) * - text-align (CSS property) * - padding (CSS property) * * @param array $calls DokuWiki calls * @return array array($commands, $abbreviations) */ function _genCommandsAndAbbrs ($calls) { $diagram_entered = false; $commands = array(); $abbrs = array(); $line_index = -1; // handle call list foreach ($calls as $call) { // get plugin related info from call if ($call[0] == 'plugin' && $call[1][0] == 'diagram_splitter') { $data = $call[1][1]; $diagram_call_state = $call[1][2]; } else { $data = null; $diagram_call_state = 0; } // wait until entering to diagram if (!$diagram_entered && $diagram_call_state != DOKU_LEXER_ENTER) continue; // just entered: skipping if (!$diagram_entered) { $diagram_entered = true; continue; } // exited from diagram: stop handling if ($diagram_call_state == DOKU_LEXER_EXIT) break; // received newline: start new line if ($diagram_call_state == DOKU_LEXER_MATCHED && $data['type'] == 'newline') { // avoid unset lines of commands if ($line_index >= 0 && !array_key_exists($line_index, $commands)) $commands[$line_index] = array (); // increment index $line_index++; // stop catching calls for abbr $abbr_met = false; if (isset($catching_abbr)) unset($catching_abbr); continue; } // must receive first newline before processing commands if ($line_index < 0) continue; // received command: add to line of commands if ($diagram_call_state == DOKU_LEXER_MATCHED && $data['type'] == 'command') { // deny commands after first abbreviation in line if (!$abbr_met) $commands[$line_index][] = $data['command']; // stop catching calls for last abbr $abbr_met = false; if (isset($catching_abbr)) unset($catching_abbr); continue; } // received abbreviation: add to line of abbrs and start catching calls if ($diagram_call_state == DOKU_LEXER_MATCHED && $data['type'] == 'abbr eval') { $abbr_met = true; $abbrs[$line_index][$data['abbr']]['content'] = array(); $abbrs[$line_index][$data['abbr']]['params'] = array(); // override some parameters by user values if (isset ($data['params'])) { $params = explode(';', $data['params']); foreach ($params as $param) { list ($key, $value) = explode(':', $param); $key = trim ($key); $value = trim ($value); $is_valid = false; switch ($key) { case 'border-color': case 'background-color': $is_valid = $this->_validateCSSColor ($value); break; case 'text-align': $is_valid = $this->_validateCSSTextAlign ($value); break; case 'padding': $is_valid = $this->_validateCSSPadding ($value); break; } if ($is_valid) $abbrs[$line_index][$data['abbr']]['params'][$key] = $value; } } $catching_abbr = &$abbrs[$line_index][$data['abbr']]['content']; continue; } // received raw unmatched text and catching is on: add cdata call to abbr if ($diagram_call_state == DOKU_LEXER_UNMATCHED && isset($catching_abbr)) { $catching_abbr[] = array('cdata', array($data['text']), $call[2]); continue; } // received arbitrary call and catching is on: add call to abbr if (isset($catching_abbr)) $catching_abbr[] = $call; // skip everything else } // remove trailing garbage for ($i = 0; $i < count($commands); $i++) { $line_length = count($commands[$i]); // remove last element, if no abbreviations found, // because last delimiter is a 'border' of the table // do not care, if someone specified garbage after last delimiter if ($line_length > 0 && !array_key_exists($i, $abbrs)) unset($commands[$i][$line_length - 1]); } return array($commands, $abbrs); } /** * Generate table's framework. * * Framework: array(row number => array (column number => cell spec)) * + array('n_rows' => number of rows, 'n_cols' => number of columns). * cell_spec: array( * 'colspan' => colspan (optional), * 'rowspan' => rowspan (optional), * 'classes' => array(css class), * 'text' => text for diagram block or abbreviation (optional), * 'content' => raw xhtml code to paste into cell, if 'text' key isn't set (optional) * ). * * @author Nikita Melnichenko [http://nikita.melnichenko.name] * @author Olesya Melnichenko [http://melnichenko.name] * * @param array $commands specification scheme * @return array */ function _genFramework ($commands) { // store number of rows $res['n_rows'] = count($commands) * 2; // number of columns is computed below $res['n_cols'] = 0; for ($i = 0, $ir = 0; $i < count($commands); $i++, $ir += 2) { for ($j = 0, $jr = 0; $j < count($commands[$i]); $j++) { // leading and trailing spaces are already ignored by splitter component $cell_text = $commands[$i][$j]; // split command to connection and arrow commands list($conn_command, $arrow_command) = $this->_splitCommand($cell_text); // 2x2 connection specs for current command $conn_cells = null; switch ($conn_command) { // === empty === case "": $conn_cells = $this->_connectionCells('nnnn', $arrow_command); break; // === solid or dashed lines === // + + + // // // // // // + +-----+ // | // | // | // | // | // + + + case ",": $conn_cells = $this->_connectionCells('nssn', $arrow_command); break; case "F": $conn_cells = $this->_connectionCells('nddn', $arrow_command); break; // + + + // // // // // // +-----+ + // | // | // | // | // | // + + + case ".": $conn_cells = $this->_connectionCells('nnss', $arrow_command); break; case "7": $conn_cells = $this->_connectionCells('nndd', $arrow_command); break; // + + + // // // // // // +-----+-----+ // | // | // | // | // | // + + + case "v": $conn_cells = $this->_connectionCells('nsss', $arrow_command); break; case "V": $conn_cells = $this->_connectionCells('nddd', $arrow_command); break; // + + + // | // | // | // | // | // + + + // | // | // | // | // | // + + + case "!": $conn_cells = $this->_connectionCells('snsn', $arrow_command); break; case ":": $conn_cells = $this->_connectionCells('dndn', $arrow_command); break; // + + + // | // | // | // | // | // +-----+-----+ // | // | // | // | // | // + + + case "+": $conn_cells = $this->_connectionCells('ssss', $arrow_command); break; case "%": $conn_cells = $this->_connectionCells('dddd', $arrow_command); break; // + + + // // // // // // +-----+-----+ // // // // // // + + + case "-": $conn_cells = $this->_connectionCells('nsns', $arrow_command); break; case "~": $conn_cells = $this->_connectionCells('ndnd', $arrow_command); break; // + + + // | // | // | // | // | // + +-----+ // // // // // // + + + case "`": $conn_cells = $this->_connectionCells('ssnn', $arrow_command); break; case "L": $conn_cells = $this->_connectionCells('ddnn', $arrow_command); break; // + + + // | // | // | // | // | // +-----+ + // // // // // // + + + case "'": $conn_cells = $this->_connectionCells('snns', $arrow_command); break; case "J": $conn_cells = $this->_connectionCells('dnnd', $arrow_command); break; // + + + // | // | // | // | // | // +-----+-----+ // // // // // // + + + case "^": $conn_cells = $this->_connectionCells('ssns', $arrow_command); break; case "A": $conn_cells = $this->_connectionCells('ddnd', $arrow_command); break; // + + + // | // | // | // | // | // +-----+ + // | // | // | // | // | // + + + case "(": $conn_cells = $this->_connectionCells('snss', $arrow_command); break; case "C": $conn_cells = $this->_connectionCells('dndd', $arrow_command); break; // + + + // | // | // | // | // | // + +-----+ // | // | // | // | // | // + + + case ")": $conn_cells = $this->_connectionCells('sssn', $arrow_command); break; case "D": $conn_cells = $this->_connectionCells('dddn', $arrow_command); break; // === mixed lines === // + + + // // // // // // +- - -+- - -+ // | // | // | // | // | // + + + case "y": $conn_cells = $this->_connectionCells('ndsd', $arrow_command); break; // + + + // | // // | // // | // +-----+-----+ // | // // | // // | // + + + case "*": $conn_cells = $this->_connectionCells('dsds', $arrow_command); break; // + + + // | // // | // // | // + +-----+ // | // // | // // | // + + + case "}": $conn_cells = $this->_connectionCells('dsdn', $arrow_command); break; // + + + // | // // | // // | // +-----+ + // | // // | // // | // + + + case "{": $conn_cells = $this->_connectionCells('dnds', $arrow_command); break; // + + + // | // | // | // | // | // + +- - -+ // | // | // | // | // | // + + + case "]": $conn_cells = $this->_connectionCells('sdsn', $arrow_command); break; // + + + // | // | // | // | // | // +- - -+ + // | // | // | // | // | // + + + case "[": $conn_cells = $this->_connectionCells('snsd', $arrow_command); break; // + + + // | // | // | // | // | // +- - -+- - -+ // // // // // // + + + case "h": $conn_cells = $this->_connectionCells('sdnd', $arrow_command); break; // + + + // | // | // | // | // | // +- - -+- - -+ // | // | // | // | // | // + + + case "#": $conn_cells = $this->_connectionCells('sdsd', $arrow_command); break; // + + + // // // // // // +-----+-----+ // | // // | // // | // + + + case "p": $conn_cells = $this->_connectionCells('nsds', $arrow_command); break; // + + + // | // // | // // | // +-----+-----+ // // // // // // + + + case "b": $conn_cells = $this->_connectionCells('dsns', $arrow_command); break; // === box === default: $res[$ir][$jr] = $this->_boxCell(6, 2, $cell_text); $jr += 6; } // apply connection cells to the result if (!is_null($conn_cells)) { // we must have a proper order of creation of elements of framework, do not use list() here $res[$ir][$jr] = $conn_cells[0]; $res[$ir][$jr + 1] = $conn_cells[1]; $res[$ir + 1][$jr] = $conn_cells[2]; $res[$ir + 1][$jr + 1] = $conn_cells[3]; $jr += 2; } } // compute number of columns if ($res['n_cols'] < $jr) $res['n_cols'] = $jr; } return $res; } /** * Split command to connection part and arrow part. * * @param string $command * @return array array($connection_command, $arrow_command) */ function _splitCommand ($command) { $command_parts = explode('@', $command, 2); if (!isset($command_parts[1]) || !preg_match("/^[0-9a-f]{1,2}$/i", $command_parts[1])) $command_parts[1] = 0; else { // convert to bits: 'a' -> '0xa', 'ab' -> '0xba' // see docs and params of _connectionCells $v = $command_parts[1]; if (strlen($v) == 2) $v = $v[1].$v[0]; $command_parts[1] = intval($v, 16); } return $command_parts; } /** * Generate box cell spec. * * Box is an entity with wiki text. * * @param integer $width colspan * @param integer $height rowspan * @param string $text box text or abbreviation * @param string $border css border * @param string $background_color css color * @return array cell spec */ function _boxCell ($width, $height, $text) { return array( 'colspan' => $width, 'rowspan' => $height, 'classes' => array($this->css_classes['block']), 'text' => $text ); } /** * Generate 2x2 pattern of connections cells. * * Each connection cell provides connection lines using its borders. * They could also contain divs with arrowheads. * * @param string $border_spec 4 chars containing line type in top, right, bottom, left directions; * line type chars are: 's' for solid, 'd' for dashed, 'n' for no line * @param int $arrow_spec 8 bits are used: * the first 4 bits indicate if arrow exists (=1) or not (=0) in top, right, bottom, left directions, * the next 4 bits indicate if arrowhead look inside (=1) or outside (=0) in top, right, bottom, left directions, * @return array array(cell_{0,0}, cell_{0,1}, cell_{1,0}, cell_{1,1}) */ function _connectionCells ($border_spec, $arrow_spec) { // direction numbers: top (0), right (1), bottom (2), left (3) // cell numbers: {0,0} -> 0, {0,1} -> 1, {1,0} -> 2, {1,1} -> 3 // + + + // | // // cell 0 cell // 0 1 // | // +- 3 -+- 1 -+ // | // // cell 2 cell // 2 3 // | // + + + // init for ($i = 0; $i < 4; $i++) $cells[$i] = array('classes' => array()); // fill borders if ($border_spec[0] != 'n') $cells[0]['classes'][] = $this->_borderClass($border_spec[0], 'right'); if ($border_spec[1] != 'n') $cells[1]['classes'][] = $this->_borderClass($border_spec[1], 'bottom'); if ($border_spec[2] != 'n') $cells[2]['classes'][] = $this->_borderClass($border_spec[2], 'right'); if ($border_spec[3] != 'n') $cells[0]['classes'][] = $this->_borderClass($border_spec[3], 'bottom'); // div elements with arrows, direction to cell number mapping // 0 -> 1, 1 -> 3. 2 -> 2, 3 -> 0 // + + + // | // // 0 0>>1 // ^ // ^ | // +- 3 -+- 1 -+ // | v // v // 2<<2 3 // // | // + + + // fill primary arrow classes if ($arrow_spec & (1 << 0)) { $cells[1]['classes'][] = $this->css_classes['arrow-top']; $cells[1]['content'] = '
'; } if ($arrow_spec & (1 << 1)) { $cells[3]['classes'][] = $this->css_classes['arrow-right']; $cells[3]['content'] = '
'; } if ($arrow_spec & (1 << 2)) { $cells[2]['classes'][] = $this->css_classes['arrow-bottom']; $cells[2]['content'] = '
'; } if ($arrow_spec & (1 << 3)) { $cells[0]['classes'][] = $this->css_classes['arrow-left']; $cells[0]['content'] = '
'; } // fill arrowhead direction if ($arrow_spec & (1 << (0 + 4))) $cells[1]['classes'][] = $this->css_classes['arrow-inside']; if ($arrow_spec & (1 << (1 + 4))) $cells[3]['classes'][] = $this->css_classes['arrow-inside']; if ($arrow_spec & (1 << (2 + 4))) $cells[2]['classes'][] = $this->css_classes['arrow-inside']; if ($arrow_spec & (1 << (3 + 4))) $cells[0]['classes'][] = $this->css_classes['arrow-inside']; // clear for ($i = 0; $i < 4; $i++) if (empty($cells[$i]['classes'])) unset($cells[$i]['classes']); return $cells; } /** * Generate border CSS class for connection cell. * * @param string $type 's' for solid, 'd' for dashed * @param string $direction 'right'or 'bottom' * @return string class name */ function _borderClass ($type, $direction) { if ($type != 's' && $type != 'd') return 'error'; $key = "border-$direction-".($type == 's' ? 'solid' : 'dashed'); return $this->css_classes[$key]; } /** * Generate table with diagram. * * @param array $framework table framework generated by _genFramework * @param array $abbrs information about abbreviations * @return string xhtml table */ function _renderDiagram ($framework, $abbrs) { $n_rows = $framework['n_rows']; $n_cols = $framework['n_cols']; // output table $table = ''."\n"; // create horizontal spacer row // first cell is for column of vertical spacers $table .= "\t\n\t\t\n"; for ($i = 0; $i < $n_cols; $i++) $table .= "\t\t\n"; $table .= "\t\n"; // create diagram rows for ($i = 0; $i < $n_rows; $i++) { // get table row spec $row = array_key_exists($i, $framework) ? $framework[$i] : array (); // line number $line_index = $i / 2; // output tr // first cell is for column of vertical spacers $table .= "\t\n\t\t\n"; foreach ($row as $cell) { // generate cell content and update style $cell_content = ''; // empty cell or connection cell if (!isset($cell['text'])) { if (isset($cell['content'])) $cell_content = $cell['content']; } // cell with abbreviation else if (array_key_exists($line_index, $abbrs) && array_key_exists($cell['text'], $abbrs[$line_index])) { $cell_content = $this->_renderWikiCalls ($abbrs[$line_index][$cell['text']]['content']); $cell['style'] = $this->_generateBlockStyle ($abbrs[$line_index][$cell['text']]['params']); } // cell with unrecognized abbreviation else $cell_content = $cell['text']; // output td $table .= "\t\t' .$cell_content ."\n"; } $table .= "\t\n"; } $table .= "
css_classes['spacer-horizontal']."\">
css_classes['spacer-vertical']."\">
\n"; return $table; } /** * Generate CSS style for diagram block. * * @param array $params supported block CSS parameters * @return string css style */ function _generateBlockStyle ($params) { $css_props = array(); foreach ($params as $param => $value) $css_props[] = "$param: $value;"; return implode(' ', $css_props); } /** * Render wiki instructions. * * @param array $calls DokuWiki calls * @return string xhtml markup */ function _renderWikiCalls ($calls) { return p_render('xhtml', $calls, $info); } /** * Check if given color will not break css style. * * @param string $color checked string * @return true, if string is good for css */ function _validateCSSColor ($color) { // color name; for ex. 'green' if (preg_match("/^[a-z]+$/", $color)) return true; // short number notation; for ex. '#e73' if (preg_match("/^#[0-9a-fA-F]{3}$/", $color)) return true; // full number notation; for ex. '#ef703f' if (preg_match("/^#[0-9a-fA-F]{6}$/", $color)) return true; // rgb notation; for ex. 'rgb(11,22,33)' or 'rgb(11%,22%,33%)' if (preg_match("/^rgb\([ ]*[0-9]{1,3}[ ]*,[ ]*[0-9]{1,3}[ ]*,[ ]*[0-9]{1,3}[ ]*\)$/", $color)) return true; if (preg_match("/^rgb\([ ]*[0-9]{1,3}%[ ]*,[ ]*[0-9]{1,3}%[ ]*,[ ]*[0-9]{1,3}%[ ]*\)$/", $color)) return true; return false; } /** * Check if given value is proper for css text-align. * * @param string $value checked string * @return true, if string is good as a value for css text-align */ function _validateCSSTextAlign ($value) { return $value == 'center' || $value == 'justify' || $value == 'left' || $value == 'right'; } /** * Check if given value is proper for css padding. * * @param string $value checked string * @return true, if string is good as a value for css padding */ function _validateCSSPadding ($value) { if (preg_match("/^((auto|[0-9]+px|[0-9]+%|[0-9]+em)[ ]*){1,4}$/", $value)) return true; return false; } }