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