Skip to content

Commit 891407e

Browse files
committed
feat(gherkin): added custom flavor registry
1 parent 4a91cfb commit 891407e

9 files changed

+339
-41
lines changed

javascript/src/GherkinInMarkdownTokenMatcher.ts

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import ITokenMatcher from './ITokenMatcher'
22
import Dialect from './Dialect'
3-
import { Token, TokenType } from './Parser'
3+
import {Token, TokenType} from './Parser'
44
import DIALECTS from './gherkin-languages.json'
5-
import { Item } from './IToken'
5+
import {Item} from './IToken'
66
import * as messages from '@cucumber/messages'
7-
import { NoSuchLanguageException } from './Errors'
7+
import {NoSuchLanguageException} from './Errors'
8+
import {KeywordPrefixes} from "./flavors/KeywordPrefixes";
89

9-
const DIALECT_DICT: { [key: string]: Dialect } = DIALECTS
10-
const DEFAULT_DOC_STRING_SEPARATOR = /^(```[`]*)(.*)/
10+
export const DIALECT_DICT: { [key: string]: Dialect } = DIALECTS
11+
export const DEFAULT_DOC_STRING_SEPARATOR = /^(```[`]*)(.*)/
1112

1213
function addKeywordTypeMappings(h: { [key: string]: messages.StepKeywordType[] }, keywords: readonly string[], keywordType: messages.StepKeywordType) {
1314
for (const k of keywords) {
@@ -19,17 +20,23 @@ function addKeywordTypeMappings(h: { [key: string]: messages.StepKeywordType[] }
1920
}
2021

2122
export default class GherkinInMarkdownTokenMatcher implements ITokenMatcher<TokenType> {
22-
private dialect: Dialect
23-
private dialectName: string
24-
private readonly nonStarStepKeywords: string[]
23+
dialect: Dialect
24+
dialectName: string
25+
readonly nonStarStepKeywords: string[]
2526
private readonly stepRegexp: RegExp
2627
private readonly headerRegexp: RegExp
2728
private activeDocStringSeparator: RegExp
2829
private indentToRemove: number
29-
private matchedFeatureLine: boolean
30+
matchedFeatureLine: boolean
31+
private prefixes: KeywordPrefixes = {
32+
// https://spec.commonmark.org/0.29/#bullet-list-marker
33+
BULLET: '^(\\s*[*+-]\\s*)',
34+
HEADER: '^(#{1,6}\\s)',
35+
}
3036
private keywordTypesMap: { [key: string]: messages.StepKeywordType[] }
3137

32-
constructor(private readonly defaultDialectName: string = 'en') {
38+
constructor(private readonly defaultDialectName: string = 'en', prefixes?: KeywordPrefixes) {
39+
prefixes ? this.prefixes = prefixes : null;
3340
this.dialect = DIALECT_DICT[defaultDialectName]
3441
this.nonStarStepKeywords = []
3542
.concat(this.dialect.given)
@@ -41,7 +48,7 @@ export default class GherkinInMarkdownTokenMatcher implements ITokenMatcher<Toke
4148
this.initializeKeywordTypes()
4249

4350
this.stepRegexp = new RegExp(
44-
`${KeywordPrefix.BULLET}(${this.nonStarStepKeywords.map(escapeRegExp).join('|')})`
51+
`${this.prefixes.BULLET}(${this.nonStarStepKeywords.map(escapeRegExp).join('|')})`
4552
)
4653

4754
const headerKeywords = []
@@ -54,7 +61,7 @@ export default class GherkinInMarkdownTokenMatcher implements ITokenMatcher<Toke
5461
.filter((value, index, self) => self.indexOf(value) === index)
5562

5663
this.headerRegexp = new RegExp(
57-
`${KeywordPrefix.HEADER}(${headerKeywords.map(escapeRegExp).join('|')})`
64+
`${this.prefixes.HEADER}(${headerKeywords.map(escapeRegExp).join('|')})`
5865
)
5966

6067
this.reset()
@@ -171,7 +178,7 @@ export default class GherkinInMarkdownTokenMatcher implements ITokenMatcher<Toke
171178
}
172179
// We first try to match "# Feature: blah"
173180
let result = this.matchTitleLine(
174-
KeywordPrefix.HEADER,
181+
this.prefixes.HEADER,
175182
this.dialect.feature,
176183
':',
177184
token,
@@ -191,7 +198,7 @@ export default class GherkinInMarkdownTokenMatcher implements ITokenMatcher<Toke
191198

192199
match_BackgroundLine(token: Token): boolean {
193200
return this.matchTitleLine(
194-
KeywordPrefix.HEADER,
201+
this.prefixes.HEADER,
195202
this.dialect.background,
196203
':',
197204
token,
@@ -201,7 +208,7 @@ export default class GherkinInMarkdownTokenMatcher implements ITokenMatcher<Toke
201208

202209
match_RuleLine(token: Token): boolean {
203210
return this.matchTitleLine(
204-
KeywordPrefix.HEADER,
211+
this.prefixes.HEADER,
205212
this.dialect.rule,
206213
':',
207214
token,
@@ -212,14 +219,14 @@ export default class GherkinInMarkdownTokenMatcher implements ITokenMatcher<Toke
212219
match_ScenarioLine(token: Token): boolean {
213220
return (
214221
this.matchTitleLine(
215-
KeywordPrefix.HEADER,
222+
this.prefixes.HEADER,
216223
this.dialect.scenario,
217224
':',
218225
token,
219226
TokenType.ScenarioLine
220227
) ||
221228
this.matchTitleLine(
222-
KeywordPrefix.HEADER,
229+
this.prefixes.HEADER,
223230
this.dialect.scenarioOutline,
224231
':',
225232
token,
@@ -230,7 +237,7 @@ export default class GherkinInMarkdownTokenMatcher implements ITokenMatcher<Toke
230237

231238
match_ExamplesLine(token: Token): boolean {
232239
return this.matchTitleLine(
233-
KeywordPrefix.HEADER,
240+
this.prefixes.HEADER,
234241
this.dialect.examples,
235242
':',
236243
token,
@@ -240,7 +247,7 @@ export default class GherkinInMarkdownTokenMatcher implements ITokenMatcher<Toke
240247

241248
match_StepLine(token: Token): boolean {
242249
return this.matchTitleLine(
243-
KeywordPrefix.BULLET,
250+
this.prefixes.BULLET,
244251
this.nonStarStepKeywords,
245252
'',
246253
token,
@@ -249,7 +256,7 @@ export default class GherkinInMarkdownTokenMatcher implements ITokenMatcher<Toke
249256
}
250257

251258
matchTitleLine(
252-
prefix: KeywordPrefix,
259+
prefix: string,
253260
keywords: readonly string[],
254261
keywordSuffix: ':' | '',
255262
token: Token,
@@ -337,12 +344,6 @@ export default class GherkinInMarkdownTokenMatcher implements ITokenMatcher<Toke
337344
}
338345
}
339346

340-
enum KeywordPrefix {
341-
// https://spec.commonmark.org/0.29/#bullet-list-marker
342-
BULLET = '^(\\s*[*+-]\\s*)',
343-
HEADER = '^(#{1,6}\\s)',
344-
}
345-
346347
// https://stackoverflow.com/questions/3115150/how-to-escape-regular-expression-special-characters-using-javascript
347348
function escapeRegExp(text: string) {
348349
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import ITokenMatcher from "../ITokenMatcher";
2+
import {TokenType} from "../Parser";
3+
import GherkinFlavor from "./GherkinFlavor";
4+
import {SourceMediaType} from "@cucumber/messages";
5+
import {CustomMediaType} from "@cucumber/messages/src";
6+
7+
8+
/**
9+
* This class provides a way to extend the gherkin language by adding flavor implementations such as
10+
* AsciiDoc flavor or Markdown flavor.
11+
*
12+
*/
13+
export default class CustomFlavorRegistry {
14+
private flavors: Array<GherkinFlavor>;
15+
16+
constructor() {
17+
this.flavors = new Array<GherkinFlavor>();
18+
}
19+
20+
public registerFlavor(name: string, fileExtension: string, tokenMatcher: ITokenMatcher<TokenType>) {
21+
this.flavors.push(new GherkinFlavor(name, fileExtension, tokenMatcher));
22+
}
23+
24+
mediaTypeFor(uri: string): CustomMediaType {
25+
const flavor = this.flavors.find(flavor => uri.endsWith(flavor.fileExtension))
26+
return flavor.mediaType;
27+
}
28+
29+
tokenMatcherFor(sourceMediaType: SourceMediaType | CustomMediaType): ITokenMatcher<TokenType> {
30+
const flavor = this.flavors.find(flavor => flavor.mediaType === sourceMediaType);
31+
return flavor.tokenMatcher;
32+
}
33+
34+
private static instance: CustomFlavorRegistry;
35+
public static getInstance() {
36+
if(!this.instance) {
37+
this.instance = new CustomFlavorRegistry();
38+
}
39+
40+
return this.instance;
41+
}
42+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import ITokenMatcher from "../ITokenMatcher";
2+
import {TokenType} from "../Parser";
3+
import {CustomMediaType} from "@cucumber/messages/src";
4+
5+
export default class GherkinFlavor {
6+
7+
constructor(public name: string, public fileExtension: string, public tokenMatcher: ITokenMatcher<TokenType>) {
8+
9+
}
10+
11+
get mediaType(): CustomMediaType {
12+
return `text/x.cucumber.gherkin+${this.name}`;
13+
}
14+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type KeywordPrefixes = {
2+
BULLET: string,
3+
HEADER: string,
4+
}

javascript/src/generateMessages.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,25 @@ import IGherkinOptions from './IGherkinOptions'
77
import makeSourceEnvelope from './makeSourceEnvelope'
88
import ITokenMatcher from './ITokenMatcher'
99
import GherkinInMarkdownTokenMatcher from './GherkinInMarkdownTokenMatcher'
10+
import CustomFlavorRegistry from "./flavors/CustomFlavorRegistry";
1011

1112
export default function generateMessages(
1213
data: string,
1314
uri: string,
14-
mediaType: messages.SourceMediaType,
15+
mediaType: messages.SourceMediaType | messages.CustomMediaType,
1516
options: IGherkinOptions
1617
): readonly messages.Envelope[] {
18+
1719
let tokenMatcher: ITokenMatcher<TokenType>
18-
switch (mediaType) {
19-
case messages.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN:
20-
tokenMatcher = new GherkinClassicTokenMatcher(options.defaultDialect)
21-
break
22-
case messages.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_MARKDOWN:
23-
tokenMatcher = new GherkinInMarkdownTokenMatcher(options.defaultDialect)
24-
break
25-
default:
20+
const customFlavorsRegistry = CustomFlavorRegistry.getInstance();
21+
22+
if (mediaType === 'text/x.cucumber.gherkin+plain') {
23+
tokenMatcher = new GherkinClassicTokenMatcher(options.defaultDialect)
24+
} else if (mediaType === 'text/x.cucumber.gherkin+markdown') {
25+
tokenMatcher = new GherkinInMarkdownTokenMatcher(options.defaultDialect)
26+
} else {
27+
tokenMatcher = customFlavorsRegistry.tokenMatcherFor(mediaType)
28+
if(!tokenMatcher)
2629
throw new Error(`Unsupported media type: ${mediaType}`)
2730
}
2831

javascript/src/makeSourceEnvelope.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import * as messages from '@cucumber/messages'
2+
import CustomFlavorRegistry from "./flavors/CustomFlavorRegistry";
23

34
export default function makeSourceEnvelope(data: string, uri: string): messages.Envelope {
4-
let mediaType: messages.SourceMediaType
5+
let mediaType: messages.SourceMediaType | messages.CustomMediaType;
6+
let customFlavorsRegistry = CustomFlavorRegistry.getInstance();
7+
58
if (uri.endsWith('.feature')) {
6-
mediaType = messages.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN
9+
mediaType = 'text/x.cucumber.gherkin+plain';
710
} else if (uri.endsWith('.md')) {
8-
mediaType = messages.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_MARKDOWN
11+
mediaType = 'text/x.cucumber.gherkin+markdown';
12+
} else {
13+
mediaType = customFlavorsRegistry.mediaTypeFor(uri);
914
}
1015
if (!mediaType) throw new Error(`The uri (${uri}) must end with .feature or .md`)
1116
return {

0 commit comments

Comments
 (0)