1<?php 2 3namespace dokuwiki\plugin\struct\meta; 4 5use dokuwiki\Extension\Event; 6 7/** 8 * Class ConfigParser 9 * 10 * Utilities to parse the configuration syntax into an array 11 * 12 * @package dokuwiki\plugin\struct\meta 13 */ 14class ConfigParser 15{ 16 protected $config = [ 17 'limit' => 0, 18 'dynfilters' => false, 19 'summarize' => false, 20 'rownumbers' => false, 21 'sepbyheaders' => false, 22 'target' => '', 23 'align' => [], 24 'headers' => [], 25 'cols' => [], 26 'widths' => [], 27 'filter' => [], 28 'schemas' => [], 29 'sort' => [], 30 'csv' => true, 31 'nesting' => 0, 32 'index' => 0, 33 'classes' => [] 34 ]; 35 36 /** 37 * Parser constructor. 38 * 39 * parses the given configuration lines 40 * 41 * @param $lines 42 */ 43 public function __construct($lines) 44 { 45 /** @var \helper_plugin_struct_config $helper */ 46 $helper = plugin_load('helper', 'struct_config'); 47 // parse info 48 foreach ($lines as $line) { 49 [$key, $val] = $this->splitLine($line); 50 if (!$key) continue; 51 52 $logic = 'OR'; 53 // handle line commands (we allow various aliases here) 54 switch ($key) { 55 case 'from': 56 case 'schema': 57 case 'tables': 58 $this->config['schemas'] = array_merge($this->config['schemas'], $this->parseSchema($val)); 59 break; 60 case 'select': 61 case 'cols': 62 case 'field': 63 case 'col': 64 $this->config['cols'] = $this->parseValues($val); 65 break; 66 case 'sepbyheaders': 67 $this->config['sepbyheaders'] = (bool)$val; 68 break; 69 case 'head': 70 case 'header': 71 case 'headers': 72 $this->config['headers'] = $this->parseValues($val); 73 break; 74 case 'align': 75 $this->config['align'] = $this->parseAlignments($val); 76 break; 77 case 'width': 78 case 'widths': 79 $this->config['widths'] = $this->parseWidths($val); 80 break; 81 case 'min': 82 $this->config['min'] = abs((int)$val); 83 break; 84 case 'limit': 85 case 'max': 86 $this->config['limit'] = abs((int)$val); 87 break; 88 case 'order': 89 case 'sort': 90 $sorts = $this->parseValues($val); 91 $sorts = array_map([$helper, 'parseSort'], $sorts); 92 $this->config['sort'] = array_merge($this->config['sort'], $sorts); 93 break; 94 case 'where': 95 case 'filter': 96 case 'filterand': // phpcs:ignore PSR2.ControlStructures.SwitchDeclaration.TerminatingComment 97 /** @noinspection PhpMissingBreakStatementInspection */ 98 case 'and': // phpcs:ignore PSR2.ControlStructures.SwitchDeclaration.TerminatingComment 99 $logic = 'AND'; 100 case 'filteror': 101 case 'or': 102 $flt = $helper->parseFilterLine($logic, $val); 103 if ($flt) { 104 $this->config['filter'][] = $flt; 105 } 106 break; 107 case 'dynfilters': 108 $this->config['dynfilters'] = (bool)$val; 109 break; 110 case 'rownumbers': 111 $this->config['rownumbers'] = (bool)$val; 112 break; 113 case 'summarize': 114 $this->config['summarize'] = (bool)$val; 115 break; 116 case 'csv': 117 $this->config['csv'] = (bool)$val; 118 break; 119 case 'target': 120 case 'page': 121 $this->config['target'] = cleanID($val); 122 break; 123 case 'nesting': 124 case 'nest': 125 $this->config['nesting'] = (int) $val; 126 break; 127 case 'index': 128 $this->config['index'] = (int) $val; 129 break; 130 case 'class': 131 case 'classes': 132 $this->config['classes'] = $this->parseClasses($val); 133 break; 134 default: 135 $data = ['config' => &$this->config, 'key' => $key, 'val' => $val]; 136 $ev = new Event('PLUGIN_STRUCT_CONFIGPARSER_UNKNOWNKEY', $data); 137 if ($ev->advise_before()) { 138 throw new StructException("unknown option '%s'", hsc($key)); 139 } 140 $ev->advise_after(); 141 } 142 } 143 144 // fill up headers - a NULL signifies that the column label is wanted 145 $this->config['headers'] = (array)$this->config['headers']; 146 $cnth = count($this->config['headers']); 147 $cntf = count($this->config['cols']); 148 for ($i = $cnth; $i < $cntf; $i++) { 149 $this->config['headers'][] = null; 150 } 151 // fill up alignments 152 $cnta = count($this->config['align']); 153 for ($i = $cnta; $i < $cntf; $i++) { 154 $this->config['align'][] = null; 155 } 156 } 157 158 /** 159 * Get the parsed configuration 160 * 161 * @return array 162 */ 163 public function getConfig() 164 { 165 return $this->config; 166 } 167 168 /** 169 * Splits the given line into key and value 170 * 171 * @param $line 172 * @return array returns ['',''] if the line is empty 173 */ 174 protected function splitLine($line) 175 { 176 // ignore comments 177 $line = preg_replace('/(?<![&\\\\])#.*$/', '', $line); 178 $line = str_replace('\\#', '#', $line); 179 $line = trim($line); 180 if (empty($line)) return ['', '']; 181 182 $line = preg_split('/\s*:\s*/', $line, 2); 183 $line[0] = strtolower($line[0]); 184 if (!isset($line[1])) $line[1] = ''; 185 186 return $line; 187 } 188 189 /** 190 * parses schema config and aliases 191 * 192 * @param $val 193 * @return array 194 */ 195 protected function parseSchema($val) 196 { 197 $schemas = []; 198 $parts = explode(',', $val); 199 foreach ($parts as $part) { 200 [$table, $alias] = sexplode(' ', trim($part), 2, ''); 201 $table = trim($table); 202 $alias = trim($alias); 203 if (!$table) continue; 204 205 $schemas[] = [$table, $alias]; 206 } 207 return $schemas; 208 } 209 210 /** 211 * Parse alignment data 212 * 213 * @param string $val 214 * @return string[] 215 */ 216 protected function parseAlignments($val) 217 { 218 $cols = explode(',', $val); 219 $data = []; 220 foreach ($cols as $col) { 221 $col = trim(strtolower($col)); 222 if ($col[0] == 'c') { 223 $align = 'center'; 224 } elseif ($col[0] == 'r') { 225 $align = 'right'; 226 } elseif ($col[0] == 'l') { 227 $align = 'left'; 228 } else { 229 $align = null; 230 } 231 $data[] = $align; 232 } 233 234 return $data; 235 } 236 237 /** 238 * Parse width data 239 * 240 * @param $val 241 * @return array 242 */ 243 protected function parseWidths($val) 244 { 245 $vals = explode(',', $val); 246 $vals = array_map('trim', $vals); 247 248 $len = count($vals); 249 for ($i = 0; $i < $len; $i++) { 250 $val = trim(strtolower($vals[$i])); 251 252 if (preg_match('/^\d+.?(\d+)?(px|em|ex|ch|rem|%|in|cm|mm|q|pt|pc)$/', $val)) { 253 // proper CSS unit? 254 $vals[$i] = $val; 255 } elseif (preg_match('/^\d+$/', $val)) { 256 // decimal only? 257 $vals[$i] = $val . 'px'; 258 } else { 259 // invalid 260 $vals[$i] = ''; 261 } 262 } 263 return $vals; 264 } 265 266 /** 267 * Split values at the commas, 268 * - Wrap with quotes to escape comma, quotes escaped by two quotes 269 * - Within quotes spaces are stored. 270 * 271 * @param string $line 272 * @return array 273 */ 274 protected function parseValues($line) 275 { 276 $values = []; 277 $inQuote = false; 278 $escapedQuote = false; 279 $value = ''; 280 $len = strlen($line); 281 for ($i = 0; $i < $len; $i++) { 282 if ($line[$i] === '"') { 283 if ($inQuote) { 284 if ($escapedQuote) { 285 $value .= '"'; 286 $escapedQuote = false; 287 continue; 288 } 289 if (isset($line[$i + 1]) && $line[$i + 1] === '"') { 290 $escapedQuote = true; 291 continue; 292 } 293 $values[] = $value; 294 $inQuote = false; 295 $value = ''; 296 continue; 297 } else { 298 $inQuote = true; 299 $value = ''; //don't store stuff before the opening quote 300 continue; 301 } 302 } elseif ($line[$i] === ',') { 303 if ($inQuote) { 304 $value .= ','; 305 continue; 306 } else { 307 if (strlen($value) < 1) { 308 continue; 309 } 310 $values[] = trim($value); 311 $value = ''; 312 continue; 313 } 314 } 315 $value .= $line[$i]; 316 } 317 if (strlen($value) > 0) { 318 $values[] = trim($value); 319 } 320 return $values; 321 } 322 323 /** 324 * Ensure custom classes are valid and don't clash 325 * 326 * @param string $line 327 * @return string[] 328 */ 329 protected function parseClasses($line) 330 { 331 $classes = $this->parseValues($line); 332 $classes = array_map(function ($class) { 333 $class = str_replace(' ', '_', $class); 334 $class = preg_replace('/[^a-zA-Z0-9_]/', '', $class); 335 return 'struct-custom-' . $class; 336 }, $classes); 337 return $classes; 338 } 339} 340