2023-01-06 15:54:31 -08:00
// template-reader *just* does schema validation
2023-03-15 15:58:44 -04:00
/* eslint-disable @typescript-eslint/no-non-null-assertion */
2023-01-06 15:54:31 -08:00
2025-12-18 13:35:48 -06:00
import { ObjectReader } from "./object-reader.js" ;
import { TemplateSchema } from "./schema/index.js" ;
import { DefinitionInfo } from "./schema/definition-info.js" ;
import { DefinitionType } from "./schema/definition-type.js" ;
import { MappingDefinition } from "./schema/mapping-definition.js" ;
import { ScalarDefinition } from "./schema/scalar-definition.js" ;
import { SequenceDefinition } from "./schema/sequence-definition.js" ;
import { ANY , CLOSE_EXPRESSION , INSERT_DIRECTIVE , OPEN_EXPRESSION } from "./template-constants.js" ;
import { TemplateContext } from "./template-context.js" ;
2023-01-06 15:54:31 -08:00
import {
BasicExpressionToken ,
ExpressionToken ,
InsertExpressionToken ,
LiteralToken ,
MappingToken ,
ScalarToken ,
StringToken ,
2023-01-09 19:02:19 -05:00
TemplateToken
2025-12-18 13:35:48 -06:00
} from "./tokens/index.js" ;
import { TokenRange } from "./tokens/token-range.js" ;
import { isString } from "./tokens/type-guards.js" ;
import { TokenType } from "./tokens/types.js" ;
2023-01-06 15:54:31 -08:00
2023-01-09 19:02:19 -05:00
const WHITESPACE_PATTERN = /\s/ ;
2023-01-06 15:54:31 -08:00
export function readTemplate (
context : TemplateContext ,
type : string ,
objectReader : ObjectReader ,
fileId : number | undefined
) : TemplateToken | undefined {
2023-01-09 19:02:19 -05:00
const reader = new TemplateReader ( context , objectReader , fileId ) ;
let value : TemplateToken | undefined ;
2023-01-06 15:54:31 -08:00
try {
2023-01-09 19:02:19 -05:00
objectReader . validateStart ( ) ;
const definition = new DefinitionInfo ( context . schema , type ) ;
value = reader . readValue ( definition ) ;
objectReader . validateEnd ( ) ;
2023-01-06 15:54:31 -08:00
} catch ( err ) {
2023-01-09 19:02:19 -05:00
context . error ( fileId , err ) ;
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
return value ;
2023-01-06 15:54:31 -08:00
}
export interface ReadTemplateResult {
2023-01-09 19:02:19 -05:00
value : TemplateToken ;
bytes : number ;
2023-01-06 15:54:31 -08:00
}
class TemplateReader {
2023-01-09 19:02:19 -05:00
private readonly _context : TemplateContext ;
private readonly _schema : TemplateSchema ;
private readonly _objectReader : ObjectReader ;
private readonly _fileId : number | undefined ;
public constructor ( context : TemplateContext , objectReader : ObjectReader , fileId : number | undefined ) {
this . _context = context ;
this . _schema = context . schema ;
this . _objectReader = objectReader ;
this . _fileId = fileId ;
2023-01-06 15:54:31 -08:00
}
public readValue ( definition : DefinitionInfo ) : TemplateToken {
// Scalar
2023-01-09 19:02:19 -05:00
const literal = this . _objectReader . allowLiteral ( ) ;
2023-01-06 15:54:31 -08:00
if ( literal ) {
2023-01-09 19:02:19 -05:00
let scalar = this . parseScalar ( literal , definition ) ;
scalar = this . validate ( scalar , definition ) ;
return scalar ;
2023-01-06 15:54:31 -08:00
}
// Sequence
2023-01-09 19:02:19 -05:00
const sequence = this . _objectReader . allowSequenceStart ( ) ;
2023-01-06 15:54:31 -08:00
if ( sequence ) {
2023-01-09 19:02:19 -05:00
const sequenceDefinition = definition . getDefinitionsOfType ( DefinitionType . Sequence ) [ 0 ] as
| SequenceDefinition
| undefined ;
2023-01-06 15:54:31 -08:00
// Legal
if ( sequenceDefinition ) {
2023-01-09 19:02:19 -05:00
const itemDefinition = new DefinitionInfo ( definition , sequenceDefinition . itemType ) ;
2023-01-06 15:54:31 -08:00
// Add each item
while ( ! this . _objectReader . allowSequenceEnd ( ) ) {
2023-01-09 19:02:19 -05:00
const item = this . readValue ( itemDefinition ) ;
sequence . add ( item ) ;
2023-01-06 15:54:31 -08:00
}
}
// Illegal
else {
// Error
2023-01-09 19:02:19 -05:00
this . _context . error ( sequence , "A sequence was not expected" ) ;
2023-01-06 15:54:31 -08:00
// Skip each item
while ( ! this . _objectReader . allowSequenceEnd ( ) ) {
2023-01-09 19:02:19 -05:00
this . skipValue ( ) ;
2023-01-06 15:54:31 -08:00
}
}
2023-01-09 19:02:19 -05:00
sequence . definitionInfo = definition ;
return sequence ;
2023-01-06 15:54:31 -08:00
}
// Mapping
2023-01-09 19:02:19 -05:00
const mapping = this . _objectReader . allowMappingStart ( ) ;
2023-01-06 15:54:31 -08:00
if ( mapping ) {
2023-01-09 19:02:19 -05:00
const mappingDefinitions = definition . getDefinitionsOfType ( DefinitionType . Mapping ) as MappingDefinition [ ] ;
2023-01-06 15:54:31 -08:00
// Legal
if ( mappingDefinitions . length > 0 ) {
if (
mappingDefinitions . length > 1 ||
Object . keys ( mappingDefinitions [ 0 ] . properties ) . length > 0 ||
! mappingDefinitions [ 0 ] . looseKeyType
) {
2023-01-09 19:02:19 -05:00
this . handleMappingWithWellKnownProperties ( definition , mappingDefinitions , mapping ) ;
2023-01-06 15:54:31 -08:00
} else {
2023-01-09 19:02:19 -05:00
const keyDefinition = new DefinitionInfo ( definition , mappingDefinitions [ 0 ] . looseKeyType ) ;
const valueDefinition = new DefinitionInfo ( definition , mappingDefinitions [ 0 ] . looseValueType ) ;
2023-01-06 15:54:31 -08:00
this . handleMappingWithAllLooseProperties (
definition ,
keyDefinition ,
valueDefinition ,
mappingDefinitions [ 0 ] ,
mapping
2023-01-09 19:02:19 -05:00
) ;
2023-01-06 15:54:31 -08:00
}
}
// Illegal
else {
2023-01-09 19:02:19 -05:00
this . _context . error ( mapping , "A mapping was not expected" ) ;
2023-01-06 15:54:31 -08:00
2023-02-08 11:37:39 -08:00
while ( ! this . _objectReader . allowMappingEnd ( ) ) {
2023-01-09 19:02:19 -05:00
this . skipValue ( ) ;
this . skipValue ( ) ;
2023-01-06 15:54:31 -08:00
}
}
// handleMappingWithWellKnownProperties will only set a definition
// if it can identify a single matching definition
if ( ! mapping . definitionInfo ) {
2023-01-09 19:02:19 -05:00
mapping . definitionInfo = definition ;
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
return mapping ;
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
throw new Error ( "Expected a scalar value, a sequence, or a mapping" ) ;
2023-01-06 15:54:31 -08:00
}
private handleMappingWithWellKnownProperties (
definition : DefinitionInfo ,
mappingDefinitions : MappingDefinition [ ] ,
mapping : MappingToken
) : void {
// Check if loose properties are allowed
2023-01-09 19:02:19 -05:00
let looseKeyType : string | undefined ;
let looseValueType : string | undefined ;
let looseKeyDefinition : DefinitionInfo | undefined ;
let looseValueDefinition : DefinitionInfo | undefined ;
2023-01-06 15:54:31 -08:00
if ( mappingDefinitions [ 0 ] . looseKeyType ) {
2023-01-09 19:02:19 -05:00
looseKeyType = mappingDefinitions [ 0 ] . looseKeyType ;
looseValueType = mappingDefinitions [ 0 ] . looseValueType ;
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
const upperKeys : { [ upperKey : string ] : boolean } = { } ;
let hasExpressionKey = false ;
2023-01-06 15:54:31 -08:00
2023-01-09 19:02:19 -05:00
let rawLiteral : LiteralToken | undefined ;
2023-01-06 15:54:31 -08:00
while ( ( rawLiteral = this . _objectReader . allowLiteral ( ) ) ) {
2023-01-09 19:02:19 -05:00
const nextKeyScalar = this . parseScalar ( rawLiteral , definition ) ;
2023-01-06 15:54:31 -08:00
// Expression
if ( nextKeyScalar . isExpression ) {
2023-01-09 19:02:19 -05:00
hasExpressionKey = true ;
2023-01-06 15:54:31 -08:00
// Legal
if ( definition . allowedContext . length > 0 ) {
2023-01-09 19:02:19 -05:00
const anyDefinition = new DefinitionInfo ( definition , ANY ) ;
mapping . add ( nextKeyScalar , this . readValue ( anyDefinition ) ) ;
2023-01-06 15:54:31 -08:00
}
// Illegal
else {
2023-01-09 19:02:19 -05:00
this . _context . error ( nextKeyScalar , "A template expression is not allowed in this context" ) ;
this . skipValue ( ) ;
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
continue ;
2023-01-06 15:54:31 -08:00
}
// Convert to StringToken if required
const nextKey =
nextKeyScalar . templateTokenType === TokenType . String
? ( nextKeyScalar as StringToken )
: new StringToken (
nextKeyScalar . file ,
nextKeyScalar . range ,
nextKeyScalar . toString ( ) ,
nextKeyScalar . definitionInfo
2023-01-09 19:02:19 -05:00
) ;
2023-01-06 15:54:31 -08:00
// Duplicate
2023-02-28 10:18:46 -08:00
if ( nextKey . value ) {
const upperKey = nextKey . value . toUpperCase ( ) ;
if ( upperKeys [ upperKey ] ) {
this . _context . error ( nextKey , ` ' ${ nextKey . value } ' is already defined ` ) ;
this . skipValue ( ) ;
continue ;
}
upperKeys [ upperKey ] = true ;
2023-01-06 15:54:31 -08:00
}
// Well known
2023-01-09 19:02:19 -05:00
const nextPropertyDef = this . _schema . matchPropertyAndFilter ( mappingDefinitions , nextKey . value ) ;
2023-01-06 15:54:31 -08:00
if ( nextPropertyDef ) {
2023-01-09 19:02:19 -05:00
const nextDefinition = new DefinitionInfo ( definition , nextPropertyDef . type ) ;
2023-01-06 15:54:31 -08:00
// Store the definition on the key, the value may have its own definition
2023-01-09 19:02:19 -05:00
nextKey . definitionInfo = nextDefinition ;
2023-01-06 15:54:31 -08:00
// If the property has a description, it's a parameter that uses a shared type
// and we need to make sure its description is set if there is one
if ( nextPropertyDef . description ) {
2023-01-09 19:02:19 -05:00
nextKey . description = nextPropertyDef . description ;
2023-01-06 15:54:31 -08:00
}
2023-01-23 17:50:54 -08:00
const nextValue = this . readValue ( nextDefinition ) ;
2023-01-09 19:02:19 -05:00
mapping . add ( nextKey , nextValue ) ;
continue ;
2023-01-06 15:54:31 -08:00
}
// Loose
if ( looseKeyType ) {
if ( ! looseKeyDefinition ) {
2023-01-09 19:02:19 -05:00
looseKeyDefinition = new DefinitionInfo ( definition , looseKeyType ) ;
looseValueDefinition = new DefinitionInfo ( definition , looseValueType ! ) ;
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
this . validate ( nextKey , looseKeyDefinition ) ;
2023-01-06 15:54:31 -08:00
// Store the definition on the key, the value may have its own definition
2023-01-09 19:02:19 -05:00
const nextDefinition = new DefinitionInfo ( definition , mappingDefinitions [ 0 ] . looseValueType ) ;
nextKey . definitionInfo = nextDefinition ;
const nextValue = this . readValue ( looseValueDefinition ! ) ;
mapping . add ( nextKey , nextValue ) ;
continue ;
2023-01-06 15:54:31 -08:00
}
// Error
2023-01-09 19:02:19 -05:00
this . _context . error ( nextKey , ` Unexpected value ' ${ nextKey . value } ' ` ) ;
this . skipValue ( ) ;
2023-01-06 15:54:31 -08:00
}
// If we matched a single definition from multiple,
// update the token's definition to enable more specific editor
// completion and validation
if ( mappingDefinitions . length === 1 ) {
2023-01-09 19:02:19 -05:00
mapping . definitionInfo = new DefinitionInfo ( definition , mappingDefinitions [ 0 ] ) ;
2023-01-06 15:54:31 -08:00
}
// Unable to filter to one definition
if ( mappingDefinitions . length > 1 ) {
2023-01-09 19:02:19 -05:00
const hitCount : { [ key : string ] : number } = { } ;
2023-01-06 15:54:31 -08:00
for ( const mappingDefinition of mappingDefinitions ) {
for ( const key of Object . keys ( mappingDefinition . properties ) ) {
2023-01-09 19:02:19 -05:00
hitCount [ key ] = ( hitCount [ key ] ? ? 0 ) + 1 ;
2023-01-06 15:54:31 -08:00
}
}
2023-01-09 19:02:19 -05:00
const nonDuplicates : string [ ] = [ ] ;
2023-01-06 15:54:31 -08:00
for ( const key of Object . keys ( hitCount ) ) {
if ( hitCount [ key ] === 1 ) {
2023-01-09 19:02:19 -05:00
nonDuplicates . push ( key ) ;
2023-01-06 15:54:31 -08:00
}
}
this . _context . error (
mapping ,
` There's not enough info to determine what you meant. Add one of these properties: ${ nonDuplicates
. sort ( )
. join ( ", " ) } `
2023-01-09 19:02:19 -05:00
) ;
2023-01-06 15:54:31 -08:00
}
// Check required properties
else if ( mappingDefinitions . length === 1 && ! hasExpressionKey ) {
2023-01-09 19:02:19 -05:00
for ( const propertyName of Object . keys ( mappingDefinitions [ 0 ] . properties ) ) {
const propertyDef = mappingDefinitions [ 0 ] . properties [ propertyName ] ;
2023-01-06 15:54:31 -08:00
if ( propertyDef . required && ! upperKeys [ propertyName . toUpperCase ( ) ] ) {
2023-01-09 19:02:19 -05:00
this . _context . error ( mapping , ` Required property is missing: ${ propertyName } ` ) ;
2023-01-06 15:54:31 -08:00
}
}
}
2023-01-09 19:02:19 -05:00
this . expectMappingEnd ( ) ;
2023-01-06 15:54:31 -08:00
}
private handleMappingWithAllLooseProperties (
definition : DefinitionInfo ,
keyDefinition : DefinitionInfo ,
valueDefinition : DefinitionInfo ,
mappingDefinition : MappingDefinition ,
mapping : MappingToken
) : void {
2023-01-09 19:02:19 -05:00
let nextValue : TemplateToken ;
const upperKeys : { [ key : string ] : boolean } = { } ;
2023-01-06 15:54:31 -08:00
2023-01-09 19:02:19 -05:00
let rawLiteral : LiteralToken | undefined ;
2023-01-06 15:54:31 -08:00
while ( ( rawLiteral = this . _objectReader . allowLiteral ( ) ) ) {
2023-01-09 19:02:19 -05:00
const nextKeyScalar = this . parseScalar ( rawLiteral , definition ) ;
nextKeyScalar . definitionInfo = keyDefinition ;
2023-01-06 15:54:31 -08:00
// Expression
if ( nextKeyScalar . isExpression ) {
// Legal
if ( definition . allowedContext . length > 0 ) {
2023-01-09 19:02:19 -05:00
nextValue = this . readValue ( valueDefinition ) ;
mapping . add ( nextKeyScalar , nextValue ) ;
2023-01-06 15:54:31 -08:00
}
// Illegal
else {
2023-01-09 19:02:19 -05:00
this . _context . error ( nextKeyScalar , "A template expression is not allowed in this context" ) ;
this . skipValue ( ) ;
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
continue ;
2023-01-06 15:54:31 -08:00
}
// Convert to StringToken if required
const nextKey =
nextKeyScalar . templateTokenType === TokenType . String
? ( nextKeyScalar as StringToken )
: new StringToken (
nextKeyScalar . file ,
nextKeyScalar . range ,
nextKeyScalar . toString ( ) ,
nextKeyScalar . definitionInfo
2023-01-09 19:02:19 -05:00
) ;
2023-01-06 15:54:31 -08:00
// Duplicate
2023-02-28 10:18:46 -08:00
if ( nextKey . value ) {
const upperKey = nextKey . value . toUpperCase ( ) ;
if ( upperKeys [ upperKey ] ) {
this . _context . error ( nextKey , ` ' ${ nextKey . value } ' is already defined ` ) ;
this . skipValue ( ) ;
continue ;
}
upperKeys [ upperKey ] = true ;
2023-01-06 15:54:31 -08:00
}
// Validate
2023-01-09 19:02:19 -05:00
this . validate ( nextKey , keyDefinition ) ;
2023-01-06 15:54:31 -08:00
// Store the definition on the key, the value may have its own definition
2023-01-09 19:02:19 -05:00
const nextDefinition = new DefinitionInfo ( definition , mappingDefinition . looseValueType ) ;
nextKey . definitionInfo = nextDefinition ;
2023-01-06 15:54:31 -08:00
// Add the pair
2023-01-09 19:02:19 -05:00
nextValue = this . readValue ( valueDefinition ) ;
mapping . add ( nextKey , nextValue ) ;
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
this . expectMappingEnd ( ) ;
2023-01-06 15:54:31 -08:00
}
private expectMappingEnd ( ) : void {
if ( ! this . _objectReader . allowMappingEnd ( ) ) {
2023-01-09 19:02:19 -05:00
throw new Error ( "Expected mapping end" ) ; // Should never happen
2023-01-06 15:54:31 -08:00
}
}
private skipValue ( ) : void {
// Scalar
if ( this . _objectReader . allowLiteral ( ) ) {
// Intentionally empty
}
// Sequence
else if ( this . _objectReader . allowSequenceStart ( ) ) {
while ( ! this . _objectReader . allowSequenceEnd ( ) ) {
2023-01-09 19:02:19 -05:00
this . skipValue ( ) ;
2023-01-06 15:54:31 -08:00
}
}
// Mapping
else if ( this . _objectReader . allowMappingStart ( ) ) {
while ( ! this . _objectReader . allowMappingEnd ( ) ) {
2023-01-09 19:02:19 -05:00
this . skipValue ( ) ;
this . skipValue ( ) ;
2023-01-06 15:54:31 -08:00
}
}
// Unexpected
else {
2023-01-09 19:02:19 -05:00
throw new Error ( "Expected a scalar value, a sequence, or a mapping" ) ;
2023-01-06 15:54:31 -08:00
}
}
2023-01-09 19:02:19 -05:00
private validate ( scalar : ScalarToken , definition : DefinitionInfo ) : ScalarToken {
2023-01-06 15:54:31 -08:00
switch ( scalar . templateTokenType ) {
case TokenType.Null :
case TokenType.Boolean :
case TokenType.Number :
case TokenType . String : {
2023-01-09 19:02:19 -05:00
const literal = scalar as LiteralToken ;
2023-01-06 15:54:31 -08:00
// Legal
2023-01-09 19:02:19 -05:00
const scalarDefinitions = definition . getScalarDefinitions ( ) ;
let relevantDefinition : ScalarDefinition | undefined ;
if ( ( relevantDefinition = scalarDefinitions . find ( x = > x . isMatch ( literal ) ) ) ) {
scalar . definitionInfo = new DefinitionInfo ( definition , relevantDefinition ) ;
return scalar ;
2023-01-06 15:54:31 -08:00
}
// Not a string, convert
if ( literal . templateTokenType !== TokenType . String ) {
const stringLiteral = new StringToken (
literal . file ,
literal . range ,
literal . toString ( ) ,
literal . definitionInfo
2023-01-09 19:02:19 -05:00
) ;
2023-01-06 15:54:31 -08:00
// Legal
2023-01-09 19:02:19 -05:00
if ( ( relevantDefinition = scalarDefinitions . find ( x = > x . isMatch ( stringLiteral ) ) ) ) {
stringLiteral . definitionInfo = new DefinitionInfo ( definition , relevantDefinition ) ;
return stringLiteral ;
2023-01-06 15:54:31 -08:00
}
}
// Illegal
2023-01-09 19:02:19 -05:00
this . _context . error ( literal , ` Unexpected value ' ${ literal . toString ( ) } ' ` ) ;
return scalar ;
2023-01-06 15:54:31 -08:00
}
case TokenType . BasicExpression :
// Illegal
if ( definition . allowedContext . length === 0 ) {
2023-01-09 19:02:19 -05:00
this . _context . error ( scalar , "A template expression is not allowed in this context" ) ;
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
return scalar ;
2023-01-06 15:54:31 -08:00
default :
2023-01-09 19:02:19 -05:00
this . _context . error ( scalar , ` Unexpected value ' ${ scalar . toString ( ) } ' ` ) ;
return scalar ;
2023-01-06 15:54:31 -08:00
}
}
2023-01-09 19:02:19 -05:00
private parseScalar ( token : LiteralToken , definitionInfo : DefinitionInfo ) : ScalarToken {
2023-01-06 15:54:31 -08:00
// Not a string
2023-02-28 10:18:46 -08:00
if ( ! isString ( token ) || ! token . value ) {
2023-01-09 19:02:19 -05:00
return token ;
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
const allowedContext = definitionInfo . allowedContext ;
2026-01-05 08:33:10 -06:00
const isSingleLine = token . range === undefined || token . range . start . line === token . range . end . line ;
// For single-line strings, use token.value (without YAML quotes) for expression detection,
// because token.source includes quote characters that would be incorrectly detected as literal text.
// For multi-line block scalars, use token.source directly because it makes position calculation easier
// (no quote characters to handle, and token.source preserves the original line/column structure in YAML).
const raw = isSingleLine ? token.value : token.source ? ? token . value ;
2023-01-06 15:54:31 -08:00
2023-01-09 19:02:19 -05:00
let startExpression : number = raw . indexOf ( OPEN_EXPRESSION ) ;
2023-01-06 15:54:31 -08:00
if ( startExpression < 0 ) {
2025-11-24 09:12:26 -06:00
// Doesn't contain "{{"
2023-01-09 19:02:19 -05:00
return token ;
2023-01-06 15:54:31 -08:00
}
// Break the value into segments of LiteralToken and ExpressionToken
2023-01-09 19:02:19 -05:00
let encounteredError = false ;
const segments : ScalarToken [ ] = [ ] ;
let i = 0 ;
2023-01-06 15:54:31 -08:00
while ( i < raw . length ) {
// An expression starts here
if ( i === startExpression ) {
// Find the end of the expression - i.e. "}}"
2023-01-09 19:02:19 -05:00
startExpression = i ;
let endExpression = - 1 ;
let inString = false ;
2023-01-06 15:54:31 -08:00
for ( i += OPEN_EXPRESSION . length ; i < raw . length ; i ++ ) {
if ( raw [ i ] === "'" ) {
2023-01-09 19:02:19 -05:00
inString = ! inString ; // Note, this handles escaped single quotes gracefully. E.x. 'foo''bar'
2023-01-06 15:54:31 -08:00
} else if ( ! inString && raw [ i ] === "}" && raw [ i - 1 ] === "}" ) {
2023-01-09 19:02:19 -05:00
endExpression = i ;
i ++ ;
break ;
2023-01-06 15:54:31 -08:00
}
}
// Check if not closed
if ( endExpression < startExpression ) {
this . _context . error (
token ,
"The expression is not closed. An unescaped ${{ sequence was found, but the closing }} sequence was not found."
2023-01-09 19:02:19 -05:00
) ;
return token ;
2023-01-06 15:54:31 -08:00
}
// Parse the expression
const rawExpression = raw . substr (
startExpression + OPEN_EXPRESSION . length ,
2023-01-09 19:02:19 -05:00
endExpression - startExpression + 1 - OPEN_EXPRESSION . length - CLOSE_EXPRESSION . length
) ;
let tr = token . range ! ;
2026-01-05 08:33:10 -06:00
if ( isSingleLine ) {
// Single-line: Adjust the range to only cover the sub-expression.
// Calculate offset to account for YAML quote characters.
// For example, `"${{ expr }}"` has source with quotes, value without.
const offset = ( token . source ? ? raw ) . indexOf ( OPEN_EXPRESSION ) - raw . indexOf ( OPEN_EXPRESSION ) ;
2023-01-06 15:54:31 -08:00
tr = {
2026-01-05 08:33:10 -06:00
start : { line : tr.start.line , column : tr.start.column + startExpression + offset } ,
end : { line : tr.end.line , column : tr.start.column + endExpression + 1 + offset }
2023-01-09 19:02:19 -05:00
} ;
2023-01-06 15:54:31 -08:00
} else {
2026-01-05 08:33:10 -06:00
// Multi-line: Adjust the range to only cover the expression
2023-01-09 19:02:19 -05:00
const startRaw = raw . substring ( 0 , startExpression ) ;
const adjustedStartLine = startRaw . split ( "\n" ) . length ;
const beginningOfLine = startRaw . lastIndexOf ( "\n" ) ;
const adjustedStart = startExpression - beginningOfLine ;
const adjustedEnd = endExpression - beginningOfLine + 1 ;
2023-01-06 15:54:31 -08:00
tr = {
2023-01-27 15:43:52 -05:00
start : { line : tr.start.line + adjustedStartLine , column : adjustedStart } ,
end : { line : tr.start.line + adjustedStartLine , column : adjustedEnd }
2023-01-09 19:02:19 -05:00
} ;
2023-01-06 15:54:31 -08:00
}
2023-01-25 13:38:28 -08:00
const expression = this . parseIntoExpressionToken ( tr , rawExpression , allowedContext , token , definitionInfo ) ;
2023-01-06 15:54:31 -08:00
if ( ! expression ) {
// Record that we've hit an error but continue to validate any other expressions
// that might be in the string
2023-01-09 19:02:19 -05:00
encounteredError = true ;
2023-01-06 15:54:31 -08:00
} else {
// Check if a directive was used when not allowed
2023-01-09 19:02:19 -05:00
if ( expression . directive && ( startExpression !== 0 || i < raw . length ) ) {
2023-01-06 15:54:31 -08:00
this . _context . error (
token ,
` The directive ' ${ expression . directive } ' is not allowed in this context. Directives are not supported for expressions that are embedded within a string. Directives are only supported when the entire value is an expression. `
2023-01-09 19:02:19 -05:00
) ;
return token ;
2023-01-06 15:54:31 -08:00
}
// Add the segment
2023-01-09 19:02:19 -05:00
segments . push ( expression ) ;
2023-01-06 15:54:31 -08:00
}
// Look for the next expression
2023-01-09 19:02:19 -05:00
startExpression = raw . indexOf ( OPEN_EXPRESSION , i ) ;
2023-01-06 15:54:31 -08:00
}
// The next expression is further ahead
else if ( i < startExpression ) {
// Append the segment
2023-01-09 19:02:19 -05:00
this . addString ( segments , token . range , raw . substr ( i , startExpression - i ) , token . definitionInfo ) ;
2023-01-06 15:54:31 -08:00
// Adjust the position
2023-01-09 19:02:19 -05:00
i = startExpression ;
2023-01-06 15:54:31 -08:00
}
// No remaining expressions
else {
2023-01-09 19:02:19 -05:00
this . addString ( segments , token . range , raw . substr ( i ) , token . definitionInfo ) ;
break ;
2023-01-06 15:54:31 -08:00
}
}
// If we've hit any error during parsing, return the original token
if ( encounteredError ) {
2023-01-09 19:02:19 -05:00
return token ;
2023-01-06 15:54:31 -08:00
}
// Check if can convert to a literal
// For example, the escaped expression: ${{ '{{ this is a literal }}' }}
2023-01-09 19:02:19 -05:00
if ( segments . length === 1 && segments [ 0 ] . templateTokenType === TokenType . BasicExpression ) {
const basicExpression = segments [ 0 ] as BasicExpressionToken ;
const str = this . getExpressionString ( basicExpression . expression ) ;
2023-01-06 15:54:31 -08:00
if ( str !== undefined ) {
2023-01-09 19:02:19 -05:00
return new StringToken ( this . _fileId , token . range , str , token . definitionInfo ) ;
2023-01-06 15:54:31 -08:00
}
}
// Check if only one segment
if ( segments . length === 1 ) {
2023-01-09 19:02:19 -05:00
return segments [ 0 ] ;
2023-01-06 15:54:31 -08:00
}
// Build the new expression, using the format function
2023-01-09 19:02:19 -05:00
const format : string [ ] = [ ] ;
const args : string [ ] = [ ] ;
const expressionTokens : BasicExpressionToken [ ] = [ ] ;
let argIndex = 0 ;
2023-01-06 15:54:31 -08:00
for ( const segment of segments ) {
if ( isString ( segment ) ) {
const text = segment . value
. replace ( /'/g , "''" ) // Escape quotes
. replace ( /\{/g , "{{" ) // Escape braces
2023-01-09 19:02:19 -05:00
. replace ( /\}/g , "}}" ) ;
format . push ( text ) ;
2023-01-06 15:54:31 -08:00
} else {
2023-01-09 19:02:19 -05:00
format . push ( ` { ${ argIndex } } ` ) ; // Append format arg
argIndex ++ ;
2023-01-06 15:54:31 -08:00
2023-01-09 19:02:19 -05:00
const expression = segment as BasicExpressionToken ;
args . push ( ", " ) ;
args . push ( expression . expression ) ;
2023-01-06 15:54:31 -08:00
2023-01-09 19:02:19 -05:00
expressionTokens . push ( expression ) ;
2023-01-06 15:54:31 -08:00
}
}
return new BasicExpressionToken (
this . _fileId ,
token . range ,
` format(' ${ format . join ( "" ) } ' ${ args . join ( "" ) } ) ` ,
2023-02-07 16:42:28 -05:00
definitionInfo ,
expressionTokens ,
2026-01-12 09:36:43 -06:00
raw ,
undefined ,
token . blockScalarHeader
2023-01-09 19:02:19 -05:00
) ;
2023-01-06 15:54:31 -08:00
}
private parseIntoExpressionToken (
tr : TokenRange ,
rawExpression : string ,
allowedContext : string [ ] ,
2023-02-02 08:52:23 -08:00
token : StringToken ,
2023-01-25 13:38:28 -08:00
definitionInfo : DefinitionInfo | undefined
2023-01-06 15:54:31 -08:00
) : ExpressionToken | undefined {
2023-02-02 08:52:23 -08:00
const parseExpressionResult = this . parseExpression ( tr , token , rawExpression , allowedContext , definitionInfo ) ;
2023-01-06 15:54:31 -08:00
// Check for error
if ( parseExpressionResult . error ) {
2023-01-09 19:02:19 -05:00
this . _context . error ( token , parseExpressionResult . error , tr ) ;
return undefined ;
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
return parseExpressionResult . expression ! ;
2023-01-06 15:54:31 -08:00
}
private parseExpression (
2023-02-02 08:52:23 -08:00
range : TokenRange ,
token : StringToken ,
2023-01-06 15:54:31 -08:00
value : string ,
allowedContext : string [ ] ,
definitionInfo : DefinitionInfo | undefined
) : ParseExpressionResult {
2023-01-09 19:02:19 -05:00
const trimmed = value . trim ( ) ;
2023-01-06 15:54:31 -08:00
// Check if the value is empty
if ( ! trimmed ) {
return < ParseExpressionResult > {
2023-01-09 19:02:19 -05:00
error : new Error ( "An expression was expected" )
} ;
2023-01-06 15:54:31 -08:00
}
// Try to find a matching directive
2023-01-09 19:02:19 -05:00
const matchDirectiveResult = this . matchDirective ( trimmed , INSERT_DIRECTIVE , 0 ) ;
2023-01-06 15:54:31 -08:00
if ( matchDirectiveResult . isMatch ) {
return < ParseExpressionResult > {
2023-01-09 19:02:19 -05:00
expression : new InsertExpressionToken ( this . _fileId , range , definitionInfo )
} ;
2023-01-06 15:54:31 -08:00
} else if ( matchDirectiveResult . error ) {
return < ParseExpressionResult > {
2023-01-09 19:02:19 -05:00
error : matchDirectiveResult.error
} ;
2023-01-06 15:54:31 -08:00
}
// Check if valid expression
try {
2023-01-09 19:02:19 -05:00
ExpressionToken . validateExpression ( trimmed , allowedContext ) ;
2023-01-06 15:54:31 -08:00
} catch ( err ) {
return < ParseExpressionResult > {
2023-01-09 19:02:19 -05:00
error : err
} ;
2023-01-06 15:54:31 -08:00
}
2023-02-02 08:52:23 -08:00
const startTrim = value . length - value . trimStart ( ) . length ;
const endTrim = value . length - value . trimEnd ( ) . length ;
const expressionRange : TokenRange = {
start : {
. . . range . start ,
column : range.start.column + OPEN_EXPRESSION . length + startTrim
} ,
end : {
. . . range . end ,
column : range.end.column - CLOSE_EXPRESSION . length - endTrim
}
} ;
2023-01-06 15:54:31 -08:00
// Return the expression
return < ParseExpressionResult > {
2023-02-02 08:52:23 -08:00
expression : new BasicExpressionToken (
this . _fileId ,
range ,
trimmed ,
definitionInfo ,
undefined ,
token . source ,
2026-01-12 09:36:43 -06:00
expressionRange ,
token . blockScalarHeader
2023-02-02 08:52:23 -08:00
) ,
2023-01-09 19:02:19 -05:00
error : undefined
} ;
2023-01-06 15:54:31 -08:00
}
private addString (
segments : ScalarToken [ ] ,
range : TokenRange | undefined ,
value : string ,
definition : DefinitionInfo | undefined
) : void {
// If the last segment was a LiteralToken, then append to the last segment
2023-01-09 19:02:19 -05:00
if ( segments . length > 0 && segments [ segments . length - 1 ] . templateTokenType === TokenType . String ) {
const lastSegment = segments [ segments . length - 1 ] as StringToken ;
segments [ segments . length - 1 ] = new StringToken ( this . _fileId , range , ` ${ lastSegment . value } ${ value } ` , definition ) ;
2023-01-06 15:54:31 -08:00
}
// Otherwise add a new LiteralToken
else {
2023-01-09 19:02:19 -05:00
segments . push ( new StringToken ( this . _fileId , range , value , definition ) ) ;
2023-01-06 15:54:31 -08:00
}
}
2023-01-09 19:02:19 -05:00
private matchDirective ( trimmed : string , directive : string , expectedParameters : number ) : MatchDirectiveResult {
const parameters : string [ ] = [ ] ;
2023-01-06 15:54:31 -08:00
if (
trimmed . startsWith ( directive ) &&
2023-01-09 19:02:19 -05:00
( trimmed . length === directive . length || WHITESPACE_PATTERN . test ( trimmed [ directive . length ] ) )
2023-01-06 15:54:31 -08:00
) {
2023-01-09 19:02:19 -05:00
let startIndex = directive . length ;
let inString = false ;
let parens = 0 ;
2023-01-06 15:54:31 -08:00
for ( let i = startIndex ; i < trimmed . length ; i ++ ) {
2023-01-09 19:02:19 -05:00
const c = trimmed [ i ] ;
2023-01-06 15:54:31 -08:00
if ( WHITESPACE_PATTERN . test ( c ) && ! inString && parens == 0 ) {
if ( startIndex < 1 ) {
2023-01-09 19:02:19 -05:00
parameters . push ( trimmed . substr ( startIndex , i - startIndex ) ) ;
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
startIndex = i + 1 ;
2023-01-06 15:54:31 -08:00
} else if ( c === "'" ) {
2023-01-09 19:02:19 -05:00
inString = ! inString ;
2023-01-06 15:54:31 -08:00
} else if ( c === "(" && ! inString ) {
2023-01-09 19:02:19 -05:00
parens ++ ;
2023-01-06 15:54:31 -08:00
} else if ( c === ")" && ! inString ) {
2023-01-09 19:02:19 -05:00
parens -- ;
2023-01-06 15:54:31 -08:00
}
}
if ( startIndex < trimmed . length ) {
2023-01-09 19:02:19 -05:00
parameters . push ( trimmed . substr ( startIndex ) ) ;
2023-01-06 15:54:31 -08:00
}
if ( expectedParameters != parameters . length ) {
return < MatchDirectiveResult > {
isMatch : false ,
parameters : [ ] ,
error : new Error (
` Exactly ${ expectedParameters } parameter(s) were expected following the directive ' ${ directive } '. Actual parameter count: ${ parameters . length } `
2023-01-09 19:02:19 -05:00
)
} ;
2023-01-06 15:54:31 -08:00
}
return < MatchDirectiveResult > {
isMatch : true ,
2023-01-09 19:02:19 -05:00
parameters : parameters
} ;
2023-01-06 15:54:31 -08:00
}
return < MatchDirectiveResult > {
isMatch : false ,
2023-01-09 19:02:19 -05:00
parameters : parameters
} ;
2023-01-06 15:54:31 -08:00
}
private getExpressionString ( trimmed : string ) : string | undefined {
2023-01-09 19:02:19 -05:00
const result : string [ ] = [ ] ;
2023-01-06 15:54:31 -08:00
2023-01-09 19:02:19 -05:00
let inString = false ;
2023-01-06 15:54:31 -08:00
for ( let i = 0 ; i < trimmed . length ; i ++ ) {
2023-01-09 19:02:19 -05:00
const c = trimmed [ i ] ;
2023-01-06 15:54:31 -08:00
if ( c === "'" ) {
2023-01-09 19:02:19 -05:00
inString = ! inString ;
2023-01-06 15:54:31 -08:00
if ( inString && i !== 0 ) {
2023-01-09 19:02:19 -05:00
result . push ( c ) ;
2023-01-06 15:54:31 -08:00
}
} else if ( ! inString ) {
2023-01-09 19:02:19 -05:00
return undefined ;
2023-01-06 15:54:31 -08:00
} else {
2023-01-09 19:02:19 -05:00
result . push ( c ) ;
2023-01-06 15:54:31 -08:00
}
}
2023-01-09 19:02:19 -05:00
return result . join ( "" ) ;
2023-01-06 15:54:31 -08:00
}
}
interface ParseExpressionResult {
2023-01-09 19:02:19 -05:00
expression : ExpressionToken | undefined ;
error : Error | undefined ;
2023-01-06 15:54:31 -08:00
}
interface MatchDirectiveResult {
2023-01-09 19:02:19 -05:00
isMatch : boolean ;
parameters : string [ ] ;
error : Error | undefined ;
2023-01-06 15:54:31 -08:00
}