diff --git a/index.js b/index.js index 43b1e50..094fc8f 100644 --- a/index.js +++ b/index.js @@ -72,6 +72,33 @@ const cleanRangeBackSlash = slashes => { return slashes.slice(0, length - length % 2) } +// POSIX character classes ('[:alpha:]', '[:digit:]', ...) are part of the +// wildmatch syntax gitignore uses, but JavaScript regular expressions have no +// equivalent, so expand each one to its range (matching Git in the C locale). +const POSIX_CLASS = { + alnum: '0-9A-Za-z', + alpha: 'A-Za-z', + blank: ' \\t', + cntrl: '\\x00-\\x1f\\x7f', + digit: '0-9', + graph: '\\x21-\\x7e', + lower: 'a-z', + print: '\\x20-\\x7e', + punct: '\\x21-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\x7e', + space: ' \\t\\n\\x0b\\f\\r', + upper: 'A-Z', + xdigit: '0-9A-Fa-f' +} + +const REGEX_POSIX_CLASS = /\[:([a-z]+):\]/g + +const expandPosixClasses = range => range.replace( + REGEX_POSIX_CLASS, + (match, name) => Object.prototype.hasOwnProperty.call(POSIX_CLASS, name) + ? POSIX_CLASS[name] + : match +) + // > If the pattern ends with a slash, // > it is removed for the purpose of the following description, // > but it would only find a match with a directory. @@ -272,7 +299,9 @@ const REPLACERS = [ // > can be used to match one of the characters in a range. // `\` is escaped by step 3 - /(\\)?\[([^\]/]*?)(\\*)($|\])/g, + // A POSIX class '[:name:]' is consumed as a unit so its inner ']' is not + // mistaken for the end of the bracket expression. + /(\\)?\[((?:\[:[a-z]+:\]|[^\]/])*?)(\\*)($|\])/g, (match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE // '\\[bar]' -> '\\\\[bar\\]' ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}` @@ -281,7 +310,7 @@ const REPLACERS = [ // A normal case, and it is a range notation // '[bar]' // '[bar\\\\]' - ? `[${negateRange(sanitizeRange(range))}${endEscape}]` + ? `[${expandPosixClasses(negateRange(sanitizeRange(range)))}${endEscape}]` // Invalid range notaton // '[bar\\]' -> '[bar\\\\]' : '[]' diff --git a/test/fixtures/cases.js b/test/fixtures/cases.js index 4720bea..6b6bc13 100644 --- a/test/fixtures/cases.js +++ b/test/fixtures/cases.js @@ -362,6 +362,58 @@ const cases = [ 'a.9': 0 } ], + [ + 'POSIX class: [[:digit:]]', + [ + '[[:digit:]].log' + ], + { + '1.log': 1, + 'a.log': 0 + } + ], + [ + 'POSIX class: [[:alpha:]]', + [ + '[[:alpha:]].log' + ], + { + 'a.log': 1, + '1.log': 0 + } + ], + [ + 'POSIX class: negated [![:digit:]]', + [ + 'x[![:digit:]]' + ], + { + 'xa': 1, + 'x1': 0 + } + ], + [ + 'POSIX class combined with a set: [[:alnum:]_]', + [ + '[[:alnum:]_].o' + ], + { + 'a.o': 1, + '_.o': 1, + '!.o': 0 + } + ], + [ + // An unknown class name is left as-is, matching Git (no match). + 'POSIX class: unknown name is inert', + [ + '[[:foo:]]' + ], + { + 'foo': 0, + 'f': 0 + } + ], [ // Just treat it as normal character set 'special case: range-like character set',