@cucumber/gherkin
Version:
Gherkin parser
332 lines (305 loc) • 8.75 kB
text/typescript
import {
type DataTable,
type DocString,
type GherkinDocument,
type IdGenerator,
type Pickle,
type PickleDocString,
type PickleStep,
type PickleStepArgument,
PickleStepType,
type PickleTable,
type PickleTag,
type Rule,
type Scenario,
type Step,
StepKeywordType,
type TableCell,
type TableRow,
type Tag,
} from '@cucumber/messages'
const pickleStepTypeFromKeyword: { [key in StepKeywordType]: PickleStepType } = {
[StepKeywordType.UNKNOWN]: PickleStepType.UNKNOWN,
[StepKeywordType.CONTEXT]: PickleStepType.CONTEXT,
[StepKeywordType.ACTION]: PickleStepType.ACTION,
[StepKeywordType.OUTCOME]: PickleStepType.OUTCOME,
[StepKeywordType.CONJUNCTION]: null,
}
export default function compile(
gherkinDocument: GherkinDocument,
uri: string,
newId: IdGenerator.NewId
): readonly Pickle[] {
const pickles: Pickle[] = []
if (gherkinDocument.feature == null) {
return pickles
}
const feature = gherkinDocument.feature
const language = feature.language
const featureTags = feature.tags
let featureBackgroundSteps: Step[] = []
feature.children.forEach((stepsContainer) => {
if (stepsContainer.background) {
featureBackgroundSteps = [].concat(stepsContainer.background.steps)
} else if (stepsContainer.rule) {
compileRule(
featureTags,
featureBackgroundSteps,
stepsContainer.rule,
language,
pickles,
uri,
newId
)
} else if (stepsContainer.scenario.examples.length === 0) {
compileScenario(
featureTags,
featureBackgroundSteps,
stepsContainer.scenario,
language,
pickles,
uri,
newId
)
} else {
compileScenarioOutline(
featureTags,
featureBackgroundSteps,
stepsContainer.scenario,
language,
pickles,
uri,
newId
)
}
})
return pickles
}
function compileRule(
featureTags: readonly Tag[],
featureBackgroundSteps: readonly Step[],
rule: Rule,
language: string,
pickles: Pickle[],
uri: string,
newId: IdGenerator.NewId
) {
let ruleBackgroundSteps = [].concat(featureBackgroundSteps)
const tags = [].concat(featureTags).concat(rule.tags)
rule.children.forEach((stepsContainer) => {
if (stepsContainer.background) {
ruleBackgroundSteps = ruleBackgroundSteps.concat(stepsContainer.background.steps)
} else if (stepsContainer.scenario.examples.length === 0) {
compileScenario(
tags,
ruleBackgroundSteps,
stepsContainer.scenario,
language,
pickles,
uri,
newId
)
} else {
compileScenarioOutline(
tags,
ruleBackgroundSteps,
stepsContainer.scenario,
language,
pickles,
uri,
newId
)
}
})
}
function compileScenario(
inheritedTags: readonly Tag[],
backgroundSteps: readonly Step[],
scenario: Scenario,
language: string,
pickles: Pickle[],
uri: string,
newId: IdGenerator.NewId
) {
let lastKeywordType = StepKeywordType.UNKNOWN
const steps = [] as PickleStep[]
if (scenario.steps.length !== 0) {
backgroundSteps.forEach((step) => {
lastKeywordType =
step.keywordType === StepKeywordType.CONJUNCTION ? lastKeywordType : step.keywordType
steps.push(pickleStep(step, [], null, newId, lastKeywordType))
})
}
const tags = [].concat(inheritedTags).concat(scenario.tags)
scenario.steps.forEach((step) => {
lastKeywordType =
step.keywordType === StepKeywordType.CONJUNCTION ? lastKeywordType : step.keywordType
steps.push(pickleStep(step, [], null, newId, lastKeywordType))
})
const pickle: Pickle = {
id: newId(),
uri,
location: scenario.location,
astNodeIds: [scenario.id],
tags: pickleTags(tags),
name: scenario.name,
language,
steps,
}
pickles.push(pickle)
}
function compileScenarioOutline(
inheritedTags: readonly Tag[],
backgroundSteps: readonly Step[],
scenario: Scenario,
language: string,
pickles: Pickle[],
uri: string,
newId: IdGenerator.NewId
) {
scenario.examples
.filter((e) => e.tableHeader)
.forEach((examples) => {
const variableCells = examples.tableHeader.cells
examples.tableBody.forEach((valuesRow) => {
let lastKeywordType = StepKeywordType.UNKNOWN
const steps = [] as PickleStep[]
if (scenario.steps.length !== 0) {
backgroundSteps.forEach((step) => {
lastKeywordType =
step.keywordType === StepKeywordType.CONJUNCTION ? lastKeywordType : step.keywordType
steps.push(pickleStep(step, [], null, newId, lastKeywordType))
})
}
scenario.steps.forEach((scenarioOutlineStep) => {
lastKeywordType =
scenarioOutlineStep.keywordType === StepKeywordType.CONJUNCTION
? lastKeywordType
: scenarioOutlineStep.keywordType
const step = pickleStep(
scenarioOutlineStep,
variableCells,
valuesRow,
newId,
lastKeywordType
)
steps.push(step)
})
const id = newId()
const tags = pickleTags(
[].concat(inheritedTags).concat(scenario.tags).concat(examples.tags)
)
pickles.push({
id,
uri,
location: valuesRow.location,
astNodeIds: [scenario.id, valuesRow.id],
name: interpolate(scenario.name, variableCells, valuesRow.cells),
language,
steps,
tags,
})
})
})
}
function pickleDocString(
argumentIndex: number,
argument: DocString,
variableCells: readonly TableCell[],
valueCells: readonly TableCell[]
): PickleDocString {
return {
argumentIndex,
content: interpolate(argument.content, variableCells, valueCells),
mediaType: argument.mediaType
? interpolate(argument.mediaType, variableCells, valueCells)
: undefined,
}
}
function pickleTable(
argumentIndex: number,
argument: DataTable,
variableCells: readonly TableCell[],
valueCells: readonly TableCell[]
): PickleTable {
return {
argumentIndex,
rows: argument.rows.map((row) => {
return {
cells: row.cells.map((cell) => {
return {
value: interpolate(cell.value, variableCells, valueCells),
}
}),
}
}),
}
}
function createPickleArguments(
step: Step,
variableCells: readonly TableCell[],
valueCells: readonly TableCell[]
): PickleStepArgument | undefined {
if (step.dataTable && step.docString) {
const tableFirst = step.docString.location.line > step.dataTable.location.line
return {
docString: pickleDocString(tableFirst ? 2 : 1, step.docString, variableCells, valueCells),
dataTable: pickleTable(tableFirst ? 1 : 2, step.dataTable, variableCells, valueCells),
}
} else if (step.dataTable) {
return {
dataTable: pickleTable(undefined, step.dataTable, variableCells, valueCells),
}
} else if (step.docString) {
return {
docString: pickleDocString(undefined, step.docString, variableCells, valueCells),
}
}
return undefined
}
function interpolate(
name: string,
variableCells: readonly TableCell[],
valueCells: readonly TableCell[]
) {
variableCells.forEach((variableCell, n) => {
const valueCell = valueCells[n]
const valuePattern = `<${variableCell.value}>`
const escapedPattern = valuePattern.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
const regexp = new RegExp(escapedPattern, 'g')
// JS Specific - dollar sign needs to be escaped with another dollar sign
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter
const replacement = valueCell.value.replace(/\$/g, '$$$$')
name = name.replace(regexp, replacement)
})
return name
}
function pickleStep(
step: Step,
variableCells: readonly TableCell[],
valuesRow: TableRow | null,
newId: IdGenerator.NewId,
keywordType: StepKeywordType
): PickleStep {
const astNodeIds = [step.id]
if (valuesRow) {
astNodeIds.push(valuesRow.id)
}
const valueCells = valuesRow ? valuesRow.cells : []
return {
id: newId(),
text: interpolate(step.text, variableCells, valueCells),
type: pickleStepTypeFromKeyword[keywordType],
argument: createPickleArguments(step, variableCells, valueCells),
astNodeIds: astNodeIds,
}
}
function pickleTags(tags: Tag[]): readonly PickleTag[] {
return tags.map(pickleTag)
}
function pickleTag(tag: Tag): PickleTag {
return {
name: tag.name,
astNodeId: tag.id,
}
}