Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
224 / 224 |
|
100.00% |
7 / 7 |
CRAP | |
100.00% |
1 / 1 |
| IgnoreFile | |
100.00% |
224 / 224 |
|
100.00% |
7 / 7 |
116 | |
100.00% |
1 / 1 |
| add | |
100.00% |
40 / 40 |
|
100.00% |
1 / 1 |
18 | |||
| filter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| ignores | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| notIgnores | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| test | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
10 | |||
| filterIterator | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| patternToRegex | |
100.00% |
150 / 150 |
|
100.00% |
1 / 1 |
83 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Implementation of the gitignore "spec". |
| 4 | * |
| 5 | * @package automattic/ignorefile |
| 6 | */ |
| 7 | |
| 8 | namespace Automattic; |
| 9 | |
| 10 | use Automattic\IgnoreFile\InvalidPatternException; |
| 11 | use CallbackFilterIterator; |
| 12 | use FilterIterator; |
| 13 | use InvalidArgumentException; |
| 14 | use Iterator; |
| 15 | use RecursiveCallbackFilterIterator; |
| 16 | use RecursiveIterator; |
| 17 | use SplFileInfo; |
| 18 | |
| 19 | /** |
| 20 | * Implementation of the gitignore "spec". |
| 21 | * |
| 22 | * @link https://git-scm.com/docs/gitignore |
| 23 | */ |
| 24 | class IgnoreFile { |
| 25 | |
| 26 | /** |
| 27 | * Set true to throw on invalid patterns. |
| 28 | * |
| 29 | * @var bool |
| 30 | */ |
| 31 | public $strictMode = false; |
| 32 | |
| 33 | /** |
| 34 | * Loaded patterns. |
| 35 | * |
| 36 | * @var array |
| 37 | */ |
| 38 | private $patterns = array(); |
| 39 | |
| 40 | /** |
| 41 | * Add one or more pattern lines, or import from another IgnoreFile instance. |
| 42 | * |
| 43 | * The `$prefix` is intended for use when you're loading multiple `.gitignore` files from |
| 44 | * multiple places in the filesystem. As you load each file, pass the path to the file |
| 45 | * as `$prefix` (either relative to a common base or absolute). |
| 46 | * |
| 47 | * @param string|IgnoreFile|(string|IgnoreFile)[] $patterns Patterns to add. Acceptable values are: |
| 48 | * - A string, possibly containing newlines, representing the contents of a `.gitignore` file. |
| 49 | * - Another `IgnoreFile` instance, which will be merged into the current instance. |
| 50 | * - An array of strings (no newlines allowed) and/or IgnoreFile instances. |
| 51 | * @param string $prefix All (string) patterns must match relative to this prefix. |
| 52 | * This is not applied to patterns copied from another Ignore instance. |
| 53 | * @throws InvalidArgumentException If arguments are invalid. |
| 54 | * @throws InvalidPatternException If patterns are invalid and `$this->strictMode` is set. |
| 55 | */ |
| 56 | public function add( $patterns, $prefix = '' ) { |
| 57 | if ( '' !== $prefix && substr( $prefix, -1 ) !== '/' ) { |
| 58 | throw new InvalidArgumentException( 'Prefix must end in `/`' ); |
| 59 | } |
| 60 | |
| 61 | if ( is_string( $patterns ) ) { |
| 62 | if ( substr( $patterns, 0, 3 ) === "\xef\xbb\xbf" ) { // UTF-8 BOM |
| 63 | $patterns = substr( $patterns, 3 ); |
| 64 | } |
| 65 | $patterns = explode( "\n", $patterns ); |
| 66 | } elseif ( ! is_array( $patterns ) ) { |
| 67 | $patterns = array( $patterns ); |
| 68 | } |
| 69 | |
| 70 | foreach ( $patterns as $idx => $pat ) { |
| 71 | if ( $pat instanceof IgnoreFile ) { |
| 72 | $this->patterns = array_merge( $this->patterns, $pat->patterns ); |
| 73 | continue; |
| 74 | } |
| 75 | |
| 76 | $pat = (string) $pat; |
| 77 | |
| 78 | // Line may not contain newlines. But DWIM and ignore trailing newlines. |
| 79 | $pat = rtrim( $pat, "\r\n" ); |
| 80 | if ( strpos( $pat, "\r" ) !== false || strpos( $pat, "\n" ) !== false ) { |
| 81 | if ( $this->strictMode ) { |
| 82 | throw new InvalidPatternException( "Pattern at index $idx may not contain newlines" ); |
| 83 | } |
| 84 | continue; |
| 85 | } |
| 86 | |
| 87 | // A blank line matches no files. Trailing spaces are ignored unless they are quoted with backslash ("\"). |
| 88 | // A line starting with # serves as a comment. |
| 89 | if ( trim( $pat ) === '' || '#' === $pat[0] ) { |
| 90 | continue; |
| 91 | } |
| 92 | |
| 93 | $orig = $pat; |
| 94 | |
| 95 | // Trailing spaces are ignored unless they are quoted with backslash ("\"). |
| 96 | $pat = preg_replace( '/(?<!\\\\) +$/', '', $pat ); |
| 97 | |
| 98 | // A prefix "!" negates the pattern. |
| 99 | $negate = '!' === $pat[0]; |
| 100 | if ( $negate ) { |
| 101 | $pat = (string) substr( $pat, 1 ); |
| 102 | if ( '' === $pat ) { |
| 103 | if ( $this->strictMode ) { |
| 104 | throw new InvalidPatternException( "Pattern at index $idx consists of only `!`" ); |
| 105 | } |
| 106 | continue; |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | try { |
| 111 | $regex = $this->patternToRegex( $pat, $idx ); |
| 112 | } catch ( InvalidPatternException $ex ) { |
| 113 | if ( $this->strictMode ) { |
| 114 | throw $ex; |
| 115 | } |
| 116 | continue; |
| 117 | } |
| 118 | |
| 119 | $this->patterns[] = array( |
| 120 | 'prefix' => $prefix, |
| 121 | 'negate' => $negate, |
| 122 | 'pattern' => $orig, |
| 123 | 'regex' => $regex, |
| 124 | ); |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | /** |
| 129 | * Filter an array of paths to remove any that are ignored. |
| 130 | * |
| 131 | * See `ignores()` for notes on how to pass paths. |
| 132 | * |
| 133 | * @param (string|SplFileInfo)[] $paths Paths. |
| 134 | * @return (string|SplFileInfo)[] |
| 135 | */ |
| 136 | public function filter( array $paths ) { |
| 137 | return array_filter( $paths, array( $this, 'notIgnores' ) ); |
| 138 | } |
| 139 | |
| 140 | /** |
| 141 | * Indicate whether the path is ignored. |
| 142 | * |
| 143 | * If `$prefix` was used with `add()`, the paths passed should be equivalent (relative to the |
| 144 | * same common base, or absolute if `add()` was passed absolute paths). |
| 145 | * |
| 146 | * Directories must include a trailing `/` to be recognized as such. Without that, they'll be |
| 147 | * treated as files. |
| 148 | * |
| 149 | * @param string|SplFileInfo $path Path to test. |
| 150 | * @return bool If the path is ignored. |
| 151 | */ |
| 152 | public function ignores( $path ) { |
| 153 | $ret = $this->test( $path ); |
| 154 | return $ret['ignored']; |
| 155 | } |
| 156 | |
| 157 | /** |
| 158 | * Indicate whether the path is not ignored. |
| 159 | * |
| 160 | * This is, basically, `! $this->ignores( $path )`. It's convenient for |
| 161 | * passing to `array_filter()` or the like. |
| 162 | * |
| 163 | * @param string $path Path to test. |
| 164 | * @return bool If the path is not ignored. |
| 165 | */ |
| 166 | public function notIgnores( $path ) { |
| 167 | $ret = $this->test( $path ); |
| 168 | return ! $ret['ignored']; |
| 169 | } |
| 170 | |
| 171 | /** |
| 172 | * Indicate whether the path is ignored or unignored. |
| 173 | * |
| 174 | * There are three possible return values: |
| 175 | * |
| 176 | * - `array( 'ignored' => true, 'unignored' => false, 'pattern' => string )` => Path is ignored. |
| 177 | * - `array( 'ignored' => false, 'unignored' => true, 'pattern' => string )` => Path is explicitly unignored. |
| 178 | * - `array( 'ignored' => false, 'unignored' => false, 'pattern' => null )` => Path is neither ignored nor unignored. |
| 179 | * |
| 180 | * @param string|SplFileInfo $path Path to test. |
| 181 | * @return array As above. |
| 182 | */ |
| 183 | public function test( $path ) { |
| 184 | if ( $path instanceof SplFileInfo ) { |
| 185 | $path = $path->isDir() ? "$path/" : "$path"; |
| 186 | } |
| 187 | |
| 188 | $parentPath = dirname( $path ) . '/'; |
| 189 | if ( './' !== $parentPath && $parentPath !== $path ) { |
| 190 | $parent = $this->test( $parentPath ); |
| 191 | if ( $parent['ignored'] ) { |
| 192 | return $parent; |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | $ret = array( |
| 197 | 'ignored' => false, |
| 198 | 'unignored' => false, |
| 199 | 'pattern' => null, |
| 200 | ); |
| 201 | |
| 202 | foreach ( $this->patterns as $pat ) { |
| 203 | // Prefix match. |
| 204 | if ( '' === $pat['prefix'] ) { |
| 205 | $p = $path; |
| 206 | } else { |
| 207 | $l = strlen( $pat['prefix'] ); |
| 208 | if ( substr( $path, 0, $l ) !== $pat['prefix'] ) { |
| 209 | continue; |
| 210 | } |
| 211 | $p = (string) substr( $path, $l ); |
| 212 | } |
| 213 | |
| 214 | if ( preg_match( $pat['regex'], $p ) ) { |
| 215 | $ret = array( |
| 216 | 'ignored' => ! $pat['negate'], |
| 217 | 'unignored' => $pat['negate'], |
| 218 | 'pattern' => $pat['pattern'], |
| 219 | ); |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | return $ret; |
| 224 | } |
| 225 | |
| 226 | /** |
| 227 | * Filter an iterator. |
| 228 | * |
| 229 | * When using FileSystemIterator or its subclasses, do not use CURRENT_AS_PATHNAME |
| 230 | * as that does not include the necessary trailing `/` on directory names. |
| 231 | * |
| 232 | * @param Iterator $iter Iterator to filter. |
| 233 | * @return FilterIterator|FilterIterator&RecursiveIterator Filtered iterator. If the input implements `RecursiveIterator`, the returned iterator does too. |
| 234 | */ |
| 235 | public function filterIterator( Iterator $iter ) { |
| 236 | if ( $iter instanceof RecursiveIterator ) { |
| 237 | return new RecursiveCallbackFilterIterator( $iter, array( $this, 'notIgnores' ) ); |
| 238 | } |
| 239 | return new CallbackFilterIterator( $iter, array( $this, 'notIgnores' ) ); |
| 240 | } |
| 241 | |
| 242 | /** |
| 243 | * Convert a pattern to a PCRE regex. |
| 244 | * |
| 245 | * @param string $pat Pattern. |
| 246 | * @param string|int $idx Index for errors. |
| 247 | * @return string Regex. |
| 248 | * @throws InvalidPatternException If pattern is invalid. |
| 249 | */ |
| 250 | protected function patternToRegex( $pat, $idx ) { |
| 251 | $orig = $pat; |
| 252 | |
| 253 | // If there is a separator at the beginning or middle (or both) of the pattern, then the pattern is relative to the $prefix. Otherwise, it has an implicit "**/" at the start. |
| 254 | $pos = strpos( $pat, '/' ); |
| 255 | if ( false === $pos || strlen( $pat ) - 1 === $pos ) { |
| 256 | $pat = ( '/' === $pat[0] ? '**' : '**/' ) . $pat; |
| 257 | } |
| 258 | |
| 259 | // Collapse any multiple "/". |
| 260 | $pat = preg_replace( '#//+#', '/', $pat ); |
| 261 | |
| 262 | // Collapse any "/**/**/". |
| 263 | $pat = preg_replace( '#(?:^|/)\*\*/(?:\*\*/)+#', '/**/', $pat ); |
| 264 | |
| 265 | $re = '#'; |
| 266 | |
| 267 | // A leading "**/" means match in all directories. Otherwise, it's anchored to the start. |
| 268 | // A leading "/**/" is the same. |
| 269 | if ( substr( $pat, 0, 3 ) === '**/' || substr( $pat, 0, 4 ) === '/**/' ) { |
| 270 | $re .= '(?:^|/)'; |
| 271 | $pat = (string) substr( $pat, '/' === $pat[0] ? 4 : 3 ); |
| 272 | } else { |
| 273 | $re .= '^'; |
| 274 | // Ignore a leading "/". |
| 275 | if ( '/' === $pat[0] ) { |
| 276 | $pat = (string) substr( $pat, 1 ); |
| 277 | } |
| 278 | } |
| 279 | |
| 280 | $atDir = true; |
| 281 | $noBT = false; |
| 282 | while ( '' !== $pat ) { |
| 283 | // A trailing "/**" matches everything inside. |
| 284 | if ( '/**' === $pat ) { |
| 285 | $re .= ( $noBT ? ')' : '' ) . ( $atDir ? '' : '/' ) . '.++'; |
| 286 | $atDir = true; |
| 287 | $noBT = false; |
| 288 | break; |
| 289 | } |
| 290 | |
| 291 | // A slash followed by two consecutive asterisks then a slash matches zero or more directories. |
| 292 | if ( substr( $pat, 0, 4 ) === '/**/' ) { |
| 293 | // Avoid creating a poorly-performing regex if someone does something weird with backslashes or something that stops the collapsing above from working. |
| 294 | if ( substr( $re, -11 ) !== '(?:[^/]+/)*' ) { |
| 295 | $re .= ( $noBT ? ')' : '' ) . ( $atDir ? '' : '/' ) . '(?:[^/]+/)*'; |
| 296 | } |
| 297 | $pat = (string) substr( $pat, 4 ); |
| 298 | $atDir = true; |
| 299 | $noBT = false; |
| 300 | continue; |
| 301 | } |
| 302 | |
| 303 | // An asterisk "*" matches anything except a slash. |
| 304 | if ( '*' === $pat[0] ) { |
| 305 | // Avoid creating a poorly-performing regex if someone does like "/foo**". |
| 306 | if ( substr( $re, -5 ) !== '[^/]*' ) { |
| 307 | if ( $atDir && ! $noBT ) { |
| 308 | $re .= '(?>'; |
| 309 | $noBT = true; |
| 310 | } |
| 311 | // Use a lookahead to ensure the component isn't empty, if the "*" is at the start of a component. |
| 312 | if ( $atDir ) { |
| 313 | $re .= '(?=[^/])'; |
| 314 | } |
| 315 | $re .= '[^/]*'; |
| 316 | } |
| 317 | $pat = (string) substr( $pat, 1 ); |
| 318 | $atDir = false; |
| 319 | continue; |
| 320 | } |
| 321 | |
| 322 | // The character "?" matches any one character except "/". |
| 323 | if ( '?' === $pat[0] ) { |
| 324 | if ( $atDir && ! $noBT ) { |
| 325 | $re .= '(?>'; |
| 326 | $noBT = true; |
| 327 | } |
| 328 | $re .= '[^/]'; |
| 329 | $pat = (string) substr( $pat, 1 ); |
| 330 | $atDir = false; |
| 331 | continue; |
| 332 | } |
| 333 | |
| 334 | // Probably a bracket expression. Complex. |
| 335 | if ( '[' === $pat[0] ) { |
| 336 | $pat = (string) substr( $pat, 1 ); |
| 337 | $savedPat = $pat; |
| 338 | $cre = ''; |
| 339 | $needLookahead = false; |
| 340 | |
| 341 | // If the charset is negated, we also need to include `/` in the list of characters to exclude. |
| 342 | // Note use of '^' as the negator is unspecified behavior. |
| 343 | if ( '' !== $pat && ( '!' === $pat[0] || '^' === $pat[0] ) ) { |
| 344 | $cre .= '^/'; |
| 345 | $pat = (string) substr( $pat, 1 ); |
| 346 | } |
| 347 | |
| 348 | $first = true; |
| 349 | while ( '' !== $pat && ( $first || ']' !== $pat[0] ) ) { |
| 350 | $first = false; |
| 351 | |
| 352 | // Special stuff. |
| 353 | if ( substr( $pat, 0, 2 ) === '[.' ) { |
| 354 | throw new InvalidPatternException( "Collating symbols (`[.` inside a bracket expression) are not supported (in pattern at index $idx)" ); |
| 355 | } |
| 356 | if ( substr( $pat, 0, 2 ) === '[=' ) { |
| 357 | throw new InvalidPatternException( "Equivalence classes (`[=` inside a bracket expression) are not supported (in pattern at index $idx)" ); |
| 358 | } |
| 359 | if ( substr( $pat, 0, 2 ) === '[:' ) { |
| 360 | $pos = strpos( $pat, ':]' ); |
| 361 | if ( false === $pos ) { |
| 362 | throw new InvalidPatternException( "Invalid character class in bracket expression near `$pat` (in pattern at index $idx)" ); |
| 363 | } |
| 364 | $class = substr( $pat, 0, $pos + 2 ); |
| 365 | $pat = (string) substr( $pat, $pos + 2 ); |
| 366 | switch ( $class ) { |
| 367 | case '[:alnum:]': // @codeCoverageIgnore |
| 368 | case '[:alpha:]': // @codeCoverageIgnore |
| 369 | case '[:blank:]': // @codeCoverageIgnore |
| 370 | case '[:cntrl:]': // @codeCoverageIgnore |
| 371 | case '[:digit:]': // @codeCoverageIgnore |
| 372 | case '[:lower:]': // @codeCoverageIgnore |
| 373 | case '[:space:]': // @codeCoverageIgnore |
| 374 | case '[:upper:]': // @codeCoverageIgnore |
| 375 | case '[:xdigit:]': // @codeCoverageIgnore |
| 376 | $cre .= $class; |
| 377 | break; |
| 378 | case '[:graph:]': // @codeCoverageIgnore |
| 379 | case '[:print:]': // @codeCoverageIgnore |
| 380 | case '[:punct:]': // @codeCoverageIgnore |
| 381 | // These contain "/", without an easy way to exclude it. So use a lookahead. |
| 382 | $needLookahead = true; |
| 383 | $cre .= $class; |
| 384 | break; |
| 385 | default: |
| 386 | throw new InvalidPatternException( "Unrecognized character class $class in bracket expression (in pattern at index $idx)" ); |
| 387 | } |
| 388 | continue; |
| 389 | } |
| 390 | |
| 391 | // Range or character. |
| 392 | $c1 = mb_substr( $pat, 0, 1, 'UTF8' ); |
| 393 | $pat = mb_substr( $pat, 1, null, 'UTF8' ); |
| 394 | if ( '\\' === $c1 ) { |
| 395 | if ( '' === $pat ) { |
| 396 | break; |
| 397 | } |
| 398 | $c1 = mb_substr( $pat, 0, 1, 'UTF8' ); |
| 399 | $pat = mb_substr( $pat, 1, null, 'UTF8' ); |
| 400 | } |
| 401 | if ( strlen( $pat ) > 1 && '-' === $pat[0] && ']' !== $pat[1] ) { |
| 402 | $c2 = mb_substr( $pat, 1, 1, 'UTF8' ); |
| 403 | $pat = mb_substr( $pat, 2, null, 'UTF8' ); |
| 404 | if ( '\\' === $c2 ) { |
| 405 | if ( '' === $pat ) { |
| 406 | break; |
| 407 | } |
| 408 | $c2 = mb_substr( $pat, 0, 1, 'UTF8' ); |
| 409 | $pat = mb_substr( $pat, 1, null, 'UTF8' ); |
| 410 | } |
| 411 | |
| 412 | // If the start of the range is `/`, move it past. |
| 413 | if ( '/' === $c1 ) { |
| 414 | $c1 = '0'; |
| 415 | } |
| 416 | // If the end of the range is `/`, move it back. |
| 417 | if ( '/' === $c2 ) { |
| 418 | $c2 = '.'; |
| 419 | } |
| 420 | // Inverted ranges match nothing. |
| 421 | if ( strcmp( $c1, $c2 ) > 0 ) { |
| 422 | continue; |
| 423 | } |
| 424 | if ( $c1 === $c2 ) { |
| 425 | $cre .= preg_quote( $c1, '#' ); |
| 426 | } elseif ( strcmp( $c1, '/' ) < 0 && strcmp( '/', $c2 ) < 0 ) { |
| 427 | // Range contains `/`, split it. |
| 428 | $cre .= '.' !== $c1 ? preg_quote( $c1, '#' ) . '-.' : '.'; |
| 429 | $cre .= '0' !== $c2 ? '0-' . preg_quote( $c2, '#' ) : '0'; |
| 430 | } else { |
| 431 | $cre .= preg_quote( $c1, '#' ) . '-' . preg_quote( $c2, '#' ); |
| 432 | } |
| 433 | } elseif ( '/' !== $c1 ) { // If it's a "/" for a positive pattern, we need to omit it. If it's a "/" for a negated pattern, we already included it. |
| 434 | $cre .= preg_quote( $c1, '#' ); |
| 435 | } |
| 436 | } |
| 437 | |
| 438 | // If we reached the end of the pattern, it's not actually a bracket expression. Record a literal "[" and continue. |
| 439 | if ( '' === $pat ) { |
| 440 | if ( $atDir && ! $noBT ) { |
| 441 | $re .= '(?>'; |
| 442 | $noBT = true; |
| 443 | } |
| 444 | $re .= preg_quote( '[', '#' ); |
| 445 | $pat = $savedPat; |
| 446 | $atDir = false; |
| 447 | continue; |
| 448 | } |
| 449 | |
| 450 | // Check for an attempt to match nothing. |
| 451 | if ( '' === $cre ) { |
| 452 | return '#(*FAIL)#'; |
| 453 | } |
| 454 | |
| 455 | if ( $atDir && ! $noBT ) { |
| 456 | $re .= '(?>'; |
| 457 | $noBT = true; |
| 458 | } |
| 459 | if ( $needLookahead ) { |
| 460 | // If we couldn't exclude it when building the bracket expression, use a lookahead to ensure it doesn't match a "/". |
| 461 | $re .= '(?!/)'; |
| 462 | } |
| 463 | $re .= "[$cre]"; |
| 464 | $pat = (string) substr( $pat, 1 ); // Remove the trailing ']'. |
| 465 | $atDir = false; |
| 466 | continue; |
| 467 | } |
| 468 | |
| 469 | // Outside of a range, backslash escapes a character. |
| 470 | if ( '\\' === $pat ) { |
| 471 | // If a pattern ends with an unescaped <backslash>, it is unspecified whether the pattern does not match anything or the pattern is treated as invalid. |
| 472 | // We treat it as invalid here. |
| 473 | throw new InvalidPatternException( "Unexpected trailing backslash in pattern `$orig` at index $idx" ); |
| 474 | } |
| 475 | if ( '\\' === $pat[0] ) { |
| 476 | if ( '/' === $pat[1] ) { |
| 477 | // Special case this becuase of so much other special "/" handling. |
| 478 | $pat = (string) substr( $pat, 1 ); |
| 479 | continue; |
| 480 | } |
| 481 | if ( $atDir && ! $noBT ) { |
| 482 | $re .= '(?>'; |
| 483 | $noBT = true; |
| 484 | } |
| 485 | $re .= preg_quote( mb_substr( $pat, 1, 1, 'UTF8' ), '#' ); |
| 486 | $pat = mb_substr( $pat, 2, null, 'UTF8' ); |
| 487 | $atDir = false; |
| 488 | continue; |
| 489 | } |
| 490 | |
| 491 | // "/" separates path components. |
| 492 | if ( '/' === $pat[0] ) { |
| 493 | $re .= ( $noBT ? ')' : '' ) . ( $atDir ? '' : '/' ); |
| 494 | $pat = (string) substr( $pat, 1 ); |
| 495 | $atDir = true; |
| 496 | $noBT = false; |
| 497 | continue; |
| 498 | } |
| 499 | |
| 500 | // Else, it's just a regular character. |
| 501 | if ( $atDir && ! $noBT ) { |
| 502 | $re .= '(?>'; |
| 503 | $noBT = true; |
| 504 | } |
| 505 | $re .= preg_quote( mb_substr( $pat, 0, 1, 'UTF8' ), '#' ); |
| 506 | $pat = mb_substr( $pat, 1, null, 'UTF8' ); |
| 507 | $atDir = false; |
| 508 | } |
| 509 | |
| 510 | $re .= ( $noBT ? ')' : '' ) . ( $atDir ? '' : '/?' ) . '$#u'; |
| 511 | |
| 512 | return $re; |
| 513 | } |
| 514 | } |