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