1<?php 2 3/** 4 * DokuWiki Plugin stepbystep (Syntax Component) 5 * 6 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 7 * @author saggi <saggi@gmx.de> 8 */ 9class syntax_plugin_stepbystep_step extends \dokuwiki\Extension\SyntaxPlugin 10{ 11 protected $tagcount = 1; 12 13 // default name and style definitions 14 protected $options = [ 15 'collapsible_class' => ' class="stepbystep_collapsible"', 16 'collapsible_class_active' => ' class="stepbystep_collapsible active"', 17 'content_height' => '', 18 'content_height_max' => ' style="max-height: fit-content;"', 19 'height_preview' => ' style="--preview: %s;"', 20 'container' => 'stepbystep', 21 'container-noback' => 'nobackground-stepbystep' 22 ]; 23 24 /** 25 * Get plugin and component name 26 * @return string 27 */ 28 public function getMode(): string 29 { 30 return sprintf("plugin_%s_%s", $this->getPluginName(), $this->getPluginComponent()); 31 } 32 33 /** @inheritDoc */ 34 public function getType() 35 { 36 return 'substition'; 37 } 38 39 /** @inheritDoc */ 40 public function getPType() 41 { 42 return 'normal'; 43 } 44 45 /** @inheritDoc */ 46 public function getSort() 47 { 48 return 410; 49 } 50 51 function getAllowedTypes() 52 { 53 return array('container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs'); 54 } 55 56 /** 57 * accept nesting 58 * @param $mode 59 * @return bool 60 */ 61 function accepts($mode) 62 { 63 if ($mode == $this->getMode()) { 64 return true; 65 } 66 return parent::accepts($mode); 67 } 68 69 /** 70 * Set the EntryPattern 71 * @param string $mode 72 */ 73 public function connectTo($mode) 74 { 75 $this->Lexer->addEntryPattern( 76 sprintf('^#{%1$d}:.*?(?=\n.*?^:#{%1$d}$)', $this->tagcount), 77 $mode, 78 $this->getmode() 79 ); 80 } 81 82 /** 83 * Set the ExitPattern 84 */ 85 public function postConnect() 86 { 87 $this->Lexer->addExitPattern("^:#{{$this->tagcount}}$", $this->getmode()); 88 } 89 90 /** 91 * Handle the match 92 * @param string $match The match of the syntax 93 * @param int $state The state of the handler 94 * @param int $pos The position in the document 95 * @param Doku_Handler $handler The handler 96 * @return array Data for the renderer 97 */ 98 public function handle($match, $state, $pos, Doku_Handler $handler) 99 { 100 switch ($state) { 101 case DOKU_LEXER_ENTER : 102 $match = trim(substr($match, $this->tagcount + 1));// returns match after '#{$tagcount}:' 103 $check = sexplode('||', $match, 2); 104 // set default values 105 $data = [ 106 'title' => '', 107 'anchor' => '', 108 'options' => [], 109 'collapsible_class' => $this->options['collapsible_class'], 110 'content_height' => $this->options['content_height'], 111 'preview' => '', 112 'container' => $this->options['container'] 113 ]; 114 if ($check[0]) { 115 $data['title'] = hsc($check[0]); 116 $data['anchor'] = str_replace([':', '.'], '_', cleanID($data['title'])); 117 $data['anchor'] = substr($data['anchor'], 0, 40); 118 } 119 $data = $this->checkOptions($check[1], $data); 120 return [$state, $data]; 121 case DOKU_LEXER_UNMATCHED : 122 return [$state, $match]; 123 case DOKU_LEXER_EXIT : 124 return [$state, '']; 125 } 126 return []; 127 } 128 129 /** 130 * Create output 131 * 132 * @param string $mode string output format being rendered 133 * @param Doku_Renderer $renderer the current renderer object 134 * @param array $data data created by handler() 135 * @return bool rendered correctly? 136 */ 137 public function render($mode, Doku_Renderer $renderer, $data) 138 { 139 if ($mode !== 'xhtml') { 140 return false; 141 } 142 list($state, $indata) = $data; 143 switch ($state) { 144 case DOKU_LEXER_ENTER : 145 $type = 'button'; 146 $renderer->doc .= '<div class="' . $indata['container'] . '">' . DOKU_LF; 147 if (is_a($renderer, 'renderer_plugin_dw2pdf')) { 148 $type = 'div'; 149 } 150 // Create the Button/Div e.g.: <button id="anchor" class="stepbystep_collapsible">title</button> 151 $renderer->doc .= sprintf( 152 '<%s id="%s"%s>%s</%s>%s', 153 $type, 154 $indata['anchor'], 155 $indata['collapsible_class'], 156 $indata['title'], 157 $type, 158 DOKU_LF 159 ); 160 $renderer->doc .= '<div class="stepbystep_content"' . $indata['content_height'] . $indata['preview'] . '>' . DOKU_LF; 161 $renderer->doc .= '<p>' . DOKU_LF; 162 break; 163 case DOKU_LEXER_UNMATCHED : 164 $renderer->cdata($indata); 165 break; 166 case DOKU_LEXER_EXIT : 167 $renderer->doc .= DOKU_LF . '</p>' . DOKU_LF; 168 $renderer->doc .= '</div>' . DOKU_LF; 169 $renderer->doc .= '</div>' . DOKU_LF; 170 break; 171 } 172 return true; 173 } 174 175 /** 176 * check if value of size is a valid length 177 * @param $size 178 * @return string|null 179 */ 180 public function checkSize($size): ?string 181 { 182 $size = hsc($size); 183 // check if value of size is a valid length 184 $result = preg_match( 185 '/^(\d*\.?\d)+(|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|px|pt|pc)$/i', 186 $size, 187 $matches 188 ); 189 if (!$result) { 190 return null; 191 } 192 $return_size = $matches[0]; 193 // check if a unit is given, add px if not 194 if (preg_match('/^(\d*\.?\d)+$/', $return_size)) { 195 $return_size .= 'px'; 196 } 197 return $return_size; 198 } 199 200 /** 201 * check and pass options to the data array 202 * @param $options 203 * @param $data 204 * @return array 205 */ 206 public function checkOptions($options, $data): array 207 { 208 if (!$options) { 209 return $data; 210 } 211 // pass all options to the renderer 212 $data['options'] = explode(' ', $options); 213 // update default values by option 214 foreach ($data['options'] as $option) { 215 switch ($option) { 216 case 'open': 217 $data['collapsible_class'] = $this->options['collapsible_class_active']; 218 $data['content_height'] = $this->options['content_height_max']; 219 $data['preview'] = ''; 220 break; 221 case (preg_match('/preview:.*/', $option) ? true : false) : 222 $preview = sexplode(':', $option, 2); 223 $size = $this->checkSize($preview[1]); 224 if (($preview[1]) && ($size)) { 225 $data['collapsible_class'] = $this->options['collapsible_class']; 226 $data['content_height'] = $this->options['content_height']; 227 $data['preview'] = sprintf($this->options['height_preview'], $size); 228 } 229 break; 230 case 'noframe' : 231 $data['container'] = $this->options['container-noback']; 232 break; 233 } 234 } 235 return $data; 236 } 237} 238 239