├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .mocharc.json ├── .npmignore ├── .rsync ├── .subrepo ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── default.mk ├── gherkin-javascript.razor ├── gherkin.berp ├── package.json ├── src ├── AstBuilder.ts ├── AstNode.ts ├── Dialect.ts ├── Errors.ts ├── GherkinClassicTokenMatcher.ts ├── GherkinInMarkdownTokenMatcher.ts ├── GherkinLine.ts ├── IAstBuilder.ts ├── IGherkinOptions.ts ├── IToken.ts ├── ITokenMatcher.ts ├── Parser.ts ├── TokenExceptions.ts ├── TokenScanner.ts ├── countSymbols.ts ├── generateMessages.ts ├── gherkin-languages.json ├── index.ts ├── makeSourceEnvelope.ts └── pickles │ └── compile.ts ├── test ├── ErrorsTest.ts ├── GherkinClassicTokenMatcherTest.ts ├── GherkinInMarkdownTokenMatcherTest.ts ├── GherkinLineTest.ts └── ParserTest.ts ├── tsconfig.build.json └── tsconfig.json /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | PLEASE DO NOT CREATE ISSUES IN THIS REPO. 2 | THIS REPO IS A READ-ONLY MIRROR. 3 | 4 | Create your issue in the Cucumber monorepo instead: 5 | https://github.com/cucumber/cucumber/issues 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | PLEASE DO NOT CREATE PULL REAUESTS IN THIS REPO. 2 | THIS REPO IS A READ-ONLY MIRROR. 3 | 4 | Create your pull request in the Cucumber monorepo instead: 5 | https://github.com/cucumber/cucumber/pulls 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .idea/ 3 | .nyc_output/ 4 | coverage/ 5 | node_modules/ 6 | yarn.lock 7 | package-lock.json 8 | *.log 9 | .deps 10 | .tested* 11 | .linted 12 | .built* 13 | .compared 14 | .codegen 15 | acceptance/ 16 | storybook-static 17 | *-go 18 | *.iml 19 | .vscode-test 20 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": ["ts-node/register", "source-map-support/register"], 3 | "extension": ["ts", "tsx"], 4 | "recursive": true, 5 | "timeout": 10000 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | berp 2 | test 3 | testdata 4 | Makefile -------------------------------------------------------------------------------- /.rsync: -------------------------------------------------------------------------------- 1 | ../LICENSE LICENSE 2 | ../../.templates/github/ .github/ 3 | ../../.templates/javascript/ . 4 | ../gherkin.berp gherkin.berp 5 | ../gherkin-languages.json src/gherkin-languages.json 6 | -------------------------------------------------------------------------------- /.subrepo: -------------------------------------------------------------------------------- 1 | cucumber/gherkin-javascript 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please read [CONTRIBUTING](https://github.com/cucumber/gherkin/blob/master/CONTRIBUTING.md) first. 2 | You should clone the [cucumber/gherkin](https://github.com/cucumber/gherkin) repo if you want 3 | to contribute. 4 | 5 | ## Run tests 6 | 7 | ### Using make 8 | 9 | Just run `make` from this directory. 10 | 11 | ### Using npm 12 | 13 | Just run `npm test` from this directory (you need to `npm install` first). 14 | 15 | Keep in mind that this will only run unit tests. The acceptance tests are only 16 | run when you build with `make`. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Cucumber Ltd, Gaspar Nagy, Björn Rasmusson, Peter Sergeant 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include default.mk 2 | 3 | GOOD_FEATURE_FILES = $(shell find ../testdata/good -name "*.feature" -o -name "*.feature.md") 4 | BAD_FEATURE_FILES = $(shell find ../testdata/bad -name "*.feature" -o -name "*.feature.md") 5 | 6 | ASTS = $(patsubst ../testdata/%,acceptance/testdata/%.ast.ndjson,$(GOOD_FEATURE_FILES)) 7 | PICKLES = $(patsubst ../testdata/%,acceptance/testdata/%.pickles.ndjson,$(GOOD_FEATURE_FILES)) 8 | SOURCES = $(patsubst ../testdata/%,acceptance/testdata/%.source.ndjson,$(GOOD_FEATURE_FILES)) 9 | ERRORS = $(patsubst ../testdata/%,acceptance/testdata/%.errors.ndjson,$(BAD_FEATURE_FILES)) 10 | 11 | GHERKIN = npx gherkin-javascript 12 | 13 | .DELETE_ON_ERROR: 14 | 15 | .codegen: src/Parser.ts 16 | 17 | src/Parser.ts: gherkin-javascript.razor gherkin.berp 18 | $(berp-generate-parser) 19 | 20 | .tested: .compared 21 | 22 | .compared: $(ASTS) $(PICKLES) $(ERRORS) $(SOURCES) 23 | touch $@ 24 | 25 | acceptance/testdata/%.ast.ndjson: ../testdata/% ../testdata/%.ast.ndjson 26 | mkdir -p $(@D) 27 | $(GHERKIN) --no-source --no-pickles --predictable-ids $< | jq --sort-keys --compact-output "." > $@ 28 | ifndef GOLDEN 29 | diff --unified <(jq "." $<.ast.ndjson) <(jq "." $@) 30 | else 31 | cp $@ $(word 2,$^) 32 | endif 33 | 34 | acceptance/testdata/%.pickles.ndjson: ../testdata/% ../testdata/%.pickles.ndjson 35 | mkdir -p $(@D) 36 | $(GHERKIN) --no-source --no-ast --predictable-ids $< | jq --sort-keys --compact-output "." > $@ 37 | ifndef GOLDEN 38 | diff --unified <(jq "." $<.pickles.ndjson) <(jq "." $@) 39 | else 40 | cp $@ $(word 2,$^) 41 | endif 42 | 43 | acceptance/testdata/%.source.ndjson: ../testdata/% ../testdata/%.source.ndjson 44 | mkdir -p $(@D) 45 | $(GHERKIN) --no-ast --no-pickles --predictable-ids $< | jq --sort-keys --compact-output "." > $@ 46 | ifndef GOLDEN 47 | diff --unified <(jq "." $<.source.ndjson) <(jq "." $@) 48 | else 49 | cp $@ $(word 2,$^) 50 | endif 51 | 52 | acceptance/testdata/%.errors.ndjson: ../testdata/% ../testdata/%.errors.ndjson 53 | mkdir -p $(@D) 54 | $(GHERKIN) --no-source --predictable-ids $< | jq --sort-keys --compact-output "." > $@ 55 | ifndef GOLDEN 56 | diff --unified <(jq "." $<.errors.ndjson) <(jq "." $@) 57 | else 58 | cp $@ $(word 2,$^) 59 | endif 60 | 61 | clean: 62 | rm -rf acceptance 63 | .PHONY: clean 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gherkin for JavaScript 2 | 3 | Gherkin parser/compiler for JavaScript. Please see [Gherkin](https://github.com/cucumber/common/tree/main/gherkin) for details. 4 | 5 | ## Usage 6 | 7 | Typical usage is via [the `gherkin-streams` package](../../gherkin-streams/javascript). 8 | -------------------------------------------------------------------------------- /default.mk: -------------------------------------------------------------------------------- 1 | SHELL := /usr/bin/env bash 2 | # https://stackoverflow.com/questions/2483182/recursive-wildcards-in-gnu-make 3 | rwildcard=$(foreach d,$(wildcard $(1:=/*)),$(call rwildcard,$d,$2) $(filter $(subst *,%,$2),$d)) 4 | TYPESCRIPT_SOURCE_FILES = $(sort $(call rwildcard,src test,*.ts *.tsx)) 5 | PRIVATE = $(shell node -e "console.log(require('./package.json').private)") 6 | IS_TESTDATA = $(findstring -testdata,${CURDIR}) 7 | NPM_MODULE = $(shell cat package.json | jq .name --raw-output) 8 | 9 | default: .tested 10 | .PHONY: default 11 | 12 | ../../node_modules ../../package-lock.json: package.json 13 | cd ../.. && npm install 14 | 15 | .codegen: 16 | touch $@ 17 | 18 | .tested: .tested-npm .built 19 | 20 | .built: $(TYPESCRIPT_SOURCE_FILES) ../../node_modules ../../package-lock.json .codegen 21 | pushd ../.. && \ 22 | npm run build && \ 23 | popd && \ 24 | touch $@ 25 | 26 | .tested-npm: $(TYPESCRIPT_SOURCE_FILES) ../../node_modules ../../package-lock.json .codegen 27 | npm run test 28 | touch $@ 29 | 30 | pre-release: clean update-version update-dependencies default 31 | .PHONY: pre-release 32 | 33 | update-dependencies: ../../node_modules ../../package-lock.json 34 | ../../node_modules/.bin/npm-check-updates --upgrade --reject hast-util-sanitize,@types/node,react-markdown,rehype-raw,rehype-sanitize,remark-gfm 35 | .PHONY: update-dependencies 36 | 37 | update-version: 38 | ifeq ($(IS_TESTDATA),-testdata) 39 | # no-op 40 | else 41 | ifdef NEW_VERSION 42 | npm --no-git-tag-version --allow-same-version version "$(NEW_VERSION)" 43 | # Update all npm packages that depend on us 44 | pushd ../.. && \ 45 | ./scripts/npm-each update_npm_dependency_if_exists package.json "$(NPM_MODULE)" "$(NEW_VERSION)" 46 | else 47 | @echo -e "\033[0;31mNEW_VERSION is not defined. Can't update version :-(\033[0m" 48 | exit 1 49 | endif 50 | endif 51 | .PHONY: update-version 52 | 53 | publish: .codegen 54 | ifeq ($(IS_TESTDATA),-testdata) 55 | # no-op 56 | else 57 | ifneq (true,$(PRIVATE)) 58 | npm publish --access public 59 | else 60 | @echo "Not publishing private npm module" 61 | endif 62 | endif 63 | .PHONY: publish 64 | 65 | post-release: 66 | .PHONY: post-release 67 | 68 | clean: clean-javascript 69 | .PHONY: clean 70 | 71 | clean-javascript: 72 | rm -rf .deps .codegen .tested* coverage dist acceptance 73 | .PHONY: clean-javascript 74 | 75 | clobber: clean 76 | rm -rf node_modules ../../node_modules 77 | .PHONY: clobber 78 | 79 | ### COMMON stuff for all platforms 80 | 81 | BERP_VERSION = 1.3.0 82 | BERP_GRAMMAR = gherkin.berp 83 | 84 | define berp-generate-parser = 85 | -! dotnet tool list --tool-path /usr/bin | grep "berp\s*$(BERP_VERSION)" && dotnet tool update Berp --version $(BERP_VERSION) --tool-path /usr/bin 86 | berp -g $(BERP_GRAMMAR) -t $< -o $@ --noBOM 87 | endef 88 | -------------------------------------------------------------------------------- /gherkin-javascript.razor: -------------------------------------------------------------------------------- 1 | @using Berp; 2 | @helper CallProduction(ProductionRule production) 3 | { 4 | switch(production.Type) 5 | { 6 | case ProductionRuleType.Start: 7 | @:this.startRule(context, RuleType.@production.RuleName); 8 | break; 9 | case ProductionRuleType.End: 10 | @:this.endRule(context); 11 | break; 12 | case ProductionRuleType.Process: 13 | @:this.build(context, token); 14 | break; 15 | } 16 | } 17 | @helper HandleParserError(IEnumerable expectedTokens, State state) 18 | { 19 | token.detach(); 20 | const expectedTokens = ["@Raw(string.Join("\", \"", expectedTokens))"]; 21 | const error = token.isEof ? 22 | UnexpectedEOFException.create(token, expectedTokens) : 23 | UnexpectedTokenException.create(token, expectedTokens); 24 | if (this.stopAtFirstError) throw error; 25 | this.addError(context, error); 26 | return @state.Id;} 27 | @helper MatchToken(TokenType tokenType) 28 | {match_@(tokenType)(context, token)} 29 | // This file is generated. Do not edit! Edit gherkin-javascript.razor instead. 30 | 31 | import * as messages from '@@cucumber/messages' 32 | import { 33 | AstBuilderException, 34 | CompositeParserException, 35 | NoSuchLanguageException, 36 | ParserException, 37 | } from './Errors' 38 | import { 39 | UnexpectedEOFException, 40 | UnexpectedTokenException, 41 | } from './TokenExceptions' 42 | import TokenScanner from './TokenScanner' 43 | import ITokenMatcher from './ITokenMatcher' 44 | import GherkinLine from './GherkinLine' 45 | import IToken, { Item } from './IToken' 46 | import { IAstBuilder } from './IAstBuilder' 47 | 48 | export class Token implements IToken { 49 | public isEof: boolean 50 | public matchedText?: string 51 | public matchedType: TokenType 52 | public matchedItems: readonly Item[] 53 | public matchedKeyword: string 54 | public matchedIndent: number 55 | public matchedGherkinDialect: string 56 | 57 | constructor( 58 | public readonly line: GherkinLine, 59 | public readonly location: messages.Location 60 | ) { 61 | this.isEof = !line 62 | } 63 | 64 | public getTokenValue(): string { 65 | return this.isEof ? 'EOF' : this.line.getLineText(-1) 66 | } 67 | 68 | public detach() { 69 | // TODO: Detach line, but is this really needed? 70 | } 71 | } 72 | 73 | export enum TokenType { 74 | None, 75 | @foreach(var rule in Model.RuleSet.TokenRules) 76 | { @rule.Name.Replace("#", ""), 77 | } 78 | } 79 | 80 | export enum RuleType { 81 | None, 82 | @foreach(var rule in Model.RuleSet.Where(r => !r.TempRule)) 83 | { @rule.Name.Replace("#", "_"), // @rule.ToString(true) 84 | } 85 | } 86 | 87 | interface Context { 88 | tokenScanner: TokenScanner 89 | tokenQueue: IToken[] 90 | errors: Error[] 91 | } 92 | 93 | export default class Parser { 94 | public stopAtFirstError = false 95 | private context: Context 96 | 97 | constructor( 98 | private readonly builder: IAstBuilder, 99 | private readonly tokenMatcher: ITokenMatcher 100 | ) {} 101 | 102 | public parse(gherkinSource: string): messages.GherkinDocument { 103 | const tokenScanner = new TokenScanner( 104 | gherkinSource, 105 | (line: string, location: messages.Location) => { 106 | const gherkinLine = 107 | line === null || line === undefined 108 | ? null 109 | : new GherkinLine(line, location.line) 110 | return new Token(gherkinLine, location) 111 | } 112 | ) 113 | this.builder.reset() 114 | this.tokenMatcher.reset() 115 | this.context = { 116 | tokenScanner, 117 | tokenQueue: [], 118 | errors: [], 119 | } 120 | this.startRule(this.context, RuleType.GherkinDocument) 121 | let state = 0 122 | let token: IToken = null 123 | while (true) { 124 | token = this.readToken(this.context) as Token 125 | state = this.matchToken(state, token, this.context) 126 | if (token.isEof) break 127 | } 128 | 129 | this.endRule(this.context) 130 | 131 | if (this.context.errors.length > 0) { 132 | throw CompositeParserException.create(this.context.errors) 133 | } 134 | 135 | return this.getResult() 136 | } 137 | 138 | private addError(context: Context, error: Error) { 139 | if (!context.errors.map(e => { return e.message }).includes(error.message)) { 140 | context.errors.push(error) 141 | if (context.errors.length > 10) 142 | throw CompositeParserException.create(context.errors) 143 | } 144 | } 145 | 146 | private startRule(context: Context, ruleType: RuleType) { 147 | this.handleAstError(context, () => this.builder.startRule(ruleType)) 148 | } 149 | 150 | private endRule(context: Context) { 151 | this.handleAstError(context, () => this.builder.endRule()) 152 | } 153 | 154 | private build(context: Context, token: IToken) { 155 | this.handleAstError(context, () => this.builder.build(token)) 156 | } 157 | 158 | private getResult() { 159 | return this.builder.getResult() 160 | } 161 | 162 | private handleAstError(context: Context, action: () => any) { 163 | this.handleExternalError(context, true, action) 164 | } 165 | 166 | private handleExternalError( 167 | context: Context, 168 | defaultValue: T, 169 | action: () => T 170 | ) { 171 | if (this.stopAtFirstError) return action() 172 | try { 173 | return action() 174 | } catch (e) { 175 | if (e instanceof CompositeParserException) { 176 | e.errors.forEach((error: Error) => this.addError(context, error)) 177 | } else if ( 178 | e instanceof ParserException || 179 | e instanceof AstBuilderException || 180 | e instanceof UnexpectedTokenException || 181 | e instanceof NoSuchLanguageException 182 | ) { 183 | this.addError(context, e) 184 | } else { 185 | throw e 186 | } 187 | } 188 | return defaultValue 189 | } 190 | 191 | private readToken(context: Context) { 192 | return context.tokenQueue.length > 0 193 | ? context.tokenQueue.shift() 194 | : context.tokenScanner.read() 195 | } 196 | 197 | private matchToken(state: number, token: IToken, context: Context) { 198 | switch(state) { 199 | @foreach(var state in Model.States.Values.Where(s => !s.IsEndState)) 200 | { 201 | @:case @state.Id: 202 | @:return this.matchTokenAt_@(state.Id)(token, context); 203 | } 204 | default: 205 | throw new Error("Unknown state: " + state); 206 | } 207 | } 208 | 209 | @foreach(var state in Model.States.Values.Where(s => !s.IsEndState)) 210 | { 211 | 212 | // @Raw(state.Comment) 213 | private matchTokenAt_@(state.Id)(token: IToken, context: Context) { 214 | @foreach(var transition in state.Transitions) 215 | { 216 | @:if(this.@MatchToken(transition.TokenType)) { 217 | if (transition.LookAheadHint != null) 218 | { 219 | @:if(this.lookahead_@(transition.LookAheadHint.Id)(context, token)) { 220 | } 221 | foreach(var production in transition.Productions) 222 | { 223 | @CallProduction(production) 224 | } 225 | @:return @transition.TargetState; 226 | if (transition.LookAheadHint != null) 227 | { 228 | @:} 229 | } 230 | @:} 231 | } 232 | @HandleParserError(state.Transitions.Select(t => "#" + t.TokenType.ToString()).Distinct(), state) 233 | } 234 | 235 | } 236 | 237 | @foreach(var rule in Model.RuleSet.TokenRules) 238 | { 239 | 240 | private match_@(rule.Name.Replace("#", ""))(context: Context, token: IToken) { 241 | @if (rule.Name != "#EOF") 242 | { 243 | @:if(token.isEof) return false; 244 | } 245 | return this.handleExternalError(context, false, () => this.tokenMatcher.match_@(rule.Name.Replace("#", ""))(token)); 246 | } 247 | 248 | } 249 | 250 | @foreach(var lookAheadHint in Model.RuleSet.LookAheadHints) 251 | { 252 | 253 | private lookahead_@(lookAheadHint.Id)(context: Context, currentToken: IToken) { 254 | currentToken.detach(); 255 | let token; 256 | const queue: IToken[] = []; 257 | let match = false; 258 | do { 259 | token = this.readToken(this.context); 260 | token.detach(); 261 | queue.push(token); 262 | 263 | if (false @foreach(var tokenType in lookAheadHint.ExpectedTokens) { || this.@MatchToken(tokenType)}) { 264 | match = true; 265 | break; 266 | } 267 | } while(false @foreach(var tokenType in lookAheadHint.Skip) { || this.@MatchToken(tokenType)}); 268 | 269 | context.tokenQueue = context.tokenQueue.concat(queue); 270 | 271 | return match; 272 | } 273 | 274 | } 275 | 276 | } 277 | -------------------------------------------------------------------------------- /gherkin.berp: -------------------------------------------------------------------------------- 1 | [ 2 | Tokens -> #Empty,#Comment,#TagLine,#FeatureLine,#RuleLine,#BackgroundLine,#ScenarioLine,#ExamplesLine,#StepLine,#DocStringSeparator,#TableRow,#Language 3 | IgnoredTokens -> #Comment,#Empty 4 | ClassName -> Parser 5 | Namespace -> Gherkin 6 | ] 7 | 8 | GherkinDocument! := Feature? 9 | Feature! := FeatureHeader Background? ScenarioDefinition* Rule* 10 | FeatureHeader! := #Language? Tags? #FeatureLine DescriptionHelper 11 | 12 | Rule! := RuleHeader Background? ScenarioDefinition* 13 | RuleHeader! := Tags? #RuleLine DescriptionHelper 14 | 15 | Background! := #BackgroundLine DescriptionHelper Step* 16 | 17 | // Interpreting a tag line is ambiguous (tag line of rule or of scenario) 18 | ScenarioDefinition! [#Empty|#Comment|#TagLine->#ScenarioLine]:= Tags? Scenario 19 | 20 | Scenario! := #ScenarioLine DescriptionHelper Step* ExamplesDefinition* 21 | // after the first "Data" block, interpreting a tag line is ambiguous (tagline of next examples or of next scenario) 22 | // because of this, we need a lookahead hint, that connects the tag line to the next examples, if there is an examples block ahead 23 | ExamplesDefinition! [#Empty|#Comment|#TagLine->#ExamplesLine]:= Tags? Examples 24 | Examples! := #ExamplesLine DescriptionHelper ExamplesTable? 25 | ExamplesTable! := #TableRow #TableRow* 26 | 27 | Step! := #StepLine StepArg? 28 | StepArg := (DataTable | DocString) 29 | 30 | DataTable! := #TableRow+ 31 | DocString! := #DocStringSeparator #Other* #DocStringSeparator 32 | 33 | Tags! := #TagLine+ 34 | 35 | // we need to explicitly mention comment, to avoid merging it into the description line's #Other token 36 | // we also eat the leading empty lines, the tailing lines are not removed by the parser to avoid lookahead, this has to be done by the AST builder 37 | DescriptionHelper := #Empty* Description? #Comment* 38 | Description! := #Other+ 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cucumber/gherkin", 3 | "version": "24.1.0", 4 | "description": "Gherkin parser", 5 | "main": "dist/src/index.js", 6 | "types": "dist/src/index.d.ts", 7 | "scripts": { 8 | "test": "mocha", 9 | "prepublishOnly": "tsc --build tsconfig.build.json" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/cucumber/gherkin-javascript.git" 14 | }, 15 | "keywords": [ 16 | "gherkin", 17 | "cucumber" 18 | ], 19 | "author": "Aslak Hellesøy", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/cucumber/cucumber/issues" 23 | }, 24 | "homepage": "https://github.com/cucumber/gherkin-javascript", 25 | "devDependencies": { 26 | "@cucumber/gherkin-streams": "^5.0.1", 27 | "@types/mocha": "10.0.0", 28 | "@types/node": "18.11.9", 29 | "core-js": "3.26.0", 30 | "mocha": "10.1.0", 31 | "ts-node": "10.9.1", 32 | "typescript": "4.8.4" 33 | }, 34 | "dependencies": { 35 | "@cucumber/messages": "^19.1.4" 36 | }, 37 | "directories": { 38 | "test": "test" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/AstBuilder.ts: -------------------------------------------------------------------------------- 1 | import AstNode from './AstNode' 2 | import * as messages from '@cucumber/messages' 3 | import { RuleType, TokenType } from './Parser' 4 | import { AstBuilderException } from './Errors' 5 | import IToken from './IToken' 6 | import { IAstBuilder } from './IAstBuilder' 7 | 8 | export default class AstBuilder implements IAstBuilder { 9 | stack: AstNode[] 10 | comments: messages.Comment[] 11 | readonly newId: messages.IdGenerator.NewId 12 | 13 | constructor(newId: messages.IdGenerator.NewId) { 14 | this.newId = newId 15 | if (!newId) { 16 | throw new Error('No newId') 17 | } 18 | this.reset() 19 | } 20 | 21 | reset() { 22 | this.stack = [new AstNode(RuleType.None)] 23 | this.comments = [] 24 | } 25 | 26 | startRule(ruleType: RuleType) { 27 | this.stack.push(new AstNode(ruleType)) 28 | } 29 | 30 | endRule() { 31 | const node = this.stack.pop() 32 | const transformedNode = this.transformNode(node) 33 | this.currentNode().add(node.ruleType, transformedNode) 34 | } 35 | 36 | build(token: IToken) { 37 | if (token.matchedType === TokenType.Comment) { 38 | this.comments.push({ 39 | location: this.getLocation(token), 40 | text: token.matchedText, 41 | }) 42 | } else { 43 | this.currentNode().add(token.matchedType, token) 44 | } 45 | } 46 | 47 | getResult() { 48 | return this.currentNode().getSingle(RuleType.GherkinDocument) 49 | } 50 | 51 | currentNode() { 52 | return this.stack[this.stack.length - 1] 53 | } 54 | 55 | getLocation(token: IToken, column?: number): messages.Location { 56 | return !column ? token.location : { line: token.location.line, column } 57 | } 58 | 59 | getTags(node: AstNode) { 60 | const tags: messages.Tag[] = [] 61 | const tagsNode = node.getSingle(RuleType.Tags) 62 | if (!tagsNode) { 63 | return tags 64 | } 65 | const tokens = tagsNode.getTokens(TokenType.TagLine) 66 | for (const token of tokens) { 67 | for (const tagItem of token.matchedItems) { 68 | tags.push({ 69 | location: this.getLocation(token, tagItem.column), 70 | name: tagItem.text, 71 | id: this.newId(), 72 | }) 73 | } 74 | } 75 | return tags 76 | } 77 | 78 | getCells(tableRowToken: IToken) { 79 | return tableRowToken.matchedItems.map((cellItem) => ({ 80 | location: this.getLocation(tableRowToken, cellItem.column), 81 | value: cellItem.text, 82 | })) 83 | } 84 | 85 | getDescription(node: AstNode) { 86 | return node.getSingle(RuleType.Description) || '' 87 | } 88 | 89 | getSteps(node: AstNode) { 90 | return node.getItems(RuleType.Step) 91 | } 92 | 93 | getTableRows(node: AstNode) { 94 | const rows = node.getTokens(TokenType.TableRow).map((token) => ({ 95 | id: this.newId(), 96 | location: this.getLocation(token), 97 | cells: this.getCells(token), 98 | })) 99 | this.ensureCellCount(rows) 100 | return rows.length === 0 ? [] : rows 101 | } 102 | 103 | ensureCellCount(rows: messages.TableRow[]) { 104 | if (rows.length === 0) { 105 | return 106 | } 107 | const cellCount = rows[0].cells.length 108 | 109 | rows.forEach((row) => { 110 | if (row.cells.length !== cellCount) { 111 | throw AstBuilderException.create('inconsistent cell count within the table', row.location) 112 | } 113 | }) 114 | } 115 | 116 | transformNode(node: AstNode) { 117 | switch (node.ruleType) { 118 | case RuleType.Step: { 119 | const stepLine = node.getToken(TokenType.StepLine) 120 | const dataTable = node.getSingle(RuleType.DataTable) 121 | const docString = node.getSingle(RuleType.DocString) 122 | 123 | const location = this.getLocation(stepLine) 124 | const step: messages.Step = { 125 | id: this.newId(), 126 | location, 127 | keyword: stepLine.matchedKeyword, 128 | keywordType: stepLine.matchedKeywordType, 129 | text: stepLine.matchedText, 130 | dataTable: dataTable, 131 | docString: docString, 132 | } 133 | return step 134 | } 135 | case RuleType.DocString: { 136 | const separatorToken = node.getTokens(TokenType.DocStringSeparator)[0] 137 | const mediaType = 138 | separatorToken.matchedText.length > 0 ? separatorToken.matchedText : undefined 139 | const lineTokens = node.getTokens(TokenType.Other) 140 | const content = lineTokens.map((t) => t.matchedText).join('\n') 141 | 142 | const result: messages.DocString = { 143 | location: this.getLocation(separatorToken), 144 | content, 145 | delimiter: separatorToken.matchedKeyword, 146 | } 147 | // conditionally add this like this (needed to make tests pass on node 0.10 as well as 4.0) 148 | if (mediaType) { 149 | result.mediaType = mediaType 150 | } 151 | return result 152 | } 153 | case RuleType.DataTable: { 154 | const rows = this.getTableRows(node) 155 | const dataTable: messages.DataTable = { 156 | location: rows[0].location, 157 | rows, 158 | } 159 | return dataTable 160 | } 161 | case RuleType.Background: { 162 | const backgroundLine = node.getToken(TokenType.BackgroundLine) 163 | const description = this.getDescription(node) 164 | const steps = this.getSteps(node) 165 | 166 | const background: messages.Background = { 167 | id: this.newId(), 168 | location: this.getLocation(backgroundLine), 169 | keyword: backgroundLine.matchedKeyword, 170 | name: backgroundLine.matchedText, 171 | description, 172 | steps, 173 | } 174 | return background 175 | } 176 | case RuleType.ScenarioDefinition: { 177 | const tags = this.getTags(node) 178 | const scenarioNode = node.getSingle(RuleType.Scenario) 179 | const scenarioLine = scenarioNode.getToken(TokenType.ScenarioLine) 180 | const description = this.getDescription(scenarioNode) 181 | const steps = this.getSteps(scenarioNode) 182 | const examples = scenarioNode.getItems(RuleType.ExamplesDefinition) 183 | const scenario: messages.Scenario = { 184 | id: this.newId(), 185 | tags, 186 | location: this.getLocation(scenarioLine), 187 | keyword: scenarioLine.matchedKeyword, 188 | name: scenarioLine.matchedText, 189 | description, 190 | steps, 191 | examples, 192 | } 193 | return scenario 194 | } 195 | case RuleType.ExamplesDefinition: { 196 | const tags = this.getTags(node) 197 | const examplesNode = node.getSingle(RuleType.Examples) 198 | const examplesLine = examplesNode.getToken(TokenType.ExamplesLine) 199 | const description = this.getDescription(examplesNode) 200 | const examplesTable: messages.TableRow[] = examplesNode.getSingle(RuleType.ExamplesTable) 201 | 202 | const examples: messages.Examples = { 203 | id: this.newId(), 204 | tags, 205 | location: this.getLocation(examplesLine), 206 | keyword: examplesLine.matchedKeyword, 207 | name: examplesLine.matchedText, 208 | description, 209 | tableHeader: examplesTable ? examplesTable[0] : undefined, 210 | tableBody: examplesTable ? examplesTable.slice(1) : [], 211 | } 212 | return examples 213 | } 214 | case RuleType.ExamplesTable: { 215 | return this.getTableRows(node) 216 | } 217 | case RuleType.Description: { 218 | let lineTokens = node.getTokens(TokenType.Other) 219 | // Trim trailing empty lines 220 | let end = lineTokens.length 221 | while (end > 0 && lineTokens[end - 1].line.trimmedLineText === '') { 222 | end-- 223 | } 224 | lineTokens = lineTokens.slice(0, end) 225 | 226 | return lineTokens.map((token) => token.matchedText).join('\n') 227 | } 228 | 229 | case RuleType.Feature: { 230 | const header = node.getSingle(RuleType.FeatureHeader) 231 | if (!header) { 232 | return null 233 | } 234 | const tags = this.getTags(header) 235 | const featureLine = header.getToken(TokenType.FeatureLine) 236 | if (!featureLine) { 237 | return null 238 | } 239 | const children: messages.FeatureChild[] = [] 240 | const background = node.getSingle(RuleType.Background) 241 | if (background) { 242 | children.push({ 243 | background, 244 | }) 245 | } 246 | for (const scenario of node.getItems(RuleType.ScenarioDefinition)) { 247 | children.push({ 248 | scenario, 249 | }) 250 | } 251 | for (const rule of node.getItems(RuleType.Rule)) { 252 | children.push({ 253 | rule, 254 | }) 255 | } 256 | 257 | const description = this.getDescription(header) 258 | const language = featureLine.matchedGherkinDialect 259 | 260 | const feature: messages.Feature = { 261 | tags, 262 | location: this.getLocation(featureLine), 263 | language, 264 | keyword: featureLine.matchedKeyword, 265 | name: featureLine.matchedText, 266 | description, 267 | children, 268 | } 269 | return feature 270 | } 271 | 272 | case RuleType.Rule: { 273 | const header = node.getSingle(RuleType.RuleHeader) 274 | if (!header) { 275 | return null 276 | } 277 | const ruleLine = header.getToken(TokenType.RuleLine) 278 | if (!ruleLine) { 279 | return null 280 | } 281 | const tags = this.getTags(header) 282 | const children: messages.RuleChild[] = [] 283 | const background = node.getSingle(RuleType.Background) 284 | if (background) { 285 | children.push({ 286 | background, 287 | }) 288 | } 289 | for (const scenario of node.getItems(RuleType.ScenarioDefinition)) { 290 | children.push({ 291 | scenario, 292 | }) 293 | } 294 | const description = this.getDescription(header) 295 | 296 | const rule: messages.Rule = { 297 | id: this.newId(), 298 | location: this.getLocation(ruleLine), 299 | keyword: ruleLine.matchedKeyword, 300 | name: ruleLine.matchedText, 301 | description, 302 | children, 303 | tags, 304 | } 305 | return rule 306 | } 307 | case RuleType.GherkinDocument: { 308 | const feature = node.getSingle(RuleType.Feature) 309 | 310 | const gherkinDocument: messages.GherkinDocument = { 311 | feature, 312 | comments: this.comments, 313 | } 314 | return gherkinDocument 315 | } 316 | default: 317 | return node 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/AstNode.ts: -------------------------------------------------------------------------------- 1 | import { RuleType, TokenType } from './Parser' 2 | import IToken from './IToken' 3 | 4 | export default class AstNode { 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | private readonly subItems = new Map() 7 | 8 | constructor(public readonly ruleType: RuleType) {} 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | public add(type: any, obj: any) { 12 | let items = this.subItems.get(type) 13 | if (items === undefined) { 14 | items = [] 15 | this.subItems.set(type, items) 16 | } 17 | items.push(obj) 18 | } 19 | 20 | public getSingle(ruleType: RuleType) { 21 | return (this.subItems.get(ruleType) || [])[0] 22 | } 23 | 24 | public getItems(ruleType: RuleType) { 25 | return this.subItems.get(ruleType) || [] 26 | } 27 | 28 | public getToken(tokenType: TokenType) { 29 | return (this.subItems.get(tokenType) || [])[0] 30 | } 31 | 32 | public getTokens(tokenType: TokenType): IToken[] { 33 | return this.subItems.get(tokenType) || [] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Dialect.ts: -------------------------------------------------------------------------------- 1 | export default interface Dialect { 2 | readonly name: string 3 | readonly native: string 4 | readonly feature: readonly string[] 5 | readonly background: readonly string[] 6 | readonly rule: readonly string[] 7 | readonly scenario: readonly string[] 8 | readonly scenarioOutline: readonly string[] 9 | readonly examples: readonly string[] 10 | readonly given: readonly string[] 11 | readonly when: readonly string[] 12 | readonly then: readonly string[] 13 | readonly and: readonly string[] 14 | readonly but: readonly string[] 15 | } 16 | -------------------------------------------------------------------------------- /src/Errors.ts: -------------------------------------------------------------------------------- 1 | import * as messages from '@cucumber/messages' 2 | 3 | export class GherkinException extends Error { 4 | public errors: Error[] 5 | public location: messages.Location 6 | 7 | constructor(message: string) { 8 | super(message) 9 | 10 | const actualProto = new.target.prototype 11 | 12 | // https://stackoverflow.com/questions/41102060/typescript-extending-error-class 13 | if (Object.setPrototypeOf) { 14 | Object.setPrototypeOf(this, actualProto) 15 | } else { 16 | // @ts-ignore 17 | this.__proto__ = actualProto 18 | } 19 | } 20 | 21 | protected static _create(message: string, location?: messages.Location) { 22 | const column = location != null ? location.column || 0 : -1 23 | const line = location != null ? location.line || 0 : -1 24 | const m = `(${line}:${column}): ${message}` 25 | const err = new this(m) 26 | err.location = location 27 | return err 28 | } 29 | } 30 | 31 | export class ParserException extends GherkinException { 32 | public static create(message: string, line: number, column: number) { 33 | const err = new this(`(${line}:${column}): ${message}`) 34 | err.location = { line, column } 35 | return err 36 | } 37 | } 38 | 39 | export class CompositeParserException extends GherkinException { 40 | public static create(errors: Error[]) { 41 | const message = 'Parser errors:\n' + errors.map((e) => e.message).join('\n') 42 | const err = new this(message) 43 | err.errors = errors 44 | return err 45 | } 46 | } 47 | 48 | export class AstBuilderException extends GherkinException { 49 | public static create(message: string, location: messages.Location) { 50 | return this._create(message, location) 51 | } 52 | } 53 | 54 | export class NoSuchLanguageException extends GherkinException { 55 | public static create(language: string, location?: messages.Location) { 56 | const message = 'Language not supported: ' + language 57 | return this._create(message, location) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/GherkinClassicTokenMatcher.ts: -------------------------------------------------------------------------------- 1 | import DIALECTS from './gherkin-languages.json' 2 | import Dialect from './Dialect' 3 | import { NoSuchLanguageException, ParserException } from './Errors' 4 | import IToken, { IGherkinLine, Item } from './IToken' 5 | import * as messages from '@cucumber/messages' 6 | import { TokenType } from './Parser' 7 | import ITokenMatcher from './ITokenMatcher' 8 | import countSymbols from './countSymbols' 9 | 10 | const DIALECT_DICT: { [key: string]: Dialect } = DIALECTS 11 | const LANGUAGE_PATTERN = /^\s*#\s*language\s*:\s*([a-zA-Z\-_]+)\s*$/ 12 | 13 | function addKeywordTypeMappings(h: { [key: string]: messages.StepKeywordType[] }, keywords: readonly string[], keywordType: messages.StepKeywordType) { 14 | for (const k of keywords) { 15 | if (!(k in h)) { 16 | h[k] = [] as messages.StepKeywordType[] 17 | } 18 | h[k].push(keywordType) 19 | } 20 | } 21 | 22 | export default class GherkinClassicTokenMatcher implements ITokenMatcher { 23 | private dialect: Dialect 24 | private dialectName: string 25 | private activeDocStringSeparator: string 26 | private indentToRemove: number 27 | private keywordTypesMap: { [key: string]: messages.StepKeywordType[] } 28 | 29 | constructor(private readonly defaultDialectName: string = 'en') { 30 | this.reset() 31 | } 32 | 33 | changeDialect(newDialectName: string, location?: messages.Location) { 34 | const newDialect = DIALECT_DICT[newDialectName] 35 | if (!newDialect) { 36 | throw NoSuchLanguageException.create(newDialectName, location) 37 | } 38 | 39 | this.dialectName = newDialectName 40 | this.dialect = newDialect 41 | this.initializeKeywordTypes() 42 | } 43 | 44 | reset() { 45 | if (this.dialectName !== this.defaultDialectName) { 46 | this.changeDialect(this.defaultDialectName) 47 | } 48 | this.activeDocStringSeparator = null 49 | this.indentToRemove = 0 50 | } 51 | 52 | initializeKeywordTypes() { 53 | this.keywordTypesMap = {} 54 | addKeywordTypeMappings(this.keywordTypesMap, this.dialect.given, messages.StepKeywordType.CONTEXT) 55 | addKeywordTypeMappings(this.keywordTypesMap, this.dialect.when, messages.StepKeywordType.ACTION) 56 | addKeywordTypeMappings(this.keywordTypesMap, this.dialect.then, messages.StepKeywordType.OUTCOME) 57 | addKeywordTypeMappings(this.keywordTypesMap, 58 | [].concat(this.dialect.and).concat(this.dialect.but), 59 | messages.StepKeywordType.CONJUNCTION) 60 | } 61 | 62 | match_TagLine(token: IToken) { 63 | if (token.line.startsWith('@')) { 64 | this.setTokenMatched(token, TokenType.TagLine, null, null, null, null, this.getTags(token.line)) 65 | return true 66 | } 67 | return false 68 | } 69 | 70 | match_FeatureLine(token: IToken) { 71 | return this.matchTitleLine(token, TokenType.FeatureLine, this.dialect.feature) 72 | } 73 | 74 | match_ScenarioLine(token: IToken) { 75 | return ( 76 | this.matchTitleLine(token, TokenType.ScenarioLine, this.dialect.scenario) || 77 | this.matchTitleLine(token, TokenType.ScenarioLine, this.dialect.scenarioOutline) 78 | ) 79 | } 80 | 81 | match_BackgroundLine(token: IToken) { 82 | return this.matchTitleLine(token, TokenType.BackgroundLine, this.dialect.background) 83 | } 84 | 85 | match_ExamplesLine(token: IToken) { 86 | return this.matchTitleLine(token, TokenType.ExamplesLine, this.dialect.examples) 87 | } 88 | 89 | match_RuleLine(token: IToken) { 90 | return this.matchTitleLine(token, TokenType.RuleLine, this.dialect.rule) 91 | } 92 | 93 | match_TableRow(token: IToken) { 94 | if (token.line.startsWith('|')) { 95 | // TODO: indent 96 | this.setTokenMatched(token, TokenType.TableRow, null, null, null, null, token.line.getTableCells()) 97 | return true 98 | } 99 | return false 100 | } 101 | 102 | match_Empty(token: IToken) { 103 | if (token.line.isEmpty) { 104 | this.setTokenMatched(token, TokenType.Empty, null, null, 0) 105 | return true 106 | } 107 | return false 108 | } 109 | 110 | match_Comment(token: IToken) { 111 | if (token.line.startsWith('#')) { 112 | const text = token.line.getLineText(0) // take the entire line, including leading space 113 | this.setTokenMatched(token, TokenType.Comment, text, null, 0) 114 | return true 115 | } 116 | return false 117 | } 118 | 119 | match_Language(token: IToken) { 120 | const match = token.line.trimmedLineText.match(LANGUAGE_PATTERN) 121 | if (match) { 122 | const newDialectName = match[1] 123 | this.setTokenMatched(token, TokenType.Language, newDialectName) 124 | 125 | this.changeDialect(newDialectName, token.location) 126 | return true 127 | } 128 | return false 129 | } 130 | 131 | match_DocStringSeparator(token: IToken) { 132 | return this.activeDocStringSeparator == null 133 | ? // open 134 | this._match_DocStringSeparator(token, '"""', true) || 135 | this._match_DocStringSeparator(token, '```', true) 136 | : // close 137 | this._match_DocStringSeparator(token, this.activeDocStringSeparator, false) 138 | } 139 | 140 | public _match_DocStringSeparator(token: IToken, separator: string, isOpen: boolean) { 141 | if (token.line.startsWith(separator)) { 142 | let mediaType = null 143 | if (isOpen) { 144 | mediaType = token.line.getRestTrimmed(separator.length) 145 | this.activeDocStringSeparator = separator 146 | this.indentToRemove = token.line.indent 147 | } else { 148 | this.activeDocStringSeparator = null 149 | this.indentToRemove = 0 150 | } 151 | 152 | this.setTokenMatched(token, TokenType.DocStringSeparator, mediaType, separator) 153 | return true 154 | } 155 | return false 156 | } 157 | 158 | match_EOF(token: IToken) { 159 | if (token.isEof) { 160 | this.setTokenMatched(token, TokenType.EOF) 161 | return true 162 | } 163 | return false 164 | } 165 | 166 | match_StepLine(token: IToken) { 167 | const keywords = [] 168 | .concat(this.dialect.given) 169 | .concat(this.dialect.when) 170 | .concat(this.dialect.then) 171 | .concat(this.dialect.and) 172 | .concat(this.dialect.but) 173 | for (const keyword of keywords) { 174 | if (token.line.startsWith(keyword)) { 175 | const title = token.line.getRestTrimmed(keyword.length) 176 | const keywordTypes = this.keywordTypesMap[keyword] 177 | let keywordType = keywordTypes[0] 178 | if (keywordTypes.length > 1) { 179 | keywordType = messages.StepKeywordType.UNKNOWN 180 | } 181 | 182 | this.setTokenMatched(token, TokenType.StepLine, title, keyword, null, keywordType) 183 | return true 184 | } 185 | } 186 | 187 | return false 188 | } 189 | 190 | match_Other(token: IToken) { 191 | const text = token.line.getLineText(this.indentToRemove) // take the entire line, except removing DocString indents 192 | this.setTokenMatched(token, TokenType.Other, this.unescapeDocString(text), null, 0) 193 | return true 194 | } 195 | 196 | getTags(line: IGherkinLine): readonly Item[] { 197 | const uncommentedLine = line.trimmedLineText.split(/\s#/g, 2)[0] 198 | let column = line.indent + 1 199 | const items = uncommentedLine.split('@') 200 | const tags: any[] = [] 201 | for (let i = 0; i < items.length; i++) { 202 | const item = items[i].trimRight() 203 | if (item.length == 0) { 204 | continue 205 | } 206 | if (!item.match(/^\S+$/)) { 207 | throw ParserException.create('A tag may not contain whitespace', line.lineNumber, column) 208 | } 209 | const span = { column, text: '@' + item } 210 | tags.push(span) 211 | column += countSymbols(items[i]) + 1 212 | } 213 | return tags 214 | } 215 | 216 | private matchTitleLine( 217 | token: IToken, 218 | tokenType: TokenType, 219 | keywords: readonly string[] 220 | ): boolean { 221 | for (const keyword of keywords) { 222 | if (token.line.startsWithTitleKeyword(keyword)) { 223 | const title = token.line.getRestTrimmed(keyword.length + ':'.length) 224 | this.setTokenMatched(token, tokenType, title, keyword) 225 | return true 226 | } 227 | } 228 | return false 229 | } 230 | 231 | private setTokenMatched( 232 | token: IToken, 233 | matchedType: TokenType, 234 | text?: string, 235 | keyword?: string, 236 | indent?: number, 237 | keywordType?: messages.StepKeywordType, 238 | items?: readonly Item[] 239 | ) { 240 | token.matchedType = matchedType 241 | token.matchedText = text 242 | token.matchedKeyword = keyword 243 | token.matchedKeywordType = keywordType 244 | token.matchedIndent = 245 | typeof indent === 'number' ? indent : token.line == null ? 0 : token.line.indent 246 | token.matchedItems = items || [] 247 | 248 | token.location.column = token.matchedIndent + 1 249 | token.matchedGherkinDialect = this.dialectName 250 | } 251 | 252 | private unescapeDocString(text: string) { 253 | if (this.activeDocStringSeparator === '"""') { 254 | return text.replace('\\"\\"\\"', '"""') 255 | } 256 | if (this.activeDocStringSeparator === '```') { 257 | return text.replace('\\`\\`\\`', '```') 258 | } 259 | return text 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/GherkinInMarkdownTokenMatcher.ts: -------------------------------------------------------------------------------- 1 | import ITokenMatcher from './ITokenMatcher' 2 | import Dialect from './Dialect' 3 | import { Token, TokenType } from './Parser' 4 | import DIALECTS from './gherkin-languages.json' 5 | import { Item } from './IToken' 6 | import * as messages from '@cucumber/messages' 7 | import { NoSuchLanguageException } from './Errors' 8 | 9 | const DIALECT_DICT: { [key: string]: Dialect } = DIALECTS 10 | const DEFAULT_DOC_STRING_SEPARATOR = /^(```[`]*)(.*)/ 11 | 12 | function addKeywordTypeMappings(h: { [key: string]: messages.StepKeywordType[] }, keywords: readonly string[], keywordType: messages.StepKeywordType) { 13 | for (const k of keywords) { 14 | if (!(k in h)) { 15 | h[k] = [] as messages.StepKeywordType[] 16 | } 17 | h[k].push(keywordType) 18 | } 19 | } 20 | 21 | export default class GherkinInMarkdownTokenMatcher implements ITokenMatcher { 22 | private dialect: Dialect 23 | private dialectName: string 24 | private readonly nonStarStepKeywords: string[] 25 | private readonly stepRegexp: RegExp 26 | private readonly headerRegexp: RegExp 27 | private activeDocStringSeparator: RegExp 28 | private indentToRemove: number 29 | private matchedFeatureLine: boolean 30 | private keywordTypesMap: { [key: string]: messages.StepKeywordType[] } 31 | 32 | constructor(private readonly defaultDialectName: string = 'en') { 33 | this.dialect = DIALECT_DICT[defaultDialectName] 34 | this.nonStarStepKeywords = [] 35 | .concat(this.dialect.given) 36 | .concat(this.dialect.when) 37 | .concat(this.dialect.then) 38 | .concat(this.dialect.and) 39 | .concat(this.dialect.but) 40 | .filter((value, index, self) => value !== '* ' && self.indexOf(value) === index) 41 | this.initializeKeywordTypes() 42 | 43 | this.stepRegexp = new RegExp( 44 | `${KeywordPrefix.BULLET}(${this.nonStarStepKeywords.map(escapeRegExp).join('|')})` 45 | ) 46 | 47 | const headerKeywords = [] 48 | .concat(this.dialect.feature) 49 | .concat(this.dialect.background) 50 | .concat(this.dialect.rule) 51 | .concat(this.dialect.scenarioOutline) 52 | .concat(this.dialect.scenario) 53 | .concat(this.dialect.examples) 54 | .filter((value, index, self) => self.indexOf(value) === index) 55 | 56 | this.headerRegexp = new RegExp( 57 | `${KeywordPrefix.HEADER}(${headerKeywords.map(escapeRegExp).join('|')})` 58 | ) 59 | 60 | this.reset() 61 | } 62 | 63 | changeDialect(newDialectName: string, location?: messages.Location) { 64 | const newDialect = DIALECT_DICT[newDialectName] 65 | if (!newDialect) { 66 | throw NoSuchLanguageException.create(newDialectName, location) 67 | } 68 | 69 | this.dialectName = newDialectName 70 | this.dialect = newDialect 71 | this.initializeKeywordTypes() 72 | } 73 | 74 | initializeKeywordTypes() { 75 | this.keywordTypesMap = {} 76 | addKeywordTypeMappings(this.keywordTypesMap, this.dialect.given, messages.StepKeywordType.CONTEXT) 77 | addKeywordTypeMappings(this.keywordTypesMap, this.dialect.when, messages.StepKeywordType.ACTION) 78 | addKeywordTypeMappings(this.keywordTypesMap, this.dialect.then, messages.StepKeywordType.OUTCOME) 79 | addKeywordTypeMappings(this.keywordTypesMap, 80 | [].concat(this.dialect.and).concat(this.dialect.but), 81 | messages.StepKeywordType.CONJUNCTION) 82 | } 83 | 84 | // We've made a deliberate choice not to support `# language: [ISO 639-1]` headers or similar 85 | // in Markdown. Users should specify a language globally. This can be done in 86 | // cucumber-js using the --language [ISO 639-1] option. 87 | match_Language(token: Token): boolean { 88 | if (!token) throw new Error('no token') 89 | return false 90 | } 91 | 92 | match_Empty(token: Token): boolean { 93 | let result = false 94 | if (token.line.isEmpty) { 95 | result = true 96 | } 97 | if ( 98 | !this.match_TagLine(token) && 99 | !this.match_FeatureLine(token) && 100 | !this.match_ScenarioLine(token) && 101 | !this.match_BackgroundLine(token) && 102 | !this.match_ExamplesLine(token) && 103 | !this.match_RuleLine(token) && 104 | !this.match_TableRow(token) && 105 | !this.match_Comment(token) && 106 | !this.match_Language(token) && 107 | !this.match_DocStringSeparator(token) && 108 | !this.match_EOF(token) && 109 | !this.match_StepLine(token) 110 | ) { 111 | // neutered 112 | result = true 113 | } 114 | 115 | if (result) { 116 | token.matchedType = TokenType.Empty 117 | } 118 | return this.setTokenMatched(token, null, result) 119 | } 120 | 121 | match_Other(token: Token): boolean { 122 | const text = token.line.getLineText(this.indentToRemove) // take the entire line, except removing DocString indents 123 | token.matchedType = TokenType.Other 124 | token.matchedText = text 125 | token.matchedIndent = 0 126 | return this.setTokenMatched(token, null, true) 127 | } 128 | 129 | match_Comment(token: Token): boolean { 130 | let result = false 131 | if (token.line.startsWith('|')) { 132 | const tableCells = token.line.getTableCells() 133 | if (this.isGfmTableSeparator(tableCells)) result = true 134 | } 135 | return this.setTokenMatched(token, null, result) 136 | } 137 | 138 | match_DocStringSeparator(token: Token) { 139 | const match = token.line.trimmedLineText.match(this.activeDocStringSeparator) 140 | const [, newSeparator, mediaType] = match || [] 141 | let result = false 142 | if (newSeparator) { 143 | if (this.activeDocStringSeparator === DEFAULT_DOC_STRING_SEPARATOR) { 144 | this.activeDocStringSeparator = new RegExp(`^(${newSeparator})$`) 145 | this.indentToRemove = token.line.indent 146 | } else { 147 | this.activeDocStringSeparator = DEFAULT_DOC_STRING_SEPARATOR 148 | } 149 | 150 | token.matchedKeyword = newSeparator 151 | token.matchedType = TokenType.DocStringSeparator 152 | token.matchedText = mediaType || '' 153 | 154 | result = true 155 | } 156 | return this.setTokenMatched(token, null, result) 157 | } 158 | 159 | match_EOF(token: Token): boolean { 160 | let result = false 161 | if (token.isEof) { 162 | token.matchedType = TokenType.EOF 163 | result = true 164 | } 165 | return this.setTokenMatched(token, null, result) 166 | } 167 | 168 | match_FeatureLine(token: Token): boolean { 169 | if (this.matchedFeatureLine) { 170 | return this.setTokenMatched(token, null, false) 171 | } 172 | // We first try to match "# Feature: blah" 173 | let result = this.matchTitleLine( 174 | KeywordPrefix.HEADER, 175 | this.dialect.feature, 176 | ':', 177 | token, 178 | TokenType.FeatureLine 179 | ) 180 | // If we didn't match "# Feature: blah", we still match this line 181 | // as a FeatureLine. 182 | // The reason for this is that users may not want to be constrained by having this as their fist line. 183 | if (!result) { 184 | token.matchedType = TokenType.FeatureLine 185 | token.matchedText = token.line.trimmedLineText 186 | result = this.setTokenMatched(token, null, true) 187 | } 188 | this.matchedFeatureLine = result 189 | return result 190 | } 191 | 192 | match_BackgroundLine(token: Token): boolean { 193 | return this.matchTitleLine( 194 | KeywordPrefix.HEADER, 195 | this.dialect.background, 196 | ':', 197 | token, 198 | TokenType.BackgroundLine 199 | ) 200 | } 201 | 202 | match_RuleLine(token: Token): boolean { 203 | return this.matchTitleLine( 204 | KeywordPrefix.HEADER, 205 | this.dialect.rule, 206 | ':', 207 | token, 208 | TokenType.RuleLine 209 | ) 210 | } 211 | 212 | match_ScenarioLine(token: Token): boolean { 213 | return ( 214 | this.matchTitleLine( 215 | KeywordPrefix.HEADER, 216 | this.dialect.scenario, 217 | ':', 218 | token, 219 | TokenType.ScenarioLine 220 | ) || 221 | this.matchTitleLine( 222 | KeywordPrefix.HEADER, 223 | this.dialect.scenarioOutline, 224 | ':', 225 | token, 226 | TokenType.ScenarioLine 227 | ) 228 | ) 229 | } 230 | 231 | match_ExamplesLine(token: Token): boolean { 232 | return this.matchTitleLine( 233 | KeywordPrefix.HEADER, 234 | this.dialect.examples, 235 | ':', 236 | token, 237 | TokenType.ExamplesLine 238 | ) 239 | } 240 | 241 | match_StepLine(token: Token): boolean { 242 | return this.matchTitleLine( 243 | KeywordPrefix.BULLET, 244 | this.nonStarStepKeywords, 245 | '', 246 | token, 247 | TokenType.StepLine 248 | ) 249 | } 250 | 251 | matchTitleLine( 252 | prefix: KeywordPrefix, 253 | keywords: readonly string[], 254 | keywordSuffix: ':' | '', 255 | token: Token, 256 | matchedType: TokenType 257 | ) { 258 | const regexp = new RegExp( 259 | `${prefix}(${keywords.map(escapeRegExp).join('|')})${keywordSuffix}(.*)` 260 | ) 261 | const match = token.line.match(regexp) 262 | let indent = token.line.indent 263 | let result = false 264 | if (match) { 265 | token.matchedType = matchedType 266 | token.matchedKeyword = match[2] 267 | 268 | if (match[2] in this.keywordTypesMap) { 269 | // only set the keyword type if this is a step keyword 270 | if (this.keywordTypesMap[match[2]].length > 1) { 271 | token.matchedKeywordType = messages.StepKeywordType.UNKNOWN 272 | } 273 | else { 274 | token.matchedKeywordType = this.keywordTypesMap[match[2]][0] 275 | } 276 | } 277 | token.matchedText = match[3].trim() 278 | indent += match[1].length 279 | result = true 280 | } 281 | return this.setTokenMatched(token, indent, result) 282 | } 283 | 284 | setTokenMatched(token: Token, indent: number | null, matched: boolean) { 285 | token.matchedGherkinDialect = this.dialectName 286 | token.matchedIndent = indent !== null ? indent : token.line == null ? 0 : token.line.indent 287 | token.location.column = token.matchedIndent + 1 288 | return matched 289 | } 290 | 291 | match_TableRow(token: Token): boolean { 292 | // Gherkin tables must be indented 2-5 spaces in order to be distinguidedn from non-Gherkin tables 293 | if (token.line.lineText.match(/^\s\s\s?\s?\s?\|/)) { 294 | const tableCells = token.line.getTableCells() 295 | if (this.isGfmTableSeparator(tableCells)) return false 296 | 297 | token.matchedKeyword = '|' 298 | token.matchedType = TokenType.TableRow 299 | token.matchedItems = tableCells 300 | return true 301 | } 302 | return false 303 | } 304 | 305 | private isGfmTableSeparator(tableCells: readonly Item[]): boolean { 306 | const separatorValues = tableCells 307 | .map((item) => item.text) 308 | .filter((value) => value.match(/^:?-+:?$/)) 309 | return separatorValues.length > 0 310 | } 311 | 312 | match_TagLine(token: Token): boolean { 313 | const tags: Item[] = [] 314 | let m: RegExpMatchArray 315 | const re = /`(@[^`]+)`/g 316 | do { 317 | m = re.exec(token.line.trimmedLineText) 318 | if (m) { 319 | tags.push({ 320 | column: token.line.indent + m.index + 2, 321 | text: m[1], 322 | }) 323 | } 324 | } while (m) 325 | 326 | if (tags.length === 0) return false 327 | token.matchedType = TokenType.TagLine 328 | token.matchedItems = tags 329 | return true 330 | } 331 | 332 | reset(): void { 333 | if (this.dialectName !== this.defaultDialectName) { 334 | this.changeDialect(this.defaultDialectName) 335 | } 336 | this.activeDocStringSeparator = DEFAULT_DOC_STRING_SEPARATOR 337 | } 338 | } 339 | 340 | enum KeywordPrefix { 341 | // https://spec.commonmark.org/0.29/#bullet-list-marker 342 | BULLET = '^(\\s*[*+-]\\s*)', 343 | HEADER = '^(#{1,6}\\s)', 344 | } 345 | 346 | // https://stackoverflow.com/questions/3115150/how-to-escape-regular-expression-special-characters-using-javascript 347 | function escapeRegExp(text: string) { 348 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') 349 | } 350 | -------------------------------------------------------------------------------- /src/GherkinLine.ts: -------------------------------------------------------------------------------- 1 | import countSymbols from './countSymbols' 2 | import { IGherkinLine, Item } from './IToken' 3 | 4 | export default class GherkinLine implements IGherkinLine { 5 | public trimmedLineText: string 6 | public isEmpty: boolean 7 | public readonly indent: number 8 | public column: number 9 | public text: string 10 | 11 | constructor(public readonly lineText: string, public readonly lineNumber: number) { 12 | this.trimmedLineText = lineText.replace(/^\s+/g, '') // ltrim 13 | this.isEmpty = this.trimmedLineText.length === 0 14 | this.indent = countSymbols(lineText) - countSymbols(this.trimmedLineText) 15 | } 16 | 17 | public startsWith(prefix: string) { 18 | return this.trimmedLineText.indexOf(prefix) === 0 19 | } 20 | 21 | public startsWithTitleKeyword(keyword: string) { 22 | return this.startsWith(keyword + ':') // The C# impl is more complicated. Find out why. 23 | } 24 | 25 | public match(regexp: RegExp) { 26 | return this.trimmedLineText.match(regexp) 27 | } 28 | 29 | public getLineText(indentToRemove: number) { 30 | if (indentToRemove < 0 || indentToRemove > this.indent) { 31 | return this.trimmedLineText 32 | } else { 33 | return this.lineText.substring(indentToRemove) 34 | } 35 | } 36 | 37 | public getRestTrimmed(length: number) { 38 | return this.trimmedLineText.substring(length).trim() 39 | } 40 | 41 | public getTableCells(): readonly Item[] { 42 | const cells = [] 43 | let col = 0 44 | let startCol = col + 1 45 | let cell = '' 46 | let firstCell = true 47 | while (col < this.trimmedLineText.length) { 48 | let chr = this.trimmedLineText[col] 49 | col++ 50 | 51 | if (chr === '|') { 52 | if (firstCell) { 53 | // First cell (content before the first |) is skipped 54 | firstCell = false 55 | } else { 56 | // Keeps newlines 57 | const trimmedLeft = cell.replace(/^[ \t\v\f\r\u0085\u00A0]*/g, '') 58 | const trimmed = trimmedLeft.replace(/[ \t\v\f\r\u0085\u00A0]*$/g, '') 59 | const cellIndent = cell.length - trimmedLeft.length 60 | const span = { 61 | column: this.indent + startCol + cellIndent, 62 | text: trimmed, 63 | } 64 | cells.push(span) 65 | } 66 | cell = '' 67 | startCol = col + 1 68 | } else if (chr === '\\') { 69 | chr = this.trimmedLineText[col] 70 | col += 1 71 | if (chr === 'n') { 72 | cell += '\n' 73 | } else { 74 | if (chr !== '|' && chr !== '\\') { 75 | cell += '\\' 76 | } 77 | cell += chr 78 | } 79 | } else { 80 | cell += chr 81 | } 82 | } 83 | 84 | return cells 85 | } 86 | } 87 | 88 | module.exports = GherkinLine 89 | -------------------------------------------------------------------------------- /src/IAstBuilder.ts: -------------------------------------------------------------------------------- 1 | import * as messages from '@cucumber/messages' 2 | import IToken from './IToken' 3 | 4 | export interface IAstBuilder { 5 | stack: AstNode[] 6 | comments: messages.Comment[] 7 | newId: messages.IdGenerator.NewId 8 | 9 | reset(): void 10 | 11 | startRule(ruleType: RuleType): void 12 | 13 | endRule(): void 14 | 15 | build(token: IToken): void 16 | 17 | getResult(): any 18 | 19 | currentNode(): any 20 | 21 | getLocation(token: IToken, column?: number): messages.Location 22 | 23 | getTags(node: AstNode): readonly messages.Tag[] 24 | 25 | getCells(tableRowToken: IToken): readonly messages.TableCell[] 26 | 27 | getDescription(node: AstNode): any 28 | 29 | getSteps(node: AstNode): any[] 30 | 31 | getTableRows(node: AstNode): readonly messages.TableRow[] | undefined 32 | 33 | ensureCellCount(rows: readonly messages.TableRow[]): void 34 | 35 | transformNode(node: AstNode): any 36 | } 37 | -------------------------------------------------------------------------------- /src/IGherkinOptions.ts: -------------------------------------------------------------------------------- 1 | import { IdGenerator } from '@cucumber/messages' 2 | 3 | export default interface IGherkinOptions { 4 | defaultDialect?: string 5 | includeSource?: boolean 6 | includeGherkinDocument?: boolean 7 | includePickles?: boolean 8 | newId?: IdGenerator.NewId 9 | } 10 | -------------------------------------------------------------------------------- /src/IToken.ts: -------------------------------------------------------------------------------- 1 | import * as messages from '@cucumber/messages' 2 | 3 | export interface IGherkinLine { 4 | readonly lineNumber: number 5 | readonly isEmpty: boolean 6 | readonly indent?: number 7 | readonly trimmedLineText: string 8 | 9 | getTableCells(): readonly Item[] 10 | 11 | startsWith(prefix: string): boolean 12 | 13 | getRestTrimmed(length: number): string 14 | 15 | getLineText(number: number): string 16 | 17 | startsWithTitleKeyword(keyword: string): boolean 18 | } 19 | 20 | // Represents tags or table cells 21 | export type Item = { 22 | column: number 23 | text: string 24 | } 25 | 26 | export default interface IToken { 27 | location: messages.Location 28 | line: IGherkinLine 29 | 30 | isEof: boolean 31 | matchedText?: string 32 | matchedType: TokenType 33 | matchedItems: readonly Item[] 34 | matchedKeyword: string 35 | matchedKeywordType: messages.StepKeywordType 36 | matchedIndent: number 37 | matchedGherkinDialect: string 38 | getTokenValue(): string 39 | detach(): void 40 | } 41 | -------------------------------------------------------------------------------- /src/ITokenMatcher.ts: -------------------------------------------------------------------------------- 1 | import * as messages from '@cucumber/messages' 2 | import IToken from './IToken' 3 | 4 | export default interface ITokenMatcher { 5 | changeDialect(newDialectName: string, location?: messages.Location): void 6 | 7 | reset(): void 8 | 9 | match_TagLine(token: IToken): boolean 10 | 11 | match_FeatureLine(token: IToken): boolean 12 | 13 | match_ScenarioLine(token: IToken): boolean 14 | 15 | match_BackgroundLine(token: IToken): boolean 16 | 17 | match_ExamplesLine(token: IToken): boolean 18 | 19 | match_RuleLine(token: IToken): boolean 20 | 21 | match_TableRow(token: IToken): boolean 22 | 23 | match_Empty(token: IToken): boolean 24 | 25 | match_Comment(token: IToken): boolean 26 | 27 | match_Language(token: IToken): boolean 28 | 29 | match_DocStringSeparator(token: IToken): boolean 30 | 31 | match_EOF(token: IToken): boolean 32 | 33 | match_StepLine(token: IToken): boolean 34 | 35 | match_Other(token: IToken): boolean 36 | } 37 | -------------------------------------------------------------------------------- /src/TokenExceptions.ts: -------------------------------------------------------------------------------- 1 | import IToken from './IToken' 2 | import { GherkinException } from './Errors' 3 | 4 | export class UnexpectedTokenException extends GherkinException { 5 | public static create(token: IToken, expectedTokenTypes: string[]) { 6 | const message = `expected: ${expectedTokenTypes.join(', ')}, got '${token 7 | .getTokenValue() 8 | .trim()}'` 9 | 10 | const location = tokenLocation(token) 11 | 12 | return this._create(message, location) 13 | } 14 | } 15 | 16 | export class UnexpectedEOFException extends GherkinException { 17 | public static create(token: IToken, expectedTokenTypes: string[]) { 18 | const message = `unexpected end of file, expected: ${expectedTokenTypes.join(', ')}` 19 | const location = tokenLocation(token) 20 | 21 | return this._create(message, location) 22 | } 23 | } 24 | 25 | function tokenLocation(token: IToken) { 26 | return token.location && token.location.line && token.line && token.line.indent !== undefined 27 | ? { 28 | line: token.location.line, 29 | column: token.line.indent + 1, 30 | } 31 | : token.location 32 | } 33 | -------------------------------------------------------------------------------- /src/TokenScanner.ts: -------------------------------------------------------------------------------- 1 | import IToken from './IToken' 2 | import * as messages from '@cucumber/messages' 3 | 4 | /** 5 | * The scanner reads a gherkin doc (typically read from a .feature file) and creates a token for each line. 6 | * The tokens are passed to the parser, which outputs an AST (Abstract Syntax Tree). 7 | * 8 | * If the scanner sees a `#` language header, it will reconfigure itself dynamically to look for 9 | * Gherkin keywords for the associated language. The keywords are defined in gherkin-languages.json. 10 | */ 11 | export default class TokenScanner { 12 | private lineNumber = 0 13 | private lines: string[] 14 | 15 | constructor( 16 | source: string, 17 | private readonly makeToken: (line: string, location: messages.Location) => IToken 18 | ) { 19 | this.lines = source.split(/\r?\n/) 20 | if (this.lines.length > 0 && this.lines[this.lines.length - 1].trim() === '') { 21 | this.lines.pop() 22 | } 23 | } 24 | 25 | public read(): IToken { 26 | const line = this.lines[this.lineNumber++] 27 | const location = { 28 | line: this.lineNumber, 29 | } 30 | return this.makeToken(line, location) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/countSymbols.ts: -------------------------------------------------------------------------------- 1 | // https://mathiasbynens.be/notes/javascript-unicode 2 | const regexAstralSymbols = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g 3 | 4 | export default function countSymbols(s: string) { 5 | return s.replace(regexAstralSymbols, '_').length 6 | } 7 | -------------------------------------------------------------------------------- /src/generateMessages.ts: -------------------------------------------------------------------------------- 1 | import Parser, { TokenType } from './Parser' 2 | import GherkinClassicTokenMatcher from './GherkinClassicTokenMatcher' 3 | import * as messages from '@cucumber/messages' 4 | import compile from './pickles/compile' 5 | import AstBuilder from './AstBuilder' 6 | import IGherkinOptions from './IGherkinOptions' 7 | import makeSourceEnvelope from './makeSourceEnvelope' 8 | import ITokenMatcher from './ITokenMatcher' 9 | import GherkinInMarkdownTokenMatcher from './GherkinInMarkdownTokenMatcher' 10 | 11 | export default function generateMessages( 12 | data: string, 13 | uri: string, 14 | mediaType: messages.SourceMediaType, 15 | options: IGherkinOptions 16 | ): readonly messages.Envelope[] { 17 | let tokenMatcher: ITokenMatcher 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: 26 | throw new Error(`Unsupported media type: ${mediaType}`) 27 | } 28 | 29 | const result = [] 30 | 31 | try { 32 | if (options.includeSource) { 33 | result.push(makeSourceEnvelope(data, uri)) 34 | } 35 | 36 | if (!options.includeGherkinDocument && !options.includePickles) { 37 | return result 38 | } 39 | 40 | const parser = new Parser(new AstBuilder(options.newId), tokenMatcher) 41 | parser.stopAtFirstError = false 42 | 43 | const gherkinDocument = parser.parse(data) 44 | 45 | if (options.includeGherkinDocument) { 46 | result.push({ 47 | gherkinDocument: { ...gherkinDocument, uri }, 48 | }) 49 | } 50 | 51 | if (options.includePickles) { 52 | const pickles = compile(gherkinDocument, uri, options.newId) 53 | for (const pickle of pickles) { 54 | result.push({ 55 | pickle, 56 | }) 57 | } 58 | } 59 | } catch (err) { 60 | const errors = err.errors || [err] 61 | for (const error of errors) { 62 | if (!error.location) { 63 | // It wasn't a parser error - throw it (this is unexpected) 64 | throw error 65 | } 66 | result.push({ 67 | parseError: { 68 | source: { 69 | uri, 70 | location: { 71 | line: error.location.line, 72 | column: error.location.column, 73 | }, 74 | }, 75 | message: error.message, 76 | }, 77 | }) 78 | } 79 | } 80 | return result 81 | } 82 | -------------------------------------------------------------------------------- /src/gherkin-languages.json: -------------------------------------------------------------------------------- 1 | { 2 | "af": { 3 | "and": [ 4 | "* ", 5 | "En " 6 | ], 7 | "background": [ 8 | "Agtergrond" 9 | ], 10 | "but": [ 11 | "* ", 12 | "Maar " 13 | ], 14 | "examples": [ 15 | "Voorbeelde" 16 | ], 17 | "feature": [ 18 | "Funksie", 19 | "Besigheid Behoefte", 20 | "Vermoë" 21 | ], 22 | "given": [ 23 | "* ", 24 | "Gegewe " 25 | ], 26 | "name": "Afrikaans", 27 | "native": "Afrikaans", 28 | "rule": [ 29 | "Regel" 30 | ], 31 | "scenario": [ 32 | "Voorbeeld", 33 | "Situasie" 34 | ], 35 | "scenarioOutline": [ 36 | "Situasie Uiteensetting" 37 | ], 38 | "then": [ 39 | "* ", 40 | "Dan " 41 | ], 42 | "when": [ 43 | "* ", 44 | "Wanneer " 45 | ] 46 | }, 47 | "am": { 48 | "and": [ 49 | "* ", 50 | "Եվ " 51 | ], 52 | "background": [ 53 | "Կոնտեքստ" 54 | ], 55 | "but": [ 56 | "* ", 57 | "Բայց " 58 | ], 59 | "examples": [ 60 | "Օրինակներ" 61 | ], 62 | "feature": [ 63 | "Ֆունկցիոնալություն", 64 | "Հատկություն" 65 | ], 66 | "given": [ 67 | "* ", 68 | "Դիցուք " 69 | ], 70 | "name": "Armenian", 71 | "native": "հայերեն", 72 | "rule": [ 73 | "Rule" 74 | ], 75 | "scenario": [ 76 | "Օրինակ", 77 | "Սցենար" 78 | ], 79 | "scenarioOutline": [ 80 | "Սցենարի կառուցվացքը" 81 | ], 82 | "then": [ 83 | "* ", 84 | "Ապա " 85 | ], 86 | "when": [ 87 | "* ", 88 | "Եթե ", 89 | "Երբ " 90 | ] 91 | }, 92 | "an": { 93 | "and": [ 94 | "* ", 95 | "Y ", 96 | "E " 97 | ], 98 | "background": [ 99 | "Antecedents" 100 | ], 101 | "but": [ 102 | "* ", 103 | "Pero " 104 | ], 105 | "examples": [ 106 | "Eixemplos" 107 | ], 108 | "feature": [ 109 | "Caracteristica" 110 | ], 111 | "given": [ 112 | "* ", 113 | "Dau ", 114 | "Dada ", 115 | "Daus ", 116 | "Dadas " 117 | ], 118 | "name": "Aragonese", 119 | "native": "Aragonés", 120 | "rule": [ 121 | "Rule" 122 | ], 123 | "scenario": [ 124 | "Eixemplo", 125 | "Caso" 126 | ], 127 | "scenarioOutline": [ 128 | "Esquema del caso" 129 | ], 130 | "then": [ 131 | "* ", 132 | "Alavez ", 133 | "Allora ", 134 | "Antonces " 135 | ], 136 | "when": [ 137 | "* ", 138 | "Cuan " 139 | ] 140 | }, 141 | "ar": { 142 | "and": [ 143 | "* ", 144 | "و " 145 | ], 146 | "background": [ 147 | "الخلفية" 148 | ], 149 | "but": [ 150 | "* ", 151 | "لكن " 152 | ], 153 | "examples": [ 154 | "امثلة" 155 | ], 156 | "feature": [ 157 | "خاصية" 158 | ], 159 | "given": [ 160 | "* ", 161 | "بفرض " 162 | ], 163 | "name": "Arabic", 164 | "native": "العربية", 165 | "rule": [ 166 | "Rule" 167 | ], 168 | "scenario": [ 169 | "مثال", 170 | "سيناريو" 171 | ], 172 | "scenarioOutline": [ 173 | "سيناريو مخطط" 174 | ], 175 | "then": [ 176 | "* ", 177 | "اذاً ", 178 | "ثم " 179 | ], 180 | "when": [ 181 | "* ", 182 | "متى ", 183 | "عندما " 184 | ] 185 | }, 186 | "ast": { 187 | "and": [ 188 | "* ", 189 | "Y ", 190 | "Ya " 191 | ], 192 | "background": [ 193 | "Antecedentes" 194 | ], 195 | "but": [ 196 | "* ", 197 | "Peru " 198 | ], 199 | "examples": [ 200 | "Exemplos" 201 | ], 202 | "feature": [ 203 | "Carauterística" 204 | ], 205 | "given": [ 206 | "* ", 207 | "Dáu ", 208 | "Dada ", 209 | "Daos ", 210 | "Daes " 211 | ], 212 | "name": "Asturian", 213 | "native": "asturianu", 214 | "rule": [ 215 | "Rule" 216 | ], 217 | "scenario": [ 218 | "Exemplo", 219 | "Casu" 220 | ], 221 | "scenarioOutline": [ 222 | "Esbozu del casu" 223 | ], 224 | "then": [ 225 | "* ", 226 | "Entós " 227 | ], 228 | "when": [ 229 | "* ", 230 | "Cuando " 231 | ] 232 | }, 233 | "az": { 234 | "and": [ 235 | "* ", 236 | "Və ", 237 | "Həm " 238 | ], 239 | "background": [ 240 | "Keçmiş", 241 | "Kontekst" 242 | ], 243 | "but": [ 244 | "* ", 245 | "Amma ", 246 | "Ancaq " 247 | ], 248 | "examples": [ 249 | "Nümunələr" 250 | ], 251 | "feature": [ 252 | "Özəllik" 253 | ], 254 | "given": [ 255 | "* ", 256 | "Tutaq ki ", 257 | "Verilir " 258 | ], 259 | "name": "Azerbaijani", 260 | "native": "Azərbaycanca", 261 | "rule": [ 262 | "Rule" 263 | ], 264 | "scenario": [ 265 | "Nümunə", 266 | "Ssenari" 267 | ], 268 | "scenarioOutline": [ 269 | "Ssenarinin strukturu" 270 | ], 271 | "then": [ 272 | "* ", 273 | "O halda " 274 | ], 275 | "when": [ 276 | "* ", 277 | "Əgər ", 278 | "Nə vaxt ki " 279 | ] 280 | }, 281 | "bg": { 282 | "and": [ 283 | "* ", 284 | "И " 285 | ], 286 | "background": [ 287 | "Предистория" 288 | ], 289 | "but": [ 290 | "* ", 291 | "Но " 292 | ], 293 | "examples": [ 294 | "Примери" 295 | ], 296 | "feature": [ 297 | "Функционалност" 298 | ], 299 | "given": [ 300 | "* ", 301 | "Дадено " 302 | ], 303 | "name": "Bulgarian", 304 | "native": "български", 305 | "rule": [ 306 | "Правило" 307 | ], 308 | "scenario": [ 309 | "Пример", 310 | "Сценарий" 311 | ], 312 | "scenarioOutline": [ 313 | "Рамка на сценарий" 314 | ], 315 | "then": [ 316 | "* ", 317 | "То " 318 | ], 319 | "when": [ 320 | "* ", 321 | "Когато " 322 | ] 323 | }, 324 | "bm": { 325 | "and": [ 326 | "* ", 327 | "Dan " 328 | ], 329 | "background": [ 330 | "Latar Belakang" 331 | ], 332 | "but": [ 333 | "* ", 334 | "Tetapi ", 335 | "Tapi " 336 | ], 337 | "examples": [ 338 | "Contoh" 339 | ], 340 | "feature": [ 341 | "Fungsi" 342 | ], 343 | "given": [ 344 | "* ", 345 | "Diberi ", 346 | "Bagi " 347 | ], 348 | "name": "Malay", 349 | "native": "Bahasa Melayu", 350 | "rule": [ 351 | "Rule" 352 | ], 353 | "scenario": [ 354 | "Senario", 355 | "Situasi", 356 | "Keadaan" 357 | ], 358 | "scenarioOutline": [ 359 | "Kerangka Senario", 360 | "Kerangka Situasi", 361 | "Kerangka Keadaan", 362 | "Garis Panduan Senario" 363 | ], 364 | "then": [ 365 | "* ", 366 | "Maka ", 367 | "Kemudian " 368 | ], 369 | "when": [ 370 | "* ", 371 | "Apabila " 372 | ] 373 | }, 374 | "bs": { 375 | "and": [ 376 | "* ", 377 | "I ", 378 | "A " 379 | ], 380 | "background": [ 381 | "Pozadina" 382 | ], 383 | "but": [ 384 | "* ", 385 | "Ali " 386 | ], 387 | "examples": [ 388 | "Primjeri" 389 | ], 390 | "feature": [ 391 | "Karakteristika" 392 | ], 393 | "given": [ 394 | "* ", 395 | "Dato " 396 | ], 397 | "name": "Bosnian", 398 | "native": "Bosanski", 399 | "rule": [ 400 | "Rule" 401 | ], 402 | "scenario": [ 403 | "Primjer", 404 | "Scenariju", 405 | "Scenario" 406 | ], 407 | "scenarioOutline": [ 408 | "Scenariju-obris", 409 | "Scenario-outline" 410 | ], 411 | "then": [ 412 | "* ", 413 | "Zatim " 414 | ], 415 | "when": [ 416 | "* ", 417 | "Kada " 418 | ] 419 | }, 420 | "ca": { 421 | "and": [ 422 | "* ", 423 | "I " 424 | ], 425 | "background": [ 426 | "Rerefons", 427 | "Antecedents" 428 | ], 429 | "but": [ 430 | "* ", 431 | "Però " 432 | ], 433 | "examples": [ 434 | "Exemples" 435 | ], 436 | "feature": [ 437 | "Característica", 438 | "Funcionalitat" 439 | ], 440 | "given": [ 441 | "* ", 442 | "Donat ", 443 | "Donada ", 444 | "Atès ", 445 | "Atesa " 446 | ], 447 | "name": "Catalan", 448 | "native": "català", 449 | "rule": [ 450 | "Rule" 451 | ], 452 | "scenario": [ 453 | "Exemple", 454 | "Escenari" 455 | ], 456 | "scenarioOutline": [ 457 | "Esquema de l'escenari" 458 | ], 459 | "then": [ 460 | "* ", 461 | "Aleshores ", 462 | "Cal " 463 | ], 464 | "when": [ 465 | "* ", 466 | "Quan " 467 | ] 468 | }, 469 | "cs": { 470 | "and": [ 471 | "* ", 472 | "A také ", 473 | "A " 474 | ], 475 | "background": [ 476 | "Pozadí", 477 | "Kontext" 478 | ], 479 | "but": [ 480 | "* ", 481 | "Ale " 482 | ], 483 | "examples": [ 484 | "Příklady" 485 | ], 486 | "feature": [ 487 | "Požadavek" 488 | ], 489 | "given": [ 490 | "* ", 491 | "Pokud ", 492 | "Za předpokladu " 493 | ], 494 | "name": "Czech", 495 | "native": "Česky", 496 | "rule": [ 497 | "Pravidlo" 498 | ], 499 | "scenario": [ 500 | "Příklad", 501 | "Scénář" 502 | ], 503 | "scenarioOutline": [ 504 | "Náčrt Scénáře", 505 | "Osnova scénáře" 506 | ], 507 | "then": [ 508 | "* ", 509 | "Pak " 510 | ], 511 | "when": [ 512 | "* ", 513 | "Když " 514 | ] 515 | }, 516 | "cy-GB": { 517 | "and": [ 518 | "* ", 519 | "A " 520 | ], 521 | "background": [ 522 | "Cefndir" 523 | ], 524 | "but": [ 525 | "* ", 526 | "Ond " 527 | ], 528 | "examples": [ 529 | "Enghreifftiau" 530 | ], 531 | "feature": [ 532 | "Arwedd" 533 | ], 534 | "given": [ 535 | "* ", 536 | "Anrhegedig a " 537 | ], 538 | "name": "Welsh", 539 | "native": "Cymraeg", 540 | "rule": [ 541 | "Rule" 542 | ], 543 | "scenario": [ 544 | "Enghraifft", 545 | "Scenario" 546 | ], 547 | "scenarioOutline": [ 548 | "Scenario Amlinellol" 549 | ], 550 | "then": [ 551 | "* ", 552 | "Yna " 553 | ], 554 | "when": [ 555 | "* ", 556 | "Pryd " 557 | ] 558 | }, 559 | "da": { 560 | "and": [ 561 | "* ", 562 | "Og " 563 | ], 564 | "background": [ 565 | "Baggrund" 566 | ], 567 | "but": [ 568 | "* ", 569 | "Men " 570 | ], 571 | "examples": [ 572 | "Eksempler" 573 | ], 574 | "feature": [ 575 | "Egenskab" 576 | ], 577 | "given": [ 578 | "* ", 579 | "Givet " 580 | ], 581 | "name": "Danish", 582 | "native": "dansk", 583 | "rule": [ 584 | "Rule" 585 | ], 586 | "scenario": [ 587 | "Eksempel", 588 | "Scenarie" 589 | ], 590 | "scenarioOutline": [ 591 | "Abstrakt Scenario" 592 | ], 593 | "then": [ 594 | "* ", 595 | "Så " 596 | ], 597 | "when": [ 598 | "* ", 599 | "Når " 600 | ] 601 | }, 602 | "de": { 603 | "and": [ 604 | "* ", 605 | "Und " 606 | ], 607 | "background": [ 608 | "Grundlage", 609 | "Hintergrund", 610 | "Voraussetzungen", 611 | "Vorbedingungen" 612 | ], 613 | "but": [ 614 | "* ", 615 | "Aber " 616 | ], 617 | "examples": [ 618 | "Beispiele" 619 | ], 620 | "feature": [ 621 | "Funktionalität", 622 | "Funktion" 623 | ], 624 | "given": [ 625 | "* ", 626 | "Angenommen ", 627 | "Gegeben sei ", 628 | "Gegeben seien " 629 | ], 630 | "name": "German", 631 | "native": "Deutsch", 632 | "rule": [ 633 | "Rule", 634 | "Regel" 635 | ], 636 | "scenario": [ 637 | "Beispiel", 638 | "Szenario" 639 | ], 640 | "scenarioOutline": [ 641 | "Szenariogrundriss", 642 | "Szenarien" 643 | ], 644 | "then": [ 645 | "* ", 646 | "Dann " 647 | ], 648 | "when": [ 649 | "* ", 650 | "Wenn " 651 | ] 652 | }, 653 | "el": { 654 | "and": [ 655 | "* ", 656 | "Και " 657 | ], 658 | "background": [ 659 | "Υπόβαθρο" 660 | ], 661 | "but": [ 662 | "* ", 663 | "Αλλά " 664 | ], 665 | "examples": [ 666 | "Παραδείγματα", 667 | "Σενάρια" 668 | ], 669 | "feature": [ 670 | "Δυνατότητα", 671 | "Λειτουργία" 672 | ], 673 | "given": [ 674 | "* ", 675 | "Δεδομένου " 676 | ], 677 | "name": "Greek", 678 | "native": "Ελληνικά", 679 | "rule": [ 680 | "Rule" 681 | ], 682 | "scenario": [ 683 | "Παράδειγμα", 684 | "Σενάριο" 685 | ], 686 | "scenarioOutline": [ 687 | "Περιγραφή Σεναρίου", 688 | "Περίγραμμα Σεναρίου" 689 | ], 690 | "then": [ 691 | "* ", 692 | "Τότε " 693 | ], 694 | "when": [ 695 | "* ", 696 | "Όταν " 697 | ] 698 | }, 699 | "em": { 700 | "and": [ 701 | "* ", 702 | "😂" 703 | ], 704 | "background": [ 705 | "💤" 706 | ], 707 | "but": [ 708 | "* ", 709 | "😔" 710 | ], 711 | "examples": [ 712 | "📓" 713 | ], 714 | "feature": [ 715 | "📚" 716 | ], 717 | "given": [ 718 | "* ", 719 | "😐" 720 | ], 721 | "name": "Emoji", 722 | "native": "😀", 723 | "rule": [ 724 | "Rule" 725 | ], 726 | "scenario": [ 727 | "🥒", 728 | "📕" 729 | ], 730 | "scenarioOutline": [ 731 | "📖" 732 | ], 733 | "then": [ 734 | "* ", 735 | "🙏" 736 | ], 737 | "when": [ 738 | "* ", 739 | "🎬" 740 | ] 741 | }, 742 | "en": { 743 | "and": [ 744 | "* ", 745 | "And " 746 | ], 747 | "background": [ 748 | "Background" 749 | ], 750 | "but": [ 751 | "* ", 752 | "But " 753 | ], 754 | "examples": [ 755 | "Examples", 756 | "Scenarios" 757 | ], 758 | "feature": [ 759 | "Feature", 760 | "Business Need", 761 | "Ability" 762 | ], 763 | "given": [ 764 | "* ", 765 | "Given " 766 | ], 767 | "name": "English", 768 | "native": "English", 769 | "rule": [ 770 | "Rule" 771 | ], 772 | "scenario": [ 773 | "Example", 774 | "Scenario" 775 | ], 776 | "scenarioOutline": [ 777 | "Scenario Outline", 778 | "Scenario Template" 779 | ], 780 | "then": [ 781 | "* ", 782 | "Then " 783 | ], 784 | "when": [ 785 | "* ", 786 | "When " 787 | ] 788 | }, 789 | "en-Scouse": { 790 | "and": [ 791 | "* ", 792 | "An " 793 | ], 794 | "background": [ 795 | "Dis is what went down" 796 | ], 797 | "but": [ 798 | "* ", 799 | "Buh " 800 | ], 801 | "examples": [ 802 | "Examples" 803 | ], 804 | "feature": [ 805 | "Feature" 806 | ], 807 | "given": [ 808 | "* ", 809 | "Givun ", 810 | "Youse know when youse got " 811 | ], 812 | "name": "Scouse", 813 | "native": "Scouse", 814 | "rule": [ 815 | "Rule" 816 | ], 817 | "scenario": [ 818 | "The thing of it is" 819 | ], 820 | "scenarioOutline": [ 821 | "Wharrimean is" 822 | ], 823 | "then": [ 824 | "* ", 825 | "Dun ", 826 | "Den youse gotta " 827 | ], 828 | "when": [ 829 | "* ", 830 | "Wun ", 831 | "Youse know like when " 832 | ] 833 | }, 834 | "en-au": { 835 | "and": [ 836 | "* ", 837 | "Too right " 838 | ], 839 | "background": [ 840 | "First off" 841 | ], 842 | "but": [ 843 | "* ", 844 | "Yeah nah " 845 | ], 846 | "examples": [ 847 | "You'll wanna" 848 | ], 849 | "feature": [ 850 | "Pretty much" 851 | ], 852 | "given": [ 853 | "* ", 854 | "Y'know " 855 | ], 856 | "name": "Australian", 857 | "native": "Australian", 858 | "rule": [ 859 | "Rule" 860 | ], 861 | "scenario": [ 862 | "Awww, look mate" 863 | ], 864 | "scenarioOutline": [ 865 | "Reckon it's like" 866 | ], 867 | "then": [ 868 | "* ", 869 | "But at the end of the day I reckon " 870 | ], 871 | "when": [ 872 | "* ", 873 | "It's just unbelievable " 874 | ] 875 | }, 876 | "en-lol": { 877 | "and": [ 878 | "* ", 879 | "AN " 880 | ], 881 | "background": [ 882 | "B4" 883 | ], 884 | "but": [ 885 | "* ", 886 | "BUT " 887 | ], 888 | "examples": [ 889 | "EXAMPLZ" 890 | ], 891 | "feature": [ 892 | "OH HAI" 893 | ], 894 | "given": [ 895 | "* ", 896 | "I CAN HAZ " 897 | ], 898 | "name": "LOLCAT", 899 | "native": "LOLCAT", 900 | "rule": [ 901 | "Rule" 902 | ], 903 | "scenario": [ 904 | "MISHUN" 905 | ], 906 | "scenarioOutline": [ 907 | "MISHUN SRSLY" 908 | ], 909 | "then": [ 910 | "* ", 911 | "DEN " 912 | ], 913 | "when": [ 914 | "* ", 915 | "WEN " 916 | ] 917 | }, 918 | "en-old": { 919 | "and": [ 920 | "* ", 921 | "Ond ", 922 | "7 " 923 | ], 924 | "background": [ 925 | "Aer", 926 | "Ær" 927 | ], 928 | "but": [ 929 | "* ", 930 | "Ac " 931 | ], 932 | "examples": [ 933 | "Se the", 934 | "Se þe", 935 | "Se ðe" 936 | ], 937 | "feature": [ 938 | "Hwaet", 939 | "Hwæt" 940 | ], 941 | "given": [ 942 | "* ", 943 | "Thurh ", 944 | "Þurh ", 945 | "Ðurh " 946 | ], 947 | "name": "Old English", 948 | "native": "Englisc", 949 | "rule": [ 950 | "Rule" 951 | ], 952 | "scenario": [ 953 | "Swa" 954 | ], 955 | "scenarioOutline": [ 956 | "Swa hwaer swa", 957 | "Swa hwær swa" 958 | ], 959 | "then": [ 960 | "* ", 961 | "Tha ", 962 | "Þa ", 963 | "Ða ", 964 | "Tha the ", 965 | "Þa þe ", 966 | "Ða ðe " 967 | ], 968 | "when": [ 969 | "* ", 970 | "Bæþsealf ", 971 | "Bæþsealfa ", 972 | "Bæþsealfe ", 973 | "Ciricæw ", 974 | "Ciricæwe ", 975 | "Ciricæwa " 976 | ] 977 | }, 978 | "en-pirate": { 979 | "and": [ 980 | "* ", 981 | "Aye " 982 | ], 983 | "background": [ 984 | "Yo-ho-ho" 985 | ], 986 | "but": [ 987 | "* ", 988 | "Avast! " 989 | ], 990 | "examples": [ 991 | "Dead men tell no tales" 992 | ], 993 | "feature": [ 994 | "Ahoy matey!" 995 | ], 996 | "given": [ 997 | "* ", 998 | "Gangway! " 999 | ], 1000 | "name": "Pirate", 1001 | "native": "Pirate", 1002 | "rule": [ 1003 | "Rule" 1004 | ], 1005 | "scenario": [ 1006 | "Heave to" 1007 | ], 1008 | "scenarioOutline": [ 1009 | "Shiver me timbers" 1010 | ], 1011 | "then": [ 1012 | "* ", 1013 | "Let go and haul " 1014 | ], 1015 | "when": [ 1016 | "* ", 1017 | "Blimey! " 1018 | ] 1019 | }, 1020 | "en-tx": { 1021 | "and": [ 1022 | "Come hell or high water " 1023 | ], 1024 | "background": [ 1025 | "Lemme tell y'all a story" 1026 | ], 1027 | "but": [ 1028 | "Well now hold on, I'll you what " 1029 | ], 1030 | "examples": [ 1031 | "Now that's a story longer than a cattle drive in July" 1032 | ], 1033 | "feature": [ 1034 | "This ain’t my first rodeo", 1035 | "All gussied up" 1036 | ], 1037 | "given": [ 1038 | "Fixin' to ", 1039 | "All git out " 1040 | ], 1041 | "name": "Texas", 1042 | "native": "Texas", 1043 | "rule": [ 1044 | "Rule " 1045 | ], 1046 | "scenario": [ 1047 | "All hat and no cattle" 1048 | ], 1049 | "scenarioOutline": [ 1050 | "Serious as a snake bite", 1051 | "Busy as a hound in flea season" 1052 | ], 1053 | "then": [ 1054 | "There’s no tree but bears some fruit " 1055 | ], 1056 | "when": [ 1057 | "Quick out of the chute " 1058 | ] 1059 | }, 1060 | "eo": { 1061 | "and": [ 1062 | "* ", 1063 | "Kaj " 1064 | ], 1065 | "background": [ 1066 | "Fono" 1067 | ], 1068 | "but": [ 1069 | "* ", 1070 | "Sed " 1071 | ], 1072 | "examples": [ 1073 | "Ekzemploj" 1074 | ], 1075 | "feature": [ 1076 | "Trajto" 1077 | ], 1078 | "given": [ 1079 | "* ", 1080 | "Donitaĵo ", 1081 | "Komence " 1082 | ], 1083 | "name": "Esperanto", 1084 | "native": "Esperanto", 1085 | "rule": [ 1086 | "Rule" 1087 | ], 1088 | "scenario": [ 1089 | "Ekzemplo", 1090 | "Scenaro", 1091 | "Kazo" 1092 | ], 1093 | "scenarioOutline": [ 1094 | "Konturo de la scenaro", 1095 | "Skizo", 1096 | "Kazo-skizo" 1097 | ], 1098 | "then": [ 1099 | "* ", 1100 | "Do " 1101 | ], 1102 | "when": [ 1103 | "* ", 1104 | "Se " 1105 | ] 1106 | }, 1107 | "es": { 1108 | "and": [ 1109 | "* ", 1110 | "Y ", 1111 | "E " 1112 | ], 1113 | "background": [ 1114 | "Antecedentes" 1115 | ], 1116 | "but": [ 1117 | "* ", 1118 | "Pero " 1119 | ], 1120 | "examples": [ 1121 | "Ejemplos" 1122 | ], 1123 | "feature": [ 1124 | "Característica", 1125 | "Necesidad del negocio", 1126 | "Requisito" 1127 | ], 1128 | "given": [ 1129 | "* ", 1130 | "Dado ", 1131 | "Dada ", 1132 | "Dados ", 1133 | "Dadas " 1134 | ], 1135 | "name": "Spanish", 1136 | "native": "español", 1137 | "rule": [ 1138 | "Regla", 1139 | "Regla de negocio" 1140 | ], 1141 | "scenario": [ 1142 | "Ejemplo", 1143 | "Escenario" 1144 | ], 1145 | "scenarioOutline": [ 1146 | "Esquema del escenario" 1147 | ], 1148 | "then": [ 1149 | "* ", 1150 | "Entonces " 1151 | ], 1152 | "when": [ 1153 | "* ", 1154 | "Cuando " 1155 | ] 1156 | }, 1157 | "et": { 1158 | "and": [ 1159 | "* ", 1160 | "Ja " 1161 | ], 1162 | "background": [ 1163 | "Taust" 1164 | ], 1165 | "but": [ 1166 | "* ", 1167 | "Kuid " 1168 | ], 1169 | "examples": [ 1170 | "Juhtumid" 1171 | ], 1172 | "feature": [ 1173 | "Omadus" 1174 | ], 1175 | "given": [ 1176 | "* ", 1177 | "Eeldades " 1178 | ], 1179 | "name": "Estonian", 1180 | "native": "eesti keel", 1181 | "rule": [ 1182 | "Reegel" 1183 | ], 1184 | "scenario": [ 1185 | "Juhtum", 1186 | "Stsenaarium" 1187 | ], 1188 | "scenarioOutline": [ 1189 | "Raamjuhtum", 1190 | "Raamstsenaarium" 1191 | ], 1192 | "then": [ 1193 | "* ", 1194 | "Siis " 1195 | ], 1196 | "when": [ 1197 | "* ", 1198 | "Kui " 1199 | ] 1200 | }, 1201 | "fa": { 1202 | "and": [ 1203 | "* ", 1204 | "و " 1205 | ], 1206 | "background": [ 1207 | "زمینه" 1208 | ], 1209 | "but": [ 1210 | "* ", 1211 | "اما " 1212 | ], 1213 | "examples": [ 1214 | "نمونه ها" 1215 | ], 1216 | "feature": [ 1217 | "وِیژگی" 1218 | ], 1219 | "given": [ 1220 | "* ", 1221 | "با فرض " 1222 | ], 1223 | "name": "Persian", 1224 | "native": "فارسی", 1225 | "rule": [ 1226 | "Rule" 1227 | ], 1228 | "scenario": [ 1229 | "مثال", 1230 | "سناریو" 1231 | ], 1232 | "scenarioOutline": [ 1233 | "الگوی سناریو" 1234 | ], 1235 | "then": [ 1236 | "* ", 1237 | "آنگاه " 1238 | ], 1239 | "when": [ 1240 | "* ", 1241 | "هنگامی " 1242 | ] 1243 | }, 1244 | "fi": { 1245 | "and": [ 1246 | "* ", 1247 | "Ja " 1248 | ], 1249 | "background": [ 1250 | "Tausta" 1251 | ], 1252 | "but": [ 1253 | "* ", 1254 | "Mutta " 1255 | ], 1256 | "examples": [ 1257 | "Tapaukset" 1258 | ], 1259 | "feature": [ 1260 | "Ominaisuus" 1261 | ], 1262 | "given": [ 1263 | "* ", 1264 | "Oletetaan " 1265 | ], 1266 | "name": "Finnish", 1267 | "native": "suomi", 1268 | "rule": [ 1269 | "Rule" 1270 | ], 1271 | "scenario": [ 1272 | "Tapaus" 1273 | ], 1274 | "scenarioOutline": [ 1275 | "Tapausaihio" 1276 | ], 1277 | "then": [ 1278 | "* ", 1279 | "Niin " 1280 | ], 1281 | "when": [ 1282 | "* ", 1283 | "Kun " 1284 | ] 1285 | }, 1286 | "fr": { 1287 | "and": [ 1288 | "* ", 1289 | "Et que ", 1290 | "Et qu'", 1291 | "Et " 1292 | ], 1293 | "background": [ 1294 | "Contexte" 1295 | ], 1296 | "but": [ 1297 | "* ", 1298 | "Mais que ", 1299 | "Mais qu'", 1300 | "Mais " 1301 | ], 1302 | "examples": [ 1303 | "Exemples" 1304 | ], 1305 | "feature": [ 1306 | "Fonctionnalité" 1307 | ], 1308 | "given": [ 1309 | "* ", 1310 | "Soit ", 1311 | "Sachant que ", 1312 | "Sachant qu'", 1313 | "Sachant ", 1314 | "Etant donné que ", 1315 | "Etant donné qu'", 1316 | "Etant donné ", 1317 | "Etant donnée ", 1318 | "Etant donnés ", 1319 | "Etant données ", 1320 | "Étant donné que ", 1321 | "Étant donné qu'", 1322 | "Étant donné ", 1323 | "Étant donnée ", 1324 | "Étant donnés ", 1325 | "Étant données " 1326 | ], 1327 | "name": "French", 1328 | "native": "français", 1329 | "rule": [ 1330 | "Règle" 1331 | ], 1332 | "scenario": [ 1333 | "Exemple", 1334 | "Scénario" 1335 | ], 1336 | "scenarioOutline": [ 1337 | "Plan du scénario", 1338 | "Plan du Scénario" 1339 | ], 1340 | "then": [ 1341 | "* ", 1342 | "Alors ", 1343 | "Donc " 1344 | ], 1345 | "when": [ 1346 | "* ", 1347 | "Quand ", 1348 | "Lorsque ", 1349 | "Lorsqu'" 1350 | ] 1351 | }, 1352 | "ga": { 1353 | "and": [ 1354 | "* ", 1355 | "Agus" 1356 | ], 1357 | "background": [ 1358 | "Cúlra" 1359 | ], 1360 | "but": [ 1361 | "* ", 1362 | "Ach" 1363 | ], 1364 | "examples": [ 1365 | "Samplaí" 1366 | ], 1367 | "feature": [ 1368 | "Gné" 1369 | ], 1370 | "given": [ 1371 | "* ", 1372 | "Cuir i gcás go", 1373 | "Cuir i gcás nach", 1374 | "Cuir i gcás gur", 1375 | "Cuir i gcás nár" 1376 | ], 1377 | "name": "Irish", 1378 | "native": "Gaeilge", 1379 | "rule": [ 1380 | "Rule" 1381 | ], 1382 | "scenario": [ 1383 | "Sampla", 1384 | "Cás" 1385 | ], 1386 | "scenarioOutline": [ 1387 | "Cás Achomair" 1388 | ], 1389 | "then": [ 1390 | "* ", 1391 | "Ansin" 1392 | ], 1393 | "when": [ 1394 | "* ", 1395 | "Nuair a", 1396 | "Nuair nach", 1397 | "Nuair ba", 1398 | "Nuair nár" 1399 | ] 1400 | }, 1401 | "gj": { 1402 | "and": [ 1403 | "* ", 1404 | "અને " 1405 | ], 1406 | "background": [ 1407 | "બેકગ્રાઉન્ડ" 1408 | ], 1409 | "but": [ 1410 | "* ", 1411 | "પણ " 1412 | ], 1413 | "examples": [ 1414 | "ઉદાહરણો" 1415 | ], 1416 | "feature": [ 1417 | "લક્ષણ", 1418 | "વ્યાપાર જરૂર", 1419 | "ક્ષમતા" 1420 | ], 1421 | "given": [ 1422 | "* ", 1423 | "આપેલ છે " 1424 | ], 1425 | "name": "Gujarati", 1426 | "native": "ગુજરાતી", 1427 | "rule": [ 1428 | "Rule" 1429 | ], 1430 | "scenario": [ 1431 | "ઉદાહરણ", 1432 | "સ્થિતિ" 1433 | ], 1434 | "scenarioOutline": [ 1435 | "પરિદ્દશ્ય રૂપરેખા", 1436 | "પરિદ્દશ્ય ઢાંચો" 1437 | ], 1438 | "then": [ 1439 | "* ", 1440 | "પછી " 1441 | ], 1442 | "when": [ 1443 | "* ", 1444 | "ક્યારે " 1445 | ] 1446 | }, 1447 | "gl": { 1448 | "and": [ 1449 | "* ", 1450 | "E " 1451 | ], 1452 | "background": [ 1453 | "Contexto" 1454 | ], 1455 | "but": [ 1456 | "* ", 1457 | "Mais ", 1458 | "Pero " 1459 | ], 1460 | "examples": [ 1461 | "Exemplos" 1462 | ], 1463 | "feature": [ 1464 | "Característica" 1465 | ], 1466 | "given": [ 1467 | "* ", 1468 | "Dado ", 1469 | "Dada ", 1470 | "Dados ", 1471 | "Dadas " 1472 | ], 1473 | "name": "Galician", 1474 | "native": "galego", 1475 | "rule": [ 1476 | "Rule" 1477 | ], 1478 | "scenario": [ 1479 | "Exemplo", 1480 | "Escenario" 1481 | ], 1482 | "scenarioOutline": [ 1483 | "Esbozo do escenario" 1484 | ], 1485 | "then": [ 1486 | "* ", 1487 | "Entón ", 1488 | "Logo " 1489 | ], 1490 | "when": [ 1491 | "* ", 1492 | "Cando " 1493 | ] 1494 | }, 1495 | "he": { 1496 | "and": [ 1497 | "* ", 1498 | "וגם " 1499 | ], 1500 | "background": [ 1501 | "רקע" 1502 | ], 1503 | "but": [ 1504 | "* ", 1505 | "אבל " 1506 | ], 1507 | "examples": [ 1508 | "דוגמאות" 1509 | ], 1510 | "feature": [ 1511 | "תכונה" 1512 | ], 1513 | "given": [ 1514 | "* ", 1515 | "בהינתן " 1516 | ], 1517 | "name": "Hebrew", 1518 | "native": "עברית", 1519 | "rule": [ 1520 | "כלל" 1521 | ], 1522 | "scenario": [ 1523 | "דוגמא", 1524 | "תרחיש" 1525 | ], 1526 | "scenarioOutline": [ 1527 | "תבנית תרחיש" 1528 | ], 1529 | "then": [ 1530 | "* ", 1531 | "אז ", 1532 | "אזי " 1533 | ], 1534 | "when": [ 1535 | "* ", 1536 | "כאשר " 1537 | ] 1538 | }, 1539 | "hi": { 1540 | "and": [ 1541 | "* ", 1542 | "और ", 1543 | "तथा " 1544 | ], 1545 | "background": [ 1546 | "पृष्ठभूमि" 1547 | ], 1548 | "but": [ 1549 | "* ", 1550 | "पर ", 1551 | "परन्तु ", 1552 | "किन्तु " 1553 | ], 1554 | "examples": [ 1555 | "उदाहरण" 1556 | ], 1557 | "feature": [ 1558 | "रूप लेख" 1559 | ], 1560 | "given": [ 1561 | "* ", 1562 | "अगर ", 1563 | "यदि ", 1564 | "चूंकि " 1565 | ], 1566 | "name": "Hindi", 1567 | "native": "हिंदी", 1568 | "rule": [ 1569 | "नियम" 1570 | ], 1571 | "scenario": [ 1572 | "परिदृश्य" 1573 | ], 1574 | "scenarioOutline": [ 1575 | "परिदृश्य रूपरेखा" 1576 | ], 1577 | "then": [ 1578 | "* ", 1579 | "तब ", 1580 | "तदा " 1581 | ], 1582 | "when": [ 1583 | "* ", 1584 | "जब ", 1585 | "कदा " 1586 | ] 1587 | }, 1588 | "hr": { 1589 | "and": [ 1590 | "* ", 1591 | "I " 1592 | ], 1593 | "background": [ 1594 | "Pozadina" 1595 | ], 1596 | "but": [ 1597 | "* ", 1598 | "Ali " 1599 | ], 1600 | "examples": [ 1601 | "Primjeri", 1602 | "Scenariji" 1603 | ], 1604 | "feature": [ 1605 | "Osobina", 1606 | "Mogućnost", 1607 | "Mogucnost" 1608 | ], 1609 | "given": [ 1610 | "* ", 1611 | "Zadan ", 1612 | "Zadani ", 1613 | "Zadano ", 1614 | "Ukoliko " 1615 | ], 1616 | "name": "Croatian", 1617 | "native": "hrvatski", 1618 | "rule": [ 1619 | "Rule" 1620 | ], 1621 | "scenario": [ 1622 | "Primjer", 1623 | "Scenarij" 1624 | ], 1625 | "scenarioOutline": [ 1626 | "Skica", 1627 | "Koncept" 1628 | ], 1629 | "then": [ 1630 | "* ", 1631 | "Onda " 1632 | ], 1633 | "when": [ 1634 | "* ", 1635 | "Kada ", 1636 | "Kad " 1637 | ] 1638 | }, 1639 | "ht": { 1640 | "and": [ 1641 | "* ", 1642 | "Ak ", 1643 | "Epi ", 1644 | "E " 1645 | ], 1646 | "background": [ 1647 | "Kontèks", 1648 | "Istorik" 1649 | ], 1650 | "but": [ 1651 | "* ", 1652 | "Men " 1653 | ], 1654 | "examples": [ 1655 | "Egzanp" 1656 | ], 1657 | "feature": [ 1658 | "Karakteristik", 1659 | "Mak", 1660 | "Fonksyonalite" 1661 | ], 1662 | "given": [ 1663 | "* ", 1664 | "Sipoze ", 1665 | "Sipoze ke ", 1666 | "Sipoze Ke " 1667 | ], 1668 | "name": "Creole", 1669 | "native": "kreyòl", 1670 | "rule": [ 1671 | "Rule" 1672 | ], 1673 | "scenario": [ 1674 | "Senaryo" 1675 | ], 1676 | "scenarioOutline": [ 1677 | "Plan senaryo", 1678 | "Plan Senaryo", 1679 | "Senaryo deskripsyon", 1680 | "Senaryo Deskripsyon", 1681 | "Dyagram senaryo", 1682 | "Dyagram Senaryo" 1683 | ], 1684 | "then": [ 1685 | "* ", 1686 | "Lè sa a ", 1687 | "Le sa a " 1688 | ], 1689 | "when": [ 1690 | "* ", 1691 | "Lè ", 1692 | "Le " 1693 | ] 1694 | }, 1695 | "hu": { 1696 | "and": [ 1697 | "* ", 1698 | "És " 1699 | ], 1700 | "background": [ 1701 | "Háttér" 1702 | ], 1703 | "but": [ 1704 | "* ", 1705 | "De " 1706 | ], 1707 | "examples": [ 1708 | "Példák" 1709 | ], 1710 | "feature": [ 1711 | "Jellemző" 1712 | ], 1713 | "given": [ 1714 | "* ", 1715 | "Amennyiben ", 1716 | "Adott " 1717 | ], 1718 | "name": "Hungarian", 1719 | "native": "magyar", 1720 | "rule": [ 1721 | "Szabály" 1722 | ], 1723 | "scenario": [ 1724 | "Példa", 1725 | "Forgatókönyv" 1726 | ], 1727 | "scenarioOutline": [ 1728 | "Forgatókönyv vázlat" 1729 | ], 1730 | "then": [ 1731 | "* ", 1732 | "Akkor " 1733 | ], 1734 | "when": [ 1735 | "* ", 1736 | "Majd ", 1737 | "Ha ", 1738 | "Amikor " 1739 | ] 1740 | }, 1741 | "id": { 1742 | "and": [ 1743 | "* ", 1744 | "Dan " 1745 | ], 1746 | "background": [ 1747 | "Dasar", 1748 | "Latar Belakang" 1749 | ], 1750 | "but": [ 1751 | "* ", 1752 | "Tapi ", 1753 | "Tetapi " 1754 | ], 1755 | "examples": [ 1756 | "Contoh", 1757 | "Misal" 1758 | ], 1759 | "feature": [ 1760 | "Fitur" 1761 | ], 1762 | "given": [ 1763 | "* ", 1764 | "Dengan ", 1765 | "Diketahui ", 1766 | "Diasumsikan ", 1767 | "Bila ", 1768 | "Jika " 1769 | ], 1770 | "name": "Indonesian", 1771 | "native": "Bahasa Indonesia", 1772 | "rule": [ 1773 | "Rule", 1774 | "Aturan" 1775 | ], 1776 | "scenario": [ 1777 | "Skenario" 1778 | ], 1779 | "scenarioOutline": [ 1780 | "Skenario konsep", 1781 | "Garis-Besar Skenario" 1782 | ], 1783 | "then": [ 1784 | "* ", 1785 | "Maka ", 1786 | "Kemudian " 1787 | ], 1788 | "when": [ 1789 | "* ", 1790 | "Ketika " 1791 | ] 1792 | }, 1793 | "is": { 1794 | "and": [ 1795 | "* ", 1796 | "Og " 1797 | ], 1798 | "background": [ 1799 | "Bakgrunnur" 1800 | ], 1801 | "but": [ 1802 | "* ", 1803 | "En " 1804 | ], 1805 | "examples": [ 1806 | "Dæmi", 1807 | "Atburðarásir" 1808 | ], 1809 | "feature": [ 1810 | "Eiginleiki" 1811 | ], 1812 | "given": [ 1813 | "* ", 1814 | "Ef " 1815 | ], 1816 | "name": "Icelandic", 1817 | "native": "Íslenska", 1818 | "rule": [ 1819 | "Rule" 1820 | ], 1821 | "scenario": [ 1822 | "Atburðarás" 1823 | ], 1824 | "scenarioOutline": [ 1825 | "Lýsing Atburðarásar", 1826 | "Lýsing Dæma" 1827 | ], 1828 | "then": [ 1829 | "* ", 1830 | "Þá " 1831 | ], 1832 | "when": [ 1833 | "* ", 1834 | "Þegar " 1835 | ] 1836 | }, 1837 | "it": { 1838 | "and": [ 1839 | "* ", 1840 | "E " 1841 | ], 1842 | "background": [ 1843 | "Contesto" 1844 | ], 1845 | "but": [ 1846 | "* ", 1847 | "Ma " 1848 | ], 1849 | "examples": [ 1850 | "Esempi" 1851 | ], 1852 | "feature": [ 1853 | "Funzionalità", 1854 | "Esigenza di Business", 1855 | "Abilità" 1856 | ], 1857 | "given": [ 1858 | "* ", 1859 | "Dato ", 1860 | "Data ", 1861 | "Dati ", 1862 | "Date " 1863 | ], 1864 | "name": "Italian", 1865 | "native": "italiano", 1866 | "rule": [ 1867 | "Regola" 1868 | ], 1869 | "scenario": [ 1870 | "Esempio", 1871 | "Scenario" 1872 | ], 1873 | "scenarioOutline": [ 1874 | "Schema dello scenario" 1875 | ], 1876 | "then": [ 1877 | "* ", 1878 | "Allora " 1879 | ], 1880 | "when": [ 1881 | "* ", 1882 | "Quando " 1883 | ] 1884 | }, 1885 | "ja": { 1886 | "and": [ 1887 | "* ", 1888 | "且つ", 1889 | "かつ" 1890 | ], 1891 | "background": [ 1892 | "背景" 1893 | ], 1894 | "but": [ 1895 | "* ", 1896 | "然し", 1897 | "しかし", 1898 | "但し", 1899 | "ただし" 1900 | ], 1901 | "examples": [ 1902 | "例", 1903 | "サンプル" 1904 | ], 1905 | "feature": [ 1906 | "フィーチャ", 1907 | "機能" 1908 | ], 1909 | "given": [ 1910 | "* ", 1911 | "前提" 1912 | ], 1913 | "name": "Japanese", 1914 | "native": "日本語", 1915 | "rule": [ 1916 | "ルール" 1917 | ], 1918 | "scenario": [ 1919 | "シナリオ" 1920 | ], 1921 | "scenarioOutline": [ 1922 | "シナリオアウトライン", 1923 | "シナリオテンプレート", 1924 | "テンプレ", 1925 | "シナリオテンプレ" 1926 | ], 1927 | "then": [ 1928 | "* ", 1929 | "ならば" 1930 | ], 1931 | "when": [ 1932 | "* ", 1933 | "もし" 1934 | ] 1935 | }, 1936 | "jv": { 1937 | "and": [ 1938 | "* ", 1939 | "Lan " 1940 | ], 1941 | "background": [ 1942 | "Dasar" 1943 | ], 1944 | "but": [ 1945 | "* ", 1946 | "Tapi ", 1947 | "Nanging ", 1948 | "Ananging " 1949 | ], 1950 | "examples": [ 1951 | "Conto", 1952 | "Contone" 1953 | ], 1954 | "feature": [ 1955 | "Fitur" 1956 | ], 1957 | "given": [ 1958 | "* ", 1959 | "Nalika ", 1960 | "Nalikaning " 1961 | ], 1962 | "name": "Javanese", 1963 | "native": "Basa Jawa", 1964 | "rule": [ 1965 | "Rule" 1966 | ], 1967 | "scenario": [ 1968 | "Skenario" 1969 | ], 1970 | "scenarioOutline": [ 1971 | "Konsep skenario" 1972 | ], 1973 | "then": [ 1974 | "* ", 1975 | "Njuk ", 1976 | "Banjur " 1977 | ], 1978 | "when": [ 1979 | "* ", 1980 | "Manawa ", 1981 | "Menawa " 1982 | ] 1983 | }, 1984 | "ka": { 1985 | "and": [ 1986 | "* ", 1987 | "და ", 1988 | "ასევე " 1989 | ], 1990 | "background": [ 1991 | "კონტექსტი" 1992 | ], 1993 | "but": [ 1994 | "* ", 1995 | "მაგრამ ", 1996 | "თუმცა " 1997 | ], 1998 | "examples": [ 1999 | "მაგალითები" 2000 | ], 2001 | "feature": [ 2002 | "თვისება", 2003 | "მოთხოვნა" 2004 | ], 2005 | "given": [ 2006 | "* ", 2007 | "მოცემული ", 2008 | "Მოცემულია ", 2009 | "ვთქვათ " 2010 | ], 2011 | "name": "Georgian", 2012 | "native": "ქართული", 2013 | "rule": [ 2014 | "წესი" 2015 | ], 2016 | "scenario": [ 2017 | "მაგალითად", 2018 | "მაგალითი", 2019 | "მაგ", 2020 | "სცენარი" 2021 | ], 2022 | "scenarioOutline": [ 2023 | "სცენარის ნიმუში", 2024 | "სცენარის შაბლონი", 2025 | "ნიმუში", 2026 | "შაბლონი" 2027 | ], 2028 | "then": [ 2029 | "* ", 2030 | "მაშინ " 2031 | ], 2032 | "when": [ 2033 | "* ", 2034 | "როდესაც ", 2035 | "როცა ", 2036 | "როგორც კი ", 2037 | "თუ " 2038 | ] 2039 | }, 2040 | "kn": { 2041 | "and": [ 2042 | "* ", 2043 | "ಮತ್ತು " 2044 | ], 2045 | "background": [ 2046 | "ಹಿನ್ನೆಲೆ" 2047 | ], 2048 | "but": [ 2049 | "* ", 2050 | "ಆದರೆ " 2051 | ], 2052 | "examples": [ 2053 | "ಉದಾಹರಣೆಗಳು" 2054 | ], 2055 | "feature": [ 2056 | "ಹೆಚ್ಚಳ" 2057 | ], 2058 | "given": [ 2059 | "* ", 2060 | "ನೀಡಿದ " 2061 | ], 2062 | "name": "Kannada", 2063 | "native": "ಕನ್ನಡ", 2064 | "rule": [ 2065 | "Rule" 2066 | ], 2067 | "scenario": [ 2068 | "ಉದಾಹರಣೆ", 2069 | "ಕಥಾಸಾರಾಂಶ" 2070 | ], 2071 | "scenarioOutline": [ 2072 | "ವಿವರಣೆ" 2073 | ], 2074 | "then": [ 2075 | "* ", 2076 | "ನಂತರ " 2077 | ], 2078 | "when": [ 2079 | "* ", 2080 | "ಸ್ಥಿತಿಯನ್ನು " 2081 | ] 2082 | }, 2083 | "ko": { 2084 | "and": [ 2085 | "* ", 2086 | "그리고" 2087 | ], 2088 | "background": [ 2089 | "배경" 2090 | ], 2091 | "but": [ 2092 | "* ", 2093 | "하지만", 2094 | "단" 2095 | ], 2096 | "examples": [ 2097 | "예" 2098 | ], 2099 | "feature": [ 2100 | "기능" 2101 | ], 2102 | "given": [ 2103 | "* ", 2104 | "조건", 2105 | "먼저" 2106 | ], 2107 | "name": "Korean", 2108 | "native": "한국어", 2109 | "rule": [ 2110 | "Rule" 2111 | ], 2112 | "scenario": [ 2113 | "시나리오" 2114 | ], 2115 | "scenarioOutline": [ 2116 | "시나리오 개요" 2117 | ], 2118 | "then": [ 2119 | "* ", 2120 | "그러면" 2121 | ], 2122 | "when": [ 2123 | "* ", 2124 | "만일", 2125 | "만약" 2126 | ] 2127 | }, 2128 | "lt": { 2129 | "and": [ 2130 | "* ", 2131 | "Ir " 2132 | ], 2133 | "background": [ 2134 | "Kontekstas" 2135 | ], 2136 | "but": [ 2137 | "* ", 2138 | "Bet " 2139 | ], 2140 | "examples": [ 2141 | "Pavyzdžiai", 2142 | "Scenarijai", 2143 | "Variantai" 2144 | ], 2145 | "feature": [ 2146 | "Savybė" 2147 | ], 2148 | "given": [ 2149 | "* ", 2150 | "Duota " 2151 | ], 2152 | "name": "Lithuanian", 2153 | "native": "lietuvių kalba", 2154 | "rule": [ 2155 | "Rule" 2156 | ], 2157 | "scenario": [ 2158 | "Pavyzdys", 2159 | "Scenarijus" 2160 | ], 2161 | "scenarioOutline": [ 2162 | "Scenarijaus šablonas" 2163 | ], 2164 | "then": [ 2165 | "* ", 2166 | "Tada " 2167 | ], 2168 | "when": [ 2169 | "* ", 2170 | "Kai " 2171 | ] 2172 | }, 2173 | "lu": { 2174 | "and": [ 2175 | "* ", 2176 | "an ", 2177 | "a " 2178 | ], 2179 | "background": [ 2180 | "Hannergrond" 2181 | ], 2182 | "but": [ 2183 | "* ", 2184 | "awer ", 2185 | "mä " 2186 | ], 2187 | "examples": [ 2188 | "Beispiller" 2189 | ], 2190 | "feature": [ 2191 | "Funktionalitéit" 2192 | ], 2193 | "given": [ 2194 | "* ", 2195 | "ugeholl " 2196 | ], 2197 | "name": "Luxemburgish", 2198 | "native": "Lëtzebuergesch", 2199 | "rule": [ 2200 | "Rule" 2201 | ], 2202 | "scenario": [ 2203 | "Beispill", 2204 | "Szenario" 2205 | ], 2206 | "scenarioOutline": [ 2207 | "Plang vum Szenario" 2208 | ], 2209 | "then": [ 2210 | "* ", 2211 | "dann " 2212 | ], 2213 | "when": [ 2214 | "* ", 2215 | "wann " 2216 | ] 2217 | }, 2218 | "lv": { 2219 | "and": [ 2220 | "* ", 2221 | "Un " 2222 | ], 2223 | "background": [ 2224 | "Konteksts", 2225 | "Situācija" 2226 | ], 2227 | "but": [ 2228 | "* ", 2229 | "Bet " 2230 | ], 2231 | "examples": [ 2232 | "Piemēri", 2233 | "Paraugs" 2234 | ], 2235 | "feature": [ 2236 | "Funkcionalitāte", 2237 | "Fīča" 2238 | ], 2239 | "given": [ 2240 | "* ", 2241 | "Kad " 2242 | ], 2243 | "name": "Latvian", 2244 | "native": "latviešu", 2245 | "rule": [ 2246 | "Rule" 2247 | ], 2248 | "scenario": [ 2249 | "Piemērs", 2250 | "Scenārijs" 2251 | ], 2252 | "scenarioOutline": [ 2253 | "Scenārijs pēc parauga" 2254 | ], 2255 | "then": [ 2256 | "* ", 2257 | "Tad " 2258 | ], 2259 | "when": [ 2260 | "* ", 2261 | "Ja " 2262 | ] 2263 | }, 2264 | "mk-Cyrl": { 2265 | "and": [ 2266 | "* ", 2267 | "И " 2268 | ], 2269 | "background": [ 2270 | "Контекст", 2271 | "Содржина" 2272 | ], 2273 | "but": [ 2274 | "* ", 2275 | "Но " 2276 | ], 2277 | "examples": [ 2278 | "Примери", 2279 | "Сценарија" 2280 | ], 2281 | "feature": [ 2282 | "Функционалност", 2283 | "Бизнис потреба", 2284 | "Можност" 2285 | ], 2286 | "given": [ 2287 | "* ", 2288 | "Дадено ", 2289 | "Дадена " 2290 | ], 2291 | "name": "Macedonian", 2292 | "native": "Македонски", 2293 | "rule": [ 2294 | "Rule" 2295 | ], 2296 | "scenario": [ 2297 | "Пример", 2298 | "Сценарио", 2299 | "На пример" 2300 | ], 2301 | "scenarioOutline": [ 2302 | "Преглед на сценарија", 2303 | "Скица", 2304 | "Концепт" 2305 | ], 2306 | "then": [ 2307 | "* ", 2308 | "Тогаш " 2309 | ], 2310 | "when": [ 2311 | "* ", 2312 | "Кога " 2313 | ] 2314 | }, 2315 | "mk-Latn": { 2316 | "and": [ 2317 | "* ", 2318 | "I " 2319 | ], 2320 | "background": [ 2321 | "Kontekst", 2322 | "Sodrzhina" 2323 | ], 2324 | "but": [ 2325 | "* ", 2326 | "No " 2327 | ], 2328 | "examples": [ 2329 | "Primeri", 2330 | "Scenaria" 2331 | ], 2332 | "feature": [ 2333 | "Funkcionalnost", 2334 | "Biznis potreba", 2335 | "Mozhnost" 2336 | ], 2337 | "given": [ 2338 | "* ", 2339 | "Dadeno ", 2340 | "Dadena " 2341 | ], 2342 | "name": "Macedonian (Latin)", 2343 | "native": "Makedonski (Latinica)", 2344 | "rule": [ 2345 | "Rule" 2346 | ], 2347 | "scenario": [ 2348 | "Scenario", 2349 | "Na primer" 2350 | ], 2351 | "scenarioOutline": [ 2352 | "Pregled na scenarija", 2353 | "Skica", 2354 | "Koncept" 2355 | ], 2356 | "then": [ 2357 | "* ", 2358 | "Togash " 2359 | ], 2360 | "when": [ 2361 | "* ", 2362 | "Koga " 2363 | ] 2364 | }, 2365 | "mn": { 2366 | "and": [ 2367 | "* ", 2368 | "Мөн ", 2369 | "Тэгээд " 2370 | ], 2371 | "background": [ 2372 | "Агуулга" 2373 | ], 2374 | "but": [ 2375 | "* ", 2376 | "Гэхдээ ", 2377 | "Харин " 2378 | ], 2379 | "examples": [ 2380 | "Тухайлбал" 2381 | ], 2382 | "feature": [ 2383 | "Функц", 2384 | "Функционал" 2385 | ], 2386 | "given": [ 2387 | "* ", 2388 | "Өгөгдсөн нь ", 2389 | "Анх " 2390 | ], 2391 | "name": "Mongolian", 2392 | "native": "монгол", 2393 | "rule": [ 2394 | "Rule" 2395 | ], 2396 | "scenario": [ 2397 | "Сценар" 2398 | ], 2399 | "scenarioOutline": [ 2400 | "Сценарын төлөвлөгөө" 2401 | ], 2402 | "then": [ 2403 | "* ", 2404 | "Тэгэхэд ", 2405 | "Үүний дараа " 2406 | ], 2407 | "when": [ 2408 | "* ", 2409 | "Хэрэв " 2410 | ] 2411 | }, 2412 | "ne": { 2413 | "and": [ 2414 | "* ", 2415 | "र ", 2416 | "अनि " 2417 | ], 2418 | "background": [ 2419 | "पृष्ठभूमी" 2420 | ], 2421 | "but": [ 2422 | "* ", 2423 | "तर " 2424 | ], 2425 | "examples": [ 2426 | "उदाहरण", 2427 | "उदाहरणहरु" 2428 | ], 2429 | "feature": [ 2430 | "सुविधा", 2431 | "विशेषता" 2432 | ], 2433 | "given": [ 2434 | "* ", 2435 | "दिइएको ", 2436 | "दिएको ", 2437 | "यदि " 2438 | ], 2439 | "name": "Nepali", 2440 | "native": "नेपाली", 2441 | "rule": [ 2442 | "नियम" 2443 | ], 2444 | "scenario": [ 2445 | "परिदृश्य" 2446 | ], 2447 | "scenarioOutline": [ 2448 | "परिदृश्य रूपरेखा" 2449 | ], 2450 | "then": [ 2451 | "* ", 2452 | "त्यसपछि ", 2453 | "अनी " 2454 | ], 2455 | "when": [ 2456 | "* ", 2457 | "जब " 2458 | ] 2459 | }, 2460 | "nl": { 2461 | "and": [ 2462 | "* ", 2463 | "En " 2464 | ], 2465 | "background": [ 2466 | "Achtergrond" 2467 | ], 2468 | "but": [ 2469 | "* ", 2470 | "Maar " 2471 | ], 2472 | "examples": [ 2473 | "Voorbeelden" 2474 | ], 2475 | "feature": [ 2476 | "Functionaliteit" 2477 | ], 2478 | "given": [ 2479 | "* ", 2480 | "Gegeven ", 2481 | "Stel " 2482 | ], 2483 | "name": "Dutch", 2484 | "native": "Nederlands", 2485 | "rule": [ 2486 | "Rule" 2487 | ], 2488 | "scenario": [ 2489 | "Voorbeeld", 2490 | "Scenario" 2491 | ], 2492 | "scenarioOutline": [ 2493 | "Abstract Scenario" 2494 | ], 2495 | "then": [ 2496 | "* ", 2497 | "Dan " 2498 | ], 2499 | "when": [ 2500 | "* ", 2501 | "Als ", 2502 | "Wanneer " 2503 | ] 2504 | }, 2505 | "no": { 2506 | "and": [ 2507 | "* ", 2508 | "Og " 2509 | ], 2510 | "background": [ 2511 | "Bakgrunn" 2512 | ], 2513 | "but": [ 2514 | "* ", 2515 | "Men " 2516 | ], 2517 | "examples": [ 2518 | "Eksempler" 2519 | ], 2520 | "feature": [ 2521 | "Egenskap" 2522 | ], 2523 | "given": [ 2524 | "* ", 2525 | "Gitt " 2526 | ], 2527 | "name": "Norwegian", 2528 | "native": "norsk", 2529 | "rule": [ 2530 | "Regel" 2531 | ], 2532 | "scenario": [ 2533 | "Eksempel", 2534 | "Scenario" 2535 | ], 2536 | "scenarioOutline": [ 2537 | "Scenariomal", 2538 | "Abstrakt Scenario" 2539 | ], 2540 | "then": [ 2541 | "* ", 2542 | "Så " 2543 | ], 2544 | "when": [ 2545 | "* ", 2546 | "Når " 2547 | ] 2548 | }, 2549 | "pa": { 2550 | "and": [ 2551 | "* ", 2552 | "ਅਤੇ " 2553 | ], 2554 | "background": [ 2555 | "ਪਿਛੋਕੜ" 2556 | ], 2557 | "but": [ 2558 | "* ", 2559 | "ਪਰ " 2560 | ], 2561 | "examples": [ 2562 | "ਉਦਾਹਰਨਾਂ" 2563 | ], 2564 | "feature": [ 2565 | "ਖਾਸੀਅਤ", 2566 | "ਮੁਹਾਂਦਰਾ", 2567 | "ਨਕਸ਼ ਨੁਹਾਰ" 2568 | ], 2569 | "given": [ 2570 | "* ", 2571 | "ਜੇਕਰ ", 2572 | "ਜਿਵੇਂ ਕਿ " 2573 | ], 2574 | "name": "Panjabi", 2575 | "native": "ਪੰਜਾਬੀ", 2576 | "rule": [ 2577 | "Rule" 2578 | ], 2579 | "scenario": [ 2580 | "ਉਦਾਹਰਨ", 2581 | "ਪਟਕਥਾ" 2582 | ], 2583 | "scenarioOutline": [ 2584 | "ਪਟਕਥਾ ਢਾਂਚਾ", 2585 | "ਪਟਕਥਾ ਰੂਪ ਰੇਖਾ" 2586 | ], 2587 | "then": [ 2588 | "* ", 2589 | "ਤਦ " 2590 | ], 2591 | "when": [ 2592 | "* ", 2593 | "ਜਦੋਂ " 2594 | ] 2595 | }, 2596 | "pl": { 2597 | "and": [ 2598 | "* ", 2599 | "Oraz ", 2600 | "I " 2601 | ], 2602 | "background": [ 2603 | "Założenia" 2604 | ], 2605 | "but": [ 2606 | "* ", 2607 | "Ale " 2608 | ], 2609 | "examples": [ 2610 | "Przykłady" 2611 | ], 2612 | "feature": [ 2613 | "Właściwość", 2614 | "Funkcja", 2615 | "Aspekt", 2616 | "Potrzeba biznesowa" 2617 | ], 2618 | "given": [ 2619 | "* ", 2620 | "Zakładając ", 2621 | "Mając ", 2622 | "Zakładając, że " 2623 | ], 2624 | "name": "Polish", 2625 | "native": "polski", 2626 | "rule": [ 2627 | "Zasada", 2628 | "Reguła" 2629 | ], 2630 | "scenario": [ 2631 | "Przykład", 2632 | "Scenariusz" 2633 | ], 2634 | "scenarioOutline": [ 2635 | "Szablon scenariusza" 2636 | ], 2637 | "then": [ 2638 | "* ", 2639 | "Wtedy " 2640 | ], 2641 | "when": [ 2642 | "* ", 2643 | "Jeżeli ", 2644 | "Jeśli ", 2645 | "Gdy ", 2646 | "Kiedy " 2647 | ] 2648 | }, 2649 | "pt": { 2650 | "and": [ 2651 | "* ", 2652 | "E " 2653 | ], 2654 | "background": [ 2655 | "Contexto", 2656 | "Cenário de Fundo", 2657 | "Cenario de Fundo", 2658 | "Fundo" 2659 | ], 2660 | "but": [ 2661 | "* ", 2662 | "Mas " 2663 | ], 2664 | "examples": [ 2665 | "Exemplos", 2666 | "Cenários", 2667 | "Cenarios" 2668 | ], 2669 | "feature": [ 2670 | "Funcionalidade", 2671 | "Característica", 2672 | "Caracteristica" 2673 | ], 2674 | "given": [ 2675 | "* ", 2676 | "Dado ", 2677 | "Dada ", 2678 | "Dados ", 2679 | "Dadas " 2680 | ], 2681 | "name": "Portuguese", 2682 | "native": "português", 2683 | "rule": [ 2684 | "Regra" 2685 | ], 2686 | "scenario": [ 2687 | "Exemplo", 2688 | "Cenário", 2689 | "Cenario" 2690 | ], 2691 | "scenarioOutline": [ 2692 | "Esquema do Cenário", 2693 | "Esquema do Cenario", 2694 | "Delineação do Cenário", 2695 | "Delineacao do Cenario" 2696 | ], 2697 | "then": [ 2698 | "* ", 2699 | "Então ", 2700 | "Entao " 2701 | ], 2702 | "when": [ 2703 | "* ", 2704 | "Quando " 2705 | ] 2706 | }, 2707 | "ro": { 2708 | "and": [ 2709 | "* ", 2710 | "Si ", 2711 | "Și ", 2712 | "Şi " 2713 | ], 2714 | "background": [ 2715 | "Context" 2716 | ], 2717 | "but": [ 2718 | "* ", 2719 | "Dar " 2720 | ], 2721 | "examples": [ 2722 | "Exemple" 2723 | ], 2724 | "feature": [ 2725 | "Functionalitate", 2726 | "Funcționalitate", 2727 | "Funcţionalitate" 2728 | ], 2729 | "given": [ 2730 | "* ", 2731 | "Date fiind ", 2732 | "Dat fiind ", 2733 | "Dată fiind", 2734 | "Dati fiind ", 2735 | "Dați fiind ", 2736 | "Daţi fiind " 2737 | ], 2738 | "name": "Romanian", 2739 | "native": "română", 2740 | "rule": [ 2741 | "Rule" 2742 | ], 2743 | "scenario": [ 2744 | "Exemplu", 2745 | "Scenariu" 2746 | ], 2747 | "scenarioOutline": [ 2748 | "Structura scenariu", 2749 | "Structură scenariu" 2750 | ], 2751 | "then": [ 2752 | "* ", 2753 | "Atunci " 2754 | ], 2755 | "when": [ 2756 | "* ", 2757 | "Cand ", 2758 | "Când " 2759 | ] 2760 | }, 2761 | "ru": { 2762 | "and": [ 2763 | "* ", 2764 | "И ", 2765 | "К тому же ", 2766 | "Также " 2767 | ], 2768 | "background": [ 2769 | "Предыстория", 2770 | "Контекст" 2771 | ], 2772 | "but": [ 2773 | "* ", 2774 | "Но ", 2775 | "А ", 2776 | "Иначе " 2777 | ], 2778 | "examples": [ 2779 | "Примеры" 2780 | ], 2781 | "feature": [ 2782 | "Функция", 2783 | "Функциональность", 2784 | "Функционал", 2785 | "Свойство", 2786 | "Фича" 2787 | ], 2788 | "given": [ 2789 | "* ", 2790 | "Допустим ", 2791 | "Дано ", 2792 | "Пусть " 2793 | ], 2794 | "name": "Russian", 2795 | "native": "русский", 2796 | "rule": [ 2797 | "Правило" 2798 | ], 2799 | "scenario": [ 2800 | "Пример", 2801 | "Сценарий" 2802 | ], 2803 | "scenarioOutline": [ 2804 | "Структура сценария", 2805 | "Шаблон сценария" 2806 | ], 2807 | "then": [ 2808 | "* ", 2809 | "То ", 2810 | "Затем ", 2811 | "Тогда " 2812 | ], 2813 | "when": [ 2814 | "* ", 2815 | "Когда ", 2816 | "Если " 2817 | ] 2818 | }, 2819 | "sk": { 2820 | "and": [ 2821 | "* ", 2822 | "A ", 2823 | "A tiež ", 2824 | "A taktiež ", 2825 | "A zároveň " 2826 | ], 2827 | "background": [ 2828 | "Pozadie" 2829 | ], 2830 | "but": [ 2831 | "* ", 2832 | "Ale " 2833 | ], 2834 | "examples": [ 2835 | "Príklady" 2836 | ], 2837 | "feature": [ 2838 | "Požiadavka", 2839 | "Funkcia", 2840 | "Vlastnosť" 2841 | ], 2842 | "given": [ 2843 | "* ", 2844 | "Pokiaľ ", 2845 | "Za predpokladu " 2846 | ], 2847 | "name": "Slovak", 2848 | "native": "Slovensky", 2849 | "rule": [ 2850 | "Rule" 2851 | ], 2852 | "scenario": [ 2853 | "Príklad", 2854 | "Scenár" 2855 | ], 2856 | "scenarioOutline": [ 2857 | "Náčrt Scenáru", 2858 | "Náčrt Scenára", 2859 | "Osnova Scenára" 2860 | ], 2861 | "then": [ 2862 | "* ", 2863 | "Tak ", 2864 | "Potom " 2865 | ], 2866 | "when": [ 2867 | "* ", 2868 | "Keď ", 2869 | "Ak " 2870 | ] 2871 | }, 2872 | "sl": { 2873 | "and": [ 2874 | "In ", 2875 | "Ter " 2876 | ], 2877 | "background": [ 2878 | "Kontekst", 2879 | "Osnova", 2880 | "Ozadje" 2881 | ], 2882 | "but": [ 2883 | "Toda ", 2884 | "Ampak ", 2885 | "Vendar " 2886 | ], 2887 | "examples": [ 2888 | "Primeri", 2889 | "Scenariji" 2890 | ], 2891 | "feature": [ 2892 | "Funkcionalnost", 2893 | "Funkcija", 2894 | "Možnosti", 2895 | "Moznosti", 2896 | "Lastnost", 2897 | "Značilnost" 2898 | ], 2899 | "given": [ 2900 | "Dano ", 2901 | "Podano ", 2902 | "Zaradi ", 2903 | "Privzeto " 2904 | ], 2905 | "name": "Slovenian", 2906 | "native": "Slovenski", 2907 | "rule": [ 2908 | "Rule" 2909 | ], 2910 | "scenario": [ 2911 | "Primer", 2912 | "Scenarij" 2913 | ], 2914 | "scenarioOutline": [ 2915 | "Struktura scenarija", 2916 | "Skica", 2917 | "Koncept", 2918 | "Oris scenarija", 2919 | "Osnutek" 2920 | ], 2921 | "then": [ 2922 | "Nato ", 2923 | "Potem ", 2924 | "Takrat " 2925 | ], 2926 | "when": [ 2927 | "Ko ", 2928 | "Ce ", 2929 | "Če ", 2930 | "Kadar " 2931 | ] 2932 | }, 2933 | "sr-Cyrl": { 2934 | "and": [ 2935 | "* ", 2936 | "И " 2937 | ], 2938 | "background": [ 2939 | "Контекст", 2940 | "Основа", 2941 | "Позадина" 2942 | ], 2943 | "but": [ 2944 | "* ", 2945 | "Али " 2946 | ], 2947 | "examples": [ 2948 | "Примери", 2949 | "Сценарији" 2950 | ], 2951 | "feature": [ 2952 | "Функционалност", 2953 | "Могућност", 2954 | "Особина" 2955 | ], 2956 | "given": [ 2957 | "* ", 2958 | "За дато ", 2959 | "За дате ", 2960 | "За дати " 2961 | ], 2962 | "name": "Serbian", 2963 | "native": "Српски", 2964 | "rule": [ 2965 | "Правило" 2966 | ], 2967 | "scenario": [ 2968 | "Пример", 2969 | "Сценарио", 2970 | "Пример" 2971 | ], 2972 | "scenarioOutline": [ 2973 | "Структура сценарија", 2974 | "Скица", 2975 | "Концепт" 2976 | ], 2977 | "then": [ 2978 | "* ", 2979 | "Онда " 2980 | ], 2981 | "when": [ 2982 | "* ", 2983 | "Када ", 2984 | "Кад " 2985 | ] 2986 | }, 2987 | "sr-Latn": { 2988 | "and": [ 2989 | "* ", 2990 | "I " 2991 | ], 2992 | "background": [ 2993 | "Kontekst", 2994 | "Osnova", 2995 | "Pozadina" 2996 | ], 2997 | "but": [ 2998 | "* ", 2999 | "Ali " 3000 | ], 3001 | "examples": [ 3002 | "Primeri", 3003 | "Scenariji" 3004 | ], 3005 | "feature": [ 3006 | "Funkcionalnost", 3007 | "Mogućnost", 3008 | "Mogucnost", 3009 | "Osobina" 3010 | ], 3011 | "given": [ 3012 | "* ", 3013 | "Za dato ", 3014 | "Za date ", 3015 | "Za dati " 3016 | ], 3017 | "name": "Serbian (Latin)", 3018 | "native": "Srpski (Latinica)", 3019 | "rule": [ 3020 | "Pravilo" 3021 | ], 3022 | "scenario": [ 3023 | "Scenario", 3024 | "Primer" 3025 | ], 3026 | "scenarioOutline": [ 3027 | "Struktura scenarija", 3028 | "Skica", 3029 | "Koncept" 3030 | ], 3031 | "then": [ 3032 | "* ", 3033 | "Onda " 3034 | ], 3035 | "when": [ 3036 | "* ", 3037 | "Kada ", 3038 | "Kad " 3039 | ] 3040 | }, 3041 | "sv": { 3042 | "and": [ 3043 | "* ", 3044 | "Och " 3045 | ], 3046 | "background": [ 3047 | "Bakgrund" 3048 | ], 3049 | "but": [ 3050 | "* ", 3051 | "Men " 3052 | ], 3053 | "examples": [ 3054 | "Exempel" 3055 | ], 3056 | "feature": [ 3057 | "Egenskap" 3058 | ], 3059 | "given": [ 3060 | "* ", 3061 | "Givet " 3062 | ], 3063 | "name": "Swedish", 3064 | "native": "Svenska", 3065 | "rule": [ 3066 | "Regel" 3067 | ], 3068 | "scenario": [ 3069 | "Scenario" 3070 | ], 3071 | "scenarioOutline": [ 3072 | "Abstrakt Scenario", 3073 | "Scenariomall" 3074 | ], 3075 | "then": [ 3076 | "* ", 3077 | "Så " 3078 | ], 3079 | "when": [ 3080 | "* ", 3081 | "När " 3082 | ] 3083 | }, 3084 | "ta": { 3085 | "and": [ 3086 | "* ", 3087 | "மேலும் ", 3088 | "மற்றும் " 3089 | ], 3090 | "background": [ 3091 | "பின்னணி" 3092 | ], 3093 | "but": [ 3094 | "* ", 3095 | "ஆனால் " 3096 | ], 3097 | "examples": [ 3098 | "எடுத்துக்காட்டுகள்", 3099 | "காட்சிகள்", 3100 | "நிலைமைகளில்" 3101 | ], 3102 | "feature": [ 3103 | "அம்சம்", 3104 | "வணிக தேவை", 3105 | "திறன்" 3106 | ], 3107 | "given": [ 3108 | "* ", 3109 | "கொடுக்கப்பட்ட " 3110 | ], 3111 | "name": "Tamil", 3112 | "native": "தமிழ்", 3113 | "rule": [ 3114 | "Rule" 3115 | ], 3116 | "scenario": [ 3117 | "உதாரணமாக", 3118 | "காட்சி" 3119 | ], 3120 | "scenarioOutline": [ 3121 | "காட்சி சுருக்கம்", 3122 | "காட்சி வார்ப்புரு" 3123 | ], 3124 | "then": [ 3125 | "* ", 3126 | "அப்பொழுது " 3127 | ], 3128 | "when": [ 3129 | "* ", 3130 | "எப்போது " 3131 | ] 3132 | }, 3133 | "th": { 3134 | "and": [ 3135 | "* ", 3136 | "และ " 3137 | ], 3138 | "background": [ 3139 | "แนวคิด" 3140 | ], 3141 | "but": [ 3142 | "* ", 3143 | "แต่ " 3144 | ], 3145 | "examples": [ 3146 | "ชุดของตัวอย่าง", 3147 | "ชุดของเหตุการณ์" 3148 | ], 3149 | "feature": [ 3150 | "โครงหลัก", 3151 | "ความต้องการทางธุรกิจ", 3152 | "ความสามารถ" 3153 | ], 3154 | "given": [ 3155 | "* ", 3156 | "กำหนดให้ " 3157 | ], 3158 | "name": "Thai", 3159 | "native": "ไทย", 3160 | "rule": [ 3161 | "Rule" 3162 | ], 3163 | "scenario": [ 3164 | "เหตุการณ์" 3165 | ], 3166 | "scenarioOutline": [ 3167 | "สรุปเหตุการณ์", 3168 | "โครงสร้างของเหตุการณ์" 3169 | ], 3170 | "then": [ 3171 | "* ", 3172 | "ดังนั้น " 3173 | ], 3174 | "when": [ 3175 | "* ", 3176 | "เมื่อ " 3177 | ] 3178 | }, 3179 | "te": { 3180 | "and": [ 3181 | "* ", 3182 | "మరియు " 3183 | ], 3184 | "background": [ 3185 | "నేపథ్యం" 3186 | ], 3187 | "but": [ 3188 | "* ", 3189 | "కాని " 3190 | ], 3191 | "examples": [ 3192 | "ఉదాహరణలు" 3193 | ], 3194 | "feature": [ 3195 | "గుణము" 3196 | ], 3197 | "given": [ 3198 | "* ", 3199 | "చెప్పబడినది " 3200 | ], 3201 | "name": "Telugu", 3202 | "native": "తెలుగు", 3203 | "rule": [ 3204 | "Rule" 3205 | ], 3206 | "scenario": [ 3207 | "ఉదాహరణ", 3208 | "సన్నివేశం" 3209 | ], 3210 | "scenarioOutline": [ 3211 | "కథనం" 3212 | ], 3213 | "then": [ 3214 | "* ", 3215 | "అప్పుడు " 3216 | ], 3217 | "when": [ 3218 | "* ", 3219 | "ఈ పరిస్థితిలో " 3220 | ] 3221 | }, 3222 | "tlh": { 3223 | "and": [ 3224 | "* ", 3225 | "'ej ", 3226 | "latlh " 3227 | ], 3228 | "background": [ 3229 | "mo'" 3230 | ], 3231 | "but": [ 3232 | "* ", 3233 | "'ach ", 3234 | "'a " 3235 | ], 3236 | "examples": [ 3237 | "ghantoH", 3238 | "lutmey" 3239 | ], 3240 | "feature": [ 3241 | "Qap", 3242 | "Qu'meH 'ut", 3243 | "perbogh", 3244 | "poQbogh malja'", 3245 | "laH" 3246 | ], 3247 | "given": [ 3248 | "* ", 3249 | "ghu' noblu' ", 3250 | "DaH ghu' bejlu' " 3251 | ], 3252 | "name": "Klingon", 3253 | "native": "tlhIngan", 3254 | "rule": [ 3255 | "Rule" 3256 | ], 3257 | "scenario": [ 3258 | "lut" 3259 | ], 3260 | "scenarioOutline": [ 3261 | "lut chovnatlh" 3262 | ], 3263 | "then": [ 3264 | "* ", 3265 | "vaj " 3266 | ], 3267 | "when": [ 3268 | "* ", 3269 | "qaSDI' " 3270 | ] 3271 | }, 3272 | "tr": { 3273 | "and": [ 3274 | "* ", 3275 | "Ve " 3276 | ], 3277 | "background": [ 3278 | "Geçmiş" 3279 | ], 3280 | "but": [ 3281 | "* ", 3282 | "Fakat ", 3283 | "Ama " 3284 | ], 3285 | "examples": [ 3286 | "Örnekler" 3287 | ], 3288 | "feature": [ 3289 | "Özellik" 3290 | ], 3291 | "given": [ 3292 | "* ", 3293 | "Diyelim ki " 3294 | ], 3295 | "name": "Turkish", 3296 | "native": "Türkçe", 3297 | "rule": [ 3298 | "Kural" 3299 | ], 3300 | "scenario": [ 3301 | "Örnek", 3302 | "Senaryo" 3303 | ], 3304 | "scenarioOutline": [ 3305 | "Senaryo taslağı" 3306 | ], 3307 | "then": [ 3308 | "* ", 3309 | "O zaman " 3310 | ], 3311 | "when": [ 3312 | "* ", 3313 | "Eğer ki " 3314 | ] 3315 | }, 3316 | "tt": { 3317 | "and": [ 3318 | "* ", 3319 | "Һәм ", 3320 | "Вә " 3321 | ], 3322 | "background": [ 3323 | "Кереш" 3324 | ], 3325 | "but": [ 3326 | "* ", 3327 | "Ләкин ", 3328 | "Әмма " 3329 | ], 3330 | "examples": [ 3331 | "Үрнәкләр", 3332 | "Мисаллар" 3333 | ], 3334 | "feature": [ 3335 | "Мөмкинлек", 3336 | "Үзенчәлеклелек" 3337 | ], 3338 | "given": [ 3339 | "* ", 3340 | "Әйтик " 3341 | ], 3342 | "name": "Tatar", 3343 | "native": "Татарча", 3344 | "rule": [ 3345 | "Rule" 3346 | ], 3347 | "scenario": [ 3348 | "Сценарий" 3349 | ], 3350 | "scenarioOutline": [ 3351 | "Сценарийның төзелеше" 3352 | ], 3353 | "then": [ 3354 | "* ", 3355 | "Нәтиҗәдә " 3356 | ], 3357 | "when": [ 3358 | "* ", 3359 | "Әгәр " 3360 | ] 3361 | }, 3362 | "uk": { 3363 | "and": [ 3364 | "* ", 3365 | "І ", 3366 | "А також ", 3367 | "Та " 3368 | ], 3369 | "background": [ 3370 | "Передумова" 3371 | ], 3372 | "but": [ 3373 | "* ", 3374 | "Але " 3375 | ], 3376 | "examples": [ 3377 | "Приклади" 3378 | ], 3379 | "feature": [ 3380 | "Функціонал" 3381 | ], 3382 | "given": [ 3383 | "* ", 3384 | "Припустимо ", 3385 | "Припустимо, що ", 3386 | "Нехай ", 3387 | "Дано " 3388 | ], 3389 | "name": "Ukrainian", 3390 | "native": "Українська", 3391 | "rule": [ 3392 | "Rule" 3393 | ], 3394 | "scenario": [ 3395 | "Приклад", 3396 | "Сценарій" 3397 | ], 3398 | "scenarioOutline": [ 3399 | "Структура сценарію" 3400 | ], 3401 | "then": [ 3402 | "* ", 3403 | "То ", 3404 | "Тоді " 3405 | ], 3406 | "when": [ 3407 | "* ", 3408 | "Якщо ", 3409 | "Коли " 3410 | ] 3411 | }, 3412 | "ur": { 3413 | "and": [ 3414 | "* ", 3415 | "اور " 3416 | ], 3417 | "background": [ 3418 | "پس منظر" 3419 | ], 3420 | "but": [ 3421 | "* ", 3422 | "لیکن " 3423 | ], 3424 | "examples": [ 3425 | "مثالیں" 3426 | ], 3427 | "feature": [ 3428 | "صلاحیت", 3429 | "کاروبار کی ضرورت", 3430 | "خصوصیت" 3431 | ], 3432 | "given": [ 3433 | "* ", 3434 | "اگر ", 3435 | "بالفرض ", 3436 | "فرض کیا " 3437 | ], 3438 | "name": "Urdu", 3439 | "native": "اردو", 3440 | "rule": [ 3441 | "Rule" 3442 | ], 3443 | "scenario": [ 3444 | "منظرنامہ" 3445 | ], 3446 | "scenarioOutline": [ 3447 | "منظر نامے کا خاکہ" 3448 | ], 3449 | "then": [ 3450 | "* ", 3451 | "پھر ", 3452 | "تب " 3453 | ], 3454 | "when": [ 3455 | "* ", 3456 | "جب " 3457 | ] 3458 | }, 3459 | "uz": { 3460 | "and": [ 3461 | "* ", 3462 | "Ва " 3463 | ], 3464 | "background": [ 3465 | "Тарих" 3466 | ], 3467 | "but": [ 3468 | "* ", 3469 | "Лекин ", 3470 | "Бирок ", 3471 | "Аммо " 3472 | ], 3473 | "examples": [ 3474 | "Мисоллар" 3475 | ], 3476 | "feature": [ 3477 | "Функционал" 3478 | ], 3479 | "given": [ 3480 | "* ", 3481 | "Belgilangan " 3482 | ], 3483 | "name": "Uzbek", 3484 | "native": "Узбекча", 3485 | "rule": [ 3486 | "Rule" 3487 | ], 3488 | "scenario": [ 3489 | "Сценарий" 3490 | ], 3491 | "scenarioOutline": [ 3492 | "Сценарий структураси" 3493 | ], 3494 | "then": [ 3495 | "* ", 3496 | "Унда " 3497 | ], 3498 | "when": [ 3499 | "* ", 3500 | "Агар " 3501 | ] 3502 | }, 3503 | "vi": { 3504 | "and": [ 3505 | "* ", 3506 | "Và " 3507 | ], 3508 | "background": [ 3509 | "Bối cảnh" 3510 | ], 3511 | "but": [ 3512 | "* ", 3513 | "Nhưng " 3514 | ], 3515 | "examples": [ 3516 | "Dữ liệu" 3517 | ], 3518 | "feature": [ 3519 | "Tính năng" 3520 | ], 3521 | "given": [ 3522 | "* ", 3523 | "Biết ", 3524 | "Cho " 3525 | ], 3526 | "name": "Vietnamese", 3527 | "native": "Tiếng Việt", 3528 | "rule": [ 3529 | "Rule" 3530 | ], 3531 | "scenario": [ 3532 | "Tình huống", 3533 | "Kịch bản" 3534 | ], 3535 | "scenarioOutline": [ 3536 | "Khung tình huống", 3537 | "Khung kịch bản" 3538 | ], 3539 | "then": [ 3540 | "* ", 3541 | "Thì " 3542 | ], 3543 | "when": [ 3544 | "* ", 3545 | "Khi " 3546 | ] 3547 | }, 3548 | "zh-CN": { 3549 | "and": [ 3550 | "* ", 3551 | "而且", 3552 | "并且", 3553 | "同时" 3554 | ], 3555 | "background": [ 3556 | "背景" 3557 | ], 3558 | "but": [ 3559 | "* ", 3560 | "但是" 3561 | ], 3562 | "examples": [ 3563 | "例子" 3564 | ], 3565 | "feature": [ 3566 | "功能" 3567 | ], 3568 | "given": [ 3569 | "* ", 3570 | "假如", 3571 | "假设", 3572 | "假定" 3573 | ], 3574 | "name": "Chinese simplified", 3575 | "native": "简体中文", 3576 | "rule": [ 3577 | "Rule", 3578 | "规则" 3579 | ], 3580 | "scenario": [ 3581 | "场景", 3582 | "剧本" 3583 | ], 3584 | "scenarioOutline": [ 3585 | "场景大纲", 3586 | "剧本大纲" 3587 | ], 3588 | "then": [ 3589 | "* ", 3590 | "那么" 3591 | ], 3592 | "when": [ 3593 | "* ", 3594 | "当" 3595 | ] 3596 | }, 3597 | "zh-TW": { 3598 | "and": [ 3599 | "* ", 3600 | "而且", 3601 | "並且", 3602 | "同時" 3603 | ], 3604 | "background": [ 3605 | "背景" 3606 | ], 3607 | "but": [ 3608 | "* ", 3609 | "但是" 3610 | ], 3611 | "examples": [ 3612 | "例子" 3613 | ], 3614 | "feature": [ 3615 | "功能" 3616 | ], 3617 | "given": [ 3618 | "* ", 3619 | "假如", 3620 | "假設", 3621 | "假定" 3622 | ], 3623 | "name": "Chinese traditional", 3624 | "native": "繁體中文", 3625 | "rule": [ 3626 | "Rule" 3627 | ], 3628 | "scenario": [ 3629 | "場景", 3630 | "劇本" 3631 | ], 3632 | "scenarioOutline": [ 3633 | "場景大綱", 3634 | "劇本大綱" 3635 | ], 3636 | "then": [ 3637 | "* ", 3638 | "那麼" 3639 | ], 3640 | "when": [ 3641 | "* ", 3642 | "當" 3643 | ] 3644 | }, 3645 | "mr": { 3646 | "and": [ 3647 | "* ", 3648 | "आणि ", 3649 | "तसेच " 3650 | ], 3651 | "background": [ 3652 | "पार्श्वभूमी" 3653 | ], 3654 | "but": [ 3655 | "* ", 3656 | "पण ", 3657 | "परंतु " 3658 | ], 3659 | "examples": [ 3660 | "उदाहरण" 3661 | ], 3662 | "feature": [ 3663 | "वैशिष्ट्य", 3664 | "सुविधा" 3665 | ], 3666 | "given": [ 3667 | "* ", 3668 | "जर", 3669 | "दिलेल्या प्रमाणे " 3670 | ], 3671 | "name": "Marathi", 3672 | "native": "मराठी", 3673 | "rule": [ 3674 | "नियम" 3675 | ], 3676 | "scenario": [ 3677 | "परिदृश्य" 3678 | ], 3679 | "scenarioOutline": [ 3680 | "परिदृश्य रूपरेखा" 3681 | ], 3682 | "then": [ 3683 | "* ", 3684 | "मग ", 3685 | "तेव्हा " 3686 | ], 3687 | "when": [ 3688 | "* ", 3689 | "जेव्हा " 3690 | ] 3691 | }, 3692 | "amh": { 3693 | "and": [ 3694 | "* ", 3695 | "እና " 3696 | ], 3697 | "background": [ 3698 | "ቅድመ ሁኔታ", 3699 | "መነሻ", 3700 | "መነሻ ሀሳብ" 3701 | ], 3702 | "but": [ 3703 | "* ", 3704 | "ግን " 3705 | ], 3706 | "examples": [ 3707 | "ምሳሌዎች", 3708 | "ሁናቴዎች" 3709 | ], 3710 | "feature": [ 3711 | "ስራ", 3712 | "የተፈለገው ስራ", 3713 | "የሚፈለገው ድርጊት" 3714 | ], 3715 | "given": [ 3716 | "* ", 3717 | "የተሰጠ " 3718 | ], 3719 | "name": "Amharic", 3720 | "native": "አማርኛ", 3721 | "rule": [ 3722 | "ህግ" 3723 | ], 3724 | "scenario": [ 3725 | "ምሳሌ", 3726 | "ሁናቴ" 3727 | ], 3728 | "scenarioOutline": [ 3729 | "ሁናቴ ዝርዝር", 3730 | "ሁናቴ አብነት" 3731 | ], 3732 | "then": [ 3733 | "* ", 3734 | "ከዚያ " 3735 | ], 3736 | "when": [ 3737 | "* ", 3738 | "መቼ " 3739 | ] 3740 | } 3741 | } 3742 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import generateMessages from './generateMessages' 2 | import makeSourceEnvelope from './makeSourceEnvelope' 3 | import IGherkinOptions from './IGherkinOptions' 4 | import Dialect from './Dialect' 5 | import Parser from './Parser' 6 | import AstBuilder from './AstBuilder' 7 | import TokenScanner from './TokenScanner' 8 | import * as Errors from './Errors' 9 | import compile from './pickles/compile' 10 | import DIALECTS from './gherkin-languages.json' 11 | import GherkinClassicTokenMatcher from './GherkinClassicTokenMatcher' 12 | import GherkinInMarkdownTokenMatcher from './GherkinInMarkdownTokenMatcher' 13 | 14 | const dialects = DIALECTS as Readonly<{ [key: string]: Dialect }> 15 | 16 | export { 17 | generateMessages, 18 | makeSourceEnvelope, 19 | IGherkinOptions, 20 | dialects, 21 | Dialect, 22 | Parser, 23 | AstBuilder, 24 | TokenScanner, 25 | Errors, 26 | GherkinClassicTokenMatcher, 27 | GherkinInMarkdownTokenMatcher, 28 | compile, 29 | } 30 | -------------------------------------------------------------------------------- /src/makeSourceEnvelope.ts: -------------------------------------------------------------------------------- 1 | import * as messages from '@cucumber/messages' 2 | 3 | export default function makeSourceEnvelope(data: string, uri: string): messages.Envelope { 4 | let mediaType: messages.SourceMediaType 5 | if (uri.endsWith('.feature')) { 6 | mediaType = messages.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN 7 | } else if (uri.endsWith('.md')) { 8 | mediaType = messages.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_MARKDOWN 9 | } 10 | if (!mediaType) throw new Error(`The uri (${uri}) must end with .feature or .md`) 11 | return { 12 | source: { 13 | data, 14 | uri, 15 | mediaType, 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/pickles/compile.ts: -------------------------------------------------------------------------------- 1 | import * as messages from '@cucumber/messages' 2 | import IGherkinDocument = messages.GherkinDocument 3 | 4 | const pickleStepTypeFromKeyword: { [key in messages.StepKeywordType]: messages.PickleStepType } = { 5 | [messages.StepKeywordType.UNKNOWN]: messages.PickleStepType.UNKNOWN, 6 | [messages.StepKeywordType.CONTEXT]: messages.PickleStepType.CONTEXT, 7 | [messages.StepKeywordType.ACTION]: messages.PickleStepType.ACTION, 8 | [messages.StepKeywordType.OUTCOME]: messages.PickleStepType.OUTCOME, 9 | [messages.StepKeywordType.CONJUNCTION]: null 10 | } 11 | 12 | export default function compile( 13 | gherkinDocument: IGherkinDocument, 14 | uri: string, 15 | newId: messages.IdGenerator.NewId 16 | ): readonly messages.Pickle[] { 17 | const pickles: messages.Pickle[] = [] 18 | 19 | if (gherkinDocument.feature == null) { 20 | return pickles 21 | } 22 | 23 | const feature = gherkinDocument.feature 24 | const language = feature.language 25 | const featureTags = feature.tags 26 | let featureBackgroundSteps: messages.Step[] = [] 27 | 28 | feature.children.forEach((stepsContainer) => { 29 | if (stepsContainer.background) { 30 | featureBackgroundSteps = [].concat(stepsContainer.background.steps) 31 | } else if (stepsContainer.rule) { 32 | compileRule( 33 | featureTags, 34 | featureBackgroundSteps, 35 | stepsContainer.rule, 36 | language, 37 | pickles, 38 | uri, 39 | newId 40 | ) 41 | } else if (stepsContainer.scenario.examples.length === 0) { 42 | compileScenario( 43 | featureTags, 44 | featureBackgroundSteps, 45 | stepsContainer.scenario, 46 | language, 47 | pickles, 48 | uri, 49 | newId 50 | ) 51 | } else { 52 | compileScenarioOutline( 53 | featureTags, 54 | featureBackgroundSteps, 55 | stepsContainer.scenario, 56 | language, 57 | pickles, 58 | uri, 59 | newId 60 | ) 61 | } 62 | }) 63 | return pickles 64 | } 65 | 66 | function compileRule( 67 | featureTags: readonly messages.Tag[], 68 | featureBackgroundSteps: readonly messages.Step[], 69 | rule: messages.Rule, 70 | language: string, 71 | pickles: messages.Pickle[], 72 | uri: string, 73 | newId: messages.IdGenerator.NewId 74 | ) { 75 | let ruleBackgroundSteps = [].concat(featureBackgroundSteps) 76 | 77 | const tags = [].concat(featureTags).concat(rule.tags) 78 | 79 | rule.children.forEach((stepsContainer) => { 80 | if (stepsContainer.background) { 81 | ruleBackgroundSteps = ruleBackgroundSteps.concat(stepsContainer.background.steps) 82 | } else if (stepsContainer.scenario.examples.length === 0) { 83 | compileScenario( 84 | tags, 85 | ruleBackgroundSteps, 86 | stepsContainer.scenario, 87 | language, 88 | pickles, 89 | uri, 90 | newId 91 | ) 92 | } else { 93 | compileScenarioOutline( 94 | tags, 95 | ruleBackgroundSteps, 96 | stepsContainer.scenario, 97 | language, 98 | pickles, 99 | uri, 100 | newId 101 | ) 102 | } 103 | }) 104 | } 105 | 106 | function compileScenario( 107 | inheritedTags: readonly messages.Tag[], 108 | backgroundSteps: readonly messages.Step[], 109 | scenario: messages.Scenario, 110 | language: string, 111 | pickles: messages.Pickle[], 112 | uri: string, 113 | newId: messages.IdGenerator.NewId 114 | ) { 115 | let lastKeywordType = messages.StepKeywordType.UNKNOWN 116 | const steps = [] as messages.PickleStep[] 117 | 118 | if (scenario.steps.length !== 0) { 119 | backgroundSteps.forEach((step) => { 120 | lastKeywordType = (step.keywordType === messages.StepKeywordType.CONJUNCTION) ? 121 | lastKeywordType : step.keywordType 122 | steps.push(pickleStep(step, [], null, newId, lastKeywordType)) 123 | }) 124 | } 125 | 126 | const tags = [].concat(inheritedTags).concat(scenario.tags) 127 | 128 | scenario.steps.forEach((step) => { 129 | lastKeywordType = (step.keywordType === messages.StepKeywordType.CONJUNCTION) ? 130 | lastKeywordType : step.keywordType 131 | steps.push(pickleStep(step, [], null, newId, lastKeywordType)) 132 | }) 133 | 134 | const pickle: messages.Pickle = { 135 | id: newId(), 136 | uri, 137 | astNodeIds: [scenario.id], 138 | tags: pickleTags(tags), 139 | name: scenario.name, 140 | language, 141 | steps, 142 | } 143 | pickles.push(pickle) 144 | } 145 | 146 | function compileScenarioOutline( 147 | inheritedTags: readonly messages.Tag[], 148 | backgroundSteps: readonly messages.Step[], 149 | scenario: messages.Scenario, 150 | language: string, 151 | pickles: messages.Pickle[], 152 | uri: string, 153 | newId: messages.IdGenerator.NewId 154 | ) { 155 | scenario.examples 156 | .filter((e) => e.tableHeader) 157 | .forEach((examples) => { 158 | const variableCells = examples.tableHeader.cells 159 | examples.tableBody.forEach((valuesRow) => { 160 | let lastKeywordType = messages.StepKeywordType.UNKNOWN 161 | const steps = [] as messages.PickleStep[] 162 | if (scenario.steps.length !== 0) { 163 | backgroundSteps.forEach((step) => { 164 | lastKeywordType = (step.keywordType === messages.StepKeywordType.CONJUNCTION) ? 165 | lastKeywordType : step.keywordType 166 | steps.push(pickleStep(step, [], null, newId, lastKeywordType)) 167 | }) 168 | } 169 | 170 | scenario.steps.forEach((scenarioOutlineStep) => { 171 | lastKeywordType = (scenarioOutlineStep.keywordType === messages.StepKeywordType.CONJUNCTION) ? 172 | lastKeywordType : scenarioOutlineStep.keywordType 173 | const step = pickleStep(scenarioOutlineStep, variableCells, valuesRow, newId, lastKeywordType) 174 | steps.push(step) 175 | }) 176 | 177 | const id = newId() 178 | const tags = pickleTags( 179 | [].concat(inheritedTags).concat(scenario.tags).concat(examples.tags) 180 | ) 181 | 182 | pickles.push({ 183 | id, 184 | uri, 185 | astNodeIds: [scenario.id, valuesRow.id], 186 | name: interpolate(scenario.name, variableCells, valuesRow.cells), 187 | language, 188 | steps, 189 | tags, 190 | }) 191 | }) 192 | }) 193 | } 194 | 195 | function createPickleArguments( 196 | step: messages.Step, 197 | variableCells: readonly messages.TableCell[], 198 | valueCells: readonly messages.TableCell[] 199 | ): messages.PickleStepArgument | undefined { 200 | if (step.dataTable) { 201 | const argument = step.dataTable 202 | const table: messages.PickleTable = { 203 | rows: argument.rows.map((row) => { 204 | return { 205 | cells: row.cells.map((cell) => { 206 | return { 207 | value: interpolate(cell.value, variableCells, valueCells), 208 | } 209 | }), 210 | } 211 | }), 212 | } 213 | return { dataTable: table } 214 | } else if (step.docString) { 215 | const argument = step.docString 216 | const docString: messages.PickleDocString = { 217 | content: interpolate(argument.content, variableCells, valueCells), 218 | } 219 | if (argument.mediaType) { 220 | docString.mediaType = interpolate(argument.mediaType, variableCells, valueCells) 221 | } 222 | return { docString } 223 | } 224 | } 225 | 226 | function interpolate( 227 | name: string, 228 | variableCells: readonly messages.TableCell[], 229 | valueCells: readonly messages.TableCell[] 230 | ) { 231 | variableCells.forEach((variableCell, n) => { 232 | const valueCell = valueCells[n] 233 | const valuePattern = '<' + variableCell.value + '>' 234 | const escapedPattern = valuePattern.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') 235 | const regexp = new RegExp(escapedPattern, 'g') 236 | // JS Specific - dollar sign needs to be escaped with another dollar sign 237 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter 238 | const replacement = valueCell.value.replace(new RegExp('\\$', 'g'), '$$$$') 239 | name = name.replace(regexp, replacement) 240 | }) 241 | return name 242 | } 243 | 244 | function pickleStep( 245 | step: messages.Step, 246 | variableCells: readonly messages.TableCell[], 247 | valuesRow: messages.TableRow | null, 248 | newId: messages.IdGenerator.NewId, 249 | keywordType: messages.StepKeywordType 250 | ): messages.PickleStep { 251 | const astNodeIds = [step.id] 252 | if (valuesRow) { 253 | astNodeIds.push(valuesRow.id) 254 | } 255 | const valueCells = valuesRow ? valuesRow.cells : [] 256 | 257 | return { 258 | id: newId(), 259 | text: interpolate(step.text, variableCells, valueCells), 260 | type: pickleStepTypeFromKeyword[keywordType], 261 | argument: createPickleArguments(step, variableCells, valueCells), 262 | astNodeIds: astNodeIds, 263 | } 264 | } 265 | 266 | function pickleTags(tags: messages.Tag[]): readonly messages.PickleTag[] { 267 | return tags.map(pickleTag) 268 | } 269 | 270 | function pickleTag(tag: messages.Tag): messages.PickleTag { 271 | return { 272 | name: tag.name, 273 | astNodeId: tag.id, 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /test/ErrorsTest.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import { AstBuilderException } from '../src/Errors' 3 | 4 | describe('Errors', () => { 5 | it('AstBuilderException is an instance of AstBuilderException', () => { 6 | const error = AstBuilderException.create('hello', { line: 1, column: 2 }) 7 | assert(error instanceof AstBuilderException) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /test/GherkinClassicTokenMatcherTest.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import GherkinClassicTokenMatcher from '../src/GherkinClassicTokenMatcher' 3 | import { NoSuchLanguageException } from '../src/Errors' 4 | import * as messages from '@cucumber/messages' 5 | import GherkinLine from '../src/GherkinLine' 6 | import { Token, TokenType } from '../src/Parser' 7 | 8 | describe('TokenMatcher', function () { 9 | it('throws for invalid languages', function () { 10 | assert.throws( 11 | () => new GherkinClassicTokenMatcher('en-US'), 12 | NoSuchLanguageException.create('en-US') 13 | ) 14 | }) 15 | 16 | it('tokenizes FeatureLine', () => { 17 | const tm = new GherkinClassicTokenMatcher() 18 | const location: messages.Location = { line: 1, column: 1 } 19 | const line = new GherkinLine('Feature: hello', location.line) 20 | const token = new Token(line, location) 21 | assert(tm.match_FeatureLine(token)) 22 | assert.strictEqual(token.matchedType, TokenType.FeatureLine) 23 | }) 24 | 25 | it('matches tags', () => { 26 | const tm = new GherkinClassicTokenMatcher() 27 | const gl = new GherkinLine(' @foo:bar @zap🥒yo', 1) 28 | assert.deepStrictEqual(tm.getTags(gl), [ 29 | { column: 4, text: '@foo:bar' }, 30 | { column: 14, text: '@zap🥒yo' }, 31 | ]) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /test/GherkinInMarkdownTokenMatcherTest.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import GherkinLine from '../src/GherkinLine' 3 | import * as messages from '@cucumber/messages' 4 | import { Token, TokenType } from '../src/Parser' 5 | import GherkinInMarkdownTokenMatcher from '../src/GherkinInMarkdownTokenMatcher' 6 | import ITokenMatcher from '../src/ITokenMatcher' 7 | import { Item } from '../src/IToken' 8 | 9 | describe('GherkinInMarkdownTokenMatcher', function () { 10 | let tm: ITokenMatcher 11 | let location: messages.Location 12 | 13 | beforeEach(() => { 14 | tm = new GherkinInMarkdownTokenMatcher('en') 15 | location = { line: 1, column: 1 } 16 | }) 17 | 18 | it('matches FeatureLine', () => { 19 | const line = new GherkinLine('## Feature: hello', location.line) 20 | const token = new Token(line, location) 21 | assert(tm.match_FeatureLine(token)) 22 | assert.strictEqual(token.matchedType, TokenType.FeatureLine) 23 | assert.strictEqual(token.matchedKeyword, 'Feature') 24 | assert.strictEqual(token.matchedText, 'hello') 25 | }) 26 | 27 | it('matches FeatureLine in French', () => { 28 | tm = new GherkinInMarkdownTokenMatcher('fr') 29 | const line = new GherkinLine('## Fonctionnalité: hello', location.line) 30 | const token = new Token(line, location) 31 | assert(tm.match_FeatureLine(token)) 32 | assert.strictEqual(token.matchedType, TokenType.FeatureLine) 33 | assert.strictEqual(token.matchedKeyword, 'Fonctionnalité') 34 | assert.strictEqual(token.matchedText, 'hello') 35 | }) 36 | 37 | it('matches bullet Step', () => { 38 | const line = new GherkinLine(' * Given I have 3 cukes', location.line) 39 | const token = new Token(line, location) 40 | assert(tm.match_StepLine(token)) 41 | assert.strictEqual(token.matchedType, TokenType.StepLine) 42 | assert.strictEqual(token.matchedKeyword, 'Given ') 43 | assert.strictEqual(token.matchedText, 'I have 3 cukes') 44 | assert.strictEqual(token.location.column, 6) 45 | }) 46 | 47 | it('matches plus Step', () => { 48 | const line = new GherkinLine(' + Given I have 3 cukes', location.line) 49 | const token = new Token(line, location) 50 | assert(tm.match_StepLine(token)) 51 | assert.strictEqual(token.matchedType, TokenType.StepLine) 52 | assert.strictEqual(token.matchedKeyword, 'Given ') 53 | assert.strictEqual(token.matchedText, 'I have 3 cukes') 54 | assert.strictEqual(token.location.column, 6) 55 | }) 56 | 57 | it('matches hyphen Step', () => { 58 | const line = new GherkinLine(' - Given I have 3 cukes', location.line) 59 | const token = new Token(line, location) 60 | assert(tm.match_StepLine(token)) 61 | assert.strictEqual(token.matchedType, TokenType.StepLine) 62 | assert.strictEqual(token.matchedKeyword, 'Given ') 63 | assert.strictEqual(token.matchedText, 'I have 3 cukes') 64 | assert.strictEqual(token.location.column, 6) 65 | }) 66 | 67 | it('matches arbitrary text as Other', () => { 68 | const line = new GherkinLine('Whatever', location.line) 69 | const token = new Token(line, location) 70 | assert(tm.match_Other(token)) 71 | assert.strictEqual(token.matchedType, TokenType.Other) 72 | }) 73 | 74 | it('matches a non-keyword line as Other', () => { 75 | const line = new GherkinLine('whatever Given', location.line) 76 | const token = new Token(line, location) 77 | assert(tm.match_Other(token)) 78 | assert.strictEqual(token.matchedType, TokenType.Other) 79 | }) 80 | 81 | it('matches a non-keyword bullet line as Other', () => { 82 | const line = new GherkinLine('* whatever Given', location.line) 83 | const token = new Token(line, location) 84 | assert(tm.match_Other(token)) 85 | assert.strictEqual(token.matchedType, TokenType.Other) 86 | }) 87 | 88 | it('matches a non-keyword header line as Other', () => { 89 | const line = new GherkinLine('## The world is wet', location.line) 90 | const token = new Token(line, location) 91 | assert(tm.match_Other(token)) 92 | assert.strictEqual(token.matchedType, TokenType.Other) 93 | }) 94 | 95 | it('matches ``` docstring separator', () => { 96 | const line = new GherkinLine(' ```somefink', location.line) 97 | const token = new Token(line, location) 98 | assert(tm.match_DocStringSeparator(token)) 99 | assert.strictEqual(token.matchedType, TokenType.DocStringSeparator) 100 | assert.strictEqual(token.matchedKeyword, '```') 101 | assert.strictEqual(token.matchedText, 'somefink') 102 | }) 103 | 104 | it('matches ```` docstring separator', () => { 105 | const t1 = new Token(new GherkinLine(' ````', location.line), location) 106 | assert(tm.match_DocStringSeparator(t1)) 107 | assert.strictEqual(t1.matchedType, TokenType.DocStringSeparator) 108 | assert.strictEqual(t1.matchedKeyword, '````') 109 | assert.strictEqual(t1.matchedIndent, 2) 110 | assert.strictEqual(t1.matchedText, '') 111 | 112 | const t2 = new Token(new GherkinLine(' ```', location.line), location) 113 | assert(tm.match_Other(t2)) 114 | assert.strictEqual(t2.matchedType, TokenType.Other) 115 | assert.strictEqual(t2.matchedKeyword, undefined) 116 | assert.strictEqual(t2.matchedText, '```') 117 | 118 | const t3 = new Token(new GherkinLine(' ````', location.line), location) 119 | assert(tm.match_DocStringSeparator(t3)) 120 | assert.strictEqual(t3.matchedType, TokenType.DocStringSeparator) 121 | assert.strictEqual(t3.matchedKeyword, '````') 122 | assert.strictEqual(t2.matchedIndent, 2) 123 | assert.strictEqual(t3.matchedText, '') 124 | }) 125 | 126 | it('matches table row indented 2 spaces', () => { 127 | const t = new Token(new GherkinLine(' |foo|bar|', location.line), location) 128 | assert(tm.match_TableRow(t)) 129 | assert.strictEqual(t.matchedType, TokenType.TableRow) 130 | assert.strictEqual(t.matchedKeyword, '|') 131 | const expectedItems: Item[] = [ 132 | { column: 4, text: 'foo' }, 133 | { column: 8, text: 'bar' }, 134 | ] 135 | assert.deepStrictEqual(t.matchedItems, expectedItems) 136 | }) 137 | 138 | it('matches table row indented 5 spaces', () => { 139 | const t = new Token(new GherkinLine(' |foo|bar|', location.line), location) 140 | assert(tm.match_TableRow(t)) 141 | assert.strictEqual(t.matchedType, TokenType.TableRow) 142 | assert.strictEqual(t.matchedKeyword, '|') 143 | const expectedItems: Item[] = [ 144 | { column: 7, text: 'foo' }, 145 | { column: 11, text: 'bar' }, 146 | ] 147 | assert.deepStrictEqual(t.matchedItems, expectedItems) 148 | }) 149 | 150 | it('does not matche table cells indented 1 space', () => { 151 | const t = new Token(new GherkinLine(' |foo|bar|', location.line), location) 152 | assert(!tm.match_TableRow(t)) 153 | }) 154 | 155 | it('does not matche table cells indented 6 spaces', () => { 156 | const t = new Token(new GherkinLine(' |foo|bar|', location.line), location) 157 | assert(!tm.match_TableRow(t)) 158 | }) 159 | 160 | it('matches table separator row as comment', () => { 161 | assert(tm.match_TableRow(new Token(new GherkinLine(' | h1 | h2 |', location.line), location))) 162 | 163 | const t2 = new Token(new GherkinLine(' | --- | --- |', location.line), location) 164 | assert(!tm.match_TableRow(t2)) 165 | assert(tm.match_Comment(t2)) 166 | }) 167 | 168 | it('matches indented tags', () => { 169 | const t = new Token(new GherkinLine(' `@foo` `@bar`', location.line), location) 170 | assert(tm.match_TagLine(t)) 171 | assert.strictEqual(t.matchedType, TokenType.TagLine) 172 | const expectedItems: Item[] = [ 173 | { column: 4, text: '@foo' }, 174 | { column: 11, text: '@bar' }, 175 | ] 176 | assert.deepStrictEqual(t.matchedItems, expectedItems) 177 | }) 178 | 179 | it('matches unindented tags', () => { 180 | const t = new Token(new GherkinLine('`@foo` `@bar`', location.line), location) 181 | assert(tm.match_TagLine(t)) 182 | assert.strictEqual(t.matchedType, TokenType.TagLine) 183 | const expectedItems: Item[] = [ 184 | { column: 2, text: '@foo' }, 185 | { column: 11, text: '@bar' }, 186 | ] 187 | assert.deepStrictEqual(t.matchedItems, expectedItems) 188 | }) 189 | }) 190 | -------------------------------------------------------------------------------- /test/GherkinLineTest.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import GherkinLine from '../src/GherkinLine' 3 | 4 | describe('GherkinLine', () => { 5 | describe('#getTableCells', () => { 6 | function getCellsText(line: string) { 7 | const gl = new GherkinLine(line, 1) 8 | 9 | return gl.getTableCells().map((span) => span.text) 10 | } 11 | 12 | it('trims white spaces before cell content', () => { 13 | assert.deepStrictEqual(getCellsText('| \t spaces before|'), ['spaces before']) 14 | }) 15 | 16 | it('trims white spaces after cell content', () => { 17 | assert.deepStrictEqual(getCellsText('|spaces after |'), ['spaces after']) 18 | }) 19 | 20 | it('trims white spaces around cell content', () => { 21 | assert.deepStrictEqual(getCellsText('| \t spaces everywhere \t|'), ['spaces everywhere']) 22 | }) 23 | 24 | it('does not delete white spaces inside a cell', () => { 25 | assert.deepStrictEqual(getCellsText('| foo()\n bar\nbaz |'), ['foo()\n bar\nbaz']) 26 | }) 27 | }) 28 | 29 | describe('#match', () => { 30 | it('provides capture groups', () => { 31 | const gl = new GherkinLine('#### Scenario: hello', 1) 32 | const match = gl.match(/(##?#?#?) (Scenario):(.*)/) 33 | assert.strictEqual(match[1], '####') 34 | assert.strictEqual(match[2], 'Scenario') 35 | assert.strictEqual(match[3], ' hello') 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /test/ParserTest.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import * as messages from '@cucumber/messages' 3 | import AstBuilder from '../src/AstBuilder' 4 | import Parser from '../src/Parser' 5 | import GherkinClassicTokenMatcher from '../src/GherkinClassicTokenMatcher' 6 | import AstNode from '../src/AstNode' 7 | import generateMessages from '../src/generateMessages' 8 | import GherkinInMarkdownTokenMatcher from '../src/GherkinInMarkdownTokenMatcher' 9 | 10 | describe('Parser', function () { 11 | describe('with Gherkin Classic', () => { 12 | let parser: Parser 13 | beforeEach( 14 | () => 15 | (parser = new Parser( 16 | new AstBuilder(messages.IdGenerator.incrementing()), 17 | new GherkinClassicTokenMatcher() 18 | )) 19 | ) 20 | 21 | it('parses a simple feature', function () { 22 | const ast = parser.parse('Feature: hello') 23 | const gherkinDocument: messages.GherkinDocument = { 24 | feature: { 25 | description: '', 26 | tags: [], 27 | location: { line: 1, column: 1 }, 28 | language: 'en', 29 | keyword: 'Feature', 30 | name: 'hello', 31 | children: [], 32 | }, 33 | comments: [], 34 | } 35 | assert.deepStrictEqual(ast, gherkinDocument) 36 | }) 37 | 38 | it('parses multiple features', function () { 39 | const ast1 = parser.parse('Feature: hello') 40 | const ast2 = parser.parse('Feature: hello again') 41 | 42 | const gherkinDocument1: messages.GherkinDocument = { 43 | feature: { 44 | tags: [], 45 | description: '', 46 | location: { line: 1, column: 1 }, 47 | language: 'en', 48 | keyword: 'Feature', 49 | name: 'hello', 50 | children: [], 51 | }, 52 | comments: [], 53 | } 54 | assert.deepStrictEqual(ast1, gherkinDocument1) 55 | const gherkinDocument2: messages.GherkinDocument = { 56 | feature: { 57 | tags: [], 58 | description: '', 59 | location: { line: 1, column: 1 }, 60 | language: 'en', 61 | keyword: 'Feature', 62 | name: 'hello again', 63 | children: [], 64 | }, 65 | comments: [], 66 | } 67 | assert.deepStrictEqual(ast2, gherkinDocument2) 68 | }) 69 | 70 | it('parses a feature description', function () { 71 | const ast = parser.parse(`Feature: hello 72 | This is the 73 | description 74 | `) 75 | 76 | const gherkinDocument: messages.GherkinDocument = { 77 | feature: { 78 | tags: [], 79 | description: ' This is the\n description', 80 | location: { line: 1, column: 1 }, 81 | language: 'en', 82 | keyword: 'Feature', 83 | name: 'hello', 84 | children: [], 85 | }, 86 | comments: [], 87 | } 88 | assert.deepStrictEqual(ast, gherkinDocument) 89 | }) 90 | 91 | it('parses feature after parse error', function () { 92 | let ast: messages.GherkinDocument 93 | try { 94 | parser.parse( 95 | '# a comment\n' + 96 | 'Feature: Foo\n' + 97 | ' Scenario: Bar\n' + 98 | ' Given x\n' + 99 | ' ```\n' + 100 | ' unclosed docstring\n' 101 | ) 102 | } catch (expected) { 103 | ast = parser.parse( 104 | 'Feature: Foo\n' + 105 | ' Scenario: Bar\n' + 106 | ' Given x\n' + 107 | ' """\n' + 108 | ' closed docstring\n' + 109 | ' """' 110 | ) 111 | } 112 | 113 | const gherkinDocument: messages.GherkinDocument = { 114 | feature: { 115 | tags: [], 116 | description: '', 117 | location: { line: 1, column: 1 }, 118 | language: 'en', 119 | keyword: 'Feature', 120 | name: 'Foo', 121 | children: [ 122 | { 123 | scenario: { 124 | id: '1', 125 | description: '', 126 | examples: [], 127 | keyword: 'Scenario', 128 | location: { line: 2, column: 3 }, 129 | name: 'Bar', 130 | steps: [ 131 | { 132 | id: '0', 133 | dataTable: undefined, 134 | docString: { 135 | content: 'closed docstring', 136 | delimiter: '"""', 137 | location: { line: 4, column: 7 }, 138 | }, 139 | keyword: 'Given ', 140 | keywordType: messages.StepKeywordType.CONTEXT, 141 | location: { line: 3, column: 5 }, 142 | text: 'x', 143 | }, 144 | ], 145 | tags: [], 146 | }, 147 | }, 148 | ], 149 | }, 150 | comments: [], 151 | } 152 | assert.deepStrictEqual(ast, gherkinDocument) 153 | }) 154 | 155 | it('interpolates data tables', function () { 156 | const envelopes = generateMessages( 157 | 'Feature: Foo\n' + 158 | ' Scenario Outline: Parenthesis\n' + 159 | ' Given the thing and has \n' + 160 | ' Examples:\n' + 161 | ' | is (not) triggered | value |\n' + 162 | ' | is triggered | foo |\n ', 163 | '', 164 | messages.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN, 165 | { includePickles: true, newId: messages.IdGenerator.incrementing() } 166 | ) 167 | 168 | const pickle = envelopes.find((envelope) => envelope.pickle).pickle 169 | 170 | assert.strictEqual(pickle.steps[0].text, 'the thing is triggered and has foo') 171 | }) 172 | 173 | it('can change the default language', function () { 174 | const matcher = new GherkinClassicTokenMatcher('no') 175 | const parser = new Parser(new AstBuilder(messages.IdGenerator.incrementing()), matcher) 176 | const ast = parser.parse('Egenskap: i18n support') 177 | const gherkinDocument: messages.GherkinDocument = { 178 | feature: { 179 | tags: [], 180 | description: '', 181 | location: { line: 1, column: 1 }, 182 | language: 'no', 183 | keyword: 'Egenskap', 184 | name: 'i18n support', 185 | children: [], 186 | }, 187 | comments: [], 188 | } 189 | assert.deepStrictEqual(ast, gherkinDocument) 190 | }) 191 | }) 192 | 193 | describe('with Gherkin In Markdown', () => { 194 | let parser: Parser 195 | beforeEach( 196 | () => 197 | (parser = new Parser( 198 | new AstBuilder(messages.IdGenerator.incrementing()), 199 | new GherkinInMarkdownTokenMatcher() 200 | )) 201 | ) 202 | 203 | it('does not parse a feature description', function () { 204 | const ast = parser.parse(`# Feature: hello 205 | This is the 206 | description 207 | `) 208 | 209 | const gherkinDocument: messages.GherkinDocument = { 210 | feature: { 211 | tags: [], 212 | description: '', 213 | location: { line: 1, column: 3 }, 214 | language: 'en', 215 | keyword: 'Feature', 216 | name: 'hello', 217 | children: [], 218 | }, 219 | comments: [], 220 | } 221 | assert.deepStrictEqual(ast, gherkinDocument) 222 | }) 223 | 224 | it('parses a feature without a # Feature header', function () { 225 | const ast = parser.parse(`# Hello 226 | This is the 227 | description 228 | 229 | ## Scenario: hello 230 | + Given a step 231 | 232 | ## Some other header 233 | `) 234 | 235 | const gherkinDocument: messages.GherkinDocument = { 236 | feature: { 237 | tags: [], 238 | location: { 239 | line: 1, 240 | column: 1, 241 | }, 242 | language: 'en', 243 | keyword: undefined, 244 | name: '# Hello', 245 | description: '', 246 | children: [ 247 | { 248 | scenario: { 249 | id: '1', 250 | tags: [], 251 | location: { 252 | line: 5, 253 | column: 4, 254 | }, 255 | keyword: 'Scenario', 256 | name: 'hello', 257 | description: '', 258 | steps: [ 259 | { 260 | id: '0', 261 | location: { 262 | line: 6, 263 | column: 3, 264 | }, 265 | keyword: 'Given ', 266 | keywordType: messages.StepKeywordType.CONTEXT, 267 | text: 'a step', 268 | dataTable: undefined, 269 | docString: undefined, 270 | }, 271 | ], 272 | examples: [], 273 | }, 274 | }, 275 | ], 276 | }, 277 | comments: [], 278 | } 279 | assert.deepStrictEqual(ast, gherkinDocument) 280 | }) 281 | 282 | it('parses DocString', function () { 283 | const markdown = ` 284 | # Feature: DocString variations 285 | ## Scenario: minimalistic 286 | * And a DocString with an escaped alternative separator inside 287 | \`\`\`\` 288 | \`\`\`what 289 | \`\`\`\` 290 | ` 291 | const envelopes = generateMessages( 292 | markdown, 293 | 'test.md', 294 | messages.SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_MARKDOWN, 295 | { 296 | includePickles: true, 297 | includeGherkinDocument: true, 298 | newId: messages.IdGenerator.incrementing(), 299 | } 300 | ) 301 | 302 | const pickle = envelopes.find((envelope) => envelope.pickle).pickle 303 | 304 | assert.strictEqual( 305 | pickle.steps[0].text, 306 | 'a DocString with an escaped alternative separator inside' 307 | ) 308 | 309 | assert.strictEqual(pickle.steps[0].argument.docString.content, '```what') 310 | }) 311 | }) 312 | }) 313 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.build.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": [ 8 | "package.json", 9 | "src/gherkin-languages.json", 10 | "src", 11 | "test" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } 4 | --------------------------------------------------------------------------------