import DIALECTS from './gherkin-languages.json' import Dialect from './Dialect' import { NoSuchLanguageException } from './Errors' import { messages } from 'cucumber-messages' import Token from './Token' import { TokenType } from './Parser' const DIALECT_DICT: { [key: string]: Dialect } = DIALECTS const LANGUAGE_PATTERN = /^\s*#\s*language\s*:\s*([a-zA-Z\-_]+)\s*$/ export default class TokenMatcher { private dialect: Dialect private dialectName: string private activeDocStringSeparator: string private indentToRemove: number constructor(private readonly defaultDialectName: string = 'en') { this.reset() } public changeDialect(newDialectName: string, location?: messages.ILocation) { const newDialect = DIALECT_DICT[newDialectName] if (!newDialect) { throw NoSuchLanguageException.create(newDialectName, location) } this.dialectName = newDialectName this.dialect = newDialect } public reset() { if (this.dialectName !== this.defaultDialectName) { this.changeDialect(this.defaultDialectName) } this.activeDocStringSeparator = null this.indentToRemove = 0 } public match_TagLine(token: Token) { if (token.line.startsWith('@')) { this.setTokenMatched( token, TokenType.TagLine, null, null, null, token.line.getTags() ) return true } return false } public match_FeatureLine(token: Token) { return this.matchTitleLine( token, TokenType.FeatureLine, this.dialect.feature ) } public match_ScenarioLine(token: Token) { return ( this.matchTitleLine( token, TokenType.ScenarioLine, this.dialect.scenario ) || this.matchTitleLine( token, TokenType.ScenarioLine, this.dialect.scenarioOutline ) ) } public match_BackgroundLine(token: Token) { return this.matchTitleLine( token, TokenType.BackgroundLine, this.dialect.background ) } public match_ExamplesLine(token: Token) { return this.matchTitleLine( token, TokenType.ExamplesLine, this.dialect.examples ) } public match_RuleLine(token: Token) { return this.matchTitleLine(token, TokenType.RuleLine, this.dialect.rule) } public match_TableRow(token: Token) { if (token.line.startsWith('|')) { // TODO: indent this.setTokenMatched( token, TokenType.TableRow, null, null, null, token.line.getTableCells() ) return true } return false } public match_Empty(token: Token) { if (token.line.isEmpty) { this.setTokenMatched(token, TokenType.Empty, null, null, 0) return true } return false } public match_Comment(token: Token) { if (token.line.startsWith('#')) { const text = token.line.getLineText(0) // take the entire line, including leading space this.setTokenMatched(token, TokenType.Comment, text, null, 0) return true } return false } public match_Language(token: Token) { const match = token.line.trimmedLineText.match(LANGUAGE_PATTERN) if (match) { const newDialectName = match[1] this.setTokenMatched(token, TokenType.Language, newDialectName) this.changeDialect(newDialectName, token.location) return true } return false } public match_DocStringSeparator(token: Token) { return this.activeDocStringSeparator == null ? // open this._match_DocStringSeparator(token, '"""', true) || this._match_DocStringSeparator(token, '```', true) : // close this._match_DocStringSeparator( token, this.activeDocStringSeparator, false ) } public _match_DocStringSeparator( token: Token, separator: string, isOpen: boolean ) { if (token.line.startsWith(separator)) { let contentType = null if (isOpen) { contentType = token.line.getRestTrimmed(separator.length) this.activeDocStringSeparator = separator this.indentToRemove = token.line.indent } else { this.activeDocStringSeparator = null this.indentToRemove = 0 } // TODO: Use the separator as keyword. That's needed for pretty printing. this.setTokenMatched(token, TokenType.DocStringSeparator, contentType) return true } return false } public match_EOF(token: Token) { if (token.isEof) { this.setTokenMatched(token, TokenType.EOF) return true } return false } public match_StepLine(token: Token) { const keywords = [] .concat(this.dialect.given) .concat(this.dialect.when) .concat(this.dialect.then) .concat(this.dialect.and) .concat(this.dialect.but) for (const keyword of keywords) { if (token.line.startsWith(keyword)) { const title = token.line.getRestTrimmed(keyword.length) this.setTokenMatched(token, TokenType.StepLine, title, keyword) return true } } return false } public match_Other(token: Token) { const text = token.line.getLineText(this.indentToRemove) // take the entire line, except removing DocString indents this.setTokenMatched( token, TokenType.Other, this.unescapeDocString(text), null, 0 ) return true } private matchTitleLine( token: Token, tokenType: TokenType, keywords: readonly string[] ) { for (const keyword of keywords) { if (token.line.startsWithTitleKeyword(keyword)) { const title = token.line.getRestTrimmed(keyword.length + ':'.length) this.setTokenMatched(token, tokenType, title, keyword) return true } } return false } private setTokenMatched( token: Token, matchedType: TokenType, text?: string, keyword?: string, indent?: number, items?: any[] ) { token.matchedType = matchedType token.matchedText = text token.matchedKeyword = keyword token.matchedIndent = typeof indent === 'number' ? indent : token.line == null ? 0 : token.line.indent token.matchedItems = items || [] token.location.column = token.matchedIndent + 1 token.matchedGherkinDialect = this.dialectName } private unescapeDocString(text: string) { return this.activeDocStringSeparator != null ? text.replace('\\"\\"\\"', '"""') : text } }