From 7b106b64b1da54944b8c1058917af47d878528e5 Mon Sep 17 00:00:00 2001 From: Jon Schlinkert Date: Sun, 31 Mar 2019 21:50:39 -0400 Subject: [PATCH] braces api --- bench/index.js | 37 +++++------ index.js | 169 ++++++++++++++++++++++++++++++++++++++++++++++- lib/compile.js | 9 +-- lib/expand.js | 7 +- lib/parse.js | 2 - lib/stringify.js | 16 +++-- lib/utils.js | 4 -- package.json | 9 +-- 8 files changed, 204 insertions(+), 49 deletions(-) diff --git a/bench/index.js b/bench/index.js index 8d464c9..00cf942 100644 --- a/bench/index.js +++ b/bench/index.js @@ -4,9 +4,7 @@ const { Suite } = require('benchmark'); const colors = require('ansi-colors'); const argv = require('minimist')(process.argv.slice(2)); const minimatch = require('minimatch'); -const compile = require('../lib/compile'); -const expand = require('../lib/expand'); -const parse = require('../lib/parse'); +const braces = require('..'); /** * Setup @@ -52,27 +50,22 @@ bench.skip = name => { return skip; }; -bench('parse set') - .add(' braces', () => parse('foo/{a,b,c}/bar')) - .add('minimatch', () => minimatch.braceExpand('foo/{a,b,c}/bar')) - .run(); +// bench('expand - set') +// .add(' braces', () => braces.expand('foo/{a,b,c}/bar')) +// .add('minimatch', () => minimatch.braceExpand('foo/{a,b,c}/bar')) +// .run(); -bench('parse nested sets') - .add(' braces', () => parse('foo/{a,b,{x,y,z}}/bar')) - .add('minimatch', () => minimatch.braceExpand('foo/{a,b,{x,y,z}}/bar')) - .run(); +// bench('expand - nested sets') +// .add(' braces', () => braces.expand('foo/{a,b,{x,y,z}}/bar')) +// .add('minimatch', () => minimatch.braceExpand('foo/{a,b,{x,y,z}}/bar')) +// .run(); -bench('parse range') - .add(' braces', () => parse('foo/{a..z}/bar')) - .add('minimatch', () => minimatch.braceExpand('foo/{a..z}/bar')) - .run(); - -bench.skip('expand') - .add(' braces', () => expand(parse('foo/{a,b,c}/bar'))) - .add('minimatch', () => minimatch.braceExpand('foo/{a,b,c}/bar')) - .run(); +// bench('expand - range') +// .add(' braces', () => braces.expand('foo/{a..z}/bar')) +// .add('minimatch', () => minimatch.braceExpand('foo/{a..z}/bar')) +// .run(); -bench.skip('compile') - .add(' braces', () => compile(parse('foo/{a,b,c}/bar'))) +bench('compile regex - set') + .add(' braces', () => braces.makeRe(parse('foo/{a,b,c}/bar'))) .add('minimatch', () => minimatch.makeRe('foo/{a,b,c}/bar')) .run(); diff --git a/index.js b/index.js index e06f28c..a3bf6bc 100644 --- a/index.js +++ b/index.js @@ -1,17 +1,59 @@ 'use strict'; const { MAX_LENGTH } = require('./lib/constants'); +const stringify = require('./lib/stringify'); const compile = require('./lib/compile'); const expand = require('./lib/expand'); const parse = require('./lib/parse'); +const toRegex = require('to-regex'); + +/** + * Expand the given pattern or create a regex-compatible string. + * + * ```js + * const braces = require('braces'); + * console.log(braces('{a,b,c}', { compile: true })); //=> ['(a|b|c)'] + * console.log(braces('{a,b,c}')); //=> ['a', 'b', 'c'] + * ``` + * @param {String} `str` + * @param {Object} `options` + * @return {String} + * @api public + */ const braces = (input, options = {}) => { + let result = []; + if (Array.isArray(input)) { + for (let i = 0; i < input.length; i++) { + result.push(...braces.create(input[i], options)); + } + } else { + result = braces.create(input, options); + } + if (options && options.nodupes === true) { + result = [...new Set(result)]; + } + return result; }; -braces.expand = (input, options = {}) => { +/** + * Parse the given `str` with the given `options`. + * + * ```js + * // braces.parse(pattern, [, options]); + * const ast = braces.parse('a/{b,c}/d'); + * console.log(ast); + * ``` + * @param {String} pattern Brace pattern to parse + * @param {Object} options + * @return {Object} Returns an AST + * @api public + */ + +braces.parse = (input, options = {}) => { if (typeof input !== 'string') { throw new TypeError('Expected a string'); } @@ -22,7 +64,130 @@ braces.expand = (input, options = {}) => { throw new SyntaxError(`Input length (${input.length}), exceeds max characters (${max})`); } - return expand(parse(input, options), options); + return parse(input, options); +}; + +/** + * Creates a braces string from an AST, or an AST node. + * + * ```js + * const braces = require('braces'); + * let ast = braces.parse('foo/{a,b}/bar'); + * console.log(stringify(ast.nodes[2])); //=> '{a,b}' + * ``` + * @param {String} `input` Brace pattern or AST. + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.stringify = (input, options = {}) => { + if (typeof input === 'string') { + return stringify(braces.parse(input, options), options); + } + return stringify(input, options); +}; + +/** + * Compiles a brace pattern into a regex-compatible, optimized string. + * This method is called by the main [braces](#braces) function by default. + * + * ```js + * const braces = require('braces'); + * console.log(braces.compile('a/{b,c}/d')); + * //=> ['a/(b|c)/d'] + * ``` + * @param {String} `input` Brace pattern or AST. + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.compile = (input, options = {}) => { + if (typeof input === 'string') { + return compile(braces.parse(input, options), options); + } + return compile(input, options); +}; + +/** + * Expands a brace pattern into an array. This method is called by the + * main [braces](#braces) function when `options.expand` is true. Before + * using this method it's recommended that you read the [performance notes](#performance)) + * and advantages of using [.compile](#compile) instead. + * + * ```js + * const braces = require('braces'); + * console.log(braces.expand('a/{b,c}/d')); + * //=> ['a/b/d', 'a/c/d']; + * ``` + * @param {String} `pattern` Brace pattern + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.expand = (input, options = {}) => { + if (typeof input === 'string') { + return expand(braces.parse(input, options), options); + } + return expand(input, options); +}; + +/** + * Processes a brace pattern and returns either an expanded array + * (if `options.expand` is true), a highly optimized regex-compatible string. + * This method is called by the main [braces](#braces) function. + * + * ```js + * const braces = require('braces'); + * console.log(braces.create('user-{200..300}/project-{a,b,c}-{1..10}')) + * //=> 'user-(20[0-9]|2[1-9][0-9]|300)/project-(a|b|c)-([1-9]|10)' + * ``` + * @param {String} `pattern` Brace pattern + * @param {Object} `options` + * @return {Array} Returns an array of expanded values. + * @api public + */ + +braces.create = (input, options = {}) => { + if (input === '' || input.length < 3) { + return [input]; + } + + let result = options.expand !== true + ? braces.compile(input, options) + : braces.expand(input, options); + + // filter out empty strings if specified + if (options.noempty === true) { + result = result.filter(Boolean); + } + + // filter out duplicates if specified + if (options.nodupes === true) { + result = [...new Set(result)]; + } + + return result; +}; + +/** + * Create a regular expression from the given string `pattern`. + * + * ```js + * const braces = require('braces'); + * console.log(braces.makeRe('foo/id-{200..300}')); + * //=> /^(?:foo/id-(20[0-9]|2[1-9][0-9]|300))$/ + * ``` + * @param {String} `pattern` The pattern to convert to regex. + * @param {Object} `options` + * @return {RegExp} + * @api public + */ + +braces.makeRe = (pattern, options) => { + return toRegex(braces.compile(pattern, options), { strictErrors: false, ...options }); }; module.exports = braces; diff --git a/lib/compile.js b/lib/compile.js index 1df632a..12824f4 100644 --- a/lib/compile.js +++ b/lib/compile.js @@ -2,8 +2,8 @@ const utils = require('./utils'); -module.exports = (ast, options = {}) => { - let compile = (node, parent = {}) => { +const compile = (ast, options = {}) => { + let walk = (node, parent = {}) => { let invalidBlock = options.escapeInvalid && utils.isInvalidBrace(parent); let invalidNode = node.invalid === true && options.escapeInvalid === true; let output = ''; @@ -17,12 +17,13 @@ module.exports = (ast, options = {}) => { if (node.nodes) { for (let child of node.nodes) { - output += compile(child, node); + output += walk(child, node); } } return output; }; - return compile(ast); + return walk(ast); }; +module.exports = compile; diff --git a/lib/expand.js b/lib/expand.js index db19905..18503b6 100644 --- a/lib/expand.js +++ b/lib/expand.js @@ -1,6 +1,5 @@ 'use strict'; -const stringify = require('./stringify'); const compile = require('./compile'); const utils = require('./utils'); @@ -27,13 +26,11 @@ const append = (queue = '', stash = '', enclose = false) => { } } } - return result; + return utils.flatten(result); }; const expand = (ast, options = {}) => { let walk = (node, parent = {}) => { - let enclose = utils.encloseBrace(node); - // let invalid = options.escapeInvalid && utils.isInvalidBrace(parent); node.queue = []; if (node.invalid || node.dollar) { @@ -46,12 +43,12 @@ const expand = (ast, options = {}) => { return; } + let enclose = utils.encloseBrace(node); for (let i = 0; i < node.nodes.length; i++) { let child = node.nodes[i]; if (child.type === 'comma') { node.queue.push(''); - if (i === 1) { node.queue.push(''); } diff --git a/lib/parse.js b/lib/parse.js index ba52393..62a3ffc 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1,7 +1,5 @@ 'use strict'; -// const compile = require('./compile'); -// const expand = require('./expand'); const utils = require('./utils'); /** diff --git a/lib/stringify.js b/lib/stringify.js index a72623b..dde829b 100644 --- a/lib/stringify.js +++ b/lib/stringify.js @@ -1,12 +1,16 @@ 'use strict'; module.exports = (ast, options = {}) => { - let compile = node => { - let invalid = parent.invalid === true && options.escapeInvalid === true; + let stringify = (node, parent = {}) => { + let invalidBlock = options.escapeInvalid && utils.isInvalidBrace(parent); + let invalidNode = node.invalid === true && options.escapeInvalid === true; let output = ''; - if (invalid && !node.escaped && (node.type === 'open' || node.type === 'close')) { - return '\\' + node.value; + if (node.value) { + if ((invalidBlock || invalidNode) && utils.isOpenOrClose(node)) { + return '\\' + node.value; + } + return node.value; } if (node.value) { @@ -15,12 +19,12 @@ module.exports = (ast, options = {}) => { if (node.nodes) { for (let child of node.nodes) { - output += compile(child); + output += stringify(child); } } return output; }; - return compile(ast); + return stringify(ast); }; diff --git a/lib/utils.js b/lib/utils.js index 3ef40dd..b2b57a8 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -66,10 +66,6 @@ exports.isOpenOrClose = node => { */ exports.flatten = (...args) => { - if (typeof Array.prototype.flat === 'function') { - return args.flat(Infinity); - } - const result = []; const flat = arr => { for (let i = 0; i < arr.length; i++) { diff --git a/package.json b/package.json index a486e51..28d33fc 100644 --- a/package.json +++ b/package.json @@ -22,18 +22,16 @@ ], "main": "index.js", "engines": { - "node": ">=0.10.0" + "node": ">=8" }, "scripts": { "test": "mocha", "benchmark": "node benchmark" }, "devDependencies": { + "ansi-colors": "^3.2.4", "bash-path": "^2.0.1", - "brace-expansion": "^1.1.11", - "cross-spawn": "^6.0.5", "gulp-format-md": "^2.0.0", - "minimatch": "^3.0.4", "mocha": "^6.0.2" }, "keywords": [ @@ -81,5 +79,8 @@ "nanomatch" ] } + }, + "dependencies": { + "to-regex": "^3.0.2" } }