Rename folders
This commit is contained in:
@@ -0,0 +1,799 @@
|
||||
// template-reader *just* does schema validation
|
||||
|
||||
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";
|
||||
import {
|
||||
BasicExpressionToken,
|
||||
ExpressionToken,
|
||||
InsertExpressionToken,
|
||||
LiteralToken,
|
||||
MappingToken,
|
||||
ScalarToken,
|
||||
StringToken,
|
||||
TemplateToken
|
||||
} from "./tokens";
|
||||
import {TokenRange} from "./tokens/token-range";
|
||||
import {isString} from "./tokens/type-guards";
|
||||
import {TokenType} from "./tokens/types";
|
||||
|
||||
const WHITESPACE_PATTERN = /\s/;
|
||||
|
||||
export function readTemplate(
|
||||
context: TemplateContext,
|
||||
type: string,
|
||||
objectReader: ObjectReader,
|
||||
fileId: number | undefined
|
||||
): TemplateToken | undefined {
|
||||
const reader = new TemplateReader(context, objectReader, fileId);
|
||||
let value: TemplateToken | undefined;
|
||||
try {
|
||||
objectReader.validateStart();
|
||||
const definition = new DefinitionInfo(context.schema, type);
|
||||
value = reader.readValue(definition);
|
||||
objectReader.validateEnd();
|
||||
} catch (err) {
|
||||
context.error(fileId, err);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export interface ReadTemplateResult {
|
||||
value: TemplateToken;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
class TemplateReader {
|
||||
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
|
||||
const literal = this._objectReader.allowLiteral();
|
||||
if (literal) {
|
||||
let scalar = this.parseScalar(literal, definition);
|
||||
scalar = this.validate(scalar, definition);
|
||||
return scalar;
|
||||
}
|
||||
|
||||
// Sequence
|
||||
const sequence = this._objectReader.allowSequenceStart();
|
||||
if (sequence) {
|
||||
const sequenceDefinition = definition.getDefinitionsOfType(DefinitionType.Sequence)[0] as
|
||||
| SequenceDefinition
|
||||
| undefined;
|
||||
|
||||
// Legal
|
||||
if (sequenceDefinition) {
|
||||
const itemDefinition = new DefinitionInfo(definition, sequenceDefinition.itemType);
|
||||
|
||||
// Add each item
|
||||
while (!this._objectReader.allowSequenceEnd()) {
|
||||
const item = this.readValue(itemDefinition);
|
||||
sequence.add(item);
|
||||
}
|
||||
}
|
||||
// Illegal
|
||||
else {
|
||||
// Error
|
||||
this._context.error(sequence, "A sequence was not expected");
|
||||
|
||||
// Skip each item
|
||||
while (!this._objectReader.allowSequenceEnd()) {
|
||||
this.skipValue();
|
||||
}
|
||||
}
|
||||
sequence.definitionInfo = definition;
|
||||
return sequence;
|
||||
}
|
||||
|
||||
// Mapping
|
||||
const mapping = this._objectReader.allowMappingStart();
|
||||
if (mapping) {
|
||||
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
|
||||
) {
|
||||
this.handleMappingWithWellKnownProperties(definition, mappingDefinitions, mapping);
|
||||
} else {
|
||||
const keyDefinition = new DefinitionInfo(definition, mappingDefinitions[0].looseKeyType);
|
||||
const valueDefinition = new DefinitionInfo(definition, mappingDefinitions[0].looseValueType);
|
||||
this.handleMappingWithAllLooseProperties(
|
||||
definition,
|
||||
keyDefinition,
|
||||
valueDefinition,
|
||||
mappingDefinitions[0],
|
||||
mapping
|
||||
);
|
||||
}
|
||||
}
|
||||
// Illegal
|
||||
else {
|
||||
this._context.error(mapping, "A mapping was not expected");
|
||||
|
||||
while (!this._objectReader.allowMappingEnd()) {
|
||||
this.skipValue();
|
||||
this.skipValue();
|
||||
}
|
||||
}
|
||||
|
||||
// handleMappingWithWellKnownProperties will only set a definition
|
||||
// if it can identify a single matching definition
|
||||
if (!mapping.definitionInfo) {
|
||||
mapping.definitionInfo = definition;
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
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
|
||||
let looseKeyType: string | undefined;
|
||||
let looseValueType: string | undefined;
|
||||
let looseKeyDefinition: DefinitionInfo | undefined;
|
||||
let looseValueDefinition: DefinitionInfo | undefined;
|
||||
if (mappingDefinitions[0].looseKeyType) {
|
||||
looseKeyType = mappingDefinitions[0].looseKeyType;
|
||||
looseValueType = mappingDefinitions[0].looseValueType;
|
||||
}
|
||||
|
||||
const upperKeys: {[upperKey: string]: boolean} = {};
|
||||
let hasExpressionKey = false;
|
||||
|
||||
let rawLiteral: LiteralToken | undefined;
|
||||
while ((rawLiteral = this._objectReader.allowLiteral())) {
|
||||
const nextKeyScalar = this.parseScalar(rawLiteral, definition);
|
||||
|
||||
// Expression
|
||||
if (nextKeyScalar.isExpression) {
|
||||
hasExpressionKey = true;
|
||||
|
||||
// Legal
|
||||
if (definition.allowedContext.length > 0) {
|
||||
const anyDefinition = new DefinitionInfo(definition, ANY);
|
||||
mapping.add(nextKeyScalar, this.readValue(anyDefinition));
|
||||
}
|
||||
// Illegal
|
||||
else {
|
||||
this._context.error(nextKeyScalar, "A template expression is not allowed in this context");
|
||||
this.skipValue();
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
// Duplicate
|
||||
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
|
||||
const nextPropertyDef = this._schema.matchPropertyAndFilter(mappingDefinitions, nextKey.value);
|
||||
if (nextPropertyDef) {
|
||||
const nextDefinition = new DefinitionInfo(definition, nextPropertyDef.type);
|
||||
|
||||
// Store the definition on the key, the value may have its own definition
|
||||
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) {
|
||||
nextKey.description = nextPropertyDef.description;
|
||||
}
|
||||
|
||||
const nextValue = this.readValue(nextDefinition);
|
||||
mapping.add(nextKey, nextValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Loose
|
||||
if (looseKeyType) {
|
||||
if (!looseKeyDefinition) {
|
||||
looseKeyDefinition = new DefinitionInfo(definition, looseKeyType);
|
||||
looseValueDefinition = new DefinitionInfo(definition, looseValueType!);
|
||||
}
|
||||
|
||||
this.validate(nextKey, looseKeyDefinition);
|
||||
|
||||
// Store the definition on the key, the value may have its own definition
|
||||
const nextDefinition = new DefinitionInfo(definition, mappingDefinitions[0].looseValueType);
|
||||
nextKey.definitionInfo = nextDefinition;
|
||||
|
||||
const nextValue = this.readValue(looseValueDefinition!);
|
||||
mapping.add(nextKey, nextValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Error
|
||||
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) {
|
||||
mapping.definitionInfo = new DefinitionInfo(definition, mappingDefinitions[0]);
|
||||
}
|
||||
|
||||
// Unable to filter to one definition
|
||||
if (mappingDefinitions.length > 1) {
|
||||
const hitCount: {[key: string]: number} = {};
|
||||
for (const mappingDefinition of mappingDefinitions) {
|
||||
for (const key of Object.keys(mappingDefinition.properties)) {
|
||||
hitCount[key] = (hitCount[key] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const nonDuplicates: string[] = [];
|
||||
for (const key of Object.keys(hitCount)) {
|
||||
if (hitCount[key] === 1) {
|
||||
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(", ")}`
|
||||
);
|
||||
}
|
||||
// Check required properties
|
||||
else if (mappingDefinitions.length === 1 && !hasExpressionKey) {
|
||||
for (const propertyName of Object.keys(mappingDefinitions[0].properties)) {
|
||||
const propertyDef = mappingDefinitions[0].properties[propertyName];
|
||||
if (propertyDef.required && !upperKeys[propertyName.toUpperCase()]) {
|
||||
this._context.error(mapping, `Required property is missing: ${propertyName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.expectMappingEnd();
|
||||
}
|
||||
|
||||
private handleMappingWithAllLooseProperties(
|
||||
definition: DefinitionInfo,
|
||||
keyDefinition: DefinitionInfo,
|
||||
valueDefinition: DefinitionInfo,
|
||||
mappingDefinition: MappingDefinition,
|
||||
mapping: MappingToken
|
||||
): void {
|
||||
let nextValue: TemplateToken;
|
||||
const upperKeys: {[key: string]: boolean} = {};
|
||||
|
||||
let rawLiteral: LiteralToken | undefined;
|
||||
while ((rawLiteral = this._objectReader.allowLiteral())) {
|
||||
const nextKeyScalar = this.parseScalar(rawLiteral, definition);
|
||||
nextKeyScalar.definitionInfo = keyDefinition;
|
||||
|
||||
// Expression
|
||||
if (nextKeyScalar.isExpression) {
|
||||
// Legal
|
||||
if (definition.allowedContext.length > 0) {
|
||||
nextValue = this.readValue(valueDefinition);
|
||||
mapping.add(nextKeyScalar, nextValue);
|
||||
}
|
||||
// Illegal
|
||||
else {
|
||||
this._context.error(nextKeyScalar, "A template expression is not allowed in this context");
|
||||
this.skipValue();
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
// Duplicate
|
||||
const upperKey = nextKey.value.toUpperCase();
|
||||
if (upperKeys[upperKey]) {
|
||||
this._context.error(nextKey, `'${nextKey.value}' is already defined`);
|
||||
this.skipValue();
|
||||
continue;
|
||||
}
|
||||
upperKeys[upperKey] = true;
|
||||
|
||||
// Validate
|
||||
this.validate(nextKey, keyDefinition);
|
||||
|
||||
// Store the definition on the key, the value may have its own definition
|
||||
const nextDefinition = new DefinitionInfo(definition, mappingDefinition.looseValueType);
|
||||
nextKey.definitionInfo = nextDefinition;
|
||||
|
||||
// Add the pair
|
||||
nextValue = this.readValue(valueDefinition);
|
||||
mapping.add(nextKey, nextValue);
|
||||
}
|
||||
|
||||
this.expectMappingEnd();
|
||||
}
|
||||
|
||||
private expectMappingEnd(): void {
|
||||
if (!this._objectReader.allowMappingEnd()) {
|
||||
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()) {
|
||||
this.skipValue();
|
||||
}
|
||||
}
|
||||
// Mapping
|
||||
else if (this._objectReader.allowMappingStart()) {
|
||||
while (!this._objectReader.allowMappingEnd()) {
|
||||
this.skipValue();
|
||||
this.skipValue();
|
||||
}
|
||||
}
|
||||
// Unexpected
|
||||
else {
|
||||
throw new Error("Expected a scalar value, a sequence, or a mapping");
|
||||
}
|
||||
}
|
||||
|
||||
private validate(scalar: ScalarToken, definition: DefinitionInfo): ScalarToken {
|
||||
switch (scalar.templateTokenType) {
|
||||
case TokenType.Null:
|
||||
case TokenType.Boolean:
|
||||
case TokenType.Number:
|
||||
case TokenType.String: {
|
||||
const literal = scalar as LiteralToken;
|
||||
|
||||
// Legal
|
||||
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
|
||||
);
|
||||
|
||||
// Legal
|
||||
if ((relevantDefinition = scalarDefinitions.find(x => x.isMatch(stringLiteral)))) {
|
||||
stringLiteral.definitionInfo = new DefinitionInfo(definition, relevantDefinition);
|
||||
return stringLiteral;
|
||||
}
|
||||
}
|
||||
|
||||
// Illegal
|
||||
this._context.error(literal, `Unexpected value '${literal.toString()}'`);
|
||||
return scalar;
|
||||
}
|
||||
case TokenType.BasicExpression:
|
||||
// Illegal
|
||||
if (definition.allowedContext.length === 0) {
|
||||
this._context.error(scalar, "A template expression is not allowed in this context");
|
||||
}
|
||||
|
||||
return scalar;
|
||||
default:
|
||||
this._context.error(scalar, `Unexpected value '${scalar.toString()}'`);
|
||||
return scalar;
|
||||
}
|
||||
}
|
||||
|
||||
private parseScalar(token: LiteralToken, definitionInfo: DefinitionInfo): ScalarToken {
|
||||
// Not a string
|
||||
if (!isString(token)) {
|
||||
return token;
|
||||
}
|
||||
|
||||
const allowedContext = definitionInfo.allowedContext;
|
||||
const raw = token.source || token.value;
|
||||
|
||||
let startExpression: number = raw.indexOf(OPEN_EXPRESSION);
|
||||
if (startExpression < 0) {
|
||||
// Doesn't contain "${{"
|
||||
// Check if value should still be evaluated as an expression
|
||||
if (definitionInfo.definition instanceof StringDefinition && definitionInfo.definition.isExpression) {
|
||||
const expression = this.parseIntoExpressionToken(token.range!, raw, allowedContext, token, definitionInfo);
|
||||
if (expression) {
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
// Break the value into segments of LiteralToken and ExpressionToken
|
||||
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. "}}"
|
||||
startExpression = i;
|
||||
let endExpression = -1;
|
||||
let inString = false;
|
||||
for (i += OPEN_EXPRESSION.length; i < raw.length; i++) {
|
||||
if (raw[i] === "'") {
|
||||
inString = !inString; // Note, this handles escaped single quotes gracefully. E.x. 'foo''bar'
|
||||
} else if (!inString && raw[i] === "}" && raw[i - 1] === "}") {
|
||||
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."
|
||||
);
|
||||
return token;
|
||||
}
|
||||
|
||||
// Parse the expression
|
||||
const rawExpression = raw.substr(
|
||||
startExpression + OPEN_EXPRESSION.length,
|
||||
endExpression - startExpression + 1 - OPEN_EXPRESSION.length - CLOSE_EXPRESSION.length
|
||||
);
|
||||
|
||||
let tr = token.range!;
|
||||
if (tr.start.line === tr.end.line) {
|
||||
// If it's a single line expression, adjust the range to only cover the sub-expression
|
||||
tr = {
|
||||
start: {line: tr.start.line, column: tr.start.column + startExpression},
|
||||
end: {line: tr.end.line, column: tr.start.column + endExpression + 1}
|
||||
};
|
||||
} else {
|
||||
// Adjust the range to only cover the expression for multi-line strings
|
||||
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 = {
|
||||
start: {line: tr.start.line + adjustedStartLine, column: adjustedStart},
|
||||
end: {line: tr.start.line + adjustedStartLine, column: adjustedEnd}
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
encounteredError = true;
|
||||
} else {
|
||||
// Check if a directive was used when not allowed
|
||||
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.`
|
||||
);
|
||||
return token;
|
||||
}
|
||||
|
||||
// Add the segment
|
||||
segments.push(expression);
|
||||
}
|
||||
|
||||
// Look for the next expression
|
||||
startExpression = raw.indexOf(OPEN_EXPRESSION, i);
|
||||
}
|
||||
// The next expression is further ahead
|
||||
else if (i < startExpression) {
|
||||
// Append the segment
|
||||
this.addString(segments, token.range, raw.substr(i, startExpression - i), token.definitionInfo);
|
||||
|
||||
// Adjust the position
|
||||
i = startExpression;
|
||||
}
|
||||
// No remaining expressions
|
||||
else {
|
||||
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) {
|
||||
return token;
|
||||
}
|
||||
|
||||
// Check if can convert to a literal
|
||||
// For example, the escaped expression: ${{ '{{ this is a literal }}' }}
|
||||
if (segments.length === 1 && segments[0].templateTokenType === TokenType.BasicExpression) {
|
||||
const basicExpression = segments[0] as BasicExpressionToken;
|
||||
const str = this.getExpressionString(basicExpression.expression);
|
||||
if (str !== undefined) {
|
||||
return new StringToken(this._fileId, token.range, str, token.definitionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if only one segment
|
||||
if (segments.length === 1) {
|
||||
return segments[0];
|
||||
}
|
||||
|
||||
// Build the new expression, using the format function
|
||||
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
|
||||
.replace(/\}/g, "}}");
|
||||
format.push(text);
|
||||
} else {
|
||||
format.push(`{${argIndex}}`); // Append format arg
|
||||
argIndex++;
|
||||
|
||||
const expression = segment as BasicExpressionToken;
|
||||
args.push(", ");
|
||||
args.push(expression.expression);
|
||||
|
||||
expressionTokens.push(expression);
|
||||
}
|
||||
}
|
||||
|
||||
return new BasicExpressionToken(
|
||||
this._fileId,
|
||||
token.range,
|
||||
`format('${format.join("")}'${args.join("")})`,
|
||||
definitionInfo,
|
||||
expressionTokens,
|
||||
raw
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
this._context.error(token, parseExpressionResult.error, tr);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return parseExpressionResult.expression!;
|
||||
}
|
||||
|
||||
private parseExpression(
|
||||
range: TokenRange,
|
||||
token: StringToken,
|
||||
value: string,
|
||||
allowedContext: string[],
|
||||
definitionInfo: DefinitionInfo | undefined
|
||||
): ParseExpressionResult {
|
||||
const trimmed = value.trim();
|
||||
|
||||
// Check if the value is empty
|
||||
if (!trimmed) {
|
||||
return <ParseExpressionResult>{
|
||||
error: new Error("An expression was expected")
|
||||
};
|
||||
}
|
||||
|
||||
// Try to find a matching directive
|
||||
const matchDirectiveResult = this.matchDirective(trimmed, INSERT_DIRECTIVE, 0);
|
||||
if (matchDirectiveResult.isMatch) {
|
||||
return <ParseExpressionResult>{
|
||||
expression: new InsertExpressionToken(this._fileId, range, definitionInfo)
|
||||
};
|
||||
} else if (matchDirectiveResult.error) {
|
||||
return <ParseExpressionResult>{
|
||||
error: matchDirectiveResult.error
|
||||
};
|
||||
}
|
||||
|
||||
// Check if valid expression
|
||||
try {
|
||||
ExpressionToken.validateExpression(trimmed, allowedContext);
|
||||
} catch (err) {
|
||||
return <ParseExpressionResult>{
|
||||
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,
|
||||
expressionRange
|
||||
),
|
||||
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
|
||||
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 {
|
||||
segments.push(new StringToken(this._fileId, range, value, definition));
|
||||
}
|
||||
}
|
||||
|
||||
private matchDirective(trimmed: string, directive: string, expectedParameters: number): MatchDirectiveResult {
|
||||
const parameters: string[] = [];
|
||||
if (
|
||||
trimmed.startsWith(directive) &&
|
||||
(trimmed.length === directive.length || WHITESPACE_PATTERN.test(trimmed[directive.length]))
|
||||
) {
|
||||
let startIndex = directive.length;
|
||||
let inString = false;
|
||||
let parens = 0;
|
||||
for (let i = startIndex; i < trimmed.length; i++) {
|
||||
const c = trimmed[i];
|
||||
if (WHITESPACE_PATTERN.test(c) && !inString && parens == 0) {
|
||||
if (startIndex < 1) {
|
||||
parameters.push(trimmed.substr(startIndex, i - startIndex));
|
||||
}
|
||||
|
||||
startIndex = i + 1;
|
||||
} else if (c === "'") {
|
||||
inString = !inString;
|
||||
} else if (c === "(" && !inString) {
|
||||
parens++;
|
||||
} else if (c === ")" && !inString) {
|
||||
parens--;
|
||||
}
|
||||
}
|
||||
|
||||
if (startIndex < trimmed.length) {
|
||||
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}`
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
return <MatchDirectiveResult>{
|
||||
isMatch: true,
|
||||
parameters: parameters
|
||||
};
|
||||
}
|
||||
|
||||
return <MatchDirectiveResult>{
|
||||
isMatch: false,
|
||||
parameters: parameters
|
||||
};
|
||||
}
|
||||
|
||||
private getExpressionString(trimmed: string): string | undefined {
|
||||
const result: string[] = [];
|
||||
|
||||
let inString = false;
|
||||
for (let i = 0; i < trimmed.length; i++) {
|
||||
const c = trimmed[i];
|
||||
if (c === "'") {
|
||||
inString = !inString;
|
||||
if (inString && i !== 0) {
|
||||
result.push(c);
|
||||
}
|
||||
} else if (!inString) {
|
||||
return undefined;
|
||||
} else {
|
||||
result.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
return result.join("");
|
||||
}
|
||||
}
|
||||
|
||||
interface ParseExpressionResult {
|
||||
expression: ExpressionToken | undefined;
|
||||
error: Error | undefined;
|
||||
}
|
||||
|
||||
interface MatchDirectiveResult {
|
||||
isMatch: boolean;
|
||||
parameters: string[];
|
||||
error: Error | undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user