From 6febba6aacb26458749bb79bbc38b53513a7286b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 11 Dec 2023 22:36:50 +0100 Subject: [PATCH 1/6] feat: anyOf component --- src/character-classes.ts | 26 ------------------- .../__tests__/any-of.test.ts | 22 ++++++++++++++++ .../__tests__/base.test.ts} | 6 ++--- src/character-classes/any-of.ts | 9 +++++++ src/character-classes/base.ts | 21 +++++++++++++++ src/character-classes/compiler.ts | 24 +++++++++++++++++ src/compiler.ts | 6 ++--- src/index.ts | 4 ++- src/index.tsx | 5 ++++ src/quantifiers/base.ts | 12 ++++----- src/types.ts | 11 +++----- src/utils.ts | 2 ++ 12 files changed, 102 insertions(+), 46 deletions(-) delete mode 100644 src/character-classes.ts create mode 100644 src/character-classes/__tests__/any-of.test.ts rename src/{__tests__/characterClasses.test.tsx => character-classes/__tests__/base.test.ts} (85%) create mode 100644 src/character-classes/any-of.ts create mode 100644 src/character-classes/base.ts create mode 100644 src/character-classes/compiler.ts create mode 100644 src/index.tsx diff --git a/src/character-classes.ts b/src/character-classes.ts deleted file mode 100644 index 9a25c83..0000000 --- a/src/character-classes.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { - Any, - CharacterClass, - Digit, - RegexElement, - Whitespace, - Word, -} from './types'; - -export const whitespace: Whitespace = { type: 'whitespace' }; -export const digit: Digit = { type: 'digit' }; -export const word: Word = { type: 'word' }; -export const any: Any = { type: 'any' }; - -export const characterClasses = { - whitespace: '\\s', - digit: '\\d', - word: '\\w', - any: '.', -} as const satisfies Record; - -export function isCharacterClass( - element: Exclude -): element is CharacterClass { - return element.type in characterClasses; -} diff --git a/src/character-classes/__tests__/any-of.test.ts b/src/character-classes/__tests__/any-of.test.ts new file mode 100644 index 0000000..a88ec2b --- /dev/null +++ b/src/character-classes/__tests__/any-of.test.ts @@ -0,0 +1,22 @@ +import { buildPattern as p } from '../../compiler'; +import { oneOrMore } from '../../quantifiers/base'; +import { anyOf } from '../any-of'; + +test('"anyOf" base cases', () => { + expect(p(anyOf('a'))).toBe('a'); + expect(p(anyOf('abc'))).toBe('[abc]'); +}); + +test('"anyOf" in context', () => { + expect(p('x', anyOf('a'), 'x')).toBe('xax'); + expect(p('x', anyOf('abc'), 'x')).toBe('x[abc]x'); + expect(p('x', oneOrMore(anyOf('abc')), 'x')).toBe('x(?:[abc])+x'); +}); + +test('"anyOf" escapes special charactes', () => { + expect(p(anyOf('abc-+.'))).toBe('[-abc\\+\\.]'); +}); + +test('"anyOf" moves hypen to the first position', () => { + expect(p(anyOf('a-bc'))).toBe('[-abc]'); +}); diff --git a/src/__tests__/characterClasses.test.tsx b/src/character-classes/__tests__/base.test.ts similarity index 85% rename from src/__tests__/characterClasses.test.tsx rename to src/character-classes/__tests__/base.test.ts index 6081cf5..0638662 100644 --- a/src/__tests__/characterClasses.test.tsx +++ b/src/character-classes/__tests__/base.test.ts @@ -1,6 +1,6 @@ -import { any, digit, whitespace, word } from '../character-classes'; -import { buildPattern } from '../compiler'; -import { one } from '../quantifiers/base'; +import { any, digit, whitespace, word } from '../base'; +import { buildPattern } from '../../compiler'; +import { one } from '../../quantifiers/base'; test('"whitespace" character class', () => { expect(buildPattern(whitespace)).toEqual(`\\s`); diff --git a/src/character-classes/any-of.ts b/src/character-classes/any-of.ts new file mode 100644 index 0000000..6776b38 --- /dev/null +++ b/src/character-classes/any-of.ts @@ -0,0 +1,9 @@ +import type { CharacterClass } from '../types'; +import { escapeText } from '../utils'; + +export function anyOf(characters: string): CharacterClass { + return { + type: 'characterClass', + characters: characters.split('').map(escapeText), + }; +} diff --git a/src/character-classes/base.ts b/src/character-classes/base.ts new file mode 100644 index 0000000..6042cff --- /dev/null +++ b/src/character-classes/base.ts @@ -0,0 +1,21 @@ +import type { CharacterClass } from '../types'; + +export const whitespace: CharacterClass = { + type: 'characterClass', + characters: ['\\s'], +}; + +export const digit: CharacterClass = { + type: 'characterClass', + characters: ['\\d'], +}; + +export const word: CharacterClass = { + type: 'characterClass', + characters: ['\\w'], +}; + +export const any: CharacterClass = { + type: 'characterClass', + characters: ['.'], +}; diff --git a/src/character-classes/compiler.ts b/src/character-classes/compiler.ts new file mode 100644 index 0000000..5c1b667 --- /dev/null +++ b/src/character-classes/compiler.ts @@ -0,0 +1,24 @@ +import type { CharacterClass } from '../types'; + +export function compileCharacterClass({ characters }: CharacterClass): string { + if (characters.length === 0) { + return ''; + } + + if (characters.length === 1) { + return characters[0] ?? ''; + } + + return `[${escapeHypen(characters).join('')}]`; +} + +// If passed characters includes hypen (`-`) it need to be moved to +// first (or last) place in order to treat it as hypen character and not a range. +// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Character_classes#types +function escapeHypen(characters: string[]) { + if (characters.includes('-')) { + return ['-', ...characters.filter((c) => c !== '-')]; + } + + return characters; +} diff --git a/src/compiler.ts b/src/compiler.ts index 5775166..c3cbf82 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -1,6 +1,6 @@ import type { RegexElement } from './types'; -import { characterClasses, isCharacterClass } from './character-classes'; import { compileChoiceOf } from './components/choiceOf'; +import { compileCharacterClass } from './character-classes/compiler'; import { baseQuantifiers, isBaseQuantifier } from './quantifiers/base'; import { compileRepeat } from './quantifiers/repeat'; import { escapeText } from './utils'; @@ -36,8 +36,8 @@ function compileSingle(element: RegexElement): string { return escapeText(element); } - if (isCharacterClass(element)) { - return characterClasses[element.type]; + if (element.type === 'characterClass') { + return compileCharacterClass(element); } if (element.type === 'choiceOf') { diff --git a/src/index.ts b/src/index.ts index bf99e6a..9e536e0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,9 @@ export type * from './types'; -export { any, digit, whitespace, word } from './character-classes'; export { buildRegex, buildPattern } from './compiler'; + +export { any, digit, whitespace, word } from './character-classes/base'; +export { anyOf } from './character-classes/any-of'; export { one, oneOrMore, optionally, zeroOrMore } from './quantifiers/base'; export { repeat } from './quantifiers/repeat'; export { choiceOf } from './components/choiceOf'; diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 0000000..abc20f1 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,5 @@ +export type * from './types'; + +export { whitespace } from './character-classes/base'; +export { buildRegex, buildPattern } from './compiler'; +export { oneOrMore, optionally } from './quantifiers/base'; diff --git a/src/quantifiers/base.ts b/src/quantifiers/base.ts index 0564322..7f12221 100644 --- a/src/quantifiers/base.ts +++ b/src/quantifiers/base.ts @@ -8,23 +8,23 @@ import type { } from '../types'; import { wrapGroup } from '../utils'; -export function oneOrMore(...children: RegexElement[]): OneOrMore { +export function one(...children: RegexElement[]): One { return { - type: 'oneOrMore', + type: 'one', children, }; } -export function optionally(...children: RegexElement[]): Optionally { +export function oneOrMore(...children: RegexElement[]): OneOrMore { return { - type: 'optionally', + type: 'oneOrMore', children, }; } -export function one(...children: RegexElement[]): One { +export function optionally(...children: RegexElement[]): Optionally { return { - type: 'one', + type: 'optionally', children, }; } diff --git a/src/types.ts b/src/types.ts index ee4beaa..32b4ed6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,11 @@ export type RegexElement = string | ChoiceOf | CharacterClass | Quantifier; -export type CharacterClass = Whitespace | Digit | Word | Any; - export type Quantifier = One | OneOrMore | Optionally | ZeroOrMore | Repeat; -// Character classes -export type Whitespace = { type: 'whitespace' }; -export type Digit = { type: 'digit' }; -export type Word = { type: 'word' }; -export type Any = { type: 'any' }; +export type CharacterClass = { + type: 'characterClass'; + characters: string[]; +}; // Components export type ChoiceOf = { diff --git a/src/utils.ts b/src/utils.ts index 6b48f1f..017aac8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import type { RegexElement } from './types'; + /** * Wraps regex string in a non-capturing group if it is more than one character long. * From 7711299765626b2f7d15f91b72029970bcc8d2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 11 Dec 2023 22:41:56 +0100 Subject: [PATCH 2/6] refactor: self code review --- src/utils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 017aac8..6b48f1f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,3 @@ -import type { RegexElement } from './types'; - /** * Wraps regex string in a non-capturing group if it is more than one character long. * From 43007de63fb3f5dad35c4c1f383bc89359a2ac68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 11 Dec 2023 22:50:30 +0100 Subject: [PATCH 3/6] refactor: improve code cov --- src/character-classes/__tests__/any-of.test.ts | 1 + src/character-classes/compiler.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/character-classes/__tests__/any-of.test.ts b/src/character-classes/__tests__/any-of.test.ts index a88ec2b..eb67598 100644 --- a/src/character-classes/__tests__/any-of.test.ts +++ b/src/character-classes/__tests__/any-of.test.ts @@ -3,6 +3,7 @@ import { oneOrMore } from '../../quantifiers/base'; import { anyOf } from '../any-of'; test('"anyOf" base cases', () => { + expect(p(anyOf(''))).toBe(''); expect(p(anyOf('a'))).toBe('a'); expect(p(anyOf('abc'))).toBe('[abc]'); }); diff --git a/src/character-classes/compiler.ts b/src/character-classes/compiler.ts index 5c1b667..89850ec 100644 --- a/src/character-classes/compiler.ts +++ b/src/character-classes/compiler.ts @@ -6,7 +6,7 @@ export function compileCharacterClass({ characters }: CharacterClass): string { } if (characters.length === 1) { - return characters[0] ?? ''; + return characters[0]!; } return `[${escapeHypen(characters).join('')}]`; From 85319940358b402254eb509b464633be1765d443 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 12 Dec 2023 14:12:37 +0100 Subject: [PATCH 4/6] refactor: colocate test files --- .../__tests__/base.test.tsx} | 4 ++-- src/{ => quantifiers}/__tests__/repeat.test.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename src/{__tests__/quantifiers.test.tsx => quantifiers/__tests__/base.test.tsx} (91%) rename src/{ => quantifiers}/__tests__/repeat.test.tsx (75%) diff --git a/src/__tests__/quantifiers.test.tsx b/src/quantifiers/__tests__/base.test.tsx similarity index 91% rename from src/__tests__/quantifiers.test.tsx rename to src/quantifiers/__tests__/base.test.tsx index 2788ae1..05cd750 100644 --- a/src/__tests__/quantifiers.test.tsx +++ b/src/quantifiers/__tests__/base.test.tsx @@ -1,5 +1,5 @@ -import { one, oneOrMore, optionally, zeroOrMore } from '../quantifiers/base'; -import { buildPattern, buildRegex } from '../compiler'; +import { one, oneOrMore, optionally, zeroOrMore } from '../base'; +import { buildPattern, buildRegex } from '../../compiler'; test('"oneOrMore" quantifier', () => { expect(buildPattern(oneOrMore('a'))).toEqual('a+'); diff --git a/src/__tests__/repeat.test.tsx b/src/quantifiers/__tests__/repeat.test.tsx similarity index 75% rename from src/__tests__/repeat.test.tsx rename to src/quantifiers/__tests__/repeat.test.tsx index 2305ea8..d0015c7 100644 --- a/src/__tests__/repeat.test.tsx +++ b/src/quantifiers/__tests__/repeat.test.tsx @@ -1,6 +1,6 @@ -import { buildPattern } from '../compiler'; -import { zeroOrMore, oneOrMore } from '../quantifiers/base'; -import { repeat } from '../quantifiers/repeat'; +import { buildPattern } from '../../compiler'; +import { zeroOrMore, oneOrMore } from '../base'; +import { repeat } from '../repeat'; test('"repeat" quantifier', () => { expect(buildPattern('a', repeat({ min: 1, max: 5 }, 'b'))).toEqual('ab{1,5}'); From 2e12801dc1dd01826dbfc4868778d33af787a4e7 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 12 Dec 2023 14:15:08 +0100 Subject: [PATCH 5/6] chore: fix typos --- src/character-classes/__tests__/any-of.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/character-classes/__tests__/any-of.test.ts b/src/character-classes/__tests__/any-of.test.ts index eb67598..0876929 100644 --- a/src/character-classes/__tests__/any-of.test.ts +++ b/src/character-classes/__tests__/any-of.test.ts @@ -14,10 +14,10 @@ test('"anyOf" in context', () => { expect(p('x', oneOrMore(anyOf('abc')), 'x')).toBe('x(?:[abc])+x'); }); -test('"anyOf" escapes special charactes', () => { +test('"anyOf" escapes special characters', () => { expect(p(anyOf('abc-+.'))).toBe('[-abc\\+\\.]'); }); -test('"anyOf" moves hypen to the first position', () => { +test('"anyOf" moves hyphen to the first position', () => { expect(p(anyOf('a-bc'))).toBe('[-abc]'); }); From d9212a63651208aeb02095c8f2453c1019a4d826 Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Tue, 12 Dec 2023 18:22:38 +0100 Subject: [PATCH 6/6] chore: fix typos --- src/character-classes/compiler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/character-classes/compiler.ts b/src/character-classes/compiler.ts index 89850ec..5ce0a46 100644 --- a/src/character-classes/compiler.ts +++ b/src/character-classes/compiler.ts @@ -9,13 +9,13 @@ export function compileCharacterClass({ characters }: CharacterClass): string { return characters[0]!; } - return `[${escapeHypen(characters).join('')}]`; + return `[${escapeHyphen(characters).join('')}]`; } -// If passed characters includes hypen (`-`) it need to be moved to -// first (or last) place in order to treat it as hypen character and not a range. +// If passed characters includes hyphen (`-`) it need to be moved to +// first (or last) place in order to treat it as hyphen character and not a range. // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Character_classes#types -function escapeHypen(characters: string[]) { +function escapeHyphen(characters: string[]) { if (characters.includes('-')) { return ['-', ...characters.filter((c) => c !== '-')]; }