diff --git a/__tests__/constants/anything.test.ts b/__tests__/constants/anything.test.ts index cad4e7d..917f591 100644 --- a/__tests__/constants/anything.test.ts +++ b/__tests__/constants/anything.test.ts @@ -1,5 +1,5 @@ import Constants from '../../src/constants'; -import { formatInternalExpression } from '../../src/utils'; +import { formatInternalExpression } from '../../src/helpers'; /** * anything constant test diff --git a/__tests__/constants/newline.test.ts b/__tests__/constants/newline.test.ts index f8703c1..40b7e60 100644 --- a/__tests__/constants/newline.test.ts +++ b/__tests__/constants/newline.test.ts @@ -1,5 +1,5 @@ import Constants from '../../src/constants'; -import { formatInternalExpression } from '../../src/utils'; +import { formatInternalExpression } from '../../src/helpers'; /** * newline constant test diff --git a/__tests__/constants/number.test.ts b/__tests__/constants/number.test.ts index 61b04cd..a2cb8b7 100644 --- a/__tests__/constants/number.test.ts +++ b/__tests__/constants/number.test.ts @@ -1,5 +1,5 @@ import Constants from '../../src/constants'; -import { formatInternalExpression } from '../../src/utils'; +import { formatInternalExpression } from '../../src/helpers'; /** * number constant test diff --git a/__tests__/constants/symbol.test.ts b/__tests__/constants/symbol.test.ts index 3c760d3..cb733ee 100644 --- a/__tests__/constants/symbol.test.ts +++ b/__tests__/constants/symbol.test.ts @@ -1,5 +1,5 @@ import Constants from '../../src/constants'; -import { formatInternalExpression } from '../../src/utils'; +import { formatInternalExpression } from '../../src/helpers'; /** * symbol constant test diff --git a/__tests__/constants/whitespace.test.ts b/__tests__/constants/whitespace.test.ts index a0340e7..44a06ea 100644 --- a/__tests__/constants/whitespace.test.ts +++ b/__tests__/constants/whitespace.test.ts @@ -1,5 +1,5 @@ import Constants from '../../src/constants'; -import { formatInternalExpression } from '../../src/utils'; +import { formatInternalExpression } from '../../src/helpers'; /** * whitespace constant test diff --git a/__tests__/constants/word.test.ts b/__tests__/constants/word.test.ts index 6057eca..a7bb368 100644 --- a/__tests__/constants/word.test.ts +++ b/__tests__/constants/word.test.ts @@ -1,5 +1,5 @@ import Constants from '../../src/constants'; -import { formatInternalExpression } from '../../src/utils'; +import { formatInternalExpression } from '../../src/helpers'; /** * word constant test diff --git a/__tests__/sequences/anything.test.ts b/__tests__/sequences/anything.test.ts index 56ce96f..22f9762 100644 --- a/__tests__/sequences/anything.test.ts +++ b/__tests__/sequences/anything.test.ts @@ -1,6 +1,6 @@ import Sequences from '../../src/sequences'; import Constants from '../../src/constants'; -import { formatInternalExpression } from '../../src/utils'; +import { formatInternalExpression } from '../../src/helpers'; /** * anything sequence test diff --git a/__tests__/sequences/newlines.test.ts b/__tests__/sequences/newlines.test.ts index a000b09..dbebd5e 100644 --- a/__tests__/sequences/newlines.test.ts +++ b/__tests__/sequences/newlines.test.ts @@ -1,6 +1,6 @@ import Sequences from '../../src/sequences'; import Constants from '../../src/constants'; -import { formatInternalExpression } from '../../src/utils'; +import { formatInternalExpression } from '../../src/helpers'; /** * newlines sequence test diff --git a/__tests__/sequences/numbers.test.ts b/__tests__/sequences/numbers.test.ts index d38369e..1a23c43 100644 --- a/__tests__/sequences/numbers.test.ts +++ b/__tests__/sequences/numbers.test.ts @@ -1,6 +1,6 @@ import Sequences from '../../src/sequences'; import Constants from '../../src/constants'; -import { formatInternalExpression } from '../../src/utils'; +import { formatInternalExpression } from '../../src/helpers'; /** * numbers sequence test diff --git a/__tests__/sequences/symbols.test.ts b/__tests__/sequences/symbols.test.ts index c94472d..832ac90 100644 --- a/__tests__/sequences/symbols.test.ts +++ b/__tests__/sequences/symbols.test.ts @@ -1,6 +1,6 @@ import Sequences from '../../src/sequences'; import Constants from '../../src/constants'; -import { formatInternalExpression } from '../../src/utils'; +import { formatInternalExpression } from '../../src/helpers'; /** * symbols sequence test diff --git a/__tests__/sequences/whitespace.test.ts b/__tests__/sequences/whitespace.test.ts index d62e0a6..8763d4b 100644 --- a/__tests__/sequences/whitespace.test.ts +++ b/__tests__/sequences/whitespace.test.ts @@ -1,6 +1,6 @@ import Sequences from '../../src/sequences'; import Constants from '../../src/constants'; -import { formatInternalExpression } from '../../src/utils'; +import { formatInternalExpression } from '../../src/helpers'; /** * whitespace sequence test diff --git a/__tests__/utils/utils.test.ts b/__tests__/utils/utils.test.ts index 6e4f423..e77c974 100644 --- a/__tests__/utils/utils.test.ts +++ b/__tests__/utils/utils.test.ts @@ -1,16 +1,15 @@ import Sequences from '../../src/sequences'; import Constants from '../../src/constants'; +import { or, anyOf } from '../../src/utils'; import { escapeString, matches, wrapOptionalExpression, wrapOrExpression, validateExpression, - or, validateFlags, getGroupsByIndex, - anyOf, -} from '../../src/utils'; +} from '../../src/helpers'; /** * beginsWith test diff --git a/package.json b/package.json index d520b91..a84d11d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "exceptional-expressions", - "version": "0.2.1", + "version": "0.2.2", "description": "An incredible way to build efficient, concise and human readable regular expressions.", "keywords": [ "typescript", diff --git a/src/assertions.ts b/src/assertions.ts new file mode 100644 index 0000000..dd795a9 --- /dev/null +++ b/src/assertions.ts @@ -0,0 +1,47 @@ +/** + * Assert that a value passed equals either null or undefined + * + * @param {any} val + * @param {string} message + */ +export const assertDoesntExist: (val: any, message?: string) => asserts val is null | undefined = ( + val: any, + message?: string +): asserts val is null | undefined => { + if (!(val === undefined || val === null)) { + throw new Error(message); + } +}; + +/** + * Asser that the value passed is not null or undefined + * + * @param {T} val + * @param {string} message + */ +export const assertExists: (val: T, message?: string) => asserts val is NonNullable = ( + val: T, + message?: string +): asserts val is NonNullable => { + if (val === undefined || val === null) { + throw new Error(message); + } +}; + +/** + * Assert that at least one of the values passed is not null or undefined + * + * @param {Array} val + * @param {string} message + */ +export const assertOneExists: (val: Array, message?: string) => asserts val is T[] = ( + val: Array, + message?: string +): asserts val is T[] => { + for (const item of val) { + if (item !== undefined && item !== null) { + return; + } + } + throw new Error(message); +}; diff --git a/src/constants.ts b/src/constants.ts index 7ccd2d2..e9d503c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,7 +7,9 @@ export default { number: '~~\\d', symbol: '~~[\\-!\\$%±§#\\^@&*\\(\\)_+|~=`{}\\[\\]:";\'<>\\?,\\.\\/]', word: "~~[A-Za-z']+\\b", - anything: '~~.' + anything: '~~.', + lowercaseLetter: '~~(?:a-z)', + uppercaseLetter: '~~(?:A-Z)', // email: // '~~(([^<>()[]\\.,;:s@"]+(.[^<>()[]\\.,;:s@"]+)*)|(".+"))@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}])|(([a-zA-Z-0-9]+.)+[a-zA-Z]{2,}))' }; diff --git a/src/exceptional-expressions.ts b/src/exceptional-expressions.ts index cefffae..8ee57a3 100644 --- a/src/exceptional-expressions.ts +++ b/src/exceptional-expressions.ts @@ -3,14 +3,14 @@ import { handleOptionalWrapping, validateExpression, wrapOrExpression, - assertExists, - assertOneExists, - assertDoesntExist, validateFlags, extractMatches, extractMatchesWithGroup, IGroupings, -} from './utils'; +} from './helpers'; + +import { assertExists, assertOneExists, assertDoesntExist } from './assertions'; + export default class ExpressionBuilder { private beginsWithExpression: string | null = null; private internal: Array = []; diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..c62b5eb --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,224 @@ +const FLAGS: string[] = ['i', 'g', 'm', 's', 'u', 'y']; + +/** + * Generate a random string of length 4 + */ +export const randomString = (): string => { + return Math.random().toString(36).substring(2, 4) + Math.random().toString(36).substring(2, 4); +}; + +/** + * Validate that flags passed match the valid javascript regex flags + * + * @param {string} flags + * + * @return {string} + */ +export const validateFlags: (flags: string) => string = (flags: string): string => { + return flags.split('').reduce((acc: string, curr: string) => { + if (FLAGS.includes(curr.toLowerCase())) { + return `${acc}${curr.toLowerCase()}`; + } + return acc; + }, ''); +}; + +/** + * Chains an array of expressions into a single OR expression + * + * @param {Array} expressions The expressions to chain + * + * @return {string} + */ +export const chainOrExpression: (expressions: Array) => string = ( + expressions: Array +): string => { + const orChain: string = expressions.reduce( + (accumulator: string, current: string, index: number) => { + if (index === expressions.length - 1) { + return `${accumulator}(?:${validateExpression(current)})`; + } + return `${accumulator}(?:${validateExpression(current)})|`; + }, + '' + ); + + return `~~(?:${orChain})`; +}; + +/** + * Handles remove the internal expression indicator + * + * @param {string} string + * + * @return {string} + */ +export const formatInternalExpression = (string: string): string => { + return string.substring(2); +}; + +/** + * Handle escaping of string characters + * + * @param {string} string The string to be escaped + * + * @return {string} + */ +export const escapeString = (string: string): string => { + return string.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\u002d'); +}; + +/** + * Test the regex against the string, more accurate than regex.test() + * + * @param {string} string The string to test + * @param {RegExp} regex The regex to be tested against + * + * @return {boolean} + */ +export const matches: (string: string, regex: RegExp) => boolean = ( + string: string, + regex: RegExp +): boolean => { + const match: RegExpMatchArray | null = string.match(regex); + + return match && match.length > 0 ? true : false; +}; + +/** + * Extract an array of matches from the string + * + * @param {string} string + * @param {RegExp} regex + * + * @return {Array} + */ +export const extractMatches: (string: string, regex: RegExp) => Array = ( + string: string, + regex: RegExp +): Array => { + return getGroupsByIndex(string, regex, 0); +}; + +export interface IGroupings { + match: string; + groups: IGroup; +} + +interface IGroup { + [key: string]: string; +} + +export const extractMatchesWithGroup = ( + string: string, + regex: RegExp, + groups: Array +): IGroupings[] => { + const extractions: Array[] = extractAllMatches(string, regex); + const matchCount: number = extractions[0].length; + const groupings: IGroupings[] = []; + + for (let i = 0; i < matchCount; i++) { + const group: IGroupings = { + match: extractions[0][i], + groups: {}, + }; + for (let j = 1; j < extractions.length; j++) { + group.groups[groups[j - 1]] = extractions[j][i]; + } + groupings.push(group); + } + + return groupings; +}; + +const extractAllMatches = (string: string, regex: RegExp) => { + const matches: Array = []; + let match: RegExpExecArray | null; + while ((match = regex.exec(string))) { + match.forEach((item, index) => { + matches[index] = matches[index] ? [...matches[index], item] : [item]; + }); + } + return matches; +}; + +export const getGroupsByIndex = ( + string: string, + regex: RegExp, + index: number = 1 +): Array => { + const matches: Array = []; + let match: RegExpExecArray | null; + while ((match = regex.exec(string))) { + matches.push(match[index]); + } + return matches; +}; + +/** + * Helper function for determining whether or not to wrap an expression with an optional statement + * + * @param {string} expression The expresion to return + * @param {boolean} optional Should wrap + * + * @return {string} + */ +export const handleOptionalWrapping: (expression: string, optional: boolean) => string = ( + expression: string, + optional: boolean +): string => { + return optional ? wrapOptionalExpression(expression) : expression; +}; + +/** + * Wraps an expression with an optional non-capturing group + * + * @param {string} expression The expressions to wrap + * + * @return {string} + */ +export const wrapOptionalExpression: (expression: string) => string = ( + expression: string +): string => { + return `(?:${expression})?`; +}; + +/** + * Wraps two expressions with an or operator + * + * @param {string} expression The expressions to wrap + * + * @return {string} + */ +export const wrapOrExpression: (firstExpression: string, secondExpression: string) => string = ( + firstExpression: string, + secondExpression: string +): string => { + return `(?:(?:${firstExpression})|(?:${secondExpression}))`; +}; + +export const isInternalRegex = (string: string): boolean => { + return ( + typeof string === 'string' && + string.length > 2 && + string.charAt(0) === '~' && + string.charAt(1) === '~' + ); +}; + +export const validateExpression = (expression: any): string => { + if (isInternalRegex(expression)) { + return formatInternalExpression(expression); + } else if (typeof expression === 'string') { + return escapeString(expression); + } else if (Array.isArray(expression)) { + return expression.reduce((accumulator, current) => { + return `${accumulator}${validateExpression(current)}`; + }, ''); + } else if (expression && typeof expression.toString !== undefined) { + return escapeString(expression.toString()); + } + + throw new TypeError(`${expression} is not a valid expression`); +}; diff --git a/src/index.ts b/src/index.ts index 359848e..3d0578d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ import ExpBuilder from './exceptional-expressions'; -import { or, anythingBut, group } from './utils'; +import { or, anythingBut, group, anyOf } from './utils'; -export { ExpBuilder, or, anythingBut, group }; +export { ExpBuilder, or, anythingBut, group, anyOf }; diff --git a/src/sequences.ts b/src/sequences.ts index 90c6540..cc929b0 100644 --- a/src/sequences.ts +++ b/src/sequences.ts @@ -1,5 +1,5 @@ import Constants from './constants'; -import { formatInternalExpression } from './utils'; +import { formatInternalExpression } from './helpers'; const validateParameters: (params: ISequenceParams | any) => void = (params: ISequenceParams) => { if (!isSequenceParam(params) && !Number.isInteger(params)) { @@ -134,5 +134,5 @@ export default { } return sequenceByBounds(params, expression, false); - } + }, }; diff --git a/src/utils.ts b/src/utils.ts index 2dc8ee8..e2115ad 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -const FLAGS: string[] = ['i', 'g', 'm', 's', 'u', 'y']; +import { validateExpression, chainOrExpression, randomString } from './helpers'; /** * Groups an array of expressions into an anyOf statement @@ -51,29 +51,6 @@ export const group: (expression: any, group?: string) => string = ( return `~~[|${group}|](${validateExpression(expression)})[|${group}|]`; }; -/** - * Generate a random string of length 4 - */ -const randomString = (): string => { - return Math.random().toString(36).substring(2, 4) + Math.random().toString(36).substring(2, 4); -}; - -/** - * Validate that flags passed match the valid javascript regex flags - * - * @param {string} flags - * - * @return {string} - */ -export const validateFlags: (flags: string) => string = (flags: string): string => { - return flags.split('').reduce((acc: string, curr: string) => { - if (FLAGS.includes(curr.toLowerCase())) { - return `${acc}${curr.toLowerCase()}`; - } - return acc; - }, ''); -}; - /** * Groups an array of expressions into an OR statement * @@ -84,251 +61,3 @@ export const validateFlags: (flags: string) => string = (flags: string): string export const anythingBut: (expression: any) => string = (expression: any): string => { return `~~(?:(?:(?!${validateExpression(expression)}).)*)`; }; - -/** - * Assert that a value passed equals either null or undefined - * - * @param {any} val - * @param {string} message - */ -export const assertDoesntExist: (val: any, message?: string) => asserts val is null | undefined = ( - val: any, - message?: string -): asserts val is null | undefined => { - if (!(val === undefined || val === null)) { - throw new Error(message); - } -}; - -/** - * Asser that the value passed is not null or undefined - * - * @param {T} val - * @param {string} message - */ -export const assertExists: (val: T, message?: string) => asserts val is NonNullable = ( - val: T, - message?: string -): asserts val is NonNullable => { - if (val === undefined || val === null) { - throw new Error(message); - } -}; - -/** - * Assert that at least one of the values passed is not null or undefined - * - * @param {Array} val - * @param {string} message - */ -export const assertOneExists: (val: Array, message?: string) => asserts val is T[] = ( - val: Array, - message?: string -): asserts val is T[] => { - for (const item of val) { - if (item !== undefined && item !== null) { - return; - } - } - throw new Error(message); -}; - -/** - * Chains an array of expressions into a single OR expression - * - * @param {Array} expressions The expressions to chain - * - * @return {string} - */ -const chainOrExpression: (expressions: Array) => string = ( - expressions: Array -): string => { - const orChain: string = expressions.reduce( - (accumulator: string, current: string, index: number) => { - if (index === expressions.length - 1) { - return `${accumulator}(?:${validateExpression(current)})`; - } - return `${accumulator}(?:${validateExpression(current)})|`; - }, - '' - ); - - return `~~(?:${orChain})`; -}; - -/** - * Handles remove the internal expression indicator - * - * @param {string} string - * - * @return {string} - */ -export const formatInternalExpression = (string: string): string => { - return string.substring(2); -}; - -/** - * Handle escaping of string characters - * - * @param {string} string The string to be escaped - * - * @return {string} - */ -export const escapeString = (string: string): string => { - return string.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\u002d'); -}; - -/** - * Test the regex against the string, more accurate than regex.test() - * - * @param {string} string The string to test - * @param {RegExp} regex The regex to be tested against - * - * @return {boolean} - */ -export const matches: (string: string, regex: RegExp) => boolean = ( - string: string, - regex: RegExp -): boolean => { - const match: RegExpMatchArray | null = string.match(regex); - - return match && match.length > 0 ? true : false; -}; - -/** - * Extract an array of matches from the string - * - * @param {string} string - * @param {RegExp} regex - * - * @return {Array} - */ -export const extractMatches: (string: string, regex: RegExp) => Array = ( - string: string, - regex: RegExp -): Array => { - return getGroupsByIndex(string, regex, 0); -}; - -export interface IGroupings { - match: string; - groups: IGroup; -} - -interface IGroup { - [key: string]: string; -} - -export const extractMatchesWithGroup = ( - string: string, - regex: RegExp, - groups: Array -): IGroupings[] => { - const extractions: Array[] = extractAllMatches(string, regex); - const matchCount: number = extractions[0].length; - const groupings: IGroupings[] = []; - - for (let i = 0; i < matchCount; i++) { - const group: IGroupings = { - match: extractions[0][i], - groups: {}, - }; - for (let j = 1; j < extractions.length; j++) { - group.groups[groups[j - 1]] = extractions[j][i]; - } - groupings.push(group); - } - - return groupings; -}; - -const extractAllMatches = (string: string, regex: RegExp) => { - const matches: Array = []; - let match: RegExpExecArray | null; - while ((match = regex.exec(string))) { - match.forEach((item, index) => { - matches[index] = matches[index] ? [...matches[index], item] : [item]; - }); - } - return matches; -}; - -export const getGroupsByIndex = ( - string: string, - regex: RegExp, - index: number = 1 -): Array => { - const matches: Array = []; - let match: RegExpExecArray | null; - while ((match = regex.exec(string))) { - matches.push(match[index]); - } - return matches; -}; - -/** - * Helper function for determining whether or not to wrap an expression with an optional statement - * - * @param {string} expression The expresion to return - * @param {boolean} optional Should wrap - * - * @return {string} - */ -export const handleOptionalWrapping: (expression: string, optional: boolean) => string = ( - expression: string, - optional: boolean -): string => { - return optional ? wrapOptionalExpression(expression) : expression; -}; - -/** - * Wraps an expression with an optional non-capturing group - * - * @param {string} expression The expressions to wrap - * - * @return {string} - */ -export const wrapOptionalExpression: (expression: string) => string = ( - expression: string -): string => { - return `(?:${expression})?`; -}; - -/** - * Wraps two expressions with an or operator - * - * @param {string} expression The expressions to wrap - * - * @return {string} - */ -export const wrapOrExpression: (firstExpression: string, secondExpression: string) => string = ( - firstExpression: string, - secondExpression: string -): string => { - return `(?:(?:${firstExpression})|(?:${secondExpression}))`; -}; - -export const isInternalRegex = (string: string): boolean => { - return ( - typeof string === 'string' && - string.length > 2 && - string.charAt(0) === '~' && - string.charAt(1) === '~' - ); -}; - -export const validateExpression = (expression: any): string => { - if (isInternalRegex(expression)) { - return formatInternalExpression(expression); - } else if (typeof expression === 'string') { - return escapeString(expression); - } else if (Array.isArray(expression)) { - return expression.reduce((accumulator, current) => { - return `${accumulator}${validateExpression(current)}`; - }, ''); - } else if (expression && typeof expression.toString !== undefined) { - return escapeString(expression.toString()); - } - - throw new TypeError(`${expression} is not a valid expression`); -};