cucumber-expressions
Version:
Cucumber Expressions - a simpler alternative to Regular Expressions
131 lines (113 loc) • 3.95 kB
text/typescript
import ParameterTypeRegistry from './ParameterTypeRegistry'
import ParameterType from './ParameterType'
import TreeRegexp from './TreeRegexp'
import Argument from './Argument'
import { CucumberExpressionError, UndefinedParameterTypeError } from './Errors'
import Expression from './Expression'
// RegExps with the g flag are stateful in JavaScript. In order to be able
// to reuse them we have to wrap them in a function.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test
// Does not include (){} characters because they have special meaning
const ESCAPE_REGEXP = () => /([\\^[$.|?*+])/g
const PARAMETER_REGEXP = () => /(\\\\)?{([^}]*)}/g
const OPTIONAL_REGEXP = () => /(\\\\)?\(([^)]+)\)/g
const ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP = () =>
/([^\s^/]+)((\/[^\s^/]+)+)/g
const DOUBLE_ESCAPE = '\\\\'
const PARAMETER_TYPES_CANNOT_BE_ALTERNATIVE =
'Parameter types cannot be alternative: '
const PARAMETER_TYPES_CANNOT_BE_OPTIONAL =
'Parameter types cannot be optional: '
export default class CucumberExpression implements Expression {
private parameterTypes: Array<ParameterType<any>> = []
private treeRegexp: TreeRegexp
/**
* @param expression
* @param parameterTypeRegistry
*/
constructor(
private readonly expression: string,
private readonly parameterTypeRegistry: ParameterTypeRegistry
) {
let expr = this.processEscapes(expression)
expr = this.processOptional(expr)
expr = this.processAlternation(expr)
expr = this.processParameters(expr, parameterTypeRegistry)
expr = `^${expr}$`
this.treeRegexp = new TreeRegexp(expr)
}
private processEscapes(expression: string) {
return expression.replace(ESCAPE_REGEXP(), '\\$1')
}
private processOptional(expression: string) {
return expression.replace(OPTIONAL_REGEXP(), (match, p1, p2) => {
if (p1 === DOUBLE_ESCAPE) {
return `\\(${p2}\\)`
}
this.checkNoParameterType(p2, PARAMETER_TYPES_CANNOT_BE_OPTIONAL)
return `(?:${p2})?`
})
}
private processAlternation(expression: string) {
return expression.replace(
ALTERNATIVE_NON_WHITESPACE_TEXT_REGEXP(),
match => {
// replace \/ with /
// replace / with |
const replacement = match.replace(/\//g, '|').replace(/\\\|/g, '/')
if (replacement.indexOf('|') !== -1) {
for (const part of replacement.split(/\|/)) {
this.checkNoParameterType(
part,
PARAMETER_TYPES_CANNOT_BE_ALTERNATIVE
)
}
return `(?:${replacement})`
} else {
return replacement
}
}
)
}
private processParameters(
expression: string,
parameterTypeRegistry: ParameterTypeRegistry
) {
return expression.replace(PARAMETER_REGEXP(), (match, p1, p2) => {
if (p1 === DOUBLE_ESCAPE) {
return `\\{${p2}\\}`
}
const typeName = p2
ParameterType.checkParameterTypeName(typeName)
const parameterType = parameterTypeRegistry.lookupByTypeName(typeName)
if (!parameterType) {
throw new UndefinedParameterTypeError(typeName)
}
this.parameterTypes.push(parameterType)
return buildCaptureRegexp(parameterType.regexpStrings)
})
}
public match(text: string): Array<Argument<any>> {
return Argument.build(this.treeRegexp, text, this.parameterTypes)
}
get regexp(): RegExp {
return this.treeRegexp.regexp
}
get source() {
return this.expression
}
private checkNoParameterType(s: string, message: string) {
if (s.match(PARAMETER_REGEXP())) {
throw new CucumberExpressionError(message + this.source)
}
}
}
function buildCaptureRegexp(regexps: string[]) {
if (regexps.length === 1) {
return `(${regexps[0]})`
}
const captureGroups = regexps.map(group => {
return `(?:${group})`
})
return `(${captureGroups.join('|')})`
}