Skip to content

Commit dafbc3e

Browse files
refactor: extract splitOutsideParens shared helper
The CLI's comma-splitter for color args and the logger's dot-splitter for color path segments shared near-identical paren-aware string walking. Extract the shared logic into lib/utils.ts, parameterized on the delimiter, and wire up both call sites.
1 parent 20aa945 commit dafbc3e

4 files changed

Lines changed: 67 additions & 50 deletions

File tree

bin/index.ts

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,9 @@ import { hideBin } from 'yargs/helpers';
77
import { assertDeprecated } from '../lib/assert.js';
88
import * as defaults from '../lib/defaults.js';
99
import { concurrently } from '../lib/index.js';
10-
import { castArray } from '../lib/utils.js';
10+
import { castArray, splitOutsideParens } from '../lib/utils.js';
1111
import { readPackageJson } from './read-package-json.js';
1212

13-
/**
14-
* Splits a color arguments string on commas, but preserves commas inside parentheses.
15-
* e.g., "red,rgb(255,255,0),blue" → ["red", "rgb(255,255,0)", "blue"]
16-
*/
17-
function splitColorArgs(input: string): string[] {
18-
const colors: string[] = [];
19-
let current = '';
20-
let parenDepth = 0;
21-
for (const char of input) {
22-
if (char === '(') parenDepth++;
23-
if (char === ')') parenDepth--;
24-
if (char === ',' && parenDepth === 0) {
25-
if (current.trim()) colors.push(current.trim());
26-
current = '';
27-
} else {
28-
current += char;
29-
}
30-
}
31-
if (current.trim()) colors.push(current.trim());
32-
return colors;
33-
}
34-
3513
const version = String(readPackageJson().version);
3614
const epilogue = `For documentation and more examples, visit:\nhttps://github.com/open-cli-tools/concurrently/tree/v${version}/docs`;
3715

@@ -278,7 +256,7 @@ concurrently(
278256
hide: args.hide.split(','),
279257
group: args.group,
280258
prefix: args.prefix,
281-
prefixColors: splitColorArgs(args.prefixColors),
259+
prefixColors: splitOutsideParens(args.prefixColors, ','),
282260
prefixLength: args.prefixLength,
283261
padPrefix: args.padPrefix,
284262
restartDelay:

lib/logger.ts

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,11 @@ import Rx from 'rxjs';
44
import { Command, CommandIdentifier } from './command.js';
55
import { DateFormatter } from './date-format.js';
66
import * as defaults from './defaults.js';
7-
import { escapeRegExp } from './utils.js';
7+
import { escapeRegExp, splitOutsideParens } from './utils.js';
88

99
const defaultChalk = chalk;
1010
const noColorChalk = new Chalk({ level: 0 });
1111

12-
/**
13-
* Parses a color string into segments, preserving function calls as single tokens.
14-
* e.g., "black.bgHex(#533AFD).dim" → ["black", "bgHex(#533AFD)", "dim"]
15-
*/
16-
function parseColorSegments(colorString: string): string[] {
17-
const segments: string[] = [];
18-
let current = '';
19-
let parenDepth = 0;
20-
21-
for (const char of colorString) {
22-
if (char === '(') parenDepth++;
23-
if (char === ')') parenDepth--;
24-
if (char === '.' && parenDepth === 0) {
25-
if (current) segments.push(current);
26-
current = '';
27-
} else {
28-
current += char;
29-
}
30-
}
31-
if (current) segments.push(current);
32-
return segments;
33-
}
34-
3512
const HEX_PATTERN = /^#[0-9A-Fa-f]{3,6}$/;
3613

3714
/**
@@ -83,7 +60,7 @@ function applySegment(color: ChalkInstance, segment: string): ChalkInstance | un
8360
* Returns undefined if any segment is invalid (triggers fallback to default).
8461
*/
8562
function applyColor(chalkInstance: ChalkInstance, colorString: string): ChalkInstance | undefined {
86-
const segments = parseColorSegments(colorString);
63+
const segments = splitOutsideParens(colorString, '.');
8764
if (segments.length === 0) return undefined;
8865

8966
let color: ChalkInstance = chalkInstance;

lib/utils.spec.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { castArray, escapeRegExp } from './utils.js';
3+
import { castArray, escapeRegExp, splitOutsideParens } from './utils.js';
44

55
describe('#escapeRegExp()', () => {
66
it('escapes all RegExp chars', () => {
@@ -37,3 +37,37 @@ describe('#castArray()', () => {
3737
});
3838
});
3939
});
40+
41+
describe('#splitOutsideParens()', () => {
42+
it('splits on the given delimiter', () => {
43+
expect(splitOutsideParens('red,blue', ',')).toEqual(['red', 'blue']);
44+
});
45+
46+
it('preserves delimiters inside parentheses', () => {
47+
expect(splitOutsideParens('red,rgb(255,0,0),blue', ',')).toEqual([
48+
'red',
49+
'rgb(255,0,0)',
50+
'blue',
51+
]);
52+
});
53+
54+
it('splits chalk-style dotted color paths, preserving function calls', () => {
55+
expect(splitOutsideParens('black.bgHex(#533AFD).dim', '.')).toEqual([
56+
'black',
57+
'bgHex(#533AFD)',
58+
'dim',
59+
]);
60+
});
61+
62+
it('trims whitespace around each segment', () => {
63+
expect(splitOutsideParens(' red , blue ', ',')).toEqual(['red', 'blue']);
64+
});
65+
66+
it('drops empty segments', () => {
67+
expect(splitOutsideParens(',,red,,', ',')).toEqual(['red']);
68+
});
69+
70+
it('returns an empty array for an empty input', () => {
71+
expect(splitOutsideParens('', ',')).toEqual([]);
72+
});
73+
});

lib/utils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,31 @@ type CastArrayResult<T> = T extends undefined | null ? never[] : T extends unkno
1313
export function castArray<T = never[]>(value?: T) {
1414
return (Array.isArray(value) ? value : value != null ? [value] : []) as CastArrayResult<T>;
1515
}
16+
17+
/**
18+
* Splits a string on `delimiter`, ignoring delimiters inside parentheses.
19+
* Trims each segment and discards empty ones.
20+
*
21+
* Examples:
22+
* splitOutsideParens('red,rgb(255,0,0),blue', ',') → ['red', 'rgb(255,0,0)', 'blue']
23+
* splitOutsideParens('black.bgHex(#533AFD).dim', '.') → ['black', 'bgHex(#533AFD)', 'dim']
24+
*/
25+
export function splitOutsideParens(input: string, delimiter: string): string[] {
26+
const segments: string[] = [];
27+
let current = '';
28+
let parenDepth = 0;
29+
for (const char of input) {
30+
if (char === '(') parenDepth++;
31+
else if (char === ')') parenDepth--;
32+
if (char === delimiter && parenDepth === 0) {
33+
const trimmed = current.trim();
34+
if (trimmed) segments.push(trimmed);
35+
current = '';
36+
} else {
37+
current += char;
38+
}
39+
}
40+
const trimmed = current.trim();
41+
if (trimmed) segments.push(trimmed);
42+
return segments;
43+
}

0 commit comments

Comments
 (0)