import { SyntaxType } from '../config' ; import backwardScanner , { sol , peek , BackwardScanner , consume } from './reader' ; import isAtHTMLTag from './is-html' ; import { isQuote } from './quotes' ; import { Brackets , bracePairs } from './brackets' ; export interface ExtractOptions { /**
     * Allow parser to look ahead of `pos` index for searching of missing
     * abbreviation parts. Most editors automatically inserts closing braces for
     * `[`, `{` and `(`, which will most likely be right after current caret position.
     * So in order to properly expand abbreviation, user must explicitly move
     * caret right after auto-inserted braces. With this option enabled, parser
     * will search for closing braces right after `pos`. Default is `true`
     */ lookAhead : boolean ; /**
     * Type of context syntax of expanded abbreviation.
     * In 'stylesheet' syntax, brackets `[]` and `{}` are not supported thus
     * not extracted.
     */ type : SyntaxType ; /**
     * A string that should precede abbreviation in order to make it successfully
     * extracted. If given, the abbreviation will be extracted from the nearest
     * `prefix` occurrence.
     */ prefix : string ; } export interface ExtractedAbbreviation { /** Extracted abbreviation */ abbreviation : string ; /** Location of abbreviation in input string */ location : number ; /** Start location of matched abbreviation, including prefix */ start : number ; /** End location of extracted abbreviation */ end : number ; } const code = ( ch ) => ch . charCodeAt ( 0 ) ; const specialChars = '#.*:$-_!@%^+>/' . split ( '' ) . map ( code ) ; const defaultOptions = { type : 'markup' , lookAhead : true , prefix : '' } ; param line param pos param options export default function extractAbbreviation ( line , pos = line . length , options = { } ) { const opt = { ... defaultOptions , ... options } ; pos = Math . min ( line . length , Math . max ( 0 , pos == null ? line . length : pos ) ) ; if ( opt . lookAhead ) { pos = offsetPastAutoClosed ( line , pos , opt ) ; } let ch ; const start = getStartOffset ( line , pos , opt . prefix || '' ) ; if ( start === - 1 ) { return void 0 ; } const scanner = backwardScanner ( line , start ) ; scanner . pos = pos ; const stack = [ ] ; while ( ! sol ( scanner ) ) { ch = peek ( scanner ) ; if ( stack . includes ( Brackets . CurlyR ) ) { if ( ch === Brackets . CurlyR ) { stack . push ( ch ) ; scanner . pos -- ; continue ; } if ( ch !== Brackets . CurlyL ) { scanner . pos -- ; continue ; } } if ( isCloseBrace ( ch , opt . type ) ) { stack . push ( ch ) ; } else if ( isOpenBrace ( ch , opt . type ) ) { if ( stack . pop ( ) !== bracePairs [ ch ] ) { break ; } } else if ( stack . includes ( Brackets . SquareR ) || stack . includes ( Brackets . CurlyR ) ) { scanner . pos -- ; continue ; } else if ( isAtHTMLTag ( scanner ) || ! isAbbreviation ( ch ) ) { break ; } scanner . pos -- ; } if ( ! stack . length && scanner . pos !== pos ) { const abbreviation = line . slice ( scanner . pos , pos ) . replace ( /^[*+>^]+/ , '' ) ; return { abbreviation , location : pos - abbreviation . length , start : options . prefix ? start - options . prefix . length : pos - abbreviation . length , end : pos } ; } } /**
 * Returns new `line` index which is right after characters beyound `pos` that
 * editor will likely automatically close, e.g. }, ], and quotes
 */ function offsetPastAutoClosed ( line , pos , options ) { if ( isQuote ( line . charCodeAt ( pos ) ) ) { pos ++ ; } while ( isCloseBrace ( line . charCodeAt ( pos ) , options . type ) ) { pos ++ ; } return pos ; } /**
 * Returns start offset (left limit) in `line` where we should stop looking for
 * abbreviation: it’s nearest to `pos` location of `prefix` token
 */ function getStartOffset ( line , pos , prefix ) { if ( ! prefix ) { return 0 ; } const scanner = backwardScanner ( line ) ; const compiledPrefix = prefix . split ( '' ) . map ( code ) ; scanner . pos = pos ; let result ; while ( ! sol ( scanner ) ) { if ( consumePair ( scanner , Brackets . SquareR , Brackets . SquareL ) || consumePair ( scanner , Brackets . CurlyR , Brackets . CurlyL ) ) { continue ; } result = scanner . pos ; if ( consumeArray ( scanner , compiledPrefix ) ) { return result ; } scanner . pos -- ; } return - 1 ; } /**
 * Consumes full character pair, if possible
 */ function consumePair ( scanner , close , open ) { const start = scanner . pos ; if ( consume ( scanner , close ) ) { while ( ! sol ( scanner ) ) { if ( consume ( scanner , open ) ) { return true ; } scanner . pos -- ; } } scanner . pos = start ; return false ; } /**
 * Consumes all character codes from given array, right-to-left, if possible
 */ function consumeArray ( scanner , arr ) { const start = scanner . pos ; let consumed = false ; for ( let i = arr . length - 1 ; i >= 0 && ! sol ( scanner ) ; i -- ) { if ( ! consume ( scanner , arr [ i ] ) ) { break ; } consumed = i === 0 ; } if ( ! consumed ) { scanner . pos = start ; } return consumed ; } function isAbbreviation ( ch ) { return ( ch > 64 && ch < 91 ) || ( ch > 96 && ch < 123 ) || ( ch > 47 && ch < 58 ) || specialChars . includes ( ch ) ; } function isOpenBrace ( ch , syntax ) { return ch === Brackets . RoundL || ( syntax === 'markup' && ( ch === Brackets . SquareL || ch === Brackets . CurlyL ) ) ; } function isCloseBrace ( ch , syntax ) { return ch === Brackets . RoundR || ( syntax === 'markup' && ( ch === Brackets . SquareR || ch === Brackets . CurlyR ) ) ; }