Skip to content

Commit f201165

Browse files
author
mrmlnc
committed
fix: return complete pattern parts from scan
Preserve leading and trailing empty segments and return the final segment for patterns without a path separator. Track nested and escaped parentheses so that only top-level, unescaped path separators split the pattern. Keep prefixes out of the returned parts. Fixes #58.
1 parent 4f41a8e commit f201165

2 files changed

Lines changed: 78 additions & 45 deletions

File tree

‎lib/scan.js‎

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const scan = (input, options) => {
5050
const opts = options || {};
5151

5252
const length = input.length - 1;
53-
const scanToEnd = opts.parts === true || opts.scanToEnd === true;
53+
const scanToEnd = opts.parts === true || opts.tokens === true || opts.scanToEnd === true;
5454
const slashes = [];
5555
const tokens = [];
5656
const parts = [];
@@ -184,15 +184,21 @@ const scan = (input, options) => {
184184
}
185185

186186
if (scanToEnd === true) {
187+
let parens = 0;
188+
187189
while (eos() !== true && (code = advance())) {
188190
if (code === CHAR_BACKWARD_SLASH) {
189191
backslashes = token.backslashes = true;
190-
code = advance();
192+
advance();
193+
continue;
194+
}
195+
196+
if (code === CHAR_LEFT_PARENTHESES) {
197+
parens++;
191198
continue;
192199
}
193200

194-
if (code === CHAR_RIGHT_PARENTHESES) {
195-
isGlob = token.isGlob = true;
201+
if (code === CHAR_RIGHT_PARENTHESES && --parens === 0) {
196202
finished = true;
197203
break;
198204
}
@@ -257,14 +263,21 @@ const scan = (input, options) => {
257263
isGlob = token.isGlob = true;
258264

259265
if (scanToEnd === true) {
266+
let parens = 1;
267+
260268
while (eos() !== true && (code = advance())) {
261-
if (code === CHAR_LEFT_PARENTHESES) {
269+
if (code === CHAR_BACKWARD_SLASH) {
262270
backslashes = token.backslashes = true;
263-
code = advance();
271+
advance();
264272
continue;
265273
}
266274

267-
if (code === CHAR_RIGHT_PARENTHESES) {
275+
if (code === CHAR_LEFT_PARENTHESES) {
276+
parens++;
277+
continue;
278+
}
279+
280+
if (code === CHAR_RIGHT_PARENTHESES && --parens === 0) {
268281
finished = true;
269282
break;
270283
}
@@ -351,7 +364,7 @@ const scan = (input, options) => {
351364
let prevIndex;
352365

353366
for (let idx = 0; idx < slashes.length; idx++) {
354-
const n = prevIndex ? prevIndex + 1 : start;
367+
const n = prevIndex !== undefined ? prevIndex + 1 : start;
355368
const i = slashes[idx];
356369
const value = input.slice(n, i);
357370
if (opts.tokens) {
@@ -364,21 +377,20 @@ const scan = (input, options) => {
364377
depth(tokens[idx]);
365378
state.maxDepth += tokens[idx].depth;
366379
}
367-
if (idx !== 0 || value !== '') {
380+
if (i >= start) {
368381
parts.push(value);
382+
prevIndex = i;
369383
}
370-
prevIndex = i;
371384
}
372385

373-
if (prevIndex && prevIndex + 1 < input.length) {
374-
const value = input.slice(prevIndex + 1);
375-
parts.push(value);
386+
const n = prevIndex !== undefined ? prevIndex + 1 : start;
387+
const value = input.slice(n);
388+
parts.push(value);
376389

377-
if (opts.tokens) {
378-
tokens[tokens.length - 1].value = value;
379-
depth(tokens[tokens.length - 1]);
380-
state.maxDepth += tokens[tokens.length - 1].depth;
381-
}
390+
if (opts.tokens && prevIndex && prevIndex + 1 < input.length) {
391+
tokens[tokens.length - 1].value = value;
392+
depth(tokens[tokens.length - 1]);
393+
state.maxDepth += tokens[tokens.length - 1].depth;
382394
}
383395

384396
state.slashes = slashes;

‎test/api.scan.js‎

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -322,34 +322,55 @@ describe('picomatch', () => {
322322
});
323323
});
324324

325-
it('should return parts of the pattern', () => {
326-
// Right now it returns []
327-
// assertParts('', ['']);
328-
// assertParts('*', ['*']);
329-
// assertParts('.*', ['.*']);
330-
// assertParts('**', ['**']);
331-
// assertParts('foo', ['foo']);
332-
// assertParts('foo*', ['foo*']);
333-
// assertParts('/', ['', '']);
334-
// assertParts('/*', ['', '*']);
335-
// assertParts('./', ['']);
336-
// assertParts('{1..9}', ['{1..9}']);
337-
// assertParts('c!(.)z', ['c!(.)z']);
338-
// assertParts('(b|a).(a)', ['(b|a).(a)']);
339-
// assertParts('+(a|b\\[)*', ['+(a|b\\[)*']);
340-
// assertParts('@(a|b).md', ['@(a|b).md']);
341-
// assertParts('(a/b)', ['(a/b)']);
342-
// assertParts('(a\\b)', ['(a\\b)']);
343-
// assertParts('foo\\[a\\/]', ['foo\\[a\\/]']);
344-
// assertParts('foo[/]bar', ['foo[/]bar']);
345-
// assertParts('/dev\\/@(tcp|udp)\\/*\\/*', ['', '/dev\\/@(tcp|udp)\\/*\\/*']);
346-
347-
// Right now it returns ['*']
348-
// assertParts('*/', ['*', '']);
349-
350-
// Right now it returns ['!(!(bar)', 'baz)']
351-
// assertParts('!(!(bar)/baz)', ['!(!(bar)/baz)']);
325+
it('should return a single part for patterns without path separators', () => {
326+
assertParts('', ['']);
327+
assertParts('*', ['*']);
328+
assertParts('.*', ['.*']);
329+
assertParts('**', ['**']);
330+
assertParts('foo', ['foo']);
331+
assertParts('!foo', ['foo']);
332+
assertParts('foo*', ['foo*']);
333+
assertParts('{1..9}', ['{1..9}']);
334+
assertParts('c!(.)z', ['c!(.)z']);
335+
assertParts('(b|a).(a)', ['(b|a).(a)']);
336+
assertParts('+(a|b\\[)*', ['+(a|b\\[)*']);
337+
assertParts('@(a|b).md', ['@(a|b).md']);
338+
assertParts('(a/b)', ['(a/b)']);
339+
assertParts('(a\\b)', ['(a\\b)']);
340+
assertParts('foo\\[a\\/]', ['foo\\[a\\/]']);
341+
assertParts('foo[/]bar', ['foo[/]bar']);
342+
});
343+
344+
it('should preserve leading and trailing empty parts', () => {
345+
assertParts('/', ['', '']);
346+
assertParts('/*', ['', '*']);
347+
assertParts('/a', ['', 'a']);
348+
assertParts('/a/b', ['', 'a', 'b']);
349+
assertParts('/a/b/', ['', 'a', 'b', '']);
350+
assertParts('*/', ['*', '']);
351+
assertParts('./', ['']);
352+
});
353+
354+
it('should return parts when tokens are requested', () => {
355+
assert.deepStrictEqual(scan('/', { tokens: true }).parts, ['', '']);
356+
assert.deepStrictEqual(scan('/a/b', { tokens: true }).parts, ['', 'a', 'b']);
357+
assert.deepStrictEqual(scan('!foo', { tokens: true }).parts, ['foo']);
358+
assert.deepStrictEqual(scan('./!a/b', { tokens: true }).parts, ['a', 'b']);
359+
assert.deepStrictEqual(scan('a/b/*/c', { tokens: true }).parts, ['a', 'b', '*', 'c']);
360+
});
361+
362+
it('should split only on unnested and unescaped path separators', () => {
363+
assertParts('/dev\\/@(tcp|udp)\\/*\\/*', ['', 'dev\\/@(tcp|udp)\\/*\\/*']);
364+
assertParts('!(!(bar)/baz)', ['!(!(bar)/baz)']);
365+
assertParts('(!(b/a))', ['(!(b/a))']);
366+
assertParts('a/((b)/c)/d', ['a', '((b)/c)', 'd']);
367+
assertParts('a/(b\\)/c)/d', ['a', '(b\\)/c)', 'd']);
368+
assertParts('(a|b)/c', ['(a|b)', 'c']);
369+
assertParts('./directory/(a|b)/*.js', ['directory', '(a|b)', '*.js']);
370+
assertParts('a/@(!(b)/c)/*.js', ['a', '@(!(b)/c)', '*.js']);
371+
});
352372

373+
it('should return parts of the pattern', () => {
353374
assertParts('./foo', ['foo']);
354375
assertParts('../foo', ['..', 'foo']);
355376

0 commit comments

Comments
 (0)