import { contains } from '../../../common/collectionUtils' ; import { toBoolean } from '../../../common/core' ; import { combinePaths , getRelativePath , isRootedDiskPath , normalizePath , normalizeSlashes , } from '../../../common/pathUtils' ; import { distlibFolder , libFolder } from '../vfs/factory' ; import { fileMetadataNames , FourSlashData , FourSlashFile , Marker , MetadataOptionNames , Range } from './fourSlashTypes' ; param basePath param contents param fileName export function parseTestData ( basePath , contents , fileName ) { const normalizedBasePath = normalizeSlashes ( basePath ) ; const optionRegex = /^\s*@(\w+):\s*(.*)\s*/ ; const files = [ ] ; const globalOptions = { } ; const lines = contents . split ( '\n' ) ; let i = 0 ; const markerPositions = new Map < string , Marker > ( ) ; const markers = [ ] ; const ranges = [ ] ; let currentFileContent ; let currentFileName = normalizeSlashes ( fileName ) ; let currentFileOptions = { } ; function nextFile ( ) { if ( currentFileContent === undefined ) { return ; } if ( toBoolean ( currentFileOptions [ MetadataOptionNames . library ] ) ) { currentFileName = normalizePath ( combinePaths ( libFolder , getRelativePath ( currentFileName , normalizedBasePath ) ) ) ; } if ( toBoolean ( currentFileOptions [ MetadataOptionNames . distLibrary ] ) ) { currentFileName = normalizePath ( combinePaths ( distlibFolder , getRelativePath ( currentFileName , normalizedBasePath ) ) ) ; } const file = parseFileContent ( currentFileContent , currentFileName , markerPositions , markers , ranges ) ; file . fileOptions = currentFileOptions ; files . push ( file ) ; currentFileContent = undefined ; currentFileOptions = { } ; currentFileName = fileName ; } for ( let line of lines ) { i ++ ; if ( line . length > 0 && line . charAt ( line . length - 1 ) === '\r' ) { line = line . substr ( 0 , line . length - 1 ) ; } if ( line . substr ( 0 , 4 ) === '////' ) { const text = line . substr ( 4 ) ; currentFileContent = currentFileContent === undefined ? text : currentFileContent + '\n' + text ; } else if ( line . substr ( 0 , 3 ) === '///' && currentFileContent !== undefined ) { throw new Error ( `Three-slash line in the middle of four-slash region at line ${ i }` ) ; } else if ( line . substr ( 0 , 2 ) === '//' ) { const match = optionRegex . exec ( line . substr ( 2 ) ) ; if ( match ) { const key = match [ 1 ] . toLowerCase ( ) ; const value = match [ 2 ] ; if ( ! contains ( fileMetadataNames , key ) ) { if ( globalOptions [ key ] !== undefined ) { throw new Error ( `Global option '${ key }' already exists` ) ; } globalOptions [ key ] = value ; } else { switch ( key ) { case MetadataOptionNames . fileName : { nextFile ( ) ; const normalizedPath = normalizeSlashes ( value ) ; currentFileName = isRootedDiskPath ( normalizedPath ) ? normalizedPath : combinePaths ( normalizedBasePath , normalizedPath ) ; currentFileOptions [ key ] = value ; break ; } default : currentFileOptions [ key ] = value ; } } } } else if ( line !== '' ) { nextFile ( ) ; } } return { markerPositions , markers , globalOptions , files , ranges , } ; } interface LocationInformation { position : number ; sourcePosition : number ; sourceLine : number ; sourceColumn : number ; } interface RangeLocationInformation extends LocationInformation { marker ? : Marker | undefined ; } const enum State { none , inSlashStarMarker , inObjectMarker } function reportError ( fileName , line , col , message ) { const errorMessage = `${ fileName }(${ line },${ col }): ${ message }` ; throw new Error ( errorMessage ) ; } function recordObjectMarker ( fileName , location , text , markerMap , markers ) { let markerValue ; try { markerValue = JSON . parse ( '{ ' + text + ' }' ) ; } catch ( e ) { reportError ( fileName , location . sourceLine , location . sourceColumn , `Unable to parse marker text ${ e . message }` ) ; } if ( markerValue === undefined ) { reportError ( fileName , location . sourceLine , location . sourceColumn , 'Object markers can not be empty' ) ; return undefined ; } const marker = { fileName , position : location . position , data : markerValue , } ; if ( markerValue . name ) { markerMap . set ( markerValue . name , marker ) ; } markers . push ( marker ) ; return marker ; } function recordMarker ( fileName , location , name , markerMap , markers ) { const marker = { fileName , position : location . position , } ; if ( markerMap . has ( name ) ) { const message = "Marker '" + name + "' is duplicated in the source file contents." ; reportError ( marker . fileName , location . sourceLine , location . sourceColumn , message ) ; return undefined ; } else { markerMap . set ( name , marker ) ; markers . push ( marker ) ; return marker ; } } function parseFileContent ( content , fileName , markerMap , markers , ranges ) { content = chompLeadingSpace ( content ) ; const validMarkerChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz$1234567890_' ; let output = '' ; let openMarker ; const openRanges = [ ] ; let localRanges = [ ] ; let lastNormalCharPosition = 0 ; let difference = 0 ; let state = State . none ; let line = 1 ; let column = 1 ; const flush = ( lastSafeCharIndex ) => { output = output + content . substr ( lastNormalCharPosition , lastSafeCharIndex === undefined ? undefined : lastSafeCharIndex - lastNormalCharPosition ) ; } ; if ( content . length > 0 ) { let previousChar = content . charAt ( 0 ) ; for ( let i = 1 ; i < content . length ; i ++ ) { const currentChar = content . charAt ( i ) ; switch ( state ) { case State . none : if ( previousChar === '[' && currentChar === '|' ) { openRanges . push ( { position : i - 1 - difference , sourcePosition : i - 1 , sourceLine : line , sourceColumn : column , } ) ; flush ( i - 1 ) ; lastNormalCharPosition = i + 1 ; difference += 2 ; } else if ( previousChar === '|' && currentChar === ']' ) { const rangeStart = openRanges . pop ( ) ; if ( ! rangeStart ) { reportError ( fileName , line , column , 'Found range end with no matching start.' ) ; } const range = { fileName , pos : rangeStart ! . position , end : i - 1 - difference , marker : rangeStart ! . marker , } ; localRanges . push ( range ) ; flush ( i - 1 ) ; lastNormalCharPosition = i + 1 ; difference += 2 ; } else if ( previousChar === '/' && currentChar === '*' ) { state = State . inSlashStarMarker ; openMarker = { position : i - 1 - difference , sourcePosition : i - 1 , sourceLine : line , sourceColumn : column , } ; } else if ( previousChar === '{' && currentChar === '|' ) { state = State . inObjectMarker ; openMarker = { position : i - 1 - difference , sourcePosition : i - 1 , sourceLine : line , sourceColumn : column , } ; flush ( i - 1 ) ; } break ; case State . inObjectMarker : if ( previousChar === '|' && currentChar === '}' ) { const objectMarkerNameText = content . substring ( openMarker ! . sourcePosition + 2 , i - 1 ) . trim ( ) ; const marker = recordObjectMarker ( fileName , openMarker ! , objectMarkerNameText , markerMap , markers ) ; if ( openRanges . length > 0 ) { openRanges [ openRanges . length - 1 ] . marker = marker ; } lastNormalCharPosition = i + 1 ; difference += i + 1 - openMarker ! . sourcePosition ; openMarker = undefined ; state = State . none ; } break ; case State . inSlashStarMarker : if ( previousChar === '*' && currentChar === '/' ) { const markerNameText = content . substring ( openMarker ! . sourcePosition + 2 , i - 1 ) . trim ( ) ; const marker = recordMarker ( fileName , openMarker ! , markerNameText , markerMap , markers ) ; if ( openRanges . length > 0 ) { openRanges [ openRanges . length - 1 ] . marker = marker ; } flush ( openMarker ! . sourcePosition ) ; lastNormalCharPosition = i + 1 ; difference += i + 1 - openMarker ! . sourcePosition ; openMarker = undefined ; state = State . none ; } else if ( validMarkerChars . indexOf ( currentChar ) < 0 ) { if ( currentChar === '*' && i < content . length - 1 && content . charAt ( i + 1 ) === '/' ) { } else { flush ( i ) ; lastNormalCharPosition = i ; openMarker = undefined ; state = State . none ; } } break ; } if ( currentChar === '\n' && previousChar === '\r' ) { continue ; } else if ( currentChar === '\n' || currentChar === '\r' ) { line ++ ; column = 1 ; continue ; } column ++ ; previousChar = currentChar ; } } flush ( undefined ) ; if ( openRanges . length > 0 ) { const openRange = openRanges [ 0 ] ; reportError ( fileName , openRange . sourceLine , openRange . sourceColumn , 'Unterminated range.' ) ; } if ( openMarker ) { reportError ( fileName , openMarker . sourceLine , openMarker . sourceColumn , 'Unterminated marker.' ) ; } localRanges = localRanges . sort ( ( a , b ) => ( a . pos < b . pos ? - 1 : a . pos === b . pos && a . end > b . end ? - 1 : 1 ) ) ; localRanges . forEach ( ( r ) => { ranges . push ( r ) ; } ) ; return { content : output , fileOptions : { } , version : 0 , fileName , } ; } function chompLeadingSpace ( content ) { const lines = content . split ( '\n' ) ; for ( const line of lines ) { if ( line . length !== 0 && line . charAt ( 0 ) !== ' ' ) { return content ; } } return lines . map ( ( s ) => s . substr ( 1 ) ) . join ( '\n' ) ; }