From 1663648cb04eb48ad7418a0f1445609bcab8a969 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Sat, 9 Jan 2021 17:56:07 -0500 Subject: [PATCH 1/7] BREAKING: Enable `namespacing` feature by default Following up on the [change] to use ES2018 constraints on the names of capture groups, this makes it so that XRegExp `namespacing` feature is enabled by default, so that named capture group matches will appear on the `.groups` property of the match object, rather than directly on the match object, in accordance with the ES2018 spec. While this is a breaking change, users can restore the old behavior by running: XRegExp.uninstall('namespacing') [change]: https://github.com/slevithan/xregexp/pull/247#issuecomment-757374810 --- src/xregexp.js | 2 +- tests/.eslintrc.js | 2 +- tests/helpers/h.js | 7 ++++--- tests/spec/s-addons-build.js | 2 +- tests/spec/s-addons-matchrecursive.js | 2 +- tests/spec/s-addons-unicode.js | 2 +- tests/spec/s-xregexp-methods.js | 16 +++++++++------- tests/spec/s-xregexp.js | 4 ++-- 8 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/xregexp.js b/src/xregexp.js index ea4388be..05f9b52f 100644 --- a/src/xregexp.js +++ b/src/xregexp.js @@ -20,7 +20,7 @@ const REGEX_DATA = 'xregexp'; // Optional features that can be installed and uninstalled const features = { astral: false, - namespacing: false + namespacing: true }; // Native methods to use and restore ('native' is an ES3 reserved keyword) const nativ = { diff --git a/tests/.eslintrc.js b/tests/.eslintrc.js index 9d4ebe17..50916a36 100644 --- a/tests/.eslintrc.js +++ b/tests/.eslintrc.js @@ -4,7 +4,7 @@ module.exports = { }, "globals": { "XRegExp": true, - "disableOptInFeatures": true, + "resetFeatures": true, "REGEX_DATA": true, "hasNativeU": true, "hasNativeY": true, diff --git a/tests/helpers/h.js b/tests/helpers/h.js index 3bd155df..404ff365 100644 --- a/tests/helpers/h.js +++ b/tests/helpers/h.js @@ -4,9 +4,10 @@ if (typeof global === 'undefined') { global.XRegExp = require('../../xregexp-all'); } -// Ensure that all opt-in features are disabled when each spec starts -global.disableOptInFeatures = function() { - XRegExp.uninstall('namespacing astral'); +// Ensure that all features are reset to default when each spec starts +global.resetFeatures = function() { + XRegExp.uninstall('astral'); + XRegExp.install('namespacing'); }; // Property name used for extended regex instance data diff --git a/tests/spec/s-addons-build.js b/tests/spec/s-addons-build.js index 2eefa78e..9494a3bc 100644 --- a/tests/spec/s-addons-build.js +++ b/tests/spec/s-addons-build.js @@ -1,5 +1,5 @@ beforeEach(function() { - global.disableOptInFeatures(); + global.resetFeatures(); global.addToEqualMatchMatcher(); }); diff --git a/tests/spec/s-addons-matchrecursive.js b/tests/spec/s-addons-matchrecursive.js index 1c872021..4dfbc4f1 100644 --- a/tests/spec/s-addons-matchrecursive.js +++ b/tests/spec/s-addons-matchrecursive.js @@ -1,5 +1,5 @@ beforeEach(function() { - global.disableOptInFeatures(); + global.resetFeatures(); global.addToEqualMatchMatcher(); }); diff --git a/tests/spec/s-addons-unicode.js b/tests/spec/s-addons-unicode.js index ebb3b60e..603c1c05 100644 --- a/tests/spec/s-addons-unicode.js +++ b/tests/spec/s-addons-unicode.js @@ -1,5 +1,5 @@ beforeEach(function() { - global.disableOptInFeatures(); + global.resetFeatures(); global.addToEqualMatchMatcher(); }); diff --git a/tests/spec/s-xregexp-methods.js b/tests/spec/s-xregexp-methods.js index da72a2c1..37a49909 100644 --- a/tests/spec/s-xregexp-methods.js +++ b/tests/spec/s-xregexp-methods.js @@ -1,5 +1,5 @@ beforeEach(function() { - global.disableOptInFeatures(); + global.resetFeatures(); global.addToEqualMatchMatcher(); }); @@ -445,6 +445,7 @@ describe('XRegExp.exec()', function() { // for the RegExp.prototype.exec and nonglobal String.prototype.match specs... it('should include named capture properties on the match array if namespacing is not installed', function() { + XRegExp.uninstall('namespacing'); var match = XRegExp.exec('a', XRegExp('(?a)')); expect(match.name).toBe('a'); @@ -452,20 +453,19 @@ describe('XRegExp.exec()', function() { }); it('should not include named capture properties on the match array if namespacing is installed', function() { - XRegExp.install('namespacing'); var match = XRegExp.exec('a', XRegExp('(?a)')); expect(match.name).toBeUndefined(); }); it('should not include named capture properties on a groups object if namespacing is not installed', function() { + XRegExp.uninstall('namespacing'); var match = XRegExp.exec('a', XRegExp('(?a)')); expect(match.groups).toBeUndefined(); }); it('should include named capture properties on a groups object if namespacing is installed', function() { - XRegExp.install('namespacing'); var match = XRegExp.exec('a', XRegExp('(?a)')); expect(match.groups.name).toBe('a'); @@ -736,7 +736,9 @@ describe('XRegExp.globalize()', function() { describe('XRegExp.install()', function() { - // NOTE: All optional features are uninstalled before each spec runs + beforeEach(function() { + XRegExp.uninstall('namespacing astral'); + }); var features = ['namespacing', 'astral']; @@ -1100,6 +1102,7 @@ describe('XRegExp.matchChain()', function() { }); it('should handle named and numbered backrefs when namespacing is not installed', function() { + XRegExp.uninstall('namespacing'); expect(XRegExp.matchChain('test', [ {regex: /.(..)/, backref: 1}, {regex: XRegExp('.(?.)'), backref: 'n'} @@ -1107,7 +1110,6 @@ describe('XRegExp.matchChain()', function() { }); it('should handle named and numbered backrefs when namespacing is installed', function() { - XRegExp.install('namespacing'); expect(XRegExp.matchChain('test', [ {regex: /.(..)/, backref: 1}, {regex: XRegExp('.(?.)'), backref: 'n'} @@ -1186,6 +1188,7 @@ describe('XRegExp.replace()', function() { }); it('should not pass the groups argument to callbacks when namespacing is not installed', function() { + XRegExp.uninstall('namespacing'); var regex = XRegExp('(?s)(?.)'); XRegExp.replace('test', regex, function(match, capture1, pos, str, groups) { expect(groups).toBeUndefined(); @@ -1193,7 +1196,6 @@ describe('XRegExp.replace()', function() { }); it('should pass the groups argument to callbacks when namespacing is installed', function() { - XRegExp.install('namespacing'); var regex = XRegExp('(?s)(?.)'); var groupsObject = Object.create(null); groupsObject.groupName = 't'; @@ -1203,13 +1205,13 @@ describe('XRegExp.replace()', function() { }); it('should allow accessing named backreferences in callbacks as properties of the first argument when namespacing is not installed', function() { + XRegExp.uninstall('namespacing'); expect(XRegExp.replace('abc', XRegExp('(?.).'), function(match) { return ':' + match.name + ':'; })).toBe(':a:c'); }); it('should not allow accessing named backreferences in callbacks as properties of the first argument when namespacing is installed', function() { - XRegExp.install('namespacing'); expect(XRegExp.replace('abc', XRegExp('(?.).'), function(match) { return ':' + match.name + ':'; })).toBe(':undefined:c'); diff --git a/tests/spec/s-xregexp.js b/tests/spec/s-xregexp.js index 13f76cd2..a0a228e5 100644 --- a/tests/spec/s-xregexp.js +++ b/tests/spec/s-xregexp.js @@ -1,5 +1,5 @@ beforeEach(function() { - global.disableOptInFeatures(); + global.resetFeatures(); global.addToEqualMatchMatcher(); }); @@ -421,6 +421,7 @@ describe('XRegExp()', function() { }); it('should throw an exception if reserved words are used as capture names if namespacing is not installed', function() { + XRegExp.uninstall('namespacing'); // Only these names are reserved ['length', '__proto__'].forEach(function(name) { expect(function() {XRegExp('(?<' + name + '>)');}).toThrowError(SyntaxError); @@ -428,7 +429,6 @@ describe('XRegExp()', function() { }); it('should not throw an exception if reserved words are used as capture names if namespacing is installed', function() { - XRegExp.install('namespacing'); ['length', '__proto__'].forEach(function(name) { expect(function() {XRegExp('(?<' + name + '>)');}).not.toThrow(); }); From 28de381f684167ab307a7dafcc62c35fff072ba2 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Sat, 9 Jan 2021 18:08:32 -0500 Subject: [PATCH 2/7] Update shadow array prototype properties test to not use namespacing --- tests/spec/s-xregexp-methods.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/spec/s-xregexp-methods.js b/tests/spec/s-xregexp-methods.js index 37a49909..ea55febe 100644 --- a/tests/spec/s-xregexp-methods.js +++ b/tests/spec/s-xregexp-methods.js @@ -472,7 +472,8 @@ describe('XRegExp.exec()', function() { expect(match[1]).toBe('a'); }); - it('should shaddow array prototype properties with named capture properties', function() { + it('should shadow array prototype properties with named capture properties if namespacing is not installed', function() { + XRegExp.uninstall('namespacing'); expect(XRegExp.exec('a', XRegExp('(?a)')).concat).toBe('a'); expect(XRegExp.exec('a', XRegExp('(?a)')).index).toBe('a'); }); From 59455694e7e6e88a97ce554cb73743e503fddb92 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Sat, 9 Jan 2021 18:11:37 -0500 Subject: [PATCH 3/7] Fix forEach test, add one with namespacing --- tests/spec/s-xregexp-methods.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/spec/s-xregexp-methods.js b/tests/spec/s-xregexp-methods.js index ea55febe..159a06dd 100644 --- a/tests/spec/s-xregexp-methods.js +++ b/tests/spec/s-xregexp-methods.js @@ -493,7 +493,17 @@ describe('XRegExp.forEach()', function() { }); }); - it('should provide named backreferences on the match array', function() { + it('should provide named backreferences on the match array\'s groups property', function() { + var result = []; + XRegExp.forEach('abc 123 def', XRegExp('(?\\w)\\w*'), function(m) { + result.push(m.groups.first); + }); + + expect(result).toEqual(['a', '1', 'd']); + }); + + it('should provide named backreferences on the match array if namespacing is not installed', function() { + XRegExp.uninstall('namespacing'); var result = []; XRegExp.forEach('abc 123 def', XRegExp('(?\\w)\\w*'), function(m) { result.push(m.first); From 99892e167f40515d09161ad88c0dca80666f85e3 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Sat, 9 Jan 2021 18:13:17 -0500 Subject: [PATCH 4/7] Fix exec test, add one with namespacing --- tests/spec/s-xregexp-methods.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/spec/s-xregexp-methods.js b/tests/spec/s-xregexp-methods.js index 159a06dd..cc081959 100644 --- a/tests/spec/s-xregexp-methods.js +++ b/tests/spec/s-xregexp-methods.js @@ -719,7 +719,14 @@ describe('XRegExp.globalize()', function() { expect(XRegExp.globalize(XRegExp('', 'Aix'))[REGEX_DATA].flags).toBe('Agix'); }); - it('should retain named capture capabilities', function() { + it('should retain named capture capabilities when namespacing is not installed', function() { + var regex = XRegExp('(?x)\\k'); + + expect(XRegExp.exec('xx', XRegExp.globalize(regex)).groups.name).toBe('x'); + }); + + it('should retain named capture capabilities when namespacing is not installed', function() { + XRegExp.uninstall('namespacing'); var regex = XRegExp('(?x)\\k'); expect(XRegExp.exec('xx', XRegExp.globalize(regex)).name).toBe('x'); From 62f1f695688c5cf0fe8a361bde11521c0af7cc66 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Sat, 9 Jan 2021 18:21:05 -0500 Subject: [PATCH 5/7] Update build/tag tests --- tests/spec/s-addons-build.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/spec/s-addons-build.js b/tests/spec/s-addons-build.js index 9494a3bc..f3aa4886 100644 --- a/tests/spec/s-addons-build.js +++ b/tests/spec/s-addons-build.js @@ -87,7 +87,7 @@ describe('XRegExp.build addon:', function() { var time = XRegExp.tag('x')`^ ${hours} (?${minutes}) $`; expect(time.test('10:59')).toBe(true); - expect(XRegExp.exec('10:59', time).minutes).toEqual('59'); + expect(XRegExp.exec('10:59', time).groups.minutes).toEqual('59'); }); }); @@ -169,7 +169,7 @@ describe('XRegExp.build addon:', function() { }); expect(time.test('10:59')).toBe(true); - expect(XRegExp.exec('10:59', time).minutes).toBe('59'); + expect(XRegExp.exec('10:59', time).groups.minutes).toBe('59'); }); it('should pass a series of complex backreference rewrites', function() { @@ -181,11 +181,11 @@ describe('XRegExp.build addon:', function() { var match = XRegExp.exec('aaaabbbbaabbbb', built); expect(match).toBeTruthy(); - expect(match.n1).toBe('aa'); - expect(match.n2).toBeUndefined(); - expect(match.nX).toBe('bb'); - expect(match.yo).toBe('a'); - expect(match.yo2).toBe('b'); + expect(match.groups.n1).toBe('aa'); + expect(match.groups.n2).toBeUndefined(); + expect(match.groups.nX).toBe('bb'); + expect(match.groups.yo).toBe('a'); + expect(match.groups.yo2).toBe('b'); }); }); From c73196eec5af958c46c9b686a18cf56e01589f22 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Sat, 9 Jan 2021 18:29:53 -0500 Subject: [PATCH 6/7] Update readme/comments --- README.md | 10 +++++----- src/xregexp.js | 4 ++-- types/index.d.ts | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 0d2dd7f0..afe0f852 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ const date = XRegExp( (? [0-9]{2} ) -? # month (? [0-9]{2} ) # day`, 'x'); -// XRegExp.exec gives you named backreferences on the match result +// XRegExp.exec gives you named backreferences on the match result's groups property let match = XRegExp.exec('2017-02-22', date); -match.year; // -> '2017' +match.groups.year; // -> '2017' // It also includes optional pos and sticky arguments let pos = 3; @@ -36,7 +36,7 @@ while (match = XRegExp.exec('<1><2><3>4<5>', /<(\d+)>/, pos, 'sticky')) { XRegExp.replace('2017-02-22', date, '$/$/$'); // -> '02/22/2017' XRegExp.replace('2017-02-22', date, (match) => { - return `${match.month}/${match.day}/${match.year}`; + return `${match.groups.month}/${match.groups.day}/${match.groups.year}`; }); // -> '02/22/2017' @@ -135,7 +135,7 @@ const time = XRegExp.build('(?x)^ {{hours}} ({{minutes}}) $', { }); time.test('10:59'); // -> true -XRegExp.exec('10:59', time).minutes; // -> '59' +XRegExp.exec('10:59', time).groups.minutes; // -> '59' ``` Named subpatterns can be provided as strings or regex objects. A leading `^` and trailing unescaped `$` are stripped from subpatterns if both are present, which allows embedding independently-useful anchored patterns. `{{…}}` tokens can be quantified as a single unit. Any backreferences in the outer pattern or provided subpatterns are automatically renumbered to work correctly within the larger combined pattern. The syntax `({{name}})` works as shorthand for named capture via `(?{{name}})`. Named subpatterns cannot be embedded within character classes. @@ -152,7 +152,7 @@ const minutes = /^[0-5][0-9]$/; // Note that explicitly naming the 'minutes' group is required for named backreferences const time = XRegExp.tag('x')`^ ${hours} (?${minutes}) $`; time.test('10:59'); // -> true -XRegExp.exec('10:59', time).minutes; // -> '59' +XRegExp.exec('10:59', time).groups.minutes; // -> '59' ``` XRegExp.tag does more than just basic interpolation. For starters, you get all the XRegExp syntax and flags. Even better, since `XRegExp.tag` uses your pattern as a raw string, you no longer need to escape all your backslashes. And since it relies on `XRegExp.build` under the hood, you get all of its extras for free. Leading `^` and trailing unescaped `$` are stripped from interpolated patterns if both are present (to allow embedding independently useful anchored regexes), interpolating into a character class is an error (to avoid unintended meaning in edge cases), interpolated patterns are treated as atomic units when quantified, interpolated strings have their special characters escaped, and any backreferences within an interpolated regex are rewritten to work within the overall pattern. diff --git a/src/xregexp.js b/src/xregexp.js index 05f9b52f..0501ddbd 100644 --- a/src/xregexp.js +++ b/src/xregexp.js @@ -822,7 +822,7 @@ XRegExp.escape = (str) => nativ.replace.call(toObject(str), /[-\[\]{}()*+?.,\\^$ * * // Basic use, with named backreference * let match = XRegExp.exec('U+2620', XRegExp('U\\+(?[0-9A-F]{4})')); - * match.hex; // -> '2620' + * match.groups.hex; // -> '2620' * * // With pos and sticky, in a loop * let pos = 2, result = [], match; @@ -1158,7 +1158,7 @@ XRegExp.matchChain = (str, chain) => (function recurseChain(values, level) { * // -> 'Smith, John' * * // Regex search, using named backreferences in replacement function - * XRegExp.replace('John Smith', name, (match) => `${match.last}, ${match.first}`); + * XRegExp.replace('John Smith', name, (match) => `${match.groups.last}, ${match.groups.first}`); * // -> 'Smith, John' * * // String search, with replace-all diff --git a/types/index.d.ts b/types/index.d.ts index dc9033dc..b0c76df4 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -577,13 +577,13 @@ declare namespace XRegExp { * * // Basic use, with named capture groups * let match = XRegExp.exec('U+2620', XRegExp('U\\+(?[0-9A-F]{4})')); - * match.hex; // -> '2620' + * match.groups.hex; // -> '2620' * * // With pos and sticky, in a loop * let pos = 2, result = [], match; * while (match = XRegExp.exec('<1><2><3><4>5<6>', /<(\d)>/, pos, 'sticky')) { * result.push(match[1]); - * pos = match.index + match[0].length; + * pos = match.groups.index + match[0].length; * } * // result -> ['2', '3', '4'] */ @@ -809,7 +809,7 @@ declare namespace XRegExp { * // -> 'Smith, John' * * // Regex search, using named backreferences in replacement function - * XRegExp.replace('John Smith', name, (match) => `${match.last as string}, ${match.first as string}`); + * XRegExp.replace('John Smith', name, (match) => `${match.groups.last as string}, ${match.groups.first as string}`); * // -> 'Smith, John' * * // String search, with replace-all From 630b656756ec1714b099534d81cc09a26108f695 Mon Sep 17 00:00:00 2001 From: Joseph Frazier <1212jtraceur@gmail.com> Date: Sat, 9 Jan 2021 18:32:45 -0500 Subject: [PATCH 7/7] Remove likely-superfluous tests --- tests/spec/s-xregexp-methods.js | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/tests/spec/s-xregexp-methods.js b/tests/spec/s-xregexp-methods.js index cc081959..03ad94b5 100644 --- a/tests/spec/s-xregexp-methods.js +++ b/tests/spec/s-xregexp-methods.js @@ -502,16 +502,6 @@ describe('XRegExp.forEach()', function() { expect(result).toEqual(['a', '1', 'd']); }); - it('should provide named backreferences on the match array if namespacing is not installed', function() { - XRegExp.uninstall('namespacing'); - var result = []; - XRegExp.forEach('abc 123 def', XRegExp('(?\\w)\\w*'), function(m) { - result.push(m.first); - }); - - expect(result).toEqual(['a', '1', 'd']); - }); - it('should provide match start indexes on the match array', function() { var result = []; XRegExp.forEach('abc 123 def', /\w+/, function(m) { @@ -719,19 +709,12 @@ describe('XRegExp.globalize()', function() { expect(XRegExp.globalize(XRegExp('', 'Aix'))[REGEX_DATA].flags).toBe('Agix'); }); - it('should retain named capture capabilities when namespacing is not installed', function() { + it('should retain named capture capabilities', function() { var regex = XRegExp('(?x)\\k'); expect(XRegExp.exec('xx', XRegExp.globalize(regex)).groups.name).toBe('x'); }); - it('should retain named capture capabilities when namespacing is not installed', function() { - XRegExp.uninstall('namespacing'); - var regex = XRegExp('(?x)\\k'); - - expect(XRegExp.exec('xx', XRegExp.globalize(regex)).name).toBe('x'); - }); - it('should throw an exception if not given a RegExp object', function() { expect(function() {XRegExp.globalize();}).toThrowError(TypeError); expect(function() {XRegExp.globalize(undefined);}).toThrowError(TypeError);