1<?php 2/** 3 * Templater Plugin: Based from the include plugin, like MediaWiki's template 4 * Usage: 5 * {{template>page}} for "page" in same namespace 6 * {{template>:page}} for "page" in top namespace 7 * {{template>namespace:page}} for "page" in namespace "namespace" 8 * {{template>.namespace:page}} for "page" in subnamespace "namespace" 9 * {{template>page#section}} for a section of "page" 10 * 11 * Replacers are handled in a simple key/value pair method: 12 * {{template>page|key=val|key2=val|key3=val}} 13 * 14 * Templates are wiki pages, with replacers being delimited like: 15 * @key1@ @key2@ @key3@ 16 * 17 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 18 * @author Jonathan Arkell <jonnay@jonnay.net> 19 * based on code by Esther Brunner <esther@kaffeehaus.ch> 20 * @maintainer Daniel Dias Rodrigues (aka Nerun) <danieldiasr@gmail.com> 21 * @contributors Vincent de Lau <vincent@delau.nl> 22 * Ximin Luo <xl269@cam.ac.uk> 23 * jack126guy <halfgray7e@gmail.com> 24 * Turq Whiteside <turq@mage.city> 25 * @version 0.8.4 (2023-12-14) 26 */ 27 28use dokuwiki\File\PageResolver; 29 30define('BEGIN_REPLACE_DELIMITER', '@'); 31define('END_REPLACE_DELIMITER', '@'); 32 33require_once('debug.php'); 34 35/** 36 * All DokuWiki plugins to extend the parser/rendering mechanism 37 * need to inherit from this class 38 */ 39class syntax_plugin_templater extends DokuWiki_Syntax_Plugin { 40 /** 41 * return some info 42 */ 43 function getInfo() { 44 return array( 45 'author' => 'Daniel Dias Rodrigues', 46 'email' => 'danieldiasr@gmail.com', 47 'date' => '2023-12-14', 48 'name' => 'Templater Plugin', 49 'desc' => 'Displays a wiki page (or a section thereof) within another, with user selectable replacements', 50 'url' => 'http://www.dokuwiki.org/plugin:templater', 51 ); 52 } 53 54 /** 55 * What kind of syntax are we? 56 */ 57 function getType() { 58 return 'container'; 59 } 60 61 function getAllowedTypes() { 62 return array('container', 'substition', 'protected', 'disabled', 'formatting'); 63 } 64 65 /** 66 * Where to sort in? 67 */ 68 function getSort() { 69 return 302; 70 } 71 72 /** 73 * Paragraph Type 74 */ 75 function getPType() { 76 return 'block'; 77 } 78 79 /** 80 * Connect pattern to lexer 81 */ 82 function connectTo($mode) { 83 $this->Lexer->addSpecialPattern("{{template>.+?}}", $mode, 'plugin_templater'); 84 } 85 86 /** 87 * Handle the match 88 */ 89 function handle($match, $state, $pos, Doku_Handler $handler) { 90 global $ID; 91 92 $match = substr($match, 11, -2); // strip markup 93 $replacers = preg_split('/(?<!\\\\)\|/', $match); // Get the replacers 94 $wikipage = array_shift($replacers); 95 96 $replacers = $this->_massageReplacers($replacers); 97 98 $wikipage = preg_split('/\#/u', $wikipage, 2); // split hash from filename 99 $parentpage = empty(self::$pagestack)? $ID : end(self::$pagestack); // get correct namespace 100 // resolve shortcuts: 101 $resolver = new PageResolver(getNS($parentpage)); 102 $wikipage[0] = $resolver->resolveId($wikipage[0]); 103 $exists = page_exists($wikipage[0]); 104 105 // check for perrmission 106 if (auth_quickaclcheck($wikipage[0]) < 1) 107 return false; 108 109 // $wikipage[1] is the header of a template enclosed within a section {{template>page#section}} 110 // Not all template calls will be {{template>page#section}}, some will be {{template>page}} 111 // It fix "Undefined array key 1" warning 112 if (array_key_exists(1, $wikipage)) { 113 $section = cleanID($wikipage[1]); 114 } else { 115 $section = null; 116 } 117 118 return array($wikipage[0], $replacers, $section); 119 } 120 121 private static $pagestack = array(); // keep track of recursing template renderings 122 123 /** 124 * Create output 125 * This is a refactoring candidate. Needs to be a little clearer. 126 */ 127 function render($mode, Doku_Renderer $renderer, $data) { 128 if ($mode != 'xhtml') 129 return false; 130 131 if ($data[0] === false) { 132 // False means no permissions 133 $renderer->doc .= '<div class="templater"> '; 134 $renderer->doc .= $this->getLang('no_permissions_view'); 135 $renderer->doc .= ' </div>'; 136 $renderer->info['cache'] = FALSE; 137 return true; 138 } 139 140 $file = wikiFN($data[0]); 141 if (!@file_exists($file)) { 142 $renderer->doc .= '<div class="templater">— '; 143 $renderer->doc .= $this->getLang('template'); 144 $renderer->doc .= ' '; 145 $renderer->internalLink($data[0]); 146 $renderer->doc .= ' '; 147 $renderer->doc .= $this->getLang('not_found'); 148 $renderer->doc .= '<br/><br/></div>'; 149 $renderer->info['cache'] = FALSE; 150 return true; 151 } else if (array_search($data[0], self::$pagestack) !== false) { 152 $renderer->doc .= '<div class="templater">— '; 153 $renderer->doc .= $this->getLang('processing_template'); 154 $renderer->doc .= ' '; 155 $renderer->internalLink($data[0]); 156 $renderer->doc .= ' '; 157 $renderer->doc .= $this->getLang('stopped_recursion'); 158 $renderer->doc .= '<br/><br/></div>'; 159 return true; 160 } 161 self::$pagestack[] = $data[0]; // push this onto the stack 162 163 // Get the raw file, and parse it into its instructions. This could be cached... maybe. 164 $rawFile = io_readfile($file); 165 166 // fill in all known values 167 if(!empty($data[1]['keys']) && !empty($data[1]['vals'])) { 168 $rawFile = str_replace($data[1]['keys'], $data[1]['vals'], $rawFile); 169 } 170 171 // replace unmatched substitutions with "" or use DEFAULT_STR from data arguments if exists. 172 $left_overs = '/'.BEGIN_REPLACE_DELIMITER.'.*'.END_REPLACE_DELIMITER.'/'; 173 174 if(!empty($data[1]['keys']) && !empty($data[1]['vals'])) { 175 $def_key = array_search(BEGIN_REPLACE_DELIMITER."DEFAULT_STR".END_REPLACE_DELIMITER, $data[1]['keys']); 176 $DEFAULT_STR = $def_key ? $data[1]['vals'][$def_key] : ""; 177 $rawFile = preg_replace($left_overs, $DEFAULT_STR, $rawFile); 178 } 179 180 $instr = p_get_instructions($rawFile); 181 182 // filter section if given 183 if ($data[2]) { 184 $getSection = $this->_getSection($data[2], $instr); 185 186 $instr = $getSection[0]; 187 188 if(!is_null($getSection[1])) { 189 $renderer->doc .= sprintf($getSection[1], $data[2]); 190 $renderer->internalLink($data[0]); 191 $renderer->doc .= '.<br/><br/></div>'; 192 } 193 } 194 195 // correct relative internal links and media 196 $instr = $this->_correctRelNS($instr, $data[0]); 197 198 // doesn't show the heading for each template if {{template>page#section}} 199 if (sizeof($instr) > 0 && !isset($getSection[1])) { 200 if (array_key_exists(0, $instr[0][1]) && $instr[0][1][0] == $data[2]) { 201 $instr[0][1][0] = null; 202 } 203 } 204 205 // render the instructructions on the fly 206 $text = p_render('xhtml', $instr, $info); 207 208 // remove toc, section edit buttons and category tags 209 $patterns = array('!<div class="toc">.*?(</div>\n</div>)!s', 210 '#<!-- SECTION \[(\d*-\d*)\] -->#', 211 '!<div class="category">.*?</div>!s'); 212 $replace = array('', '', ''); 213 $text = preg_replace($patterns, $replace, $text); 214 215 // prevent caching to ensure the included page is always fresh 216 $renderer->info['cache'] = FALSE; 217 218 // embed the included page 219 $renderer->doc .= '<div class="templater">'; 220 $renderer->doc .= $text; 221 $renderer->doc .= '</div>'; 222 223 array_pop(self::$pagestack); // pop off the stack when done 224 return true; 225 } 226 227 /** 228 * Get a section including its subsections 229 */ 230 function _getSection($title, $instructions) { 231 $i = (array) null; 232 $level = null; 233 $no_section = null; 234 235 foreach ($instructions as $instruction) { 236 if ($instruction[0] == 'header') { 237 238 // found the right header 239 if (cleanID($instruction[1][0]) == $title) { 240 $level = $instruction[1][1]; 241 $i[] = $instruction; 242 } else { 243 if (isset($level) && isset($i)) { 244 if ($instruction[1][1] > $level) { 245 $i[] = $instruction; 246 // next header of the same level or higher -> exit 247 } else { 248 return array($i,null); 249 } 250 } 251 } 252 } else { // content between headers 253 if (isset($level) && isset($i)) { 254 $i[] = $instruction; 255 } 256 } 257 } 258 259 // Fix for when page#section doesn't exist 260 if(sizeof($i) == 0) { 261 $no_section_begin = '<div class="templater">— '; 262 $no_section_end = $this->getLang('no_such_section'); 263 $no_section = $no_section_begin . $no_section_end . ' '; 264 } 265 266 return array($i,$no_section); 267 } 268 269 /** 270 * Corrects relative internal links and media 271 */ 272 function _correctRelNS($instr, $incl) { 273 global $ID; 274 275 // check if included page is in same namespace 276 $iNS = getNS($incl); 277 if (getNS($ID) == $iNS) 278 return $instr; 279 280 // convert internal links and media from relative to absolute 281 $n = count($instr); 282 for($i = 0; $i < $n; $i++) { 283 if (substr($instr[$i][0], 0, 8) != 'internal') 284 continue; 285 286 // relative subnamespace 287 if ($instr[$i][1][0][0] == '.') { 288 $instr[$i][1][0] = $iNS.':'.substr($instr[$i][1][0], 1); 289 290 // relative link 291 } else if (strpos($instr[$i][1][0], ':') === false) { 292 $instr[$i][1][0] = $iNS.':'.$instr[$i][1][0]; 293 } 294 } 295 296 return $instr; 297 } 298 299 /** 300 * Handles the replacement array 301 */ 302 function _massageReplacers($replacers) { 303 $r = array(); 304 if (is_null($replacers)) { 305 $r['keys'] = null; 306 $r['vals'] = null; 307 } else if (is_string($replacers)) { 308 if ( str_contains($replacers, '=') && (substr(trim($replacers), -1) != '=') ){ 309 list($k, $v) = explode('=', $replacers, 2); 310 $r['keys'] = BEGIN_REPLACE_DELIMITER.trim($k).END_REPLACE_DELIMITER; 311 $r['vals'] = trim(str_replace('\|', '|', $v)); 312 } 313 } else if ( is_array($replacers) ) { 314 foreach($replacers as $rep) { 315 if ( str_contains($rep, '=') && (substr(trim($rep), -1) != '=') ){ 316 list($k, $v) = explode('=', $rep, 2); 317 $r['keys'][] = BEGIN_REPLACE_DELIMITER.trim($k).END_REPLACE_DELIMITER; 318 if (trim($v)[0] == '"' and trim($v)[-1] == '"') { 319 $r['vals'][] = substr(trim(str_replace('\|','|',$v)), 1, -1); 320 } else { 321 $r['vals'][] = trim(str_replace('\|','|',$v)); 322 } 323 } 324 } 325 } else { 326 // This is an assertion failure. We should NEVER get here. 327 //die("FATAL ERROR! Unknown type passed to syntax_plugin_templater::massageReplaceMentArray() can't message syntax_plugin_templater::\$replacers! Type is:".gettype($r)." Value is:".$r); 328 $r['keys'] = null; 329 $r['vals'] = null; 330 } 331 return $r; 332 } 333} 334?> 335 336