1<?php 2 3/** 4 * DokuWiki plugin Struct Template generic syntax 5 * 6 * @author Iain Hallam <iain@nineworlds.net> 7 * @copyright © 2022 Iain Hallam 8 * @license GPL-2.0-only (http://www.gnu.org/licenses/gpl-2.0.html) 9 */ 10 11declare(strict_types=1); 12 13namespace dokuwiki\plugin\structtemplate\meta; 14 15use Doku_Handler; 16use Doku_Renderer; 17use dokuwiki\Extension\SyntaxPlugin; 18use dokuwiki\plugin\struct\meta\ConfigParser; 19use dokuwiki\plugin\struct\meta\SearchConfig; 20use dokuwiki\plugin\struct\meta\StructException; 21 22/** 23 * Syntax plugin extending standard DokuWiki class 24 */ 25class StructTemplateSyntax extends SyntaxPlugin 26{ 27 /** @var string TAG The tag to be used in Wiki markup between < and > */ 28 /** @var string PLUGIN The system name of the plugin */ 29 /** @var string OPEN_SYNTAX Interpolation syntax */ 30 /** @var string CLOSE_SYNTAX Interpolation syntax */ 31 public const TAG = 'struct-template'; 32 public const PLUGIN = 'structtemplate'; 33 public const OPEN_SYNTAX = '{{$$'; 34 public const CLOSE_SYNTAX = '}}'; 35 36 /** 37 * Define the type of syntax plugin 38 * 39 * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 40 */ 41 public function getType() 42 { 43 return 'substition'; 44 } 45 46 /** 47 * Define the precedence of this plugin to the parser 48 * 49 * @see https://www.dokuwiki.org/devel:parser:getsort_list 50 */ 51 public function getSort() 52 { 53 return 45; 54 } 55 56 /** 57 * Handle matches 58 * 59 * @see https://www.dokuwiki.org/devel:syntax_plugins#handle_method 60 * 61 * @param string $match Text matched by the patterns 62 * @param int $state Lexer state for the match 63 * @param int $position Character position of the matched text 64 * @param object $handler Doku_Handler object 65 * @return array Data for render() 66 */ 67 public function handle($match, $state, $position, Doku_Handler $handler): array 68 { 69 // Configuration 70 // ------------------------------------------------------------- 71 72 // Access global configuration settings 73 global $conf; 74 // Disable section editing for the template 75 $old_maxseclevel = $conf['maxseclevel']; 76 $conf['maxseclevel'] = 0; 77 78 // Extract the data block and template 79 // ------------------------------------------------------------- 80 81 $template_start_index = 0; 82 // Reduce match to Struct search config 83 $lines = explode("\n", $match); 84 // Ignore first two lines (tag and data header) and last line (closing tag) 85 for ($line_index = 2; $line_index <= count($lines) - 1; $line_index++) { 86 if (preg_match('/^----+$/', $lines[$line_index])) { 87 // Reached the end of the data block 88 $template_start_index = $line_index + 1; 89 break; 90 } 91 92 $struct_syntax[] = $lines[$line_index]; 93 } 94 // -1: ignore last line containing closing tag 95 $template = implode("\n", array_slice($lines, $template_start_index, -1)); 96 97 // Check flags 98 $options['html'] = (preg_match('/\bhtml\b/', strtolower($lines[0])) === 1); //FIXME: check if HTML is allowed 99 100 // Configure the Struct search 101 // ------------------------------------------------------------- 102 103 try { 104 $parser = new ConfigParser($struct_syntax); 105 $search_config = $parser->getConfig(); 106 } catch (StructException $e) { 107 msg($e->getMessage(), -1, $e->getLine(), $e->getFile()); 108 if ($conf['allowdebug']) { 109 msg('<pre>' . hsc($e->getTraceAsString()) . '</pre>', -1); 110 } 111 // Re-enable section editing 112 $conf['maxseclevel'] = $old_maxseclevel; 113 return []; 114 } 115 116 // Return data for rendering 117 // ------------------------------------------------------------- 118 119 // Re-enable section editing 120 $conf['maxseclevel'] = $old_maxseclevel; 121 122 return [$search_config, $template, $options]; 123 } 124 125 /** 126 * Output rendered matches 127 * 128 * @see https://www.dokuwiki.org/devel:syntax_plugins#render_method 129 * 130 * @param string $mode Output format to generate 131 * @param object $renderer Doku_Renderer object 132 * @param array $data Data created by handle() 133 * @return bool Whether the syntax rendered OK 134 */ 135 public function render($mode, Doku_Renderer $renderer, $data): bool 136 { 137 $search_config = $data[0]; 138 $template = $data[1]; 139 $options = $data[2]; 140 141 // Access global configuration settings 142 global $conf; 143 144 // Disable section editing for the template 145 $old_maxseclevel = $conf['maxseclevel']; 146 $conf['maxseclevel'] = 0; 147 148 // Run the search (can't be in handler as that is cached) 149 try { 150 $search = new SearchConfig($search_config); 151 152 // Get all matching data, no pagination 153 $search->setLimit(0); 154 $search->setOffset(0); 155 156 // Run the search 157 $struct_data = $search->execute(); 158 $this->n_rows = $search->getCount(); 159 } catch (StructException $e) { 160 msg($e->getMessage(), -1, $e->getLine(), $e->getFile()); 161 if ($conf['allowdebug']) { 162 msg('<pre>' . hsc($e->getTraceAsString()) . '</pre>', -1); 163 } 164 165 // Re-enable section editing 166 $conf['maxseclevel'] = $old_maxseclevel; 167 168 return false; 169 } 170 171 // Construct a lookup table for column names and indices in the result 172 $columns = $search->getColumns(); 173 foreach ($columns as $index => $column) { 174 $column_id = $column->getFullQualifiedLabel(false); 175 // getFullColumnName takes false to disable enforceSingleColumn 176 $column_indices[$column_id] = $index; 177 } 178 179 foreach ($struct_data as $row_index => $row) { 180 $chunks = explode(self::OPEN_SYNTAX, $template); 181 182 // First entry contains no fields 183 $interpolated = $chunks[0]; 184 $chunks = array_slice($chunks, 1); 185 186 foreach ($chunks as $chunk) { 187 // Since the string was exploded on the open marker, this must start with a field 188 $chunk_parts = explode(self::CLOSE_SYNTAX, $chunk, 2); 189 $column_request = $chunk_parts[0]; 190 $next_output = $chunk_parts[1]; 191 192 if (array_key_exists($column_request, $column_indices)) { 193 $interpolated .= $row[$column_indices[$column_request]]->getDisplayValue(); 194 } else { 195 if ($this->getConf('show_not_found')) { 196 $renderer->cdata($this->getLang('none')); 197 } 198 } 199 $interpolated .= $next_output; 200 } 201 202 if ($conf['htmlok'] == true and $options['html'] == true) { 203 $html = $interpolated; 204 } else { 205 // Rendering needs an array to write 206 $html_info = []; 207 $html = p_render($mode, p_get_instructions($interpolated), $html_info); 208 } 209 210 // Send to document 211 $renderer->doc .= $html; 212 } 213 214 // Re-enable section editing 215 $conf['maxseclevel'] = $old_maxseclevel; 216 217 return true; 218 } 219} 220