2023-01-06 15:54:31 -08:00
// template-reader *just* does schema validation
2023-01-09 19:02:19 -05:00
import { ObjectReader } from "./object-reader" ;
import { TemplateSchema } from "./schema" ;
import { DefinitionInfo } from "./schema/definition-info" ;
import { DefinitionType } from "./schema/definition-type" ;
import { MappingDefinition } from "./schema/mapping-definition" ;
import { ScalarDefinition } from "./schema/scalar-definition" ;
import { SequenceDefinition } from "./schema/sequence-definition" ;
import { StringDefinition } from "./schema/string-definition" ;
import { ANY , CLOSE_EXPRESSION , INSERT_DIRECTIVE , OPEN_EXPRESSION } from "./template-constants" ;
import { TemplateContext } from "./template-context" ;
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
} from "./tokens" ;
import { TokenRange } from "./tokens/token-range" ;
import { isString } from "./tokens/type-guards" ;
import { TokenType } from "./tokens/types" ;
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 ;
2023-01-06 15:54:31 -08:00
2023-01-09 19:02:19 -05:00
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
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-01-09 19:02:19 -05:00
const upperKey = nextKey . value . toUpperCase ();
2023-01-06 15:54:31 -08:00
if ( upperKeys [ upperKey ]) {
2023-01-09 19:02:19 -05:00
this . _context . error ( nextKey , `' ${ nextKey . value } ' is already defined` );
this . skipValue ();
continue ;
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
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 ;
2023-01-06 15:54:31 -08:00
2023-01-09 19:02:19 -05:00
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-01-09 19:02:19 -05:00
const upperKey = nextKey . value . toUpperCase ();
2023-01-06 15:54:31 -08:00
if ( upperKeys [ upperKey ]) {
2023-01-09 19:02:19 -05:00
this . _context . error ( nextKey , `' ${ nextKey . value } ' is already defined` );
this . skipValue ();
continue ;
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
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
if ( ! isString ( token )) {
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 ;
const raw = 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 ) {
// Doesn't contain "${{"
// Check if value should still be evaluated as an expression
2023-01-09 19:02:19 -05:00
if ( definitionInfo . definition instanceof StringDefinition && definitionInfo . definition . isExpression ) {
const expression = this . parseIntoExpressionToken ( token . range ! , raw , allowedContext , token );
2023-01-06 15:54:31 -08:00
if ( expression ) {
2023-01-09 19:02:19 -05:00
return expression ;
2023-01-06 15:54:31 -08:00
}
}
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
);
2023-01-06 15:54:31 -08:00
2023-01-09 19:02:19 -05:00
let tr = token . range ! ;
2023-01-06 15:54:31 -08:00
if ( tr . start [ 0 ] === tr . end [ 0 ]) {
// If it's a single line expression, adjust the range to only cover the sub-expression
tr = {
start : [ tr . start [ 0 ], tr . start [ 1 ] + startExpression ],
2023-01-09 19:02:19 -05:00
end : [ tr . end [ 0 ], tr . start [ 1 ] + endExpression + 1 ]
};
2023-01-06 15:54:31 -08:00
} else {
// Adjust the range to only cover the expression for multi-line strings
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 = {
start : [ tr . start [ 0 ] + adjustedStartLine , adjustedStart ],
2023-01-09 19:02:19 -05:00
end : [ tr . start [ 0 ] + adjustedStartLine , adjustedEnd ]
};
2023-01-06 15:54:31 -08:00
}
2023-01-09 19:02:19 -05:00
const expression = this . parseIntoExpressionToken ( tr , rawExpression , allowedContext , token );
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 ( "" ) } )` ,
token . definitionInfo ,
expressionTokens
2023-01-09 19:02:19 -05:00
);
2023-01-06 15:54:31 -08:00
}
private parseIntoExpressionToken (
tr : TokenRange ,
rawExpression : string ,
allowedContext : string [],
token : TemplateToken
) : ExpressionToken | undefined {
2023-01-09 19:02:19 -05:00
const parseExpressionResult = this . parseExpression ( tr , rawExpression , allowedContext , token . 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 (
range : TokenRange | undefined ,
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
}
// Return the expression
return < ParseExpressionResult >{
2023-01-09 19:02:19 -05:00
expression : new BasicExpressionToken ( this . _fileId , range , trimmed , definitionInfo , undefined ),
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
}