Files
languageservices/workflow-parser/src/templates/template-reader.ts
T

808 lines
26 KiB
TypeScript
Raw Normal View History

// template-reader *just* does schema validation
2023-03-15 15:58:44 -04:00
/* eslint-disable @typescript-eslint/no-non-null-assertion */
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";
import {
BasicExpressionToken,
ExpressionToken,
InsertExpressionToken,
LiteralToken,
MappingToken,
ScalarToken,
StringToken,
2023-01-09 19:02:19 -05:00
TemplateToken
} 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-09 19:02:19 -05:00
const WHITESPACE_PATTERN = /\s/;
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;
try {
2023-01-09 19:02:19 -05:00
objectReader.validateStart();
const definition = new DefinitionInfo(context.schema, type);
value = reader.readValue(definition);
objectReader.validateEnd();
} catch (err) {
2023-01-09 19:02:19 -05:00
context.error(fileId, err);
}
2023-01-09 19:02:19 -05:00
return value;
}
export interface ReadTemplateResult {
2023-01-09 19:02:19 -05:00
value: TemplateToken;
bytes: number;
}
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;
}
public readValue(definition: DefinitionInfo): TemplateToken {
// Scalar
2023-01-09 19:02:19 -05:00
const literal = this._objectReader.allowLiteral();
if (literal) {
2023-01-09 19:02:19 -05:00
let scalar = this.parseScalar(literal, definition);
scalar = this.validate(scalar, definition);
return scalar;
}
// Sequence
2023-01-09 19:02:19 -05:00
const sequence = this._objectReader.allowSequenceStart();
if (sequence) {
2023-01-09 19:02:19 -05:00
const sequenceDefinition = definition.getDefinitionsOfType(DefinitionType.Sequence)[0] as
| SequenceDefinition
| undefined;
// Legal
if (sequenceDefinition) {
2023-01-09 19:02:19 -05:00
const itemDefinition = new DefinitionInfo(definition, sequenceDefinition.itemType);
// Add each item
while (!this._objectReader.allowSequenceEnd()) {
2023-01-09 19:02:19 -05:00
const item = this.readValue(itemDefinition);
sequence.add(item);
}
}
// Illegal
else {
// Error
2023-01-09 19:02:19 -05:00
this._context.error(sequence, "A sequence was not expected");
// Skip each item
while (!this._objectReader.allowSequenceEnd()) {
2023-01-09 19:02:19 -05:00
this.skipValue();
}
}
2023-01-09 19:02:19 -05:00
sequence.definitionInfo = definition;
return sequence;
}
// Mapping
2023-01-09 19:02:19 -05:00
const mapping = this._objectReader.allowMappingStart();
if (mapping) {
2023-01-09 19:02:19 -05:00
const mappingDefinitions = definition.getDefinitionsOfType(DefinitionType.Mapping) as MappingDefinition[];
// 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);
} 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);
this.handleMappingWithAllLooseProperties(
definition,
keyDefinition,
valueDefinition,
mappingDefinitions[0],
mapping
2023-01-09 19:02:19 -05:00
);
}
}
// Illegal
else {
2023-01-09 19:02:19 -05:00
this._context.error(mapping, "A mapping was not expected");
2023-02-08 11:37:39 -08:00
while (!this._objectReader.allowMappingEnd()) {
2023-01-09 19:02:19 -05:00
this.skipValue();
this.skipValue();
}
}
// 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-09 19:02:19 -05:00
return mapping;
}
2023-01-09 19:02:19 -05:00
throw new Error("Expected a scalar value, a sequence, or a mapping");
}
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;
if (mappingDefinitions[0].looseKeyType) {
2023-01-09 19:02:19 -05:00
looseKeyType = mappingDefinitions[0].looseKeyType;
looseValueType = mappingDefinitions[0].looseValueType;
}
2023-01-09 19:02:19 -05:00
const upperKeys: {[upperKey: string]: boolean} = {};
let hasExpressionKey = false;
2023-01-09 19:02:19 -05:00
let rawLiteral: LiteralToken | undefined;
while ((rawLiteral = this._objectReader.allowLiteral())) {
2023-01-09 19:02:19 -05:00
const nextKeyScalar = this.parseScalar(rawLiteral, definition);
// Expression
if (nextKeyScalar.isExpression) {
2023-01-09 19:02:19 -05:00
hasExpressionKey = true;
// 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));
}
// 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-09 19:02:19 -05:00
continue;
}
// 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
);
// 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;
}
// Well known
2023-01-09 19:02:19 -05:00
const nextPropertyDef = this._schema.matchPropertyAndFilter(mappingDefinitions, nextKey.value);
if (nextPropertyDef) {
2023-01-09 19:02:19 -05:00
const nextDefinition = new DefinitionInfo(definition, nextPropertyDef.type);
// Store the definition on the key, the value may have its own definition
2023-01-09 19:02:19 -05:00
nextKey.definitionInfo = nextDefinition;
// 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-23 17:50:54 -08:00
const nextValue = this.readValue(nextDefinition);
2023-01-09 19:02:19 -05:00
mapping.add(nextKey, nextValue);
continue;
}
// 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-09 19:02:19 -05:00
this.validate(nextKey, looseKeyDefinition);
// 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;
}
// Error
2023-01-09 19:02:19 -05:00
this._context.error(nextKey, `Unexpected value '${nextKey.value}'`);
this.skipValue();
}
// 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]);
}
// Unable to filter to one definition
if (mappingDefinitions.length > 1) {
2023-01-09 19:02:19 -05:00
const hitCount: {[key: string]: number} = {};
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-09 19:02:19 -05:00
const nonDuplicates: string[] = [];
for (const key of Object.keys(hitCount)) {
if (hitCount[key] === 1) {
2023-01-09 19:02:19 -05:00
nonDuplicates.push(key);
}
}
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
);
}
// 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];
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-09 19:02:19 -05:00
this.expectMappingEnd();
}
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-09 19:02:19 -05:00
let rawLiteral: LiteralToken | undefined;
while ((rawLiteral = this._objectReader.allowLiteral())) {
2023-01-09 19:02:19 -05:00
const nextKeyScalar = this.parseScalar(rawLiteral, definition);
nextKeyScalar.definitionInfo = keyDefinition;
// 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);
}
// 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-09 19:02:19 -05:00
continue;
}
// 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
);
// 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;
}
// Validate
2023-01-09 19:02:19 -05:00
this.validate(nextKey, keyDefinition);
// 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;
// Add the pair
2023-01-09 19:02:19 -05:00
nextValue = this.readValue(valueDefinition);
mapping.add(nextKey, nextValue);
}
2023-01-09 19:02:19 -05:00
this.expectMappingEnd();
}
private expectMappingEnd(): void {
if (!this._objectReader.allowMappingEnd()) {
2023-01-09 19:02:19 -05:00
throw new Error("Expected mapping end"); // Should never happen
}
}
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();
}
}
// Mapping
else if (this._objectReader.allowMappingStart()) {
while (!this._objectReader.allowMappingEnd()) {
2023-01-09 19:02:19 -05:00
this.skipValue();
this.skipValue();
}
}
// Unexpected
else {
2023-01-09 19:02:19 -05:00
throw new Error("Expected a scalar value, a sequence, or a mapping");
}
}
2023-01-09 19:02:19 -05:00
private validate(scalar: ScalarToken, definition: DefinitionInfo): ScalarToken {
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;
// 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;
}
// 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
);
// 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;
}
}
// Illegal
2023-01-09 19:02:19 -05:00
this._context.error(literal, `Unexpected value '${literal.toString()}'`);
return scalar;
}
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-09 19:02:19 -05:00
return scalar;
default:
2023-01-09 19:02:19 -05:00
this._context.error(scalar, `Unexpected value '${scalar.toString()}'`);
return scalar;
}
}
2023-01-09 19:02:19 -05:00
private parseScalar(token: LiteralToken, definitionInfo: DefinitionInfo): ScalarToken {
// 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-09 19:02:19 -05:00
const allowedContext = definitionInfo.allowedContext;
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-09 19:02:19 -05:00
let startExpression: number = raw.indexOf(OPEN_EXPRESSION);
if (startExpression < 0) {
// Doesn't contain "{{"
2023-01-09 19:02:19 -05:00
return token;
}
// 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;
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;
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'
} else if (!inString && raw[i] === "}" && raw[i - 1] === "}") {
2023-01-09 19:02:19 -05:00
endExpression = i;
i++;
break;
}
}
// 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;
}
// 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!;
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);
tr = {
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
};
} else {
// 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;
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
};
}
const expression = this.parseIntoExpressionToken(tr, rawExpression, allowedContext, token, definitionInfo);
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;
} 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)) {
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;
}
// Add the segment
2023-01-09 19:02:19 -05:00
segments.push(expression);
}
// Look for the next expression
2023-01-09 19:02:19 -05:00
startExpression = raw.indexOf(OPEN_EXPRESSION, i);
}
// 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);
// Adjust the position
2023-01-09 19:02:19 -05:00
i = startExpression;
}
// No remaining expressions
else {
2023-01-09 19:02:19 -05:00
this.addString(segments, token.range, raw.substr(i), token.definitionInfo);
break;
}
}
// If we've hit any error during parsing, return the original token
if (encounteredError) {
2023-01-09 19:02:19 -05:00
return token;
}
// 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);
if (str !== undefined) {
2023-01-09 19:02:19 -05:00
return new StringToken(this._fileId, token.range, str, token.definitionInfo);
}
}
// Check if only one segment
if (segments.length === 1) {
2023-01-09 19:02:19 -05:00
return segments[0];
}
// 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;
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);
} else {
2023-01-09 19:02:19 -05:00
format.push(`{${argIndex}}`); // Append format arg
argIndex++;
2023-01-09 19:02:19 -05:00
const expression = segment as BasicExpressionToken;
args.push(", ");
args.push(expression.expression);
2023-01-09 19:02:19 -05:00
expressionTokens.push(expression);
}
}
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
);
}
private parseIntoExpressionToken(
tr: TokenRange,
rawExpression: string,
allowedContext: string[],
token: StringToken,
definitionInfo: DefinitionInfo | undefined
): ExpressionToken | undefined {
const parseExpressionResult = this.parseExpression(tr, token, rawExpression, allowedContext, definitionInfo);
// 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-09 19:02:19 -05:00
return parseExpressionResult.expression!;
}
private parseExpression(
range: TokenRange,
token: StringToken,
value: string,
allowedContext: string[],
definitionInfo: DefinitionInfo | undefined
): ParseExpressionResult {
2023-01-09 19:02:19 -05:00
const trimmed = value.trim();
// 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")
};
}
// Try to find a matching directive
2023-01-09 19:02:19 -05:00
const matchDirectiveResult = this.matchDirective(trimmed, INSERT_DIRECTIVE, 0);
if (matchDirectiveResult.isMatch) {
return <ParseExpressionResult>{
2023-01-09 19:02:19 -05:00
expression: new InsertExpressionToken(this._fileId, range, definitionInfo)
};
} else if (matchDirectiveResult.error) {
return <ParseExpressionResult>{
2023-01-09 19:02:19 -05:00
error: matchDirectiveResult.error
};
}
// Check if valid expression
try {
2023-01-09 19:02:19 -05:00
ExpressionToken.validateExpression(trimmed, allowedContext);
} catch (err) {
return <ParseExpressionResult>{
2023-01-09 19:02:19 -05:00
error: err
};
}
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
}
};
// Return the expression
return <ParseExpressionResult>{
expression: new BasicExpressionToken(
this._fileId,
range,
trimmed,
definitionInfo,
undefined,
token.source,
2026-01-12 09:36:43 -06:00
expressionRange,
token.blockScalarHeader
),
2023-01-09 19:02:19 -05:00
error: undefined
};
}
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);
}
// 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-09 19:02:19 -05:00
private matchDirective(trimmed: string, directive: string, expectedParameters: number): MatchDirectiveResult {
const parameters: string[] = [];
if (
trimmed.startsWith(directive) &&
2023-01-09 19:02:19 -05:00
(trimmed.length === directive.length || WHITESPACE_PATTERN.test(trimmed[directive.length]))
) {
2023-01-09 19:02:19 -05:00
let startIndex = directive.length;
let inString = false;
let parens = 0;
for (let i = startIndex; i < trimmed.length; i++) {
2023-01-09 19:02:19 -05:00
const c = trimmed[i];
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-09 19:02:19 -05:00
startIndex = i + 1;
} else if (c === "'") {
2023-01-09 19:02:19 -05:00
inString = !inString;
} else if (c === "(" && !inString) {
2023-01-09 19:02:19 -05:00
parens++;
} else if (c === ")" && !inString) {
2023-01-09 19:02:19 -05:00
parens--;
}
}
if (startIndex < trimmed.length) {
2023-01-09 19:02:19 -05:00
parameters.push(trimmed.substr(startIndex));
}
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
)
};
}
return <MatchDirectiveResult>{
isMatch: true,
2023-01-09 19:02:19 -05:00
parameters: parameters
};
}
return <MatchDirectiveResult>{
isMatch: false,
2023-01-09 19:02:19 -05:00
parameters: parameters
};
}
private getExpressionString(trimmed: string): string | undefined {
2023-01-09 19:02:19 -05:00
const result: string[] = [];
2023-01-09 19:02:19 -05:00
let inString = false;
for (let i = 0; i < trimmed.length; i++) {
2023-01-09 19:02:19 -05:00
const c = trimmed[i];
if (c === "'") {
2023-01-09 19:02:19 -05:00
inString = !inString;
if (inString && i !== 0) {
2023-01-09 19:02:19 -05:00
result.push(c);
}
} else if (!inString) {
2023-01-09 19:02:19 -05:00
return undefined;
} else {
2023-01-09 19:02:19 -05:00
result.push(c);
}
}
2023-01-09 19:02:19 -05:00
return result.join("");
}
}
interface ParseExpressionResult {
2023-01-09 19:02:19 -05:00
expression: ExpressionToken | undefined;
error: Error | undefined;
}
interface MatchDirectiveResult {
2023-01-09 19:02:19 -05:00
isMatch: boolean;
parameters: string[];
error: Error | undefined;
}