import { TokenGroup , TokenStatement , TokenElement , TokenAttribute , isQuote , isBracket } from './parser' ; import { Abbreviation , ParserOptions , AbbreviationNode , ConvertState , Value , AbbreviationAttribute , AttributeType } from './types' ; import { Repeater , ValueToken , Quote , Field } from './tokenizer' ; import stringify from './stringify' ; const urlRegex = /^((https?:|ftp:|file:)?\/\/|(www|ftp)\.)[^ ]*$/ ; const emailRegex = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,5}$/ ; /**
 * Converts given token-based abbreviation into simplified and unrolled node-based
 * abbreviation
 */ export default function convert ( abbr , options = { } ) { let textInserted = false ; let cleanText ; if ( options . text ) { if ( Array . isArray ( options . text ) ) { cleanText = options . text . filter ( s => s . trim ( ) ) ; } else { cleanText = options . text ; } } const result = { type : 'Abbreviation' , children : convertGroup ( abbr , { inserted : false , repeaters : [ ] , text : options . text , cleanText , repeatGuard : options . maxRepeat || Number . POSITIVE_INFINITY , getText ( pos ) { textInserted = true ; let value ; if ( Array . isArray ( options . text ) ) { if ( pos !== undefined && pos >= 0 && pos < cleanText ! . length ) { return cleanText ! [ pos ] ; } value = pos !== undefined ? options . text [ pos ] : options . text . join ( '\n' ) ; } else { value = options . text ?? '' ; } return value ; } , getVariable ( name ) { const varValue = options . variables && options . variables [ name ] ; return varValue != null ? varValue : name ; } } ) } ; if ( options . text != null && ! textInserted ) { const deepest = deepestNode ( last ( result . children ) ) ; if ( deepest ) { const text = Array . isArray ( options . text ) ? options . text . join ( '\n' ) : options . text ; insertText ( deepest , text ) ; if ( deepest . name === 'a' && options . href ) { insertHref ( deepest , text ) ; } } } return result ; } /**
 * Converts given statement to abbreviation nodes
 */ function convertStatement ( node , state ) { let result = [ ] ; if ( node . repeat ) { const original = node . repeat ; const repeat = { ... original } as Repeater ; repeat . count = repeat . implicit && Array . isArray ( state . text ) ? state . cleanText ! . length : ( repeat . count || 1 ) ; let items ; state . repeaters . push ( repeat ) ; for ( let i = 0 ; i < repeat . count ; i ++ ) { repeat . value = i ; node . repeat = repeat ; items = isGroup ( node ) ? convertGroup ( node , state ) : convertElement ( node , state ) ; if ( repeat . implicit && ! state . inserted ) { const target = last ( items ) ; const deepest = target && deepestNode ( target ) ; if ( deepest ) { insertText ( deepest , state . getText ( repeat . value ) ) ; } } result = result . concat ( items ) ; if ( -- state . repeatGuard <= 0 ) { break ; } } state . repeaters . pop ( ) ; node . repeat = original ; if ( repeat . implicit ) { state . inserted = true ; } } else { result = result . concat ( isGroup ( node ) ? convertGroup ( node , state ) : convertElement ( node , state ) ) ; } return result ; } function convertElement ( node , state ) { let children = [ ] ; const elem = { type : 'AbbreviationNode' , name : node . name && stringifyName ( node . name , state ) , value : node . value && stringifyValue ( node . value , state ) , attributes : void 0 , children , repeat : node . repeat && { ... node . repeat } , selfClosing : node . selfClose , } as AbbreviationNode ; let result = [ elem ] ; for ( const child of node . elements ) { children = children . concat ( convertStatement ( child , state ) ) ; } if ( node . attributes ) { elem . attributes = [ ] ; for ( const attr of node . attributes ) { elem . attributes . push ( convertAttribute ( attr , state ) ) ; } } if ( ! elem . name && ! elem . attributes && elem . value && ! elem . value . some ( isField ) ) { result = result . concat ( children ) ; } else { elem . children = children ; } return result ; } function convertGroup ( node , state ) { let result = [ ] ; for ( const child of node . elements ) { result = result . concat ( convertStatement ( child , state ) ) ; } if ( node . repeat ) { result = attachRepeater ( result , node . repeat ) ; } return result ; } function convertAttribute ( node , state ) { let implied = false ; let isBoolean = false ; let valueType = node . expression ? 'expression' : 'raw' ; let value ; const name = node . name && stringifyName ( node . name , state ) ; if ( name && name [ 0 ] === '!' ) { implied = true ; } if ( name && name [ name . length - 1 ] === '.' ) { isBoolean = true ; } if ( node . value ) { const tokens = node . value . slice ( ) ; if ( isQuote ( tokens [ 0 ] ) ) { const quote = tokens . shift ( ) as Quote ; if ( tokens . length && last ( tokens ) . type === quote . type ) { tokens . pop ( ) ; } valueType = quote . single ? 'singleQuote' : 'doubleQuote' ; } else if ( isBracket ( tokens [ 0 ] , 'expression' , true ) ) { valueType = 'expression' ; tokens . shift ( ) ; if ( isBracket ( last ( tokens ) , 'expression' , false ) ) { tokens . pop ( ) ; } } value = stringifyValue ( tokens , state ) ; } return { name : isBoolean || implied ? name ! . slice ( implied ? 1 : 0 , isBoolean ? - 1 : void 0 ) : name , value , boolean : isBoolean , implied , valueType } ; } /**
 * Converts given token list to string
 */ function stringifyName ( tokens , state ) { let str = '' ; for ( let i = 0 ; i < tokens . length ; i ++ ) { str += stringify ( tokens [ i ] , state ) ; } return str ; } /**
 * Converts given token list to value list
 */ function stringifyValue ( tokens , state ) { const result = [ ] ; let str = '' ; for ( let i = 0 , token ; i < tokens . length ; i ++ ) { token = tokens [ i ] ; if ( isField ( token ) ) { if ( str ) { result . push ( str ) ; str = '' ; } result . push ( token ) ; } else { str += stringify ( token , state ) ; } } if ( str ) { result . push ( str ) ; } return result ; } export function isGroup ( node ) { return node . type === 'TokenGroup' ; } function isField ( token ) { return typeof token === 'object' && token . type === 'Field' && token . index != null ; } function last < T > ( arr ) { return arr [ arr . length - 1 ] ; } function deepestNode ( node ) { return node . children . length ? deepestNode ( last ( node . children ) ) : node ; } function insertText ( node , text ) { if ( node . value ) { const lastToken = last ( node . value ) ; if ( typeof lastToken === 'string' ) { node . value [ node . value . length - 1 ] += text ; } else { node . value . push ( text ) ; } } else { node . value = [ text ] ; } } function insertHref ( node , text ) { let href = '' ; if ( urlRegex . test ( text ) ) { href = text ; if ( ! /\w+:/ . test ( href ) && ! href . startsWith ( '//' ) ) { href = `http://${ href }` ; } } else if ( emailRegex . test ( text ) ) { href = `mailto:${ text }` ; } const hrefAttribute = node . attributes ?. find ( attr => attr . name === 'href' ) ; if ( ! hrefAttribute ) { node . attributes = [ { name : 'href' , value : [ href ] , valueType : 'doubleQuote' } ] ; } else if ( ! hrefAttribute . value ) { hrefAttribute . value = [ href ] ; } } function attachRepeater ( items , repeater ) { for ( const item of items ) { if ( ! item . repeat ) { item . repeat = { ... repeater } ; } } return items ; }