This commit is contained in:
@@ -2,19 +2,20 @@ import { Category } from '@/domain/Category';
|
||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||
import { parseCategory } from './CategoryParser';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { parseScriptingDefinition } from './ScriptingDefinitionParser';
|
||||
import { createEnumParser } from '../Common/Enum';
|
||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
||||
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||
|
||||
export function parseCategoryCollection(
|
||||
content: CollectionData,
|
||||
info: IProjectInformation,
|
||||
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
|
||||
validate(content);
|
||||
const scripting = parseScriptingDefinition(content.scripting, info);
|
||||
const scripting = new ScriptingDefinitionParser()
|
||||
.parse(content.scripting, info);
|
||||
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
||||
const categories = new Array<Category>();
|
||||
for (const action of content.actions) {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
import { ExpressionArguments, IExpression } from './IExpression';
|
||||
|
||||
export type ExpressionEvaluator = (args?: ExpressionArguments) => string;
|
||||
export class Expression implements IExpression {
|
||||
constructor(
|
||||
public readonly position: ExpressionPosition,
|
||||
public readonly evaluator: ExpressionEvaluator,
|
||||
public readonly parameters: readonly string[] = new Array<string>()) {
|
||||
if (!position) {
|
||||
throw new Error('undefined position');
|
||||
}
|
||||
if (!evaluator) {
|
||||
throw new Error('undefined evaluator');
|
||||
}
|
||||
}
|
||||
public evaluate(args?: ExpressionArguments): string {
|
||||
args = filterUnusedArguments(this.parameters, args);
|
||||
return this.evaluator(args);
|
||||
}
|
||||
}
|
||||
|
||||
function filterUnusedArguments(
|
||||
parameters: readonly string[], args: ExpressionArguments): ExpressionArguments {
|
||||
let result: ExpressionArguments = {};
|
||||
for (const parameter of Object.keys(args)) {
|
||||
if (parameters.includes(parameter)) {
|
||||
result = {
|
||||
...result,
|
||||
[parameter]: args[parameter],
|
||||
};
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
export class ExpressionPosition {
|
||||
constructor(
|
||||
public readonly start: number,
|
||||
public readonly end: number) {
|
||||
if (start === end) {
|
||||
throw new Error(`no length (start = end = ${start})`);
|
||||
}
|
||||
if (start > end) {
|
||||
throw Error(`start (${start}) after end (${end})`);
|
||||
}
|
||||
if (start < 0) {
|
||||
throw Error(`negative start position: ${start}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
|
||||
export interface IExpression {
|
||||
readonly position: ExpressionPosition;
|
||||
readonly parameters?: readonly string[];
|
||||
evaluate(args?: ExpressionArguments): string;
|
||||
}
|
||||
|
||||
export interface ExpressionArguments {
|
||||
readonly [parameter: string]: string;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,49 @@
|
||||
import { IExpressionsCompiler, ParameterValueDictionary } from './IExpressionsCompiler';
|
||||
import { generateIlCode, IILCode } from './ILCode';
|
||||
import { IExpression } from './Expression/IExpression';
|
||||
import { IExpressionParser } from './Parser/IExpressionParser';
|
||||
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
||||
|
||||
export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||
public static readonly instance: IExpressionsCompiler = new ExpressionsCompiler();
|
||||
protected constructor() { }
|
||||
public constructor(private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { }
|
||||
public compileExpressions(code: string, parameters?: ParameterValueDictionary): string {
|
||||
let intermediateCode = generateIlCode(code);
|
||||
intermediateCode = substituteParameters(intermediateCode, parameters);
|
||||
return intermediateCode.compile();
|
||||
const expressions = this.extractor.findExpressions(code);
|
||||
const requiredParameterNames = expressions.map((e) => e.parameters).filter((p) => p).flat();
|
||||
const uniqueParameterNames = Array.from(new Set(requiredParameterNames));
|
||||
ensureRequiredArgsProvided(uniqueParameterNames, parameters);
|
||||
return compileExpressions(expressions, code, parameters);
|
||||
}
|
||||
}
|
||||
|
||||
function substituteParameters(intermediateCode: IILCode, parameters: ParameterValueDictionary): IILCode {
|
||||
const parameterNames = intermediateCode.getUniqueParameterNames();
|
||||
ensureValuesProvided(parameterNames, parameters);
|
||||
for (const parameterName of parameterNames) {
|
||||
const parameterValue = parameters[parameterName];
|
||||
intermediateCode = intermediateCode.substituteParameter(parameterName, parameterValue);
|
||||
function compileExpressions(expressions: IExpression[], code: string, parameters?: ParameterValueDictionary) {
|
||||
let compiledCode = '';
|
||||
expressions = expressions
|
||||
.slice() // copy the array to not mutate the parameter
|
||||
.sort((a, b) => b.position.start - a.position.start);
|
||||
let index = 0;
|
||||
while (index !== code.length) {
|
||||
const nextExpression = expressions.pop();
|
||||
if (nextExpression) {
|
||||
compiledCode += code.substring(index, nextExpression.position.start);
|
||||
const expressionCode = nextExpression.evaluate(parameters);
|
||||
compiledCode += expressionCode;
|
||||
index = nextExpression.position.end;
|
||||
} else {
|
||||
compiledCode += code.substring(index, code.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return intermediateCode;
|
||||
return compiledCode;
|
||||
}
|
||||
|
||||
function ensureValuesProvided(names: string[], nameValues: ParameterValueDictionary) {
|
||||
nameValues = nameValues || {};
|
||||
const notProvidedNames = names.filter((name) => !Boolean(nameValues[name]));
|
||||
if (notProvidedNames.length) {
|
||||
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedNames)}`);
|
||||
function ensureRequiredArgsProvided(parameters: readonly string[], args: ParameterValueDictionary) {
|
||||
parameters = parameters || [];
|
||||
args = args || {};
|
||||
if (!parameters.length) {
|
||||
return;
|
||||
}
|
||||
const notProvidedParameters = parameters.filter((parameter) => !Boolean(args[parameter]));
|
||||
if (notProvidedParameters.length) {
|
||||
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
export interface IILCode {
|
||||
compile(): string;
|
||||
getUniqueParameterNames(): string[];
|
||||
substituteParameter(parameterName: string, parameterValue: string): IILCode;
|
||||
}
|
||||
|
||||
export function generateIlCode(rawText: string): IILCode {
|
||||
const ilCode = generateIl(rawText);
|
||||
return new ILCode(ilCode);
|
||||
}
|
||||
|
||||
class ILCode implements IILCode {
|
||||
private readonly ilCode: string;
|
||||
|
||||
constructor(ilCode: string) {
|
||||
this.ilCode = ilCode;
|
||||
}
|
||||
|
||||
public substituteParameter(parameterName: string, parameterValue: string): IILCode {
|
||||
const newCode = substituteParameter(this.ilCode, parameterName, parameterValue);
|
||||
return new ILCode(newCode);
|
||||
}
|
||||
|
||||
public getUniqueParameterNames(): string[] {
|
||||
return getUniqueParameterNames(this.ilCode);
|
||||
}
|
||||
|
||||
public compile(): string {
|
||||
ensureNoExpressionLeft(this.ilCode);
|
||||
return this.ilCode;
|
||||
}
|
||||
}
|
||||
|
||||
// Trim each expression and put them inside "{{exp|}}" e.g. "{{ $hello }}" becomes "{{exp|$hello}}"
|
||||
function generateIl(rawText: string): string {
|
||||
return rawText.replace(/\{\{([\s]*[^;\s\{]+[\s]*)\}\}/g, (_, match) => {
|
||||
return `\{\{exp|${match.trim()}\}\}`;
|
||||
});
|
||||
}
|
||||
|
||||
// finds all "{{exp|..}} left"
|
||||
function ensureNoExpressionLeft(ilCode: string) {
|
||||
const allSubstitutions = ilCode.matchAll(/\{\{exp\|(.*?)\}\}/g);
|
||||
const allMatches = Array.from(allSubstitutions, (match) => match[1]);
|
||||
const uniqueExpressions = getDistinctValues(allMatches);
|
||||
if (uniqueExpressions.length > 0) {
|
||||
throw new Error(`unknown expression: ${printList(uniqueExpressions)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Parses all distinct usages of {{exp|$parameterName}}
|
||||
function getUniqueParameterNames(ilCode: string) {
|
||||
const allSubstitutions = ilCode.matchAll(/\{\{exp\|\$([^;\s\{]+[\s]*)\}\}/g);
|
||||
const allParameters = Array.from(allSubstitutions, (match) => match[1]);
|
||||
const uniqueParameterNames = getDistinctValues(allParameters);
|
||||
return uniqueParameterNames;
|
||||
}
|
||||
|
||||
// substitutes {{exp|$parameterName}} to value of the parameter
|
||||
function substituteParameter(ilCode: string, parameterName: string, parameterValue: string) {
|
||||
const pattern = `{{exp|$${parameterName}}}`;
|
||||
return ilCode.split(pattern).join(parameterValue); // as .replaceAll() is not yet supported by TS
|
||||
}
|
||||
|
||||
function getDistinctValues(values: readonly string[]): string[] {
|
||||
return values.filter((value, index, self) => {
|
||||
return self.indexOf(value) === index;
|
||||
});
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('","')}"`;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { IExpression } from '../Expression/IExpression';
|
||||
import { IExpressionParser } from './IExpressionParser';
|
||||
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
||||
|
||||
const parsers = [
|
||||
new ParameterSubstitutionParser(),
|
||||
];
|
||||
|
||||
export class CompositeExpressionParser implements IExpressionParser {
|
||||
public constructor(private readonly leafs: readonly IExpressionParser[] = parsers) {
|
||||
if (leafs.some((leaf) => !leaf)) { throw new Error('undefined leaf'); }
|
||||
}
|
||||
public findExpressions(code: string): IExpression[] {
|
||||
const expressions = new Array<IExpression>();
|
||||
for (const parser of this.leafs) {
|
||||
const newExpressions = parser.findExpressions(code);
|
||||
if (newExpressions && newExpressions.length) {
|
||||
expressions.push(...newExpressions);
|
||||
}
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { IExpression } from '../Expression/IExpression';
|
||||
|
||||
export interface IExpressionParser {
|
||||
findExpressions(code: string): IExpression[];
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { IExpressionParser } from './IExpressionParser';
|
||||
import { ExpressionPosition } from '../Expression/ExpressionPosition';
|
||||
import { IExpression } from '../Expression/IExpression';
|
||||
import { Expression, ExpressionEvaluator } from '../Expression/Expression';
|
||||
|
||||
export abstract class RegexParser implements IExpressionParser {
|
||||
protected abstract readonly regex: RegExp;
|
||||
public findExpressions(code: string): IExpression[] {
|
||||
return Array.from(this.findRegexExpressions(code));
|
||||
}
|
||||
|
||||
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
||||
|
||||
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||
const matches = Array.from(code.matchAll(this.regex));
|
||||
for (const match of matches) {
|
||||
const startPos = match.index;
|
||||
const endPos = startPos + match[0].length;
|
||||
let position: ExpressionPosition;
|
||||
try {
|
||||
position = new ExpressionPosition(startPos, endPos);
|
||||
} catch (error) {
|
||||
throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`);
|
||||
}
|
||||
const primitiveExpression = this.buildExpression(match);
|
||||
const expression = new Expression(position, primitiveExpression.evaluator, primitiveExpression.parameters);
|
||||
yield expression;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IPrimitiveExpression {
|
||||
evaluator: ExpressionEvaluator;
|
||||
parameters?: readonly string[];
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { RegexParser, IPrimitiveExpression } from '../Parser/RegexParser';
|
||||
|
||||
export class ParameterSubstitutionParser extends RegexParser {
|
||||
protected readonly regex = /{{\s*\$\s*([^}| ]+)\s*}}/g;
|
||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||
const parameterName = match[1];
|
||||
return {
|
||||
parameters: [ parameterName ],
|
||||
evaluator: (args) => args[parameterName],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||
ensureNoDuplicatesInParameterNames(functions);
|
||||
ensureNoDuplicateCode(functions);
|
||||
ensureEitherCallOrCodeIsDefined(functions);
|
||||
ensureExpectedParameterNameTypes(functions);
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
@@ -67,6 +68,17 @@ function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[])
|
||||
throw new Error(`neither "code" or "call" is defined in ${printNames(hasEitherCodeOrCall)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function ensureExpectedParameterNameTypes(functions: readonly FunctionData[]) {
|
||||
const unexpectedFunctions = functions.filter((func) => func.parameters && !isArrayOfStrings(func.parameters));
|
||||
if (unexpectedFunctions.length) {
|
||||
throw new Error(`unexpected parameter name type in ${printNames(unexpectedFunctions)}`);
|
||||
}
|
||||
function isArrayOfStrings(value: any): boolean {
|
||||
return Array.isArray(value) && value.every((item) => typeof item === 'string');
|
||||
}
|
||||
}
|
||||
|
||||
function printNames(holders: readonly InstructionHolder[]) {
|
||||
return printList(holders.map((holder) => holder.name));
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ExpressionsCompiler } from '../Expressions/ExpressionsCompiler';
|
||||
export class FunctionCallCompiler implements IFunctionCallCompiler {
|
||||
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
|
||||
protected constructor(
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = ExpressionsCompiler.instance) { }
|
||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) { }
|
||||
public compileCall(
|
||||
call: ScriptFunctionCallData,
|
||||
functions: ISharedFunctionCollection): ICompiledCode {
|
||||
@@ -32,11 +32,12 @@ export class FunctionCallCompiler implements IFunctionCallCompiler {
|
||||
}
|
||||
|
||||
function ensureExpectedParameters(func: FunctionData, call: FunctionCallData) {
|
||||
if (!func.parameters && !call.parameters) {
|
||||
const actual = Object.keys(call.parameters || {});
|
||||
const expected = func.parameters || [];
|
||||
if (!actual.length && !expected.length) {
|
||||
return;
|
||||
}
|
||||
const unexpectedParameters = Object.keys(call.parameters || {})
|
||||
.filter((callParam) => !func.parameters.includes(callParam));
|
||||
const unexpectedParameters = actual.filter((callParam) => !expected.includes(callParam));
|
||||
if (unexpectedParameters.length) {
|
||||
throw new Error(
|
||||
`function "${func.name}" has unexpected parameter(s) provided: "${unexpectedParameters.join('", "')}"`);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { IExpressionsCompiler, ParameterValueDictionary } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||
import { ParameterSubstitutionParser } from '@/application/Parser/Script/Compiler/Expressions/SyntaxParsers/ParameterSubstitutionParser';
|
||||
import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser';
|
||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ICodeSubstituter } from './ICodeSubstituter';
|
||||
|
||||
export class CodeSubstituter implements ICodeSubstituter {
|
||||
constructor(
|
||||
private readonly compiler: IExpressionsCompiler = createSubstituteCompiler(),
|
||||
private readonly date = new Date(),
|
||||
) {
|
||||
|
||||
}
|
||||
public substitute(code: string, info: IProjectInformation): string {
|
||||
if (!code) { throw new Error('undefined code'); }
|
||||
if (!info) { throw new Error('undefined info'); }
|
||||
const parameters: ParameterValueDictionary = {
|
||||
homepage: info.homepage,
|
||||
version: info.version,
|
||||
date: this.date.toUTCString(),
|
||||
};
|
||||
const compiledCode = this.compiler.compileExpressions(code, parameters);
|
||||
return compiledCode;
|
||||
}
|
||||
}
|
||||
|
||||
function createSubstituteCompiler(): IExpressionsCompiler {
|
||||
const parsers = [ new ParameterSubstitutionParser() ];
|
||||
const parser = new CompositeExpressionParser(parsers);
|
||||
const expressionCompiler = new ExpressionsCompiler(parser);
|
||||
return expressionCompiler;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
|
||||
export interface ICodeSubstituter {
|
||||
substitute(code: string, info: IProjectInformation): string;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
|
||||
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { createEnumParser } from '../../Common/Enum';
|
||||
import { ICodeSubstituter } from './ICodeSubstituter';
|
||||
import { CodeSubstituter } from './CodeSubstituter';
|
||||
|
||||
export class ScriptingDefinitionParser {
|
||||
constructor(
|
||||
private readonly languageParser = createEnumParser(ScriptingLanguage),
|
||||
private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(),
|
||||
) {
|
||||
}
|
||||
public parse(
|
||||
definition: ScriptingDefinitionData,
|
||||
info: IProjectInformation): IScriptingDefinition {
|
||||
if (!info) { throw new Error('undefined info'); }
|
||||
if (!definition) { throw new Error('undefined definition'); }
|
||||
const language = this.languageParser.parseEnum(definition.language, 'language');
|
||||
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
|
||||
const endCode = this.codeSubstituter.substitute(definition.endCode, info);
|
||||
return new ScriptingDefinition(
|
||||
language,
|
||||
startCode,
|
||||
endCode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
|
||||
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { createEnumParser } from '../Common/Enum';
|
||||
import { generateIlCode } from './Script/Compiler/Expressions/ILCode';
|
||||
|
||||
export function parseScriptingDefinition(
|
||||
definition: ScriptingDefinitionData,
|
||||
info: IProjectInformation,
|
||||
date = new Date(),
|
||||
languageParser = createEnumParser(ScriptingLanguage)): IScriptingDefinition {
|
||||
if (!info) {
|
||||
throw new Error('undefined info');
|
||||
}
|
||||
if (!definition) {
|
||||
throw new Error('undefined definition');
|
||||
}
|
||||
const language = languageParser.parseEnum(definition.language, 'language');
|
||||
const startCode = applySubstitutions(definition.startCode, info, date);
|
||||
const endCode = applySubstitutions(definition.endCode, info, date);
|
||||
return new ScriptingDefinition(
|
||||
language,
|
||||
startCode,
|
||||
endCode,
|
||||
);
|
||||
}
|
||||
|
||||
function applySubstitutions(code: string, info: IProjectInformation, date: Date): string {
|
||||
let ilCode = generateIlCode(code);
|
||||
ilCode = ilCode.substituteParameter('homepage', info.homepage);
|
||||
ilCode = ilCode.substituteParameter('version', info.version);
|
||||
ilCode = ilCode.substituteParameter('date', date.toUTCString());
|
||||
return ilCode.compile();
|
||||
}
|
||||
Reference in New Issue
Block a user