import ITokenMatcher from './ITokenMatcher' ; import Dialect from './Dialect' ; import { Token , TokenType } from './Parser' ; import DIALECTS from './gherkin-languages.json' ; import { Item } from './IToken' ; import * as messages from '@cucumber/messages' ; import { NoSuchLanguageException } from './Errors' ; const DIALECT_DICT = DIALECTS ; const DEFAULT_DOC_STRING_SEPARATOR = /^(```[`]*)(.*)/ ; export default class GherkinInMarkdownTokenMatcher implements ITokenMatcher < TokenType > { private dialect : Dialect ; private dialectName : string ; private readonly nonStarStepKeywords : string [ ] ; private readonly stepRegexp : RegExp ; private readonly headerRegexp : RegExp ; private activeDocStringSeparator : RegExp ; private indentToRemove : number ; private matchedFeatureLine : boolean ; constructor ( private readonly defaultDialectName = 'en' ) { this . dialect = DIALECT_DICT [ defaultDialectName ] ; this . nonStarStepKeywords = [ ] . concat ( this . dialect . given ) . concat ( this . dialect . when ) . concat ( this . dialect . then ) . concat ( this . dialect . and ) . concat ( this . dialect . but ) . filter ( ( value , index , self ) => value !== '* ' && self . indexOf ( value ) === index ) ; this . stepRegexp = new RegExp ( `${ KeywordPrefix . BULLET }(${ this . nonStarStepKeywords . map ( escapeRegExp ) . join ( '|' ) })` ) ; const headerKeywords = [ ] . concat ( this . dialect . feature ) . concat ( this . dialect . background ) . concat ( this . dialect . rule ) . concat ( this . dialect . scenarioOutline ) . concat ( this . dialect . scenario ) . concat ( this . dialect . examples ) . filter ( ( value , index , self ) => self . indexOf ( value ) === index ) ; this . headerRegexp = new RegExp ( `${ KeywordPrefix . HEADER }(${ headerKeywords . map ( escapeRegExp ) . join ( '|' ) })` ) ; this . reset ( ) ; } changeDialect ( newDialectName , location ? ) { const newDialect = DIALECT_DICT [ newDialectName ] ; if ( ! newDialect ) { throw NoSuchLanguageException . create ( newDialectName , location ) ; } this . dialectName = newDialectName ; this . dialect = newDialect ; } match_Language ( token ) { if ( ! token ) throw new Error ( 'no token' ) ; return false ; } match_Empty ( token ) { let result = false ; if ( token . line . isEmpty ) { result = true ; } if ( ! this . match_TagLine ( token ) && ! this . match_FeatureLine ( token ) && ! this . match_ScenarioLine ( token ) && ! this . match_BackgroundLine ( token ) && ! this . match_ExamplesLine ( token ) && ! this . match_RuleLine ( token ) && ! this . match_TableRow ( token ) && ! this . match_Comment ( token ) && ! this . match_Language ( token ) && ! this . match_DocStringSeparator ( token ) && ! this . match_EOF ( token ) && ! this . match_StepLine ( token ) ) { result = true ; } if ( result ) { token . matchedType = TokenType . Empty ; } return this . setTokenMatched ( token , null , result ) ; } match_Other ( token ) { const text = token . line . getLineText ( this . indentToRemove ) ; token . matchedType = TokenType . Other ; token . matchedText = text ; token . matchedIndent = 0 ; return this . setTokenMatched ( token , null , true ) ; } match_Comment ( token ) { let result = false ; if ( token . line . startsWith ( '|' ) ) { const tableCells = token . line . getTableCells ( ) ; if ( this . isGfmTableSeparator ( tableCells ) ) result = true ; } return this . setTokenMatched ( token , null , result ) ; } match_DocStringSeparator ( token ) { const match = token . line . trimmedLineText . match ( this . activeDocStringSeparator ) ; const [ , newSeparator , mediaType ] = match || [ ] ; let result = false ; if ( newSeparator ) { if ( this . activeDocStringSeparator === DEFAULT_DOC_STRING_SEPARATOR ) { this . activeDocStringSeparator = new RegExp ( `^(${ newSeparator })$` ) ; this . indentToRemove = token . line . indent ; } else { this . activeDocStringSeparator = DEFAULT_DOC_STRING_SEPARATOR ; } token . matchedKeyword = newSeparator ; token . matchedType = TokenType . DocStringSeparator ; token . matchedText = mediaType || '' ; result = true ; } return this . setTokenMatched ( token , null , result ) ; } match_EOF ( token ) { let result = false ; if ( token . isEof ) { token . matchedType = TokenType . EOF ; result = true ; } return this . setTokenMatched ( token , null , result ) ; } match_FeatureLine ( token ) { if ( this . matchedFeatureLine ) { return this . setTokenMatched ( token , null , false ) ; } let result = this . matchTitleLine ( KeywordPrefix . HEADER , this . dialect . feature , ':' , token , TokenType . FeatureLine ) ; if ( ! result ) { token . matchedType = TokenType . FeatureLine ; token . matchedText = token . line . trimmedLineText ; result = this . setTokenMatched ( token , null , true ) ; } this . matchedFeatureLine = result ; return result ; } match_BackgroundLine ( token ) { return this . matchTitleLine ( KeywordPrefix . HEADER , this . dialect . background , ':' , token , TokenType . BackgroundLine ) ; } match_RuleLine ( token ) { return this . matchTitleLine ( KeywordPrefix . HEADER , this . dialect . rule , ':' , token , TokenType . RuleLine ) ; } match_ScenarioLine ( token ) { return ( this . matchTitleLine ( KeywordPrefix . HEADER , this . dialect . scenario , ':' , token , TokenType . ScenarioLine ) || this . matchTitleLine ( KeywordPrefix . HEADER , this . dialect . scenarioOutline , ':' , token , TokenType . ScenarioLine ) ) ; } match_ExamplesLine ( token ) { return this . matchTitleLine ( KeywordPrefix . HEADER , this . dialect . examples , ':' , token , TokenType . ExamplesLine ) ; } match_StepLine ( token ) { return this . matchTitleLine ( KeywordPrefix . BULLET , this . nonStarStepKeywords , '' , token , TokenType . StepLine ) ; } matchTitleLine ( prefix , keywords , keywordSuffix , token , matchedType ) { const regexp = new RegExp ( `${ prefix }(${ keywords . map ( escapeRegExp ) . join ( '|' ) })${ keywordSuffix }(.*)` ) ; const match = token . line . match ( regexp ) ; let indent = token . line . indent ; let result = false ; if ( match ) { token . matchedType = matchedType ; token . matchedKeyword = match [ 2 ] ; token . matchedText = match [ 3 ] . trim ( ) ; indent += match [ 1 ] . length ; result = true ; } return this . setTokenMatched ( token , indent , result ) ; } setTokenMatched ( token , indent , matched ) { token . matchedGherkinDialect = this . dialectName ; token . matchedIndent = indent !== null ? indent : token . line == null ? 0 : token . line . indent ; token . location . column = token . matchedIndent + 1 ; return matched ; } match_TableRow ( token ) { if ( token . line . lineText . match ( /^\s\s\s?\s?\s?\|/ ) ) { const tableCells = token . line . getTableCells ( ) ; if ( this . isGfmTableSeparator ( tableCells ) ) return false ; token . matchedKeyword = '|' ; token . matchedType = TokenType . TableRow ; token . matchedItems = tableCells ; return true ; } return false ; } private isGfmTableSeparator ( tableCells ) { const separatorValues = tableCells . map ( ( item ) => item . text ) . filter ( ( value ) => value . match ( /^:?-+:?$/ ) ) ; return separatorValues . length > 0 ; } match_TagLine ( token ) { const tags = [ ] ; let m ; const re = /`(@[^`]+)`/g ; do { m = re . exec ( token . line . trimmedLineText ) ; if ( m ) { tags . push ( { column : token . line . indent + m . index + 2 , text : m [ 1 ] , } ) ; } } while ( m ) ; if ( tags . length === 0 ) return false ; token . matchedType = TokenType . TagLine ; token . matchedItems = tags ; return true ; } reset ( ) { if ( this . dialectName !== this . defaultDialectName ) { this . changeDialect ( this . defaultDialectName ) ; } this . activeDocStringSeparator = DEFAULT_DOC_STRING_SEPARATOR ; } } enum KeywordPrefix { BULLET = '^(\\s*[*+-]\\s*)' , HEADER = '^(#{1,6}\\s)' } function escapeRegExp ( text ) { return text . replace ( /[-[\]{}()*+?.,\\^$|#\s]/g , '\\$&' ) ; }