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