← All changes
|
class/Common/Importer/Parser/AbstractParser.php
+143
-22
2.14.24
→
2.15.0
View file →
| @@ -137,13 +137,12 @@ | ||
| 137 | 137 | * @return string |
| 138 | 138 | */ |
| 139 | 139 | public function query_string($query) |
| 140 | 140 | { |
| 141 | - | |
| 142 | - $output = preg_replace_callback('/{(.*?)}/', array($this, 'query_matches'), $query); | |
| 143 | - $output = $this->handle_custom_methods($output); | |
| 144 | - | |
| 145 | - return $output; | |
| 141 | + // Parse [iwp:method(...)] before substituting {column} values, otherwise | |
| 142 | + // parentheses in the data (e.g. Excel =Hyperlink("url")) can close a | |
| 143 | + // malformed mapping that is missing ")". | |
| 144 | + return $this->interpolate_braces($this->handle_custom_methods($query)); | |
| 146 | 145 | } |
| 147 | 146 | |
| 148 | 147 | public function query_matches($matches) |
| 149 | 148 | { |
| @@ -162,34 +161,156 @@ | ||
| 162 | 161 | } |
| 163 | 162 | |
| 164 | 163 | public function handle_custom_methods($input) |
| 165 | 164 | { |
| 166 | - // m: Multiline modifier | |
| 167 | - // s: matches all characters including newlines | |
| 168 | - $input = preg_replace_callback('/\[([\w]+)\((.*?)\)\]/ms', function ($matches) { | |
| 165 | + // Prefixed [iwp:method(...)] to avoid colliding with shortcodes / Gutenberg content. | |
| 166 | + if (!is_string($input) || $input === '' || strpos($input, '[iwp:') === false) { | |
| 167 | + return $input; | |
| 168 | + } | |
| 169 | 169 | |
| 170 | - $method = $matches[1]; | |
| 170 | + $offset = 0; | |
| 171 | + $output = ''; | |
| 172 | + $length = strlen($input); | |
| 171 | 173 | |
| 172 | - $result = []; | |
| 173 | - $args = []; | |
| 174 | + while (($start = strpos($input, '[iwp:', $offset)) !== false) { | |
| 175 | + $output .= substr($input, $offset, $start - $offset); | |
| 174 | 176 | |
| 175 | - // Dont split comma's if they are inside a double quote | |
| 176 | - if (preg_match_all('/(?:".*?"|[^",\s]+)(?=\s*,|\s*$)/s', $matches[2], $result) > 0) { | |
| 177 | - $args = $result[0]; | |
| 178 | - foreach ($args as &$arg) { | |
| 177 | + $parsed = $this->parse_custom_method_at($input, $start, $length); | |
| 178 | + if ($parsed === null) { | |
| 179 | + $output .= '[iwp:'; | |
| 180 | + $offset = $start + 5; | |
| 181 | + continue; | |
| 182 | + } | |
| 179 | 183 | |
| 180 | - // Strip commas from start and end of string | |
| 181 | - $arg = preg_replace('/^(\'(.*)\'|"(.*)")$/s', '$2$3', $arg); | |
| 184 | + $output .= $parsed[1]; | |
| 185 | + $offset = $parsed[0]; | |
| 186 | + } | |
| 187 | + | |
| 188 | + return $output . substr($input, $offset); | |
| 189 | + } | |
| 190 | + | |
| 191 | + /** | |
| 192 | + * Substitute {column} / {xpath} selectors. | |
| 193 | + * | |
| 194 | + * @param string $query | |
| 195 | + * @return string | |
| 196 | + */ | |
| 197 | + private function interpolate_braces($query) | |
| 198 | + { | |
| 199 | + if (!is_string($query) || $query === '' || strpos($query, '{') === false) { | |
| 200 | + return $query; | |
| 201 | + } | |
| 202 | + | |
| 203 | + return preg_replace_callback('/{(.*?)}/', array($this, 'query_matches'), $query); | |
| 204 | + } | |
| 205 | + | |
| 206 | + /** | |
| 207 | + * @param string $input | |
| 208 | + * @param int $start | |
| 209 | + * @param int $length | |
| 210 | + * @return array{0:int,1:string}|null End offset and replacement, or null if not a complete call. | |
| 211 | + */ | |
| 212 | + private function parse_custom_method_at($input, $start, $length) | |
| 213 | + { | |
| 214 | + $i = $start + 5; | |
| 215 | + $name_len = strspn($input, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_', $i); | |
| 216 | + if ($name_len < 1) { | |
| 217 | + return null; | |
| 218 | + } | |
| 219 | + | |
| 220 | + $method = substr($input, $i, $name_len); | |
| 221 | + $i += $name_len; | |
| 222 | + if ($i >= $length || $input[$i] !== '(') { | |
| 223 | + return null; | |
| 224 | + } | |
| 225 | + $i++; | |
| 226 | + | |
| 227 | + $extracted = $this->extract_balanced_method_args($input, $i, $length); | |
| 228 | + if ($extracted === null) { | |
| 229 | + return null; | |
| 230 | + } | |
| 231 | + | |
| 232 | + list($raw_args, $after_paren) = $extracted; | |
| 233 | + if ($after_paren >= $length || $input[$after_paren] !== ']') { | |
| 234 | + return null; | |
| 235 | + } | |
| 236 | + | |
| 237 | + $end = $after_paren + 1; | |
| 238 | + $original = substr($input, $start, $end - $start); | |
| 239 | + $args = $this->split_custom_method_args($raw_args); | |
| 240 | + foreach ($args as &$arg) { | |
| 241 | + $arg = $this->interpolate_braces($arg); | |
| 242 | + } | |
| 243 | + unset($arg); | |
| 244 | + | |
| 245 | + if (is_callable($method)) { | |
| 246 | + return array($end, call_user_func_array($method, $args)); | |
| 247 | + } | |
| 248 | + | |
| 249 | + return array($end, $original); | |
| 250 | + } | |
| 251 | + | |
| 252 | + /** | |
| 253 | + * Read until the method's closing ")", ignoring parentheses inside quotes. | |
| 254 | + * | |
| 255 | + * @param string $input | |
| 256 | + * @param int $start | |
| 257 | + * @param int $length | |
| 258 | + * @return array{0:string,1:int}|null Raw args and offset after ")", or null if unterminated. | |
| 259 | + */ | |
| 260 | + private function extract_balanced_method_args($input, $start, $length) | |
| 261 | + { | |
| 262 | + $depth = 1; | |
| 263 | + $quote = null; | |
| 264 | + for ($i = $start; $i < $length; $i++) { | |
| 265 | + $ch = $input[$i]; | |
| 266 | + if ($quote !== null) { | |
| 267 | + if ($ch === $quote) { | |
| 268 | + $quote = null; | |
| 182 | 269 | } |
| 270 | + continue; | |
| 183 | 271 | } |
| 272 | + if ($ch === '"' || $ch === "'") { | |
| 273 | + $quote = $ch; | |
| 274 | + continue; | |
| 275 | + } | |
| 276 | + if ($ch === '(') { | |
| 277 | + $depth++; | |
| 278 | + continue; | |
| 279 | + } | |
| 280 | + if ($ch === ')') { | |
| 281 | + $depth--; | |
| 282 | + if ($depth === 0) { | |
| 283 | + return array(substr($input, $start, $i - $start), $i + 1); | |
| 284 | + } | |
| 285 | + } | |
| 286 | + } | |
| 184 | 287 | |
| 185 | - if (is_callable($method)) { | |
| 186 | - return call_user_func_array($method, $args); | |
| 288 | + return null; | |
| 289 | + } | |
| 290 | + | |
| 291 | + /** | |
| 292 | + * @param string $raw_args | |
| 293 | + * @return array | |
| 294 | + */ | |
| 295 | + private function split_custom_method_args($raw_args) | |
| 296 | + { | |
| 297 | + $args = []; | |
| 298 | + if ($raw_args === '') { | |
| 299 | + return $args; | |
| 300 | + } | |
| 301 | + | |
| 302 | + // Dont split comma's if they are inside a double quote | |
| 303 | + if (preg_match_all('/(?:".*?"|[^",\s]+)(?=\s*,|\s*$)/s', $raw_args, $result) > 0) { | |
| 304 | + $args = $result[0]; | |
| 305 | + foreach ($args as &$arg) { | |
| 306 | + // Strip quotes from start and end of string | |
| 307 | + $arg = preg_replace('/^(\'(.*)\'|"(.*)")$/s', '$2$3', $arg); | |
| 187 | 308 | } |
| 309 | + unset($arg); | |
| 310 | + } | |
| 188 | 311 | |
| 189 | - return $matches[0]; | |
| 190 | - }, $input); | |
| 191 | - return $input; | |
| 312 | + return $args; | |
| 192 | 313 | } |
| 193 | 314 | |
| 194 | 315 | public function map_field_data($input, $map) |
| 195 | 316 | { |