import { clamp , isInRange , regexLastIndexOf } from '../../utils' ; import { Position , Range } from 'vscode-languageserver' ; import { Node , HTMLDocument } from 'vscode-html-languageservice' ; import * as path from 'path' ; import { parseHtml } from './parseHtml' ; export interface TagInformation { content : string ; attributes : Record < string , string > ; start : number ; end : number ; startPos : Position ; endPos : Position ; container : { start : number ; end : number ; } ; } function parseAttributes ( rawAttrs ) { const attrs = { } ; if ( ! rawAttrs ) { return attrs ; } Object . keys ( rawAttrs ) . forEach ( ( attrName ) => { const attrValue = rawAttrs [ attrName ] ; attrs [ attrName ] = attrValue === null ? attrName : removeOuterQuotes ( attrValue ) ; } ) ; return attrs ; function removeOuterQuotes ( attrValue ) { if ( ( attrValue . startsWith ( '"' ) && attrValue . endsWith ( '"' ) ) || ( attrValue . startsWith ( "'" ) && attrValue . endsWith ( "'" ) ) ) { return attrValue . slice ( 1 , attrValue . length - 1 ) ; } return attrValue ; } } const regexIf = new RegExp ( '{#if\\s.*?}' , 'gms' ) ; const regexIfEnd = new RegExp ( '{/if}' , 'gms' ) ; const regexEach = new RegExp ( '{#each\\s.*?}' , 'gms' ) ; const regexEachEnd = new RegExp ( '{/each}' , 'gms' ) ; const regexAwait = new RegExp ( '{#await\\s.*?}' , 'gms' ) ; const regexAwaitEnd = new RegExp ( '{/await}' , 'gms' ) ; const regexHtml = new RegExp ( '{@html\\s.*?' , 'gms' ) ; param source param tag function extractTags ( text , tag , html ? ) { const rootNodes = html ?. roots || parseHtml ( text ) . roots ; const matchedNodes = rootNodes . filter ( ( node ) => node . tag === tag ) . filter ( ( tag ) => { return isNotInsideControlFlowTag ( tag ) && isNotInsideHtmlTag ( tag ) ; } ) ; return matchedNodes . map ( transformToTagInfo ) ; /**
     * For every match AFTER the tag do a search for `{/X`.
     * If that is BEFORE `{#X`, we are inside a moustache tag.
     */ function isNotInsideControlFlowTag ( tag ) { const nodes = rootNodes . slice ( rootNodes . indexOf ( tag ) ) ; const rootContentAfterTag = nodes . map ( ( node , idx ) => { const start = node . startTagEnd ? node . end : node . start + ( node . tag ?. length || 0 ) ; return text . substring ( start , nodes [ idx + 1 ] ?. start ) ; } ) . join ( '' ) ; return ! [ [ regexIf , regexIfEnd ] , [ regexEach , regexEachEnd ] , [ regexAwait , regexAwaitEnd ] ] . some ( ( pair ) => { pair [ 0 ] . lastIndex = 0 ; pair [ 1 ] . lastIndex = 0 ; const start = pair [ 0 ] . exec ( rootContentAfterTag ) ; const end = pair [ 1 ] . exec ( rootContentAfterTag ) ; return ( end ?. index ?? text . length ) < ( start ?. index ?? text . length ) ; } ) ; } /**
     * For every match BEFORE the tag do a search for `{@html`.
     * If that is BEFORE `}`, we are inside a moustache tag.
     */ function isNotInsideHtmlTag ( tag ) { const nodes = rootNodes . slice ( 0 , rootNodes . indexOf ( tag ) ) ; const rootContentBeforeTag = [ { start : 0 , end : 0 } , ... nodes ] . map ( ( node , idx ) => { return text . substring ( node . end , nodes [ idx ] ?. start ) ; } ) . join ( '' ) ; return ! ( regexLastIndexOf ( rootContentBeforeTag , regexHtml ) > rootContentBeforeTag . lastIndexOf ( '}' ) ) ; } function transformToTagInfo ( matchedNode ) { const start = matchedNode . startTagEnd ?? matchedNode . start ; const end = matchedNode . endTagStart ?? matchedNode . end ; const startPos = positionAt ( start , text ) ; const endPos = positionAt ( end , text ) ; const container = { start : matchedNode . start , end : matchedNode . end } ; const content = text . substring ( start , end ) ; return { content , attributes : parseAttributes ( matchedNode . attributes ) , start , end , startPos , endPos , container } ; } } export function extractScriptTags ( source , html ? ) { const scripts = extractTags ( source , 'script' , html ) ; if ( ! scripts . length ) { return null ; } const script = scripts . find ( ( s ) => s . attributes [ 'context' ] !== 'module' ) ; const moduleScript = scripts . find ( ( s ) => s . attributes [ 'context' ] === 'module' ) ; return { script , moduleScript } ; } export function extractStyleTag ( source , html ? ) { const styles = extractTags ( source , 'style' , html ) ; if ( ! styles . length ) { return null ; } return styles [ 0 ] ; } export function extractTemplateTag ( source , html ? ) { const templates = extractTags ( source , 'template' , html ) ; if ( ! templates . length ) { return null ; } return templates [ 0 ] ; } param offset param text param lineOffsets export function positionAt ( offset , text , lineOffsets = getLineOffsets ( text ) ) { offset = clamp ( offset , 0 , text . length ) ; let low = 0 ; let high = lineOffsets . length ; if ( high === 0 ) { return Position . create ( 0 , offset ) ; } while ( low <= high ) { const mid = Math . floor ( ( low + high ) / 2 ) ; const lineOffset = lineOffsets [ mid ] ; if ( lineOffset === offset ) { return Position . create ( mid , 0 ) ; } else if ( offset > lineOffset ) { low = mid + 1 ; } else { high = mid - 1 ; } } const line = low - 1 ; return Position . create ( line , offset - lineOffsets [ line ] ) ; } param position param text param lineOffsets export function offsetAt ( position , text , lineOffsets = getLineOffsets ( text ) ) { if ( position . line >= lineOffsets . length ) { return text . length ; } else if ( position . line < 0 ) { return 0 ; } const lineOffset = lineOffsets [ position . line ] ; const nextLineOffset = position . line + 1 < lineOffsets . length ? lineOffsets [ position . line + 1 ] : text . length ; return clamp ( nextLineOffset , lineOffset , lineOffset + position . character ) ; } export function getLineOffsets ( text ) { const lineOffsets = [ ] ; let isLineStart = true ; for ( let i = 0 ; i < text . length ; i ++ ) { if ( isLineStart ) { lineOffsets . push ( i ) ; isLineStart = false ; } const ch = text . charAt ( i ) ; isLineStart = ch === '\r' || ch === '\n' ; if ( ch === '\r' && i + 1 < text . length && text . charAt ( i + 1 ) === '\n' ) { i ++ ; } } if ( isLineStart && text . length > 0 ) { lineOffsets . push ( text . length ) ; } return lineOffsets ; } export function isInTag ( position , tagInfo ) { return ! ! tagInfo && isInRange ( Range . create ( tagInfo . startPos , tagInfo . endPos ) , position ) ; } export function isRangeInTag ( range , tagInfo ) { return isInTag ( range . start , tagInfo ) && isInTag ( range . end , tagInfo ) ; } export function getTextInRange ( range , text ) { return text . substring ( offsetAt ( range . start , text ) , offsetAt ( range . end , text ) ) ; } export function getLineAtPosition ( position , text ) { return text . substring ( offsetAt ( { line : position . line , character : 0 } , text ) , offsetAt ( { line : position . line , character : Number . MAX_VALUE } , text ) ) ; } /**
 * Assumption: Is called with a line. A line does only contain line break characters
 * at its end.
 */ export function isAtEndOfLine ( line , offset ) { return [ undefined , '\r' , '\n' ] . includes ( line [ offset ] ) ; } param oldPath param newPath param relativeImportPath export function updateRelativeImport ( oldPath , newPath , relativeImportPath ) { let newImportPath = path . join ( path . relative ( newPath , oldPath ) , relativeImportPath ) . replace ( /\\/g , '/' ) ; if ( ! newImportPath . startsWith ( '.' ) ) { newImportPath = './' + newImportPath ; } return newImportPath ; } /**
 * Returns the node if offset is inside a component's starttag
 */ export function getNodeIfIsInComponentStartTag ( html , offset ) { const node = html . findNodeAt ( offset ) ; if ( ! ! node . tag && node . tag [ 0 ] === node . tag [ 0 ] . toUpperCase ( ) && ( ! node . startTagEnd || offset < node . startTagEnd ) ) { return node ; } } /**
 * Returns the node if offset is inside a HTML starttag
 */ export function getNodeIfIsInHTMLStartTag ( html , offset ) { const node = html . findNodeAt ( offset ) ; if ( ! ! node . tag && node . tag [ 0 ] === node . tag [ 0 ] . toLowerCase ( ) && ( ! node . startTagEnd || offset < node . startTagEnd ) ) { return node ; } } /**
 * Returns the node if offset is inside a starttag (HTML or component)
 */ export function getNodeIfIsInStartTag ( html , offset ) { const node = html . findNodeAt ( offset ) ; if ( ! ! node . tag && ( ! node . startTagEnd || offset < node . startTagEnd ) ) { return node ; } } /**
 * Gets word range at position.
 * Delimiter is by default a whitespace, but can be adjusted.
 */ export function getWordRangeAt ( str , pos , delimiterRegex = { left : /\S+$/ , right : /\s/ } ) { let start = str . slice ( 0 , pos ) . search ( delimiterRegex . left ) ; if ( start < 0 ) { start = pos ; } let end = str . slice ( pos ) . search ( delimiterRegex . right ) ; if ( end < 0 ) { end = str . length ; } else { end = end + pos ; } return { start , end } ; } /**
 * Gets word at position.
 * Delimiter is by default a whitespace, but can be adjusted.
 */ export function getWordAt ( str , pos , delimiterRegex = { left : /\S+$/ , right : /\s/ } ) { const { start , end } = getWordRangeAt ( str , pos , delimiterRegex ) ; return str . slice ( start , end ) ; } /**
 * Returns start/end offset of a text into a range
 */ export function toRange ( str , start , end ) { return Range . create ( positionAt ( start , str ) , positionAt ( end , str ) ) ; } /**
 * Returns the language from the given tags, return the first from which a language is found.
 * Searches inside lang and type and removes leading 'text/'
 */ export function getLangAttribute ( ... tags ) { const tag = tags . find ( ( tag ) => tag ?. attributes . lang || tag ?. attributes . type ) ; if ( ! tag ) { return null ; } const attribute = tag . attributes . lang || tag . attributes . type ; if ( ! attribute ) { return null ; } return attribute . replace ( /^text\// , '' ) ; } /**
 * Checks whether given position is inside a moustache tag (which includes control flow tags)
 * using a simple bracket matching heuristic which might fail under conditions like
 * `{#if {a: true}.a}`
 */ export function isInsideMoustacheTag ( html , tagStart , position ) { if ( tagStart === null ) { const charactersBeforePosition = html . substring ( 0 , position ) ; return ( Math . max ( charactersBeforePosition . lastIndexOf ( '{#' ) , charactersBeforePosition . lastIndexOf ( '{:' ) ) > charactersBeforePosition . lastIndexOf ( '}' ) ) ; } else { const charactersInNode = html . substring ( tagStart , position ) ; return charactersInNode . lastIndexOf ( '{' ) > charactersInNode . lastIndexOf ( '}' ) ; } }