From 28ce8dbe2b63f208b1e3d71649ea60370cbbbac8 Mon Sep 17 00:00:00 2001 From: jsenjoy Date: Sat, 24 Sep 2016 22:18:58 +0800 Subject: [PATCH 1/3] Apply g flag as default, remove keyword 'all', and tweak test cases. --- lib/Builder.js | 67 ++++++++++++++-- lib/Language/Helpers/methodMatch.js | 1 - test/builder-test.js | 65 +++++++-------- test/cache-test.js | 2 +- test/interpreter-test.js | 120 ++++++++++++++-------------- test/rules-test.js | 24 +++--- 6 files changed, 163 insertions(+), 116 deletions(-) diff --git a/lib/Builder.js b/lib/Builder.js index ecc6dc8..52c0751 100644 --- a/lib/Builder.js +++ b/lib/Builder.js @@ -80,7 +80,7 @@ class Builder { this._regEx = [] /** @var {string} _modifiers Raw modifier to apply on. */ - this._modifiers = '' + this._modifiers = 'g' /** @var {number} _lastMethodType Type of last method, to avoid invalid builds. */ this._lastMethodType = METHOD_TYPE_BEGIN @@ -379,10 +379,6 @@ class Builder { /* MODIFIER MAPPER */ /**********************************************************/ - all() { - return this._addUniqueModifier('g') - } - multiLine() { return this._addUniqueModifier('m') } @@ -486,6 +482,7 @@ class Builder { this._regEx.push(condition) return this } + /** * Validate method call. This will throw an exception if the called method makes no sense at this point. * Will add the current type as the last method type. @@ -619,6 +616,8 @@ class Builder { } /** + * Clone a new builder object. + * * @return {Builder} */ clone() { @@ -633,6 +632,18 @@ class Builder { return clone } + /** + * Remote specific flag. + * + * @param {string} flag + * @return {Builder} + */ + removeModifier(flag) { + this._modifiers.replace(flag, '') + + return this + } + /**********************************************************/ /* REGEX METHODS */ /**********************************************************/ @@ -645,6 +656,52 @@ class Builder { const regexp = this.get() return regexp.test.apply(regexp, arguments) } + + /**********************************************************/ + /* ADDITIONAL METHODS */ + /**********************************************************/ + + /** + * Just like test in RegExp, but reset lastIndex. + * + * @param {string} target + * @return {boolean} + */ + isMatching(target) { + const result = this.test(target) + this.get().lastIndex = 0 + return result + } + + /** + * Just like match in String, but reset lastIndex. + * + * @param {string} target + * @return {array|null} + */ + getMatch(target) { + const regex = this.get() + const result = regex.exec(target) + regex.lastIndex = 0 + return result + } + + /** + * Get all matches, just like loop for RegExp.exec. + * @param {string} target + */ + getMatches(target) { + const result = [] + const regex = this.get() + let temp = null + + while(temp = regex.exec(target)) { + result.push(temp) + } + regex.lastIndex = 0 + + return result + } } module.exports = Builder diff --git a/lib/Language/Helpers/methodMatch.js b/lib/Language/Helpers/methodMatch.js index e552a5a..f577ebb 100644 --- a/lib/Language/Helpers/methodMatch.js +++ b/lib/Language/Helpers/methodMatch.js @@ -23,7 +23,6 @@ const mapper = { 'new line': { 'class': SimpleMethod, 'method': 'newLine' }, 'whitespace': { 'class': SimpleMethod, 'method': 'whitespace' }, 'no whitespace': { 'class': SimpleMethod, 'method': 'noWhitespace' }, - 'all': { 'class': SimpleMethod, 'method': 'all' }, 'anything': { 'class': SimpleMethod, 'method': 'any' }, 'tab': { 'class': SimpleMethod, 'method': 'atb' }, 'digit': { 'class': SimpleMethod, 'method': 'digit' }, diff --git a/test/builder-test.js b/test/builder-test.js index 56bd816..ef7e214 100644 --- a/test/builder-test.js +++ b/test/builder-test.js @@ -3,7 +3,7 @@ const assert = require('assert') const SRL = require('../lib/Builder') -describe('Builder Test', () => { +describe('Builder isMatching', () => { it('Simple Phone Number Format', () => { const regex = new SRL() .startsWith() @@ -15,12 +15,12 @@ describe('Builder Test', () => { .digit().onceOrMore() .mustEnd() - assert.ok(regex.test('+49 123-45')) - assert.ok(regex.exec('+492 1235-4')) - assert.ok(!regex.test('+49 123 45')) - assert.ok(!regex.exec('49 123-45')) - assert.ok(!regex.test('a+49 123-45')) - assert.ok(!regex.test('+49 123-45b')) + assert.ok(regex.isMatching('+49 123-45')) + assert.ok(regex.isMatching('+492 1235-4')) + assert.ok(!regex.isMatching('+49 123 45')) + assert.ok(!regex.isMatching('49 123-45')) + assert.ok(!regex.isMatching('a+49 123-45')) + assert.ok(!regex.isMatching('+49 123-45b')) }) it('Simple Email Format', () => { @@ -39,15 +39,14 @@ describe('Builder Test', () => { .letter().atLeast(2) .mustEnd() .caseInsensitive() - .get() // Use get() to test resulting RegExp object. - - assert.equal('sample@example.com'.match(regex)[0], 'sample@example.com') - assert.equal(regex.exec('super-He4vy.add+ress@top-Le.ve1.domains'), 'super-He4vy.add+ress@top-Le.ve1.domains') - assert.ok(!regex.test('sample.example.com')) - assert.ok(!regex.test('missing@tld')) - assert.ok(!regex.test('hav ing@spac.es')) - assert.ok(!regex.test('no@pe.123')) - assert.ok(!regex.test('invalid@email.com123')) + + assert.equal(regex.getMatch('sample@example.com')[0], 'sample@example.com') + assert.equal(regex.getMatch('super-He4vy.add+ress@top-Le.ve1.domains')[0], 'super-He4vy.add+ress@top-Le.ve1.domains') + assert.ok(!regex.isMatching('sample.example.com')) + assert.ok(!regex.isMatching('missing@tld')) + assert.ok(!regex.isMatching('hav ing@spac.es')) + assert.ok(!regex.isMatching('no@pe.123')) + assert.ok(!regex.isMatching('invalid@email.com123')) }) it('Capture Group', () => { @@ -65,14 +64,13 @@ describe('Builder Test', () => { query.letter().onceOrMore() }) .literally('.') - .get() - assert.ok(regex.test('my favorite color: blue.')) - assert.ok(regex.test('my favorite colour is green.')) - assert.ok(!regex.test('my favorite colour is green!')) + assert.ok(regex.isMatching('my favorite color: blue.')) + assert.ok(regex.isMatching('my favorite colour is green.')) + assert.ok(!regex.isMatching('my favorite colour is green!')) const testcase = 'my favorite colour is green. And my favorite color: yellow.' - const matches = testcase.match(regex) + const matches = regex.getMatch(testcase) assert.equal(matches[1], 'green') }) @@ -86,12 +84,12 @@ describe('Builder Test', () => { .tab() .mustEnd() .multiLine() - .get() + const target = ` ba\t aaabbb ` - assert.ok(regex.test(target)) + assert.ok(regex.isMatching(target)) const regex2 = new SRL() .startsWith() @@ -101,10 +99,10 @@ describe('Builder Test', () => { .onceOrMore() .literally('b') .mustEnd() - .get() + const target2 = `a b` - assert.ok(regex2.test(target2)) + assert.ok(regex2.isMatching(target2)) }) it('Replace', () => { @@ -133,9 +131,8 @@ describe('Builder Test', () => { .whitespace().optional() .lazy() }) - .get() - const matches = ',, '.match(regex) + const matches = regex.getMatch(',, ') assert.equal(matches[1], ',,') assert.notEqual(matches[1], ',, ') @@ -143,18 +140,16 @@ describe('Builder Test', () => { .literally(',') .atLeast(1) .lazy() - .get() - const matches2 = regex2.exec(',,,,,') + const matches2 = regex2.getMatch(',,,,,') assert.equal(matches2[0], ',') assert.notEqual(matches2[0], ',,,,,') }) - it('Global', () => { + it('Global as Default', () => { const regex = new SRL() .literally('a') - .all() .get() let count = 0 @@ -169,9 +164,9 @@ describe('Builder Test', () => { .raw('b[a-z]r') .raw(/\d+/) - assert.ok(regex.test('foobzr123')) - assert.ok(regex.test('foobar1')) - assert.ok(!regex.test('fooa')) - assert.ok(!regex.test('foobar')) + assert.ok(regex.isMatching('foobzr123')) + assert.ok(regex.isMatching('foobar1')) + assert.ok(!regex.isMatching('fooa')) + assert.ok(!regex.isMatching('foobar')) }) }) diff --git a/test/cache-test.js b/test/cache-test.js index ef4b486..93660fe 100644 --- a/test/cache-test.js +++ b/test/cache-test.js @@ -12,7 +12,7 @@ describe('Cache', () => { }) it('In interpreter', () => { - const RE = /(?:a)/ + const RE = /(?:a)/g const query = new Interpreter('Literally "a"') assert.deepEqual(query.get(), RE) diff --git a/test/interpreter-test.js b/test/interpreter-test.js index 70159de..81b373a 100644 --- a/test/interpreter-test.js +++ b/test/interpreter-test.js @@ -3,79 +3,79 @@ const assert = require('assert') const Interpreter = require('../lib/Language/Interpreter') -describe('Interpreter Test', () => { +describe('Interpreter isMatching', () => { it('Parser', () => { - let regex = new Interpreter('aNy Character ONCE or more literAlly "fO/o"').get() - assert.deepEqual(regex, /\w+(?:fO\/o)/) + let query = new Interpreter('aNy Character ONCE or more literAlly "fO/o"') + assert.deepEqual(query.get(), /\w+(?:fO\/o)/g) - regex = new Interpreter(` + query = new Interpreter(` begin with literally "http", optional "s", literally "://", optional "www.", anything once or more, literally ".com", must end - `).get() - assert.deepEqual(regex, /^(?:http)(?:(?:s))?(?::\/\/)(?:(?:www\.))?.+(?:\.com)$/) - assert.ok(regex.test('http://www.ebay.com')) - assert.ok(regex.test('https://google.com')) - assert.ok(!regex.test('htt://google.com')) - assert.ok(!regex.test('http://.com')) + `) + assert.deepEqual(query.get(), /^(?:http)(?:(?:s))?(?::\/\/)(?:(?:www\.))?.+(?:\.com)$/g) + assert.ok(query.builder.isMatching('http://www.ebay.com')) + assert.ok(query.builder.isMatching('https://google.com')) + assert.ok(!query.builder.isMatching('htt://google.com')) + assert.ok(!query.builder.isMatching('http://.com')) - regex = new Interpreter( + query = new Interpreter( 'begin with capture (digit from 0 to 8 once or more) if followed by "foo"' - ).get() - assert.deepEqual(regex, /^([0-8]+)(?=(?:foo))/) - assert.ok(regex.test('142foo')) - assert.ok(!regex.test('149foo')) - assert.ok(!regex.test('14bar')) - assert.equal('142foo'.match(regex)[1], '142') + ) + assert.deepEqual(query.get(), /^([0-8]+)(?=(?:foo))/g) + assert.ok(query.builder.isMatching('142foo')) + assert.ok(!query.builder.isMatching('149foo')) + assert.ok(!query.builder.isMatching('14bar')) + assert.equal(query.builder.getMatch('142foo')[1], '142') - regex = new Interpreter('literally "colo", optional "u", literally "r"').get() - assert.ok(regex.test('color')) - assert.ok(regex.test('colour')) + query = new Interpreter('literally "colo", optional "u", literally "r"') + assert.ok(query.builder.isMatching('color')) + assert.ok(query.builder.isMatching('colour')) - regex = new Interpreter( + query = new Interpreter( 'starts with number from 0 to 5 between 3 and 5 times, must end' - ).get() - assert.ok(regex.test('015')) - assert.ok(regex.test('44444')) - assert.ok(!regex.test('444444')) - assert.ok(!regex.test('1')) - assert.ok(!regex.test('563')) + ) + assert.ok(query.builder.isMatching('015')) + assert.ok(query.builder.isMatching('44444')) + assert.ok(!query.builder.isMatching('444444')) + assert.ok(!query.builder.isMatching('1')) + assert.ok(!query.builder.isMatching('563')) - regex = new Interpreter( + query = new Interpreter( 'starts with digit exactly 2 times, letter at least 3 time' - ).get() - assert.deepEqual(regex, /^[0-9]{2}[a-z]{3,}/) - assert.ok(regex.test('12abc')) - assert.ok(regex.test('12abcd')) - assert.ok(!regex.test('123abc')) - assert.ok(!regex.test('1a')) - assert.ok(!regex.test('')) + ) + assert.deepEqual(query.get(), /^[0-9]{2}[a-z]{3,}/g) + assert.ok(query.builder.isMatching('12abc')) + assert.ok(query.builder.isMatching('12abcd')) + assert.ok(!query.builder.isMatching('123abc')) + assert.ok(!query.builder.isMatching('1a')) + assert.ok(!query.builder.isMatching('')) }) it('Email', () => { - const regex = new Interpreter(` + const query = new Interpreter(` begin with any of (digit, letter, one of "._%+-") once or more, literally "@", either of (digit, letter, one of ".-") once or more, literally ".", letter at least 2, must end, case insensitive - `).get() + `) - assert.ok(regex.test('sample@example.com')) - assert.ok(regex.test('super-He4vy.add+ress@top-Le.ve1.domains')) - assert.ok(!regex.test('sample.example.com')) - assert.ok(!regex.test('missing@tld')) - assert.ok(!regex.test('hav ing@spac.es')) - assert.ok(!regex.test('no@pe.123')) - assert.ok(!regex.test('invalid@email.com123')) + assert.ok(query.builder.isMatching('sample@example.com')) + assert.ok(query.builder.isMatching('super-He4vy.add+ress@top-Le.ve1.domains')) + assert.ok(!query.builder.isMatching('sample.example.com')) + assert.ok(!query.builder.isMatching('missing@tld')) + assert.ok(!query.builder.isMatching('hav ing@spac.es')) + assert.ok(!query.builder.isMatching('no@pe.123')) + assert.ok(!query.builder.isMatching('invalid@email.com123')) }) it('Capture Group', () => { - const regex = new Interpreter( + const query = new Interpreter( 'literally "color:", whitespace, capture (letter once or more), literally ".", all' - ).get() + ) const target = 'Favorite color: green. Another color: yellow.' const matches = [] let result = null - while (result = regex.exec(target)) { + while (result = query.builder.exec(target)) { matches.push(result[1]) } @@ -84,22 +84,22 @@ describe('Interpreter Test', () => { }) it('Parentheses', () => { - let regex = new Interpreter( + let query = new Interpreter( 'begin with (literally "foo", literally "bar") twice must end' - ).get() - assert.deepEqual(regex, /^(?:(?:foo)(?:bar)){2}$/) - assert.ok(regex.test('foobarfoobar')) - assert.ok(!regex.test('foobar')) + ) + assert.deepEqual(query.get(), /^(?:(?:foo)(?:bar)){2}$/g) + assert.ok(query.builder.isMatching('foobarfoobar')) + assert.ok(!query.builder.isMatching('foobar')) - regex = new Interpreter( + query = new Interpreter( 'begin with literally "bar", (literally "foo", literally "bar") twice must end' - ).get() - assert.deepEqual(regex, /^(?:bar)(?:(?:foo)(?:bar)){2}$/) - assert.ok(regex.test('barfoobarfoobar')) + ) + assert.deepEqual(query.get(), /^(?:bar)(?:(?:foo)(?:bar)){2}$/g) + assert.ok(query.builder.isMatching('barfoobarfoobar')) - regex = new Interpreter('(literally "foo") twice').get() - assert.deepEqual(regex, /(?:(?:foo)){2}/) - assert.ok(regex.test('foofoo')) - assert.ok(!regex.test('foo')) + query = new Interpreter('(literally "foo") twice') + assert.deepEqual(query.get(), /(?:(?:foo)){2}/g) + assert.ok(query.builder.isMatching('foofoo')) + assert.ok(!query.builder.isMatching('foo')) }) }) diff --git a/test/rules-test.js b/test/rules-test.js index 20033d8..bc8025c 100644 --- a/test/rules-test.js +++ b/test/rules-test.js @@ -102,7 +102,7 @@ function runAssertions(data) { data.matches.forEach((match) => { assert( - query.test(match), + query.isMatching(match), `Failed asserting that this query matches '${match}'.${getExpression(data.srl, query)}` ) assertionMade = true @@ -110,7 +110,7 @@ function runAssertions(data) { data.no_matches.forEach((noMatch) => { assert( - !query.test(noMatch), + !query.isMatching(noMatch), `Failed asserting that this query does not match '${noMatch}'.${getExpression(data.srl, query)}` ) assertionMade = true @@ -118,32 +118,28 @@ function runAssertions(data) { Object.keys(data.captures).forEach((test) => { const expected = data.captures[test] - const matches = [] - const regex = query.all() + let matches = null try { - let result = null - while ((result = regex.exec(test))) { - matches.push(result.map((item) => item === undefined ? '' : item).slice(1)) - - if (regex.lastIndex === test.length - 1) { - break - } - } + matches = query.getMatches(test) } catch (e) { assert(false, `Parser error: ${e.message}${getExpression(data.srl, query)}`) } assert.equal( - expected.length, matches.length, + expected.length, `Invalid match count for test ${test}.${getExpression(data.srl, query)}` ) matches.forEach((capture, index) => { + const result = Array.from(capture).slice(1).map((item) => { + return item === undefined ? '' : item + }) + assert.deepEqual( + result, expected[index], - capture, `The capture group did not return the expected results for test ${test}.${getExpression(data.srl, query)}` ) }) From b54b05b583ea0f39ae03205fb43199c027ee6710 Mon Sep 17 00:00:00 2001 From: jsenjoy Date: Sun, 25 Sep 2016 12:34:03 +0800 Subject: [PATCH 2/3] RemoveModifier Test --- lib/Builder.js | 3 ++- test/builder-test.js | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/Builder.js b/lib/Builder.js index 52c0751..cbc1361 100644 --- a/lib/Builder.js +++ b/lib/Builder.js @@ -639,7 +639,8 @@ class Builder { * @return {Builder} */ removeModifier(flag) { - this._modifiers.replace(flag, '') + this._modifiers = this._modifiers.replace(flag, '') + this._result = null return this } diff --git a/test/builder-test.js b/test/builder-test.js index ef7e214..dbfb3ed 100644 --- a/test/builder-test.js +++ b/test/builder-test.js @@ -1,7 +1,7 @@ 'use strict' const assert = require('assert') -const SRL = require('../lib/Builder') +const SRL = require('../') describe('Builder isMatching', () => { it('Simple Phone Number Format', () => { @@ -169,4 +169,13 @@ describe('Builder isMatching', () => { assert.ok(!regex.isMatching('fooa')) assert.ok(!regex.isMatching('foobar')) }) + + it('Remove modifier', () => { + const regex = new SRL() + .literally('foo') + .removeModifier('g') + .get() + + assert.deepEqual(regex, /(?:foo)/) + }) }) From ca38519406eefac9e745b18f17727d9130c55270 Mon Sep 17 00:00:00 2001 From: jsenjoy Date: Sat, 24 Sep 2016 22:45:32 +0800 Subject: [PATCH 3/3] Update readme, release 0.2.0, close #3 --- README.md | 20 ++++++++++++++------ lib/Builder.js | 2 +- package.json | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3163130..530cf98 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ JavaScript implementation of [Simple Regex](https://simple-regex.com/) :tada::ta > Because of the JavaScript regex engine, there is something different from [Simple Regex](https://simple-regex.com/) - NOT support `as` to assign capture name. - NOT support `if already had/if not already had` -- NO `firstMatch`, since in JavaScript `lazy` means non-greedy (matching the fewest possible characters). +- NO `first match` and NO `all lazy`, since in JavaScript `lazy` means non-greedy (matching the fewest possible characters). ## Installation @@ -26,21 +26,29 @@ The builder can agent `test/exec` method to the generated regex object. Or you c ```js const SRL = require('srl') const query = new SRL('letter exactly 3 times') -const regex = query.get() // /[a-z]{3}/ -query.test('aaa') // true -query.exec('aaa') // [ 'aaa', index: 0, input: 'aaa' ] +query.isMatching('aaa') // true +query.getMatch('aaa') // [ 'aaa', index: 0, input: 'aaa' ] query .digit() .neverOrMore() .mustEnd() - .get() // /[a-z]{3}[0-9]*$/ + .get() // /[a-z]{3}[0-9]*$/g ``` Required Node 6.0+ for the ES6 support, Or you can use [Babel](http://babeljs.io/) to support Node below 6.0. -Using [Webpack](http://webpack.github.io) and [babel-loader](https://github.com/babel/babel-loader) to pack it if want to use in browsers. +Using [Webpack](http://webpack.github.io) and [babel-loader](https://github.com/babel/babel-loader) to pack it if want to use in browsers. + +## Additional + +In SRL-JavaScript we apply `g` flag as default to follow the [Simple Regex](https://simple-regex.com/) "standard", so we provide more API to use regex conveniently. + +- `isMatching` - Validate if the expression matches the given string. +- `getMatch` - Get first match of the given string, like run `regex.exec` once. +- `getMatches` - Get all matches of the given string, like a loop to run `regex.exec`. +- `removeModifier` - Remove specific flag. ## Development diff --git a/lib/Builder.js b/lib/Builder.js index cbc1361..bbc67f7 100644 --- a/lib/Builder.js +++ b/lib/Builder.js @@ -696,7 +696,7 @@ class Builder { const regex = this.get() let temp = null - while(temp = regex.exec(target)) { + while (temp = regex.exec(target)) { result.push(temp) } regex.lastIndex = 0 diff --git a/package.json b/package.json index a93960e..b373735 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "srl", - "version": "0.1.0", + "version": "0.2.0", "description": "Simple Regex Language", "main": "lib/SRL.js", "scripts": {