{{ site.description | default: site.github.project_tagline }}
- - {% if site.logo %} -View the Project on GitHub
{{ site.github.repository_nwo }}
VerEx
• VerbalExpression
-
- ", multiple(/./), "
"); + const [match] = para.exec("foo
bar
"); + + expect(match).toEqual("foo
bar
"); + }); + + describe("multiple.greedy", () => { + it("should be an alias for multiple", () => { + expect(multiple.greedy).toEqual(multiple); + }); + }); + + describe("multiple.lazy", () => { + it("should be lazy", () => { + const para = VerEx("", multiple.lazy(/./), "
"); + const [match] = para.exec("foo
bar
"); + + expect(match).toEqual("foo
"); + }); + }); +}); + +describe("zeroOrMore", () => { + it("should be an alias for multiple", () => { + expect(zeroOrMore).toEqual(multiple); + }); +}); diff --git a/test/one-or-more.test.ts b/test/one-or-more.test.ts new file mode 100644 index 00000000..e3427160 --- /dev/null +++ b/test/one-or-more.test.ts @@ -0,0 +1,47 @@ +import {oneOrMore} from "../src/one-or-more"; +import {VerEx} from "../src/verex"; +import "./custom-matchers"; + +describe("oneOrMore", () => { + it("should be a function", () => { + expect(oneOrMore).toBeInstanceOf(Function); + }); + + const nFoos = VerEx(/^/, oneOrMore("foo"), /$/); + it("should match one repetition", () => { + expect(nFoos).toMatchString("foo"); + }); + + it("should match more than one repetition", () => { + expect(nFoos).toMatchString("foofoo"); + + const manyFoos = "foo".repeat(100); + expect(nFoos).toMatchString(manyFoos); + }); + + it("should not match zero repetitions", () => { + expect(nFoos).not.toMatchString(""); + }); + + it("should be greedy", () => { + const para = VerEx("", oneOrMore(/./), "
"); + const [match] = para.exec("foo
bar
"); + + expect(match).toEqual("foo
bar
"); + }); + + describe("oneOrMore.greedy", () => { + it("should be an alias for oneOrMore", () => { + expect(oneOrMore.greedy).toEqual(oneOrMore); + }); + }); + + describe("oneOrMore.lazy", () => { + it("should be lazy", () => { + const para = VerEx("", oneOrMore.lazy(/./), "
"); + const [match] = para.exec("foo
bar
"); + + expect(match).toEqual("foo
"); + }); + }); +}); diff --git a/test/or.test.ts b/test/or.test.ts new file mode 100644 index 00000000..67a7542e --- /dev/null +++ b/test/or.test.ts @@ -0,0 +1,44 @@ +import {or} from "../src/or"; +import {VerEx} from "../src/verex"; +import "./custom-matchers"; + +describe("or", () => { + const abcOrDef = VerEx(/^/, or("abc", "def"), /$/); + + it("should be a function", () => { + expect(or).toBeInstanceOf(Function); + }); + + it("should match either of the options", () => { + expect(abcOrDef).toMatchString("abc"); + expect(abcOrDef).toMatchString("def"); + }); + + it("should group each option", () => { + // `abcOrDef` should not be `abc|def` + + expect(abcOrDef).not.toMatchString("abcef"); + expect(abcOrDef).not.toMatchString("abdef"); + }); + + it("should wrap the alternation in a non-capturing group", () => { + const exp = VerEx("a", or("b", "c"), "d"); + + expect(exp).toMatchString("abd"); + expect(exp).toMatchString("acd"); + + expect(exp).not.toMatchString("ab"); + expect(exp).not.toMatchString("cd"); + }); + + it("should not match when neither of the options is present", () => { + expect(abcOrDef).not.toMatchString(""); + }); + + it("should work with one argument", () => { + const abc = VerEx(/^/, or("abc"), /$/); + + expect(abc).toMatchString("abc"); + expect(abc).not.toMatchString("a"); + }); +}); diff --git a/test/repeat.test.ts b/test/repeat.test.ts new file mode 100644 index 00000000..103da8d2 --- /dev/null +++ b/test/repeat.test.ts @@ -0,0 +1,135 @@ +import {repeat} from "../src/repeat"; +import {VerEx} from "../src/verex"; +import "./custom-matchers"; + +describe("repeat", () => { + it("should be a function", () => { + expect(repeat).toBeInstanceOf(Function); + }); + + describe("repeat.greedy", () => { + it("should be an alias for repeat", () => { + expect(repeat.greedy).toEqual(repeat); + }); + }); + + describe("repeat(expression, n)", () => { + const exp = VerEx(/^/, repeat("foo", 2), /$/); + + it("should match exactly `n` repetitions", () => { + expect(exp).toMatchString("foo".repeat(2)); + }); + + it("should not match less than `n` repetitions", () => { + expect(exp).not.toMatchString(""); + expect(exp).not.toMatchString("foo"); + }); + + it("should not match more than `n` repetitions", () => { + expect(exp).not.toMatchString("foo".repeat(3)); + expect(exp).not.toMatchString("foo".repeat(4)); + }); + + it("should throw an error when an invalid range passed", () => { + expect(() => repeat("foo", 2.2)).toThrowError(TypeError); + expect(() => repeat("foo", -2)).toThrowError(TypeError); + expect(() => repeat("foo", NaN)).toThrowError(TypeError); + }); + }); + + describe("repeat(expression, min, Infinity)", () => { + const exp = VerEx(/^/, repeat("foo", 2, Infinity), /$/); + + it("should not match less than `min` repetitions", () => { + expect(exp).not.toMatchString(""); + expect(exp).not.toMatchString("foo"); + }); + + it("should match `min` repetitions", () => { + expect(exp).toMatchString("foo".repeat(2)); + }); + + it("should match more than `min` repetitions", () => { + for (let i = 2; i < 20; i++) { + expect(exp).toMatchString("foo".repeat(i)); + } + }); + + it("should be greedy", () => { + const para = VerEx("", repeat(/./, 0, Infinity), "
"); + const [match] = para.exec("foo
bar
"); + + expect(match).toEqual("foo
bar
"); + }); + + it("should throw an error when an invalid range passed", () => { + expect(() => repeat("foo", 2.2, Infinity)).toThrowError(TypeError); + expect(() => repeat("foo", -2, Infinity)).toThrowError(TypeError); + expect(() => repeat("foo", NaN, Infinity)).toThrowError(TypeError); + }); + + describe("repeat.lazy(expression, min, Infinity)", () => { + it("should be lazy", () => { + const para = VerEx("", repeat.lazy(/./, 0, Infinity), "
"); + const [match] = para.exec("foo
bar
"); + + expect(match).toEqual("foo
"); + }); + }); + }); + + describe("repeat(expression, min, max)", () => { + const exp = VerEx(/^/, repeat("foo", 3, 10), /$/); + + it("should not match less than `min` repetitions", () => { + expect(exp).not.toMatchString(""); + expect(exp).not.toMatchString("foo".repeat(1)); + expect(exp).not.toMatchString("foo".repeat(2)); + }); + + it("should match `min` repetitions", () => { + expect(exp).toMatchString("foo".repeat(3)); + }); + + it("should match `min < x < max` repetitions", () => { + for (let i = 4; i < 10; i++) { + expect(exp).toMatchString("foo".repeat(i)); + } + }); + + it("should match `max` repetitions", () => { + expect(exp).toMatchString("foo".repeat(10)); + }); + + it("should not match more than `max` repetitions", () => { + expect(exp).not.toMatchString("foo".repeat(11)); + expect(exp).not.toMatchString("foo".repeat(12)); + expect(exp).not.toMatchString("foo".repeat(13)); + }); + + it("should be greedy", () => { + const para = VerEx("", repeat(/./, 0, 20), "
"); + const [match] = para.exec("foo
bar
"); + + expect(match).toEqual("foo
bar
"); + }); + + it("should throw an error when an invalid range passed", () => { + expect(() => repeat("foo", 2.2, 3.2)).toThrowError(TypeError); + expect(() => repeat("foo", 2, 3.2)).toThrowError(TypeError); + expect(() => repeat("foo", 2, -2)).toThrowError(TypeError); + expect(() => repeat("foo", 2, NaN)).toThrowError(TypeError); + + expect(() => repeat("foo", 3, 2)).toThrowError(RangeError); + }); + + describe("repeat.lazy(expression, min, max)", () => { + it("should be lazy", () => { + const para = VerEx("", repeat.lazy(/./, 0, 20), "
"); + const [match] = para.exec("foo
bar
"); + + expect(match).toEqual("foo
"); + }); + }); + }); +}); diff --git a/test/sanitize.test.ts b/test/sanitize.test.ts new file mode 100644 index 00000000..738bfd7a --- /dev/null +++ b/test/sanitize.test.ts @@ -0,0 +1,33 @@ +import sanitize from "../src/util/sanitize"; + +const {raw} = String; + +describe("sanitize", () => { + it("should escape escape-worthy characters", () => { + const unescaped = raw`\.|*?+(){}^$-[]`; + const expected = raw`\\\.\|\*\?\+\(\)\{\}\^\$\-\[\]`; + + expect(sanitize(unescaped)).toEqual(expected); + }); + + it("should not unnecessarily escape characters", () => { + const unescaped = "foo %@#! 1234"; + + expect(sanitize(unescaped)).toEqual(unescaped); + }); + + it("should correctly escape a bunch of back slashes", () => { + expect(sanitize(raw`\\\\`)).toEqual(raw`\\\\\\\\`); + expect(sanitize(raw`\\.`)).toEqual(raw`\\\\\.`); + }); + + it("should sanitize decimals in numbers", () => { + expect(sanitize(1.5)).toEqual(raw`1\.5`); + }); + + it("should sanitize unusual numbers", () => { + expect(sanitize(Infinity)).toEqual("Infinity"); + expect(sanitize(-Infinity)).toEqual(raw`\-Infinity`); + expect(sanitize(NaN)).toEqual("NaN"); + }); +}); diff --git a/test/tests.js b/test/tests.js deleted file mode 100644 index 29a93c6c..00000000 --- a/test/tests.js +++ /dev/null @@ -1,559 +0,0 @@ -import test from 'ava'; -import VerEx from '../dist/verbalexpressions'; - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test#Using_test()_on_a_regex_with_the_global_flag -function resetLastIndex(regex) { - regex.lastIndex = 0; -} - -test('constructor', (t) => { - const testRegex = VerEx(); - - t.true(testRegex instanceof RegExp, 'Should extend RegExp'); - t.is(testRegex.toString(), '/(?:)/gm', 'Should be empty regex with global, multiline matching'); -}); - -// Utility // - -test('sanitize', (t) => { - const testString = '$a^b\\c|d(e)f[g]h{i}j.k*l+m?n:o=p'; - const escaped = '\\$a\\^b\\\\c\\|d\\(e\\)f\\[g\\]h\\{i\\}j\\.k\\*l\\+m\\?n\\:o\\=p'; - - t.is(VerEx().sanitize(testString), escaped, 'Special characters should be sanitized'); - - t.is(VerEx().sanitize(42), 42); - t.is(VerEx().sanitize(/foo/), 'foo'); - - t.notThrows(() => VerEx().sanitize()); - t.notThrows(() => VerEx().sanitize(NaN)); - t.notThrows(() => VerEx().sanitize(null)); - t.notThrows(() => VerEx().sanitize(true)); -}); - -test('add', (t) => { - let testRegex = VerEx().startOfLine().withAnyCase().endOfLine(); - testRegex = testRegex.add('(?:foo)?'); - - t.true(testRegex.source.startsWith('^'), 'Should retain old prefixes'); - t.true(testRegex.source.endsWith('$'), 'Should retain old suffixes'); - - t.true(testRegex.test('foo'), 'Should add new rules'); - resetLastIndex(testRegex); - t.true(testRegex.test(''), 'Should add new rules'); - - t.true(testRegex.flags.includes('i'), 'Should retain old modifiers'); -}); - -// Rules // - -test('startOfLine', (t) => { - let testRegex = VerEx().startOfLine().then('a'); - let testString = 'a'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'ba'; - t.false(testRegex.test(testString)); - - testRegex = testRegex.startOfLine(false); // start of line is no longer necessary - testString = 'ba'; - t.true(testRegex.test(testString)); -}); - -test('endOfLine', (t) => { - let testRegex = VerEx().find('a').endOfLine(); - let testString = 'a'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'ab'; - t.false(testRegex.test(testString)); - - testRegex = testRegex.endOfLine(false); // end of line is no longer necessary - testString = 'ab'; - t.true(testRegex.test(testString)); -}); - -function then(name, t) { - let testRegex = VerEx()[name]('a'); - let testString = 'a'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'b'; - t.false(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = ''; - t.false(testRegex.test(testString)); - - testRegex = VerEx()[name]('a')[name]('b'); - testString = 'ab'; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'ac'; - t.false(testRegex.test(testString)); -} - -test('then', (t) => { - then('then', t); -}); - -test('find', (t) => { - then('find', t); -}); - -test('maybe', (t) => { - const testRegex = VerEx().startOfLine().then('a').maybe('b'); - let testString = 'acb'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'abc'; - t.true(testRegex.test(testString)); -}); - -test('or', (t) => { - let testRegex = VerEx().startOfLine().then('abc').or('def'); - let testString = 'defzzz'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'abczzz'; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'xyzabc'; - t.false(testRegex.test(testString)); - - testRegex = VerEx().startOfLine().then('abc').or().then('def'); - testString = 'defzzz'; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'abczzz'; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'xyzabc'; - t.false(testRegex.test(testString)); -}); - -test('anything', (t) => { - const testRegex = VerEx().startOfLine().anything(); - let testString = 'foo'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = ''; - t.true(testRegex.test(testString), 'Should be able to match zero characters'); -}); - -test('anythingBut', (t) => { - let testRegex = VerEx().startOfLine().anythingBut('br').endOfLine(); - let testString = 'foobar'; - - t.false(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'foo_a_'; - t.true(testRegex.test(testString)); - - testRegex = VerEx().startOfLine().anythingBut('br'); - testString = 'bar'; - t.true(testRegex.test(testString), 'Should be able to match zero characters'); - - testRegex = VerEx().startOfLine().anythingBut(['b', 'r']).endOfLine(); - testString = 'foobar'; - t.false(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'foo_a_'; - t.true(testRegex.test(testString)); - - testRegex = VerEx().startOfLine().anythingBut(['b', 'r']); - testString = 'bar'; - t.true(testRegex.test(testString), 'Should be able to match zero characters'); -}); - -test('something', (t) => { - const testRegex = VerEx().something(); - let testString = ''; - - t.false(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'a'; - t.true(testRegex.test(testString)); -}); - -test('somethingBut', (t) => { - let testRegex = VerEx().startOfLine().somethingBut('abc').endOfLine(); - let testString = ''; - t.false(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'foo'; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'fab'; - t.false(testRegex.test(testString)); - - testRegex = VerEx().startOfLine().somethingBut(['a', 'b', 'c']).endOfLine(); - testString = ''; - t.false(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'foo'; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'fab'; - t.false(testRegex.test(testString)); -}); - -function anyOf(name, t) { - let testRegex = VerEx().startOfLine().then('a')[name]('xyz'); - let testString = 'ay'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'ab'; - t.false(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'a'; - t.false(testRegex.test(testString)); - - testRegex = VerEx().startOfLine().then('a')[name](['x', 'y', 'z']); - testString = 'ay'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'ab'; - t.false(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'a'; - t.false(testRegex.test(testString)); -} - -test('anyOf', (t) => { - anyOf('anyOf', t); -}); - -test('any', (t) => { - anyOf('any', t); -}); - -test('not', (t) => { - const testRegex = VerEx().startOfLine().not('foo').anything().endOfLine(); - let testString = 'foobar'; - - t.false(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'bar'; - t.true(testRegex.test(testString)); -}); - -test('range', (t) => { - let testRegex = VerEx().startOfLine().range('a', 'z', '0', '9').oneOrMore().endOfLine(); - let testString = 'foobarbaz123'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'fooBarBaz_123'; - t.false(testRegex.test(testString)); - - testRegex = VerEx().startOfLine().range('a', 'z', '0').oneOrMore().endOfLine(); - testString = 'foobarbaz'; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'foobarbaz123'; - t.false(testRegex.test(testString), 'Should ignore extra parameters'); -}); - -// Special characters // - -function lineBreak(name, t) { - const testRegex = VerEx().startOfLine().then('abc')[name]().then('def'); - let testString = 'abc\r\ndef'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'abc\ndef'; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'abc\rdef'; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'abc\r\n\ndef'; - t.false(testRegex.test(testString)); -} - -test('lineBreak', (t) => { - lineBreak('lineBreak', t); -}); - -test('br', (t) => { - lineBreak('br', t); -}); - -test('tab', (t) => { - const testRegex = VerEx().startOfLine().tab().then('abc'); - let testString = '\tabc'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'abc'; - t.false(testRegex.test(testString)); -}); - -test('word', (t) => { - let testRegex = VerEx().startOfLine().word().endOfLine(); - let testString = 'azertyuiopqsdfghjklmwxcvbn0123456789_'; - - t.true(testRegex.test(testString)); - - testRegex = VerEx().word(); - testString = '. @[]|,&~-'; - t.false(testRegex.test(testString)); -}); - -test('digit', (t) => { - let testRegex = VerEx().startOfLine().digit().oneOrMore().endOfLine(); - let testString = '0123456789'; - - t.true(testRegex.test(testString)); - - testRegex = VerEx().digit(); - testString = '-.azertyuiopqsdfghjklmwxcvbn @[]|,_&~'; - t.false(testRegex.test(testString)); -}); - -test('whitespace', (t) => { - const testRegex = VerEx().startOfLine().whitespace().oneOrMore().searchOneLine().endOfLine(); - let testString = ' \t\r\n\v\f'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'a z'; - t.false(testRegex.test(testString)); -}); - -// Modifiers // - -test('addModifier', (t) => { - let testRegex = VerEx().addModifier('y'); - t.true(testRegex.flags.includes('y')); - - t.notThrows(() => { - testRegex = VerEx().addModifier('g'); - }, 'Should not add extra modifier if it already exists'); -}); - -test('removeModifier', (t) => { - const testRegex = VerEx().removeModifier('g'); - t.false(testRegex.flags.includes('g')); -}); - -test('withAnyCase', (t) => { - let testRegex = VerEx().startOfLine().then('a'); - let testString = 'A'; - - t.false(testRegex.test(testString)); - - testRegex = VerEx().startOfLine().then('a').withAnyCase(); - testString = 'A'; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'a'; - t.true(testRegex.test(testString)); - - testRegex = VerEx().startOfLine().then('a').withAnyCase(false); - testString = 'A'; - t.false(testRegex.test(testString)); -}); - -test('stopAtFirst', (t) => { - let testRegex = VerEx().find('foo'); - const testString = 'foofoofoo'; - - t.is(testString.match(testRegex).length, 3, 'Should match all "foo"s'); - - testRegex = VerEx().find('foo').stopAtFirst(); - t.is(testString.match(testRegex).length, 1, 'Should match one "foo"'); - - testRegex = VerEx().find('foo').stopAtFirst(false); - t.is(testString.match(testRegex).length, 3, 'Should match all "foo"s'); -}); - -test('searchOneLine', (t) => { - let testRegex = VerEx().startOfLine().then('b').endOfLine(); - const testString = 'a\nb\nc'; - - t.true(testRegex.test(testString)); - - testRegex = VerEx().startOfLine().then('b').endOfLine().searchOneLine(); - t.false(testRegex.test(testString)); - - testRegex = VerEx().startOfLine().then('b').endOfLine().searchOneLine(false); - t.true(testRegex.test(testString)); -}); - -// Loops // - -test('repeatPrevious', (t) => { - let testRegex = VerEx().startOfLine().find('foo').repeatPrevious(3).endOfLine(); - let testString = 'foofoofoo'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'foofoo'; - t.false(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'foofoofoofoo'; - t.false(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'bar'; - t.false(testRegex.test(testString)); - - testRegex = VerEx().startOfLine().find('foo').repeatPrevious(1, 3).endOfLine(); - - for (let i = 0; i <= 4; i++) { - resetLastIndex(testRegex); - testString = 'foo'.repeat(i); - - if (i < 1 || i > 3) { - t.false(testRegex.test(testString)); - } else { - t.true(testRegex.test(testString)); - } - } - - testRegex = VerEx().startOfLine().find('foo').repeatPrevious().endOfLine(); - testString = 'foofoo'; - t.false(testRegex.test(testString), 'Should silently fail on edge cases'); - - testRegex = VerEx().startOfLine().find('foo').repeatPrevious(1, 2, 3).endOfLine(); - testString = 'foofoo'; - t.false(testRegex.test(testString), 'Should silently fail on edge cases'); -}); - -test('oneOrMore', (t) => { - const testRegex = VerEx().startOfLine().then('foo').oneOrMore().endOfLine(); - let testString = 'foo'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'foofoo'; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'bar'; - t.false(testRegex.test(testString)); -}); - -test('multiple', (t) => { - let testRegex = VerEx().startOfLine().find(' ').multiple().endOfLine(); - let testString = ' '; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = ' a '; - t.false(testRegex.test(testString)); - - testRegex = VerEx().startOfLine().multiple('foo').endOfLine(); - testString = 'foo'; - - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'foofoofoo'; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = ''; - t.true(testRegex.test(testString)); - - testRegex = VerEx().startOfLine().multiple('foo', 2).endOfLine(); - testString = 'foo'; - t.false(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'foofoo'; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - testString = 'foofoofoo'; - t.true(testRegex.test(testString)); - - testRegex = VerEx().startOfLine().multiple('foo', 2, 5).endOfLine(); - - for (let i = 0; i <= 6; i++) { - resetLastIndex(testRegex); - testString = 'foo'.repeat(i); - - if (i < 2 || i > 5) { - t.false(testRegex.test(testString)); - } else { - t.true(testRegex.test(testString)); - } - } -}); - -// Capture groups // - -test('capture groups', (t) => { - let testRegex = VerEx().find('foo').beginCapture().then('bar'); - let testString = 'foobar'; - - t.true(testRegex.test(testString), 'Expressions with incomplete capture groups should work'); - - testRegex = testRegex.endCapture().then('baz'); - testString = 'foobarbaz'; - t.true(testRegex.test(testString)); - - resetLastIndex(testRegex); - const matches = testRegex.exec(testString); - t.is(matches[1], 'bar'); -}); - -// Miscellaneous // - -test('replace', (t) => { - const testRegex = VerEx().find(' '); - const testString = 'foo bar baz'; - - t.is(testRegex.replace(testString, '_'), 'foo_bar_baz'); -}); - -test('toRegExp', (t) => { - const testRegex = VerEx().anything(); - const converted = testRegex.toRegExp(); - - t.is(converted.toString(), testRegex.toString(), 'Converted regex should have same behaviour'); -}); diff --git a/test/verex.test.ts b/test/verex.test.ts new file mode 100644 index 00000000..32b71414 --- /dev/null +++ b/test/verex.test.ts @@ -0,0 +1,386 @@ +import {VerEx, VerExp} from "../src/verex"; +import "./custom-matchers"; + +describe("VerEx", () => { + it("should be a function", () => { + expect(VerEx).toBeInstanceOf(Function); + }); + + describe("VerEx(...expressions)", () => { + it("should return an instance of `RegExp`", () => { + const expression = VerEx("foo"); + + expect(expression).toBeInstanceOf(RegExp); + }); + + it("should correctly handle zero arguments", () => { + expect(VerEx()).toEqual(/(?:)/); + }); + + it("should accept multiple strings", () => { + const expression = VerEx("foo", "bar", "baz"); + + expect(expression).toMatchString("foobarbaz"); + }); + + it("should accept regular expressions", () => { + const expression = VerEx(/^/, /foo/, /(?:bar)?/, /$/); + + expect(expression).toMatchString("foo"); + expect(expression).toMatchString("foobar"); + }); + + it("should accept numbers", () => { + const expression = VerEx(3.14159); + + expect(expression).toMatchString("3.14159"); + expect(expression).toMatchString("33.14159%"); + + expect(expression).not.toMatchString("3014159"); + }); + }); + + describe("VerEx(flags, ...expressions)", () => { + describe("defaults", () => { + it("should should set gm flags by default", () => { + const exp = VerEx("foo"); + expect(exp.flags).toEqual("gm"); + }); + + it("should work with an empty flags object", () => { + const exp = VerEx({}, "foo"); + expect(exp.flags).toEqual("gm"); + }); + + it("should extend defaults rather than replace them", () => { + const exp = VerEx({ + ignoreCase: true, + multiline: false + }, "foo"); + + expect(exp.flags).toEqual("gi"); + }); + }); + + describe("ignoreCase", () => { + describe("ignoreCase: true", () => { + it("should allow case insensitive matches", () => { + const foo = VerEx({ignoreCase: true}, /^/, "foo", /$/); + + expect(foo).toMatchString("foo"); + expect(foo).toMatchString("FOO"); + expect(foo).toMatchString("Foo"); + }); + }); + + describe("ignoreCase: false", () => { + it("should not allow case insensitive matches", () => { + const foo = VerEx({ignoreCase: false}, /^/, "foo", /$/); + + expect(foo).toMatchString("foo"); + expect(foo).not.toMatchString("FOO"); + expect(foo).not.toMatchString("Foo"); + }); + }); + }); + + describe("dotAll", () => { + describe("dotAll: true", () => { + it("should allow `.` to match line separators", () => { + const dot = VerEx({dotAll: true}, /^/, /./, /$/); + + expect(dot).toMatchString("\n"); + expect(dot).toMatchString("\r"); + expect(dot).toMatchString("\u2028"); + expect(dot).toMatchString("\u2029"); + }); + }); + + describe("dotAll: false", () => { + it("should not allow `.` to match line separators", () => { + const dot = VerEx({dotAll: false}, /^/, /./, /$/); + + expect(dot).not.toMatchString("\n"); + expect(dot).not.toMatchString("\r"); + expect(dot).not.toMatchString("\u2028"); + expect(dot).not.toMatchString("\u2029"); + }); + }); + }); + + describe("global", () => { + describe("global: true", () => { + it("should allow matching all occurrences", () => { + const foo = VerEx({global: true}, "foo"); + + // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec + expect("foo foo foo".match(foo)).toEqual(["foo", "foo", "foo"]); + }); + }); + + describe("global: false", () => { + it("should stop matching after first occurrence", () => { + const foo = VerEx({global: false}, "foo"); + + // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec + const result = "foo foo foo".match(foo); + + expect(result.length).toEqual(1); + expect(result[0]).toEqual("foo"); + expect(result.index).toEqual(0); + expect(result.input).toEqual("foo foo foo"); + expect(result.groups).toEqual(undefined); + }); + }); + }); + + describe("multiline", () => { + describe("multiline: true", () => { + it("should make line anchors match start and end of each line", () => { + const fooOnALine = VerEx({multiline: true}, /^/, "foo", /$/); + + expect(fooOnALine).toMatchString("foo"); + expect(fooOnALine).toMatchString("bar\nfoo\nbaz"); + expect(fooOnALine).toMatchString("foo\nbar\nbaz"); + expect(fooOnALine).toMatchString("bar\nbaz\nfoo"); + }); + }); + + describe("multiline: false", () => { + it("should make line anchors match start and end of the string", () => { + const fooOnALine = VerEx({multiline: false}, /^/, "foo", /$/); + + expect(fooOnALine).toMatchString("foo"); + expect(fooOnALine).not.toMatchString("bar\nfoo\nbaz"); + expect(fooOnALine).not.toMatchString("foo\nbar\nbaz"); + expect(fooOnALine).not.toMatchString("bar\nbaz\nfoo"); + }); + }); + }); + + describe("sticky", () => { + describe("sticky: true", () => { + it("should begin matching from lastIndex", () => { + const foo = VerEx({sticky: true}, "foo"); + + expect(foo).not.toMatchString("## foo ##"); + + foo.lastIndex = 3; + expect(foo).toMatchString("## foo ##"); + + foo.lastIndex = 5; + expect(foo).not.toMatchString("## foo ##"); + }); + + it("should should override the global flag", () => { + const foo = VerEx( + {global: true, sticky: true}, + "foo" + ); + + const matches = foo.exec("foo foo foo"); + + expect(matches).not.toEqual(["foo", "foo", "foo"]); + expect(matches.length).toEqual(1); + expect(matches[0]).toEqual("foo"); + expect(matches.index).toEqual(0); + expect(matches.input).toEqual("foo foo foo"); + expect(matches.groups).toEqual(undefined); + + foo.lastIndex = 3; + expect(foo).toMatchString("## foo ##"); + expect(foo).not.toMatchString("## foo ##"); + }); + }); + + describe("sticky: false", () => { + it("should begin matching from the beginning of the string", () => { + const foo = VerEx({sticky: false}, "foo"); + + expect(foo).toMatchString("## foo ##"); + expect(foo).toMatchString("## foo ##"); + expect(foo).toMatchString("## foo ##"); + }); + }); + }); + + describe("unicode", () => { + describe("unicode: true", () => { + it("should allow `.` to match astral symbols", () => { + const dot = VerEx({unicode: true}, /^/, /./, /$/); + + expect(dot).toMatchString("𝌆"); + }); + + it("should make quantifiers to apply to astral symbols", () => { + const oneOrMore = VerEx({unicode: true}, /^/, /𝌆+/, /$/); + const zeroOrMore = VerEx({unicode: true}, /^/, /𝌆*/, /$/); + const three = VerEx({unicode: true}, /^/, /𝌆{3}/, /$/); + const twoOrMore = VerEx({unicode: true}, /^/, /𝌆{2,}/, /$/); + const twoToFour = VerEx({unicode: true}, /^/, /𝌆{2,4}/, /$/); + + expect(oneOrMore).toMatchString("𝌆𝌆𝌆"); + expect(zeroOrMore).toMatchString("𝌆𝌆𝌆"); + expect(three).toMatchString("𝌆𝌆𝌆"); + expect(twoOrMore).toMatchString("𝌆𝌆𝌆"); + expect(twoToFour).toMatchString("𝌆𝌆𝌆"); + }); + + it("should allow character classes to match astral symbols", () => { + const tetragram = VerEx({unicode: true}, /^/, /[𝌆]/u, /$/); + expect(tetragram).toMatchString("𝌆"); + + const emojiRange = VerEx({unicode: true}, /^/, /[🂡-🃞]/u, /$/); + expect(emojiRange).not.toMatchString("⚂"); + expect(emojiRange).toMatchString("🂡"); + expect(emojiRange).toMatchString("🂼"); + expect(emojiRange).toMatchString("🃄"); + expect(emojiRange).toMatchString("🃞"); + expect(emojiRange).not.toMatchString("🥳"); + }); + + it("should allow `\\D`, `\\S`, `\\W` to match astral symbols", () => { + const notDigit = VerEx({unicode: true}, /^/, /\D/, /$/); + const notWhitespace = VerEx({unicode: true}, /^/, /\S/, /$/); + const notWordCharacter = VerEx({unicode: true}, /^/, /\W/, /$/); + + expect(notDigit).toMatchString("𝌆"); + expect(notWhitespace).toMatchString("𝌆"); + expect(notWordCharacter).toMatchString("𝌆"); + }); + + it("should permit unicode property escapes", () => { + const greek = VerEx( + {unicode: true}, + /^/, /\p{Script_Extensions=Greek}/u, /$/ + ); + + expect(greek).toMatchString("π"); + expect(greek).toMatchString("Ω"); + + expect(greek).not.toMatchString("Z"); + + const number = VerEx( + {unicode: true}, + /^/, /\p{Number}/u, /$/ + ); + + expect(number).toMatchString("3"); + expect(number).toMatchString("³"); + expect(number).toMatchString("𝟑"); + expect(number).toMatchString("𝟯"); + expect(number).toMatchString("Ⅲ"); + + expect(number).not.toMatchString("three"); + expect(number).not.toMatchString("t"); + }); + + it("should allow codepoint escapes", () => { + const tetragram = VerEx({unicode: true}, /^/, /\u{1D306}/u, /$/); + expect(tetragram).toMatchString("𝌆"); + }); + }); + + describe("unicode: false", () => { + it("should not allow `.` to match astral symbols", () => { + const dot = VerEx({unicode: false}, /^/, /./, /$/); + + expect(dot).not.toMatchString("𝌆"); + }); + + it("should not allow quantifiers to apply to astral symbols", () => { + const oneOrMore = VerEx({unicode: false}, /^/, /𝌆+/, /$/); + const zeroOrMore = VerEx({unicode: false}, /^/, /𝌆*/, /$/); + const three = VerEx({unicode: false}, /^/, /𝌆{3}/, /$/); + const twoOrMore = VerEx({unicode: false}, /^/, /𝌆{2,}/, /$/); + const twoToFour = VerEx({unicode: false}, /^/, /𝌆{2,4}/, /$/); + + expect(oneOrMore).not.toMatchString("𝌆𝌆𝌆"); + expect(zeroOrMore).not.toMatchString("𝌆𝌆𝌆"); + expect(three).not.toMatchString("𝌆𝌆𝌆"); + expect(twoOrMore).not.toMatchString("𝌆𝌆𝌆"); + expect(twoToFour).not.toMatchString("𝌆𝌆𝌆"); + }); + + it("should not allow character classes to match astral symbols", () => { + const tetragram = VerEx({unicode: false}, /^/, /[𝌆]/u, /$/); + expect(tetragram).not.toMatchString("𝌆"); + expect(tetragram).toMatchString("\uD834"); // Low surrogate + expect(tetragram).toMatchString("\uDF06"); // High surrogate + + expect(() => { + VerEx({unicode: false}, /^/, /[🂡-🃞]/u, /$/); + }).toThrow(SyntaxError); + }); + + it("should not allow `\\D`, `\\S`, `\\W` to match astral symbols", () => { + const notDigit = VerEx({unicode: false}, /^/, /\D/, /$/); + const notWhitespace = VerEx({unicode: false}, /^/, /\S/, /$/); + const notWordCharacter = VerEx({unicode: false}, /^/, /\W/, /$/); + + expect(notDigit).not.toMatchString("𝌆"); + expect(notWhitespace).not.toMatchString("𝌆"); + expect(notWordCharacter).not.toMatchString("𝌆"); + }); + + it("should not permit unicode property escapes", () => { + const greek = VerEx( + {unicode: false}, + /^/, /\p{Script_Extensions=Greek}/u, /$/ + ); + + expect(greek).not.toMatchString("π"); + expect(greek).not.toMatchString("Ω"); + expect(greek).not.toMatchString("Z"); + + const number = VerEx( + {unicode: false}, + /^/, /\p{Number}/u, /$/ + ); + + expect(number).not.toMatchString("3"); + expect(number).not.toMatchString("³"); + expect(number).not.toMatchString("𝟑"); + expect(number).not.toMatchString("𝟯"); + expect(number).not.toMatchString("Ⅲ"); + expect(number).not.toMatchString("three"); + expect(number).not.toMatchString("t"); + }); + + it("should not allow codepoint escapes", () => { + const tetragram = VerEx({unicode: false}, /\u{1D306}/u); + expect(tetragram).not.toMatchString("𝌆"); + + const tetragramSurrogates = VerEx( + {unicode: false}, + /^/, /\uD834\uDF06/, /$/ + ); + + expect(tetragramSurrogates).toMatchString("𝌆"); + }); + }); + }); + }); + + describe("VerEx.extend", () => { + const VerExIM = VerEx.extend({ + ignoreCase: true, + multiline: true + }); + + const exp = VerExIM("foo"); + + expect(exp.global).toBe(true); + expect(exp.ignoreCase).toBe(true); + expect(exp.multiline).toBe(true); + expect(exp.dotAll).toBe(false); + expect(exp.unicode).toBe(false); + expect(exp.sticky).toBe(false); + }); +}); + +describe("VerExp", () => { + it("should be an alias for VerEx", () => { + expect(VerExp).toEqual(VerEx); + }); +}); diff --git a/test/wildcards.test.ts b/test/wildcards.test.ts new file mode 100644 index 00000000..d27ae8cc --- /dev/null +++ b/test/wildcards.test.ts @@ -0,0 +1,58 @@ +import {anyCharacter, anything, something} from "../src/wildcards"; +import {VerEx} from "../src/verex"; +import "./custom-matchers"; + +describe("anyCharacter", () => { + const aCharacter = VerEx(/^/, anyCharacter, /$/); + + it("should match any character", () => { + expect(aCharacter).toMatchString("a"); + expect(aCharacter).toMatchString("1"); + expect(aCharacter).toMatchString("%"); + expect(aCharacter).toMatchString("ℳ"); + expect(aCharacter).toMatchString("µ"); + }); + + it("should not match line terminators", () => { + expect(aCharacter).not.toMatchString("\n"); + expect(aCharacter).not.toMatchString("\r"); + expect(aCharacter).not.toMatchString("\u2028"); + expect(aCharacter).not.toMatchString("\u2029"); + }); + + it("should not match more than one character", () => { + expect(aCharacter).not.toMatchString("abc"); + }); +}); + +describe("anything", () => { + it("should match a non-empty string", () => { + expect(VerEx(anything)).toMatchString("foobar"); + }); + + it("should match an empty string", () => { + expect(VerEx(anything)).toMatchString(""); + expect(VerEx("foo", anything, "bar")).toMatchString("foobar"); + }); + + it("should be usable in conjunction with other arguments", () => { + expect(VerEx("foo", anything)).toMatchString("foobar"); + expect(VerEx("foo", anything)).not.toMatchString("bar"); + }); +}); + +describe("something", () => { + it("should match a non-empty string", () => { + expect(VerEx(something)).toMatchString("foobar"); + }); + + it("should not match an empty string", () => { + expect(VerEx(something)).not.toMatchString(""); + expect(VerEx("foo", something, "bar")).not.toMatchString("foobar"); + }); + + it("should be usable in conjunction with other arguments", () => { + expect(VerEx("foo", something)).toMatchString("foobar"); + expect(VerEx("foo", something)).not.toMatchString("bar"); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..8ab9c2c6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationDir": "dist", + "esModuleInterop": true, + "lib": [ + "dom", + "es2017", + "es2018" + ] + }, + "include": ["src", "test", "./*.js"] +} diff --git a/typings/VerbalExpressions.d.ts b/typings/VerbalExpressions.d.ts deleted file mode 100644 index 2346d932..00000000 --- a/typings/VerbalExpressions.d.ts +++ /dev/null @@ -1,101 +0,0 @@ -// Type definitions for JSVerbalExpressions -// Project: https://github.com/VerbalExpressions/JSVerbalExpressions -// Definitions by: Mihai Ionut Vilcu