Refactor code to comply with ESLint rules
Major refactoring using ESLint with rules from AirBnb and Vue. Enable most of the ESLint rules and do necessary linting in the code. Also add more information for rules that are disabled to describe what they are and why they are disabled. Allow logging (`console.log`) in test files, and in development mode (e.g. when working with `npm run serve`), but disable it when environment is production (as pre-configured by Vue). Also add flag (`--mode production`) in `lint:eslint` command so production linting is executed earlier in lifecycle. Disable rules that requires a separate work. Such as ESLint rules that are broken in TypeScript: no-useless-constructor (eslint/eslint#14118) and no-shadow (eslint/eslint#13014).
This commit is contained in:
@@ -1,62 +1,63 @@
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
import { IExpression } from './IExpression';
|
||||
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
||||
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||
import { ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||
import { IExpression } from './IExpression';
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
import { ExpressionEvaluationContext, IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||
|
||||
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
||||
export class Expression implements IExpression {
|
||||
constructor(
|
||||
public readonly position: ExpressionPosition,
|
||||
public readonly evaluator: ExpressionEvaluator,
|
||||
public readonly parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection()) {
|
||||
if (!position) {
|
||||
throw new Error('undefined position');
|
||||
}
|
||||
if (!evaluator) {
|
||||
throw new Error('undefined evaluator');
|
||||
}
|
||||
constructor(
|
||||
public readonly position: ExpressionPosition,
|
||||
public readonly evaluator: ExpressionEvaluator,
|
||||
public readonly parameters
|
||||
: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection(),
|
||||
) {
|
||||
if (!position) {
|
||||
throw new Error('undefined position');
|
||||
}
|
||||
public evaluate(context: IExpressionEvaluationContext): string {
|
||||
if (!context) {
|
||||
throw new Error('undefined context');
|
||||
}
|
||||
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||
const args = filterUnusedArguments(this.parameters, context.args);
|
||||
context = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||
return this.evaluator(context);
|
||||
if (!evaluator) {
|
||||
throw new Error('undefined evaluator');
|
||||
}
|
||||
}
|
||||
|
||||
public evaluate(context: IExpressionEvaluationContext): string {
|
||||
if (!context) {
|
||||
throw new Error('undefined context');
|
||||
}
|
||||
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||
const args = filterUnusedArguments(this.parameters, context.args);
|
||||
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||
return this.evaluator(filteredContext);
|
||||
}
|
||||
}
|
||||
|
||||
function validateThatAllRequiredParametersAreSatisfied(
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
) {
|
||||
const requiredParameterNames = parameters
|
||||
.all
|
||||
.filter((parameter) => !parameter.isOptional)
|
||||
.map((parameter) => parameter.name);
|
||||
const missingParameterNames = requiredParameterNames
|
||||
.filter((parameterName) => !args.hasArgument(parameterName));
|
||||
if (missingParameterNames.length) {
|
||||
throw new Error(
|
||||
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`);
|
||||
}
|
||||
const requiredParameterNames = parameters
|
||||
.all
|
||||
.filter((parameter) => !parameter.isOptional)
|
||||
.map((parameter) => parameter.name);
|
||||
const missingParameterNames = requiredParameterNames
|
||||
.filter((parameterName) => !args.hasArgument(parameterName));
|
||||
if (missingParameterNames.length) {
|
||||
throw new Error(
|
||||
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function filterUnusedArguments(
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection): IReadOnlyFunctionCallArgumentCollection {
|
||||
const specificCallArgs = new FunctionCallArgumentCollection();
|
||||
for (const parameter of parameters.all) {
|
||||
if (parameter.isOptional && !allFunctionArgs.hasArgument(parameter.name)) {
|
||||
continue; // Optional parameter is not necessarily provided
|
||||
}
|
||||
const arg = allFunctionArgs.getArgument(parameter.name);
|
||||
specificCallArgs.addArgument(arg);
|
||||
}
|
||||
return specificCallArgs;
|
||||
parameters: IReadOnlyFunctionParameterCollection,
|
||||
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
): IReadOnlyFunctionCallArgumentCollection {
|
||||
const specificCallArgs = new FunctionCallArgumentCollection();
|
||||
parameters.all
|
||||
.filter((parameter) => allFunctionArgs.hasArgument(parameter.name))
|
||||
.map((parameter) => allFunctionArgs.getArgument(parameter.name))
|
||||
.forEach((argument) => specificCallArgs.addArgument(argument));
|
||||
return specificCallArgs;
|
||||
}
|
||||
|
||||
@@ -3,16 +3,17 @@ import { IPipelineCompiler } from '../Pipes/IPipelineCompiler';
|
||||
import { PipelineCompiler } from '../Pipes/PipelineCompiler';
|
||||
|
||||
export interface IExpressionEvaluationContext {
|
||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||
readonly pipelineCompiler: IPipelineCompiler;
|
||||
readonly args: IReadOnlyFunctionCallArgumentCollection;
|
||||
readonly pipelineCompiler: IPipelineCompiler;
|
||||
}
|
||||
|
||||
export class ExpressionEvaluationContext implements IExpressionEvaluationContext {
|
||||
constructor(
|
||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler()) {
|
||||
if (!args) {
|
||||
throw new Error('undefined args, send empty collection instead');
|
||||
}
|
||||
constructor(
|
||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
||||
) {
|
||||
if (!args) {
|
||||
throw new Error('undefined args, send empty collection instead');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
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}`);
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
|
||||
import { ExpressionPosition } from './ExpressionPosition';
|
||||
import { IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||
|
||||
export interface IExpression {
|
||||
readonly position: ExpressionPosition;
|
||||
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||
evaluate(context: IExpressionEvaluationContext): string;
|
||||
readonly position: ExpressionPosition;
|
||||
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||
evaluate(context: IExpressionEvaluationContext): string;
|
||||
}
|
||||
|
||||
@@ -1,81 +1,86 @@
|
||||
import { IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { IExpressionsCompiler } from './IExpressionsCompiler';
|
||||
import { IExpression } from './Expression/IExpression';
|
||||
import { IExpressionParser } from './Parser/IExpressionParser';
|
||||
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
import { ExpressionEvaluationContext } from './Expression/ExpressionEvaluationContext';
|
||||
import { IExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||
|
||||
export class ExpressionsCompiler implements IExpressionsCompiler {
|
||||
public constructor(
|
||||
private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { }
|
||||
public compileExpressions(
|
||||
code: string | undefined,
|
||||
args: IReadOnlyFunctionCallArgumentCollection): string {
|
||||
if (!args) {
|
||||
throw new Error('undefined args, send empty collection instead');
|
||||
}
|
||||
if (!code) {
|
||||
return code;
|
||||
}
|
||||
const expressions = this.extractor.findExpressions(code);
|
||||
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
||||
const context = new ExpressionEvaluationContext(args);
|
||||
const compiledCode = compileExpressions(expressions, code, context);
|
||||
return compiledCode;
|
||||
public constructor(
|
||||
private readonly extractor: IExpressionParser = new CompositeExpressionParser(),
|
||||
) { }
|
||||
|
||||
public compileExpressions(
|
||||
code: string | undefined,
|
||||
args: IReadOnlyFunctionCallArgumentCollection,
|
||||
): string {
|
||||
if (!args) {
|
||||
throw new Error('undefined args, send empty collection instead');
|
||||
}
|
||||
if (!code) {
|
||||
return code;
|
||||
}
|
||||
const expressions = this.extractor.findExpressions(code);
|
||||
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
|
||||
const context = new ExpressionEvaluationContext(args);
|
||||
const compiledCode = compileExpressions(expressions, code, context);
|
||||
return compiledCode;
|
||||
}
|
||||
}
|
||||
|
||||
function compileExpressions(
|
||||
expressions: readonly IExpression[],
|
||||
code: string,
|
||||
context: IExpressionEvaluationContext) {
|
||||
let compiledCode = '';
|
||||
const sortedExpressions = 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 = sortedExpressions.pop();
|
||||
if (nextExpression) {
|
||||
compiledCode += code.substring(index, nextExpression.position.start);
|
||||
const expressionCode = nextExpression.evaluate(context);
|
||||
compiledCode += expressionCode;
|
||||
index = nextExpression.position.end;
|
||||
} else {
|
||||
compiledCode += code.substring(index, code.length);
|
||||
break;
|
||||
}
|
||||
expressions: readonly IExpression[],
|
||||
code: string,
|
||||
context: IExpressionEvaluationContext,
|
||||
) {
|
||||
let compiledCode = '';
|
||||
const sortedExpressions = 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 = sortedExpressions.pop();
|
||||
if (nextExpression) {
|
||||
compiledCode += code.substring(index, nextExpression.position.start);
|
||||
const expressionCode = nextExpression.evaluate(context);
|
||||
compiledCode += expressionCode;
|
||||
index = nextExpression.position.end;
|
||||
} else {
|
||||
compiledCode += code.substring(index, code.length);
|
||||
break;
|
||||
}
|
||||
return compiledCode;
|
||||
}
|
||||
return compiledCode;
|
||||
}
|
||||
|
||||
function extractRequiredParameterNames(
|
||||
expressions: readonly IExpression[]): string[] {
|
||||
const usedParameterNames = expressions
|
||||
.map((e) => e.parameters.all
|
||||
.filter((p) => !p.isOptional)
|
||||
.map((p) => p.name))
|
||||
.filter((p) => p)
|
||||
.flat();
|
||||
const uniqueParameterNames = Array.from(new Set(usedParameterNames));
|
||||
return uniqueParameterNames;
|
||||
expressions: readonly IExpression[],
|
||||
): string[] {
|
||||
const usedParameterNames = expressions
|
||||
.map((e) => e.parameters.all
|
||||
.filter((p) => !p.isOptional)
|
||||
.map((p) => p.name))
|
||||
.filter((p) => p)
|
||||
.flat();
|
||||
const uniqueParameterNames = Array.from(new Set(usedParameterNames));
|
||||
return uniqueParameterNames;
|
||||
}
|
||||
|
||||
function ensureParamsUsedInCodeHasArgsProvided(
|
||||
expressions: readonly IExpression[],
|
||||
providedArgs: IReadOnlyFunctionCallArgumentCollection): void {
|
||||
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||
if (!usedParameterNames?.length) {
|
||||
return;
|
||||
}
|
||||
const notProvidedParameters = usedParameterNames
|
||||
.filter((parameterName) => !providedArgs.hasArgument(parameterName));
|
||||
if (notProvidedParameters.length) {
|
||||
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
|
||||
}
|
||||
expressions: readonly IExpression[],
|
||||
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||
): void {
|
||||
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||
if (!usedParameterNames?.length) {
|
||||
return;
|
||||
}
|
||||
const notProvidedParameters = usedParameterNames
|
||||
.filter((parameterName) => !providedArgs.hasArgument(parameterName));
|
||||
if (notProvidedParameters.length) {
|
||||
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
|
||||
}
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('", "')}"`;
|
||||
return `"${list.join('", "')}"`;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||
|
||||
export interface IExpressionsCompiler {
|
||||
compileExpressions(
|
||||
code: string | undefined,
|
||||
args: IReadOnlyFunctionCallArgumentCollection): string;
|
||||
compileExpressions(
|
||||
code: string | undefined,
|
||||
args: IReadOnlyFunctionCallArgumentCollection): string;
|
||||
}
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
import { IExpression } from '../Expression/IExpression';
|
||||
import { IExpressionParser } from './IExpressionParser';
|
||||
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
|
||||
import { WithParser } from '../SyntaxParsers/WithParser';
|
||||
import { IExpressionParser } from './IExpressionParser';
|
||||
|
||||
const Parsers = [
|
||||
new ParameterSubstitutionParser(),
|
||||
new WithParser(),
|
||||
new ParameterSubstitutionParser(),
|
||||
new WithParser(),
|
||||
];
|
||||
|
||||
export class CompositeExpressionParser implements IExpressionParser {
|
||||
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||
if (leafs.some((leaf) => !leaf)) {
|
||||
throw new Error('undefined leaf');
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IExpression } from '../Expression/IExpression';
|
||||
|
||||
export interface IExpressionParser {
|
||||
findExpressions(code: string): IExpression[];
|
||||
findExpressions(code: string): IExpression[];
|
||||
}
|
||||
|
||||
@@ -1,59 +1,60 @@
|
||||
export class ExpressionRegexBuilder {
|
||||
private readonly parts = new Array<string>();
|
||||
private readonly parts = new Array<string>();
|
||||
|
||||
public expectCharacters(characters: string) {
|
||||
return this.addRawRegex(
|
||||
characters
|
||||
.replaceAll('$', '\\$')
|
||||
.replaceAll('.', '\\.'),
|
||||
);
|
||||
}
|
||||
public expectCharacters(characters: string) {
|
||||
return this.addRawRegex(
|
||||
characters
|
||||
.replaceAll('$', '\\$')
|
||||
.replaceAll('.', '\\.'),
|
||||
);
|
||||
}
|
||||
|
||||
public expectOneOrMoreWhitespaces() {
|
||||
return this
|
||||
.addRawRegex('\\s+');
|
||||
}
|
||||
public expectOneOrMoreWhitespaces() {
|
||||
return this
|
||||
.addRawRegex('\\s+');
|
||||
}
|
||||
|
||||
public matchPipeline() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.addRawRegex('(\\|\\s*.+?)?');
|
||||
}
|
||||
public matchPipeline() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.addRawRegex('(\\|\\s*.+?)?');
|
||||
}
|
||||
|
||||
public matchUntilFirstWhitespace() {
|
||||
return this
|
||||
.addRawRegex('([^|\\s]+)');
|
||||
}
|
||||
public matchUntilFirstWhitespace() {
|
||||
return this
|
||||
.addRawRegex('([^|\\s]+)');
|
||||
}
|
||||
|
||||
public matchAnythingExceptSurroundingWhitespaces() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.addRawRegex('(.+?)')
|
||||
.expectZeroOrMoreWhitespaces();
|
||||
}
|
||||
public matchAnythingExceptSurroundingWhitespaces() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.addRawRegex('(.+?)')
|
||||
.expectZeroOrMoreWhitespaces();
|
||||
}
|
||||
|
||||
public expectExpressionStart() {
|
||||
return this
|
||||
.expectCharacters('{{')
|
||||
.expectZeroOrMoreWhitespaces();
|
||||
}
|
||||
public expectExpressionStart() {
|
||||
return this
|
||||
.expectCharacters('{{')
|
||||
.expectZeroOrMoreWhitespaces();
|
||||
}
|
||||
|
||||
public expectExpressionEnd() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.expectCharacters('}}');
|
||||
}
|
||||
public expectExpressionEnd() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.expectCharacters('}}');
|
||||
}
|
||||
|
||||
public buildRegExp(): RegExp {
|
||||
return new RegExp(this.parts.join(''), 'g');
|
||||
}
|
||||
public buildRegExp(): RegExp {
|
||||
return new RegExp(this.parts.join(''), 'g');
|
||||
}
|
||||
|
||||
private expectZeroOrMoreWhitespaces() {
|
||||
return this
|
||||
.addRawRegex('\\s*');
|
||||
}
|
||||
private addRawRegex(regex: string) {
|
||||
this.parts.push(regex);
|
||||
return this;
|
||||
}
|
||||
private expectZeroOrMoreWhitespaces() {
|
||||
return this
|
||||
.addRawRegex('\\s*');
|
||||
}
|
||||
|
||||
private addRawRegex(regex: string) {
|
||||
this.parts.push(regex);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,46 +6,47 @@ import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParamet
|
||||
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
||||
|
||||
export abstract class RegexParser implements IExpressionParser {
|
||||
protected abstract readonly regex: RegExp;
|
||||
protected abstract readonly regex: RegExp;
|
||||
|
||||
public findExpressions(code: string): IExpression[] {
|
||||
return Array.from(this.findRegexExpressions(code));
|
||||
public findExpressions(code: string): IExpression[] {
|
||||
return Array.from(this.findRegexExpressions(code));
|
||||
}
|
||||
|
||||
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
||||
|
||||
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||
if (!code) {
|
||||
throw new Error('undefined code');
|
||||
}
|
||||
|
||||
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
||||
|
||||
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||
if (!code) {
|
||||
throw new Error('undefined code');
|
||||
}
|
||||
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 parameters = getParameters(primitiveExpression);
|
||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||
yield expression;
|
||||
}
|
||||
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 parameters = getParameters(primitiveExpression);
|
||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||
yield expression;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface IPrimitiveExpression {
|
||||
evaluator: ExpressionEvaluator;
|
||||
parameters?: readonly IFunctionParameter[];
|
||||
evaluator: ExpressionEvaluator;
|
||||
parameters?: readonly IFunctionParameter[];
|
||||
}
|
||||
|
||||
function getParameters(
|
||||
expression: IPrimitiveExpression): FunctionParameterCollection {
|
||||
const parameters = new FunctionParameterCollection();
|
||||
for (const parameter of expression.parameters || []) {
|
||||
parameters.addParameter(parameter);
|
||||
}
|
||||
return parameters;
|
||||
expression: IPrimitiveExpression,
|
||||
): FunctionParameterCollection {
|
||||
const parameters = new FunctionParameterCollection();
|
||||
for (const parameter of expression.parameters || []) {
|
||||
parameters.addParameter(parameter);
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface IPipe {
|
||||
readonly name: string;
|
||||
apply(input: string): string;
|
||||
readonly name: string;
|
||||
apply(input: string): string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface IPipelineCompiler {
|
||||
compile(value: string, pipeline: string): string;
|
||||
compile(value: string, pipeline: string): string;
|
||||
}
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import { IPipe } from '../IPipe';
|
||||
|
||||
export class EscapeDoubleQuotes implements IPipe {
|
||||
public readonly name: string = 'escapeDoubleQuotes';
|
||||
public apply(raw: string): string {
|
||||
return raw?.replaceAll('"', '"^""');
|
||||
/*
|
||||
"^"" is the most robust and stable choice.
|
||||
Other options:
|
||||
""
|
||||
Breaks, because it is fundamentally unsupported
|
||||
""""
|
||||
Does not work with consecutive double quotes.
|
||||
E.g. PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"
|
||||
Works when using: PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"
|
||||
\"
|
||||
May break as they are interpreted by cmd.exe as metacharacters breaking the command
|
||||
E.g. PowerShell -Command "Write-Host 'Hello \"w&orld\"'" does not work due to unescaped "&"
|
||||
Works when using: PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"
|
||||
\""
|
||||
Normalizes interior whitespace
|
||||
E.g. PowerShell -Command "\""a& c\"".length", outputs 4 and discards one of two whitespaces
|
||||
Works when using "^"": PowerShell -Command ""^""a& c"^"".length"
|
||||
A good explanation: https://stackoverflow.com/a/31413730
|
||||
*/
|
||||
}
|
||||
public readonly name: string = 'escapeDoubleQuotes';
|
||||
|
||||
public apply(raw: string): string {
|
||||
return raw?.replaceAll('"', '"^""');
|
||||
/* eslint-disable max-len */
|
||||
/*
|
||||
"^"" is the most robust and stable choice.
|
||||
Other options:
|
||||
""
|
||||
Breaks, because it is fundamentally unsupported
|
||||
""""
|
||||
Does not work with consecutive double quotes.
|
||||
E.g. `PowerShell -Command "$name='aq'; Write-Host """"Disabled `""""$name`"""""""";"`
|
||||
Works when using: `PowerShell -Command "$name='aq'; Write-Host "^""Disabled `"^""$name`"^"" "^"";"`
|
||||
\"
|
||||
May break as they are interpreted by cmd.exe as metacharacters breaking the command
|
||||
E.g. `PowerShell -Command "Write-Host 'Hello \"w&orld\"'"` does not work due to unescaped "&"
|
||||
Works when using: `PowerShell -Command "Write-Host 'Hello "^""w&orld"^""'"`
|
||||
\""
|
||||
Normalizes interior whitespace
|
||||
E.g. `PowerShell -Command "\""a& c\"".length"`, outputs 4 and discards one of two whitespaces
|
||||
Works when using "^"": `PowerShell -Command ""^""a& c"^"".length"`
|
||||
A good explanation: https://stackoverflow.com/a/31413730
|
||||
*/
|
||||
/* eslint-enable max-len */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,155 +1,166 @@
|
||||
import { IPipe } from '../IPipe';
|
||||
|
||||
export class InlinePowerShell implements IPipe {
|
||||
public readonly name: string = 'inlinePowerShell';
|
||||
public apply(code: string): string {
|
||||
if (!code || !hasLines(code)) {
|
||||
return code;
|
||||
}
|
||||
code = inlineComments(code);
|
||||
code = mergeLinesWithBacktick(code);
|
||||
code = mergeHereStrings(code);
|
||||
const lines = getLines(code)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0);
|
||||
return lines
|
||||
.join('; ');
|
||||
public readonly name: string = 'inlinePowerShell';
|
||||
|
||||
public apply(code: string): string {
|
||||
if (!code || !hasLines(code)) {
|
||||
return code;
|
||||
}
|
||||
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
|
||||
inlineComments,
|
||||
mergeLinesWithBacktick,
|
||||
mergeHereStrings,
|
||||
mergeNewLines,
|
||||
]).reduce((a, b) => (data) => b(a(data)));
|
||||
const newCode = processor(code);
|
||||
return newCode;
|
||||
}
|
||||
}
|
||||
|
||||
function hasLines(text: string) {
|
||||
return text.includes('\n') || text.includes('\r');
|
||||
return text.includes('\n') || text.includes('\r');
|
||||
}
|
||||
|
||||
/*
|
||||
Line comments using "#" are replaced with inline comment syntax <# comment.. #>
|
||||
Otherwise single # comments out rest of the code
|
||||
Line comments using "#" are replaced with inline comment syntax <# comment.. #>
|
||||
Otherwise single # comments out rest of the code
|
||||
*/
|
||||
function inlineComments(code: string): string {
|
||||
const makeInlineComment = (comment: string) => {
|
||||
const value = comment?.trim();
|
||||
if (!value) {
|
||||
return '<##>';
|
||||
}
|
||||
return `<# ${value} #>`;
|
||||
};
|
||||
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
|
||||
if (captureComment === undefined) {
|
||||
const makeInlineComment = (comment: string) => {
|
||||
const value = comment?.trim();
|
||||
if (!value) {
|
||||
return '<##>';
|
||||
}
|
||||
return `<# ${value} #>`;
|
||||
};
|
||||
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
|
||||
if (captureComment === undefined) {
|
||||
return match;
|
||||
}
|
||||
return makeInlineComment(captureComment);
|
||||
});
|
||||
/*
|
||||
Other alternatives considered:
|
||||
--------------------------
|
||||
/#(?<!<#)(?![<>])(.*)$/gm
|
||||
-------------------------
|
||||
✅ Simple, yet matches and captures only what's necessary
|
||||
❌ Fails to match some cases
|
||||
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
|
||||
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
|
||||
❌ `Write-Host "hi" #>Comment starting like inline comment end but not one`
|
||||
❌ Uses lookbehind
|
||||
Safari does not yet support lookbehind and syntax, leading application to not
|
||||
load and throw "Invalid regular expression: invalid group specifier name"
|
||||
https://caniuse.com/js-regexp-lookbehind
|
||||
⏩ Usage
|
||||
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (match, captureComment) => {
|
||||
return makeInlineComment(captureComment)
|
||||
});
|
||||
----------------
|
||||
/<#.*?#>|#(.*)/g
|
||||
----------------
|
||||
✅ Simple yet affective
|
||||
❌ Matches all comments, but only captures dash comments
|
||||
❌ Fails to match some cases
|
||||
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
|
||||
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
|
||||
⏩ Usage
|
||||
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
|
||||
if (captureComment === undefined) {
|
||||
return match;
|
||||
}
|
||||
return makeInlineComment(captureComment);
|
||||
});
|
||||
/*
|
||||
Other alternatives considered:
|
||||
--------------------------
|
||||
/#(?<!<#)(?![<>])(.*)$/gm
|
||||
-------------------------
|
||||
✅ Simple, yet matches and captures only what's necessary
|
||||
❌ Fails to match some cases
|
||||
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
|
||||
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
|
||||
❌ `Write-Host "hi" #>Comment starting like inline comment end but not one`
|
||||
❌ Uses lookbehind
|
||||
Safari does not yet support lookbehind and syntax, leading application to not
|
||||
load and throw "Invalid regular expression: invalid group specifier name"
|
||||
https://caniuse.com/js-regexp-lookbehind
|
||||
⏩ Usage
|
||||
return code.replaceAll(/#(?<!<#)(?![<>])(.*)$/gm, (match, captureComment) => {
|
||||
return makeInlineComment(captureComment)
|
||||
});
|
||||
----------------
|
||||
/<#.*?#>|#(.*)/g
|
||||
----------------
|
||||
✅ Simple yet affective
|
||||
❌ Matches all comments, but only captures dash comments
|
||||
❌ Fails to match some cases
|
||||
❌ `Write-Host "hi" # Comment ending line inline comment but not one #>`
|
||||
❌ `Write-Host "hi" <#Comment starting like inline comment start but not one`
|
||||
⏩ Usage
|
||||
return code.replaceAll(/<#.*?#>|#(.*)/g, (match, captureComment) => {
|
||||
if (captureComment === undefined) {
|
||||
return match;
|
||||
}
|
||||
return makeInlineComment(captureComment);
|
||||
});
|
||||
------------------------------------
|
||||
/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm
|
||||
------------------------------------
|
||||
✅ Covers all cases
|
||||
❌ Matches every line, three capture groups are used to build result
|
||||
⏩ Usage
|
||||
return code.replaceAll(/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm,
|
||||
(match, captureLeft, captureDash, captureComment) => {
|
||||
if (!captureDash) {
|
||||
return match;
|
||||
}
|
||||
return captureLeft + makeInlineComment(captureComment);
|
||||
});
|
||||
}
|
||||
return makeInlineComment(captureComment);
|
||||
});
|
||||
------------------------------------
|
||||
/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm
|
||||
------------------------------------
|
||||
✅ Covers all cases
|
||||
❌ Matches every line, three capture groups are used to build result
|
||||
⏩ Usage
|
||||
return code.replaceAll(/(^(?:<#.*?#>|[^#])*)(?:(#)(.*))?/gm,
|
||||
(match, captureLeft, captureDash, captureComment) => {
|
||||
if (!captureDash) {
|
||||
return match;
|
||||
}
|
||||
return captureLeft + makeInlineComment(captureComment);
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
function getLines(code: string): string [] {
|
||||
return (code?.split(/\r\n|\r|\n/) || []);
|
||||
function getLines(code: string): string[] {
|
||||
return (code?.split(/\r\n|\r|\n/) || []);
|
||||
}
|
||||
|
||||
/*
|
||||
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
||||
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
|
||||
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
||||
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
|
||||
*/
|
||||
function mergeHereStrings(code: string) {
|
||||
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
|
||||
return code.replaceAll(regex, (_$, quotes, scope) => {
|
||||
const newString = getHereStringHandler(quotes);
|
||||
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
||||
const lines = getLines(escaped);
|
||||
const inlined = lines.join(newString.separator);
|
||||
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
||||
return quoted;
|
||||
});
|
||||
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
|
||||
return code.replaceAll(regex, (_$, quotes, scope) => {
|
||||
const newString = getHereStringHandler(quotes);
|
||||
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
||||
const lines = getLines(escaped);
|
||||
const inlined = lines.join(newString.separator);
|
||||
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
||||
return quoted;
|
||||
});
|
||||
}
|
||||
interface IInlinedHereString {
|
||||
readonly quotesAround: string;
|
||||
readonly escapedQuotes: string;
|
||||
readonly separator: string;
|
||||
readonly quotesAround: string;
|
||||
readonly escapedQuotes: string;
|
||||
readonly separator: string;
|
||||
}
|
||||
// We handle @' and @" differently so single quotes are interpreted literally and doubles are expandable
|
||||
function getHereStringHandler(quotes: string): IInlinedHereString {
|
||||
const expandableNewLine = '`r`n';
|
||||
switch (quotes) {
|
||||
case '\'':
|
||||
return {
|
||||
quotesAround: '\'',
|
||||
escapedQuotes: '\'\'',
|
||||
separator: `\'+"${expandableNewLine}"+\'`,
|
||||
};
|
||||
case '"':
|
||||
return {
|
||||
quotesAround: '"',
|
||||
escapedQuotes: '`"',
|
||||
separator: expandableNewLine,
|
||||
};
|
||||
default:
|
||||
throw new Error(`expected quotes: ${quotes}`);
|
||||
}
|
||||
/*
|
||||
We handle @' and @" differently.
|
||||
Single quotes are interpreted literally and doubles are expandable.
|
||||
*/
|
||||
const expandableNewLine = '`r`n';
|
||||
switch (quotes) {
|
||||
case '\'':
|
||||
return {
|
||||
quotesAround: '\'',
|
||||
escapedQuotes: '\'\'',
|
||||
separator: `'+"${expandableNewLine}"+'`,
|
||||
};
|
||||
case '"':
|
||||
return {
|
||||
quotesAround: '"',
|
||||
escapedQuotes: '`"',
|
||||
separator: expandableNewLine,
|
||||
};
|
||||
default:
|
||||
throw new Error(`expected quotes: ${quotes}`);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Input ->
|
||||
Get-Service * `
|
||||
Sort-Object StartType `
|
||||
Format-Table Name, ServiceType, Status -AutoSize
|
||||
Output ->
|
||||
Get-Service * | Sort-Object StartType | Format-Table -AutoSize
|
||||
Input ->
|
||||
Get-Service * `
|
||||
Sort-Object StartType `
|
||||
Format-Table Name, ServiceType, Status -AutoSize
|
||||
Output ->
|
||||
Get-Service * | Sort-Object StartType | Format-Table -AutoSize
|
||||
*/
|
||||
function mergeLinesWithBacktick(code: string) {
|
||||
/*
|
||||
The regex actually wraps any whitespace character after backtick and before newline
|
||||
However, this is not always the case for PowerShell.
|
||||
I see two behaviors:
|
||||
1. If inside string, it's accepted (inside " or ')
|
||||
2. If part of a command, PowerShell throws "An empty pipe element is not allowed"
|
||||
However we don't need to be so robust and handle this complexity (yet), so for easier regex
|
||||
we wrap it anyway
|
||||
*/
|
||||
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
||||
/*
|
||||
The regex actually wraps any whitespace character after backtick and before newline
|
||||
However, this is not always the case for PowerShell.
|
||||
I see two behaviors:
|
||||
1. If inside string, it's accepted (inside " or ')
|
||||
2. If part of a command, PowerShell throws "An empty pipe element is not allowed"
|
||||
However we don't need to be so robust and handle this complexity (yet), so for easier regex
|
||||
we wrap it anyway
|
||||
*/
|
||||
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
||||
}
|
||||
|
||||
function mergeNewLines(code: string) {
|
||||
return getLines(code)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
@@ -3,45 +3,48 @@ import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
|
||||
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
||||
|
||||
const RegisteredPipes = [
|
||||
new EscapeDoubleQuotes(),
|
||||
new InlinePowerShell(),
|
||||
new EscapeDoubleQuotes(),
|
||||
new InlinePowerShell(),
|
||||
];
|
||||
|
||||
export interface IPipeFactory {
|
||||
get(pipeName: string): IPipe;
|
||||
get(pipeName: string): IPipe;
|
||||
}
|
||||
|
||||
export class PipeFactory implements IPipeFactory {
|
||||
private readonly pipes = new Map<string, IPipe>();
|
||||
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||
if (pipes.some((pipe) => !pipe)) {
|
||||
throw new Error('undefined pipe in list');
|
||||
}
|
||||
for (const pipe of pipes) {
|
||||
this.registerPipe(pipe);
|
||||
}
|
||||
private readonly pipes = new Map<string, IPipe>();
|
||||
|
||||
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||
if (pipes.some((pipe) => !pipe)) {
|
||||
throw new Error('undefined pipe in list');
|
||||
}
|
||||
public get(pipeName: string): IPipe {
|
||||
validatePipeName(pipeName);
|
||||
if (!this.pipes.has(pipeName)) {
|
||||
throw new Error(`Unknown pipe: "${pipeName}"`);
|
||||
}
|
||||
return this.pipes.get(pipeName);
|
||||
for (const pipe of pipes) {
|
||||
this.registerPipe(pipe);
|
||||
}
|
||||
private registerPipe(pipe: IPipe): void {
|
||||
validatePipeName(pipe.name);
|
||||
if (this.pipes.has(pipe.name)) {
|
||||
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
||||
}
|
||||
this.pipes.set(pipe.name, pipe);
|
||||
}
|
||||
|
||||
public get(pipeName: string): IPipe {
|
||||
validatePipeName(pipeName);
|
||||
if (!this.pipes.has(pipeName)) {
|
||||
throw new Error(`Unknown pipe: "${pipeName}"`);
|
||||
}
|
||||
return this.pipes.get(pipeName);
|
||||
}
|
||||
|
||||
private registerPipe(pipe: IPipe): void {
|
||||
validatePipeName(pipe.name);
|
||||
if (this.pipes.has(pipe.name)) {
|
||||
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
||||
}
|
||||
this.pipes.set(pipe.name, pipe);
|
||||
}
|
||||
}
|
||||
|
||||
function validatePipeName(name: string) {
|
||||
if (!name) {
|
||||
throw new Error('empty pipe name');
|
||||
}
|
||||
if (!/^[a-z][A-Za-z]*$/.test(name)) {
|
||||
throw new Error(`Pipe name should be camelCase: "${name}"`);
|
||||
}
|
||||
if (!name) {
|
||||
throw new Error('empty pipe name');
|
||||
}
|
||||
if (!/^[a-z][A-Za-z]*$/.test(name)) {
|
||||
throw new Error(`Pipe name should be camelCase: "${name}"`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,30 +2,32 @@ import { IPipeFactory, PipeFactory } from './PipeFactory';
|
||||
import { IPipelineCompiler } from './IPipelineCompiler';
|
||||
|
||||
export class PipelineCompiler implements IPipelineCompiler {
|
||||
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
|
||||
public compile(value: string, pipeline: string): string {
|
||||
ensureValidArguments(value, pipeline);
|
||||
const pipeNames = extractPipeNames(pipeline);
|
||||
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
||||
for (const pipe of pipes) {
|
||||
value = pipe.apply(value);
|
||||
}
|
||||
return value;
|
||||
constructor(private readonly factory: IPipeFactory = new PipeFactory()) { }
|
||||
|
||||
public compile(value: string, pipeline: string): string {
|
||||
ensureValidArguments(value, pipeline);
|
||||
const pipeNames = extractPipeNames(pipeline);
|
||||
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
||||
let valueInCompilation = value;
|
||||
for (const pipe of pipes) {
|
||||
valueInCompilation = pipe.apply(valueInCompilation);
|
||||
}
|
||||
return valueInCompilation;
|
||||
}
|
||||
}
|
||||
|
||||
function extractPipeNames(pipeline: string): string[] {
|
||||
return pipeline
|
||||
.trim()
|
||||
.split('|')
|
||||
.slice(1)
|
||||
.map((p) => p.trim());
|
||||
return pipeline
|
||||
.trim()
|
||||
.split('|')
|
||||
.slice(1)
|
||||
.map((p) => p.trim());
|
||||
}
|
||||
|
||||
function ensureValidArguments(value: string, pipeline: string) {
|
||||
if (!value) { throw new Error('undefined value'); }
|
||||
if (!pipeline) { throw new Error('undefined pipeline'); }
|
||||
if (!pipeline.trimStart().startsWith('|')) {
|
||||
throw new Error('pipeline does not start with pipe');
|
||||
}
|
||||
if (!value) { throw new Error('undefined value'); }
|
||||
if (!pipeline) { throw new Error('undefined pipeline'); }
|
||||
if (!pipeline.trimStart().startsWith('|')) {
|
||||
throw new Error('pipeline does not start with pipe');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||
|
||||
export class ParameterSubstitutionParser extends RegexParser {
|
||||
protected readonly regex = new ExpressionRegexBuilder()
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('$')
|
||||
.matchUntilFirstWhitespace() // First match: Parameter name
|
||||
.matchPipeline() // Second match: Pipeline
|
||||
.expectExpressionEnd()
|
||||
.buildRegExp();
|
||||
protected readonly regex = new ExpressionRegexBuilder()
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('$')
|
||||
.matchUntilFirstWhitespace() // First match: Parameter name
|
||||
.matchPipeline() // Second match: Pipeline
|
||||
.expectExpressionEnd()
|
||||
.buildRegExp();
|
||||
|
||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||
const parameterName = match[1];
|
||||
const pipeline = match[2];
|
||||
return {
|
||||
parameters: [ new FunctionParameter(parameterName, false) ],
|
||||
evaluator: (context) => {
|
||||
const argumentValue = context.args.getArgument(parameterName).argumentValue;
|
||||
if (!pipeline) {
|
||||
return argumentValue;
|
||||
}
|
||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||
},
|
||||
};
|
||||
}
|
||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||
const parameterName = match[1];
|
||||
const pipeline = match[2];
|
||||
return {
|
||||
parameters: [new FunctionParameter(parameterName, false)],
|
||||
evaluator: (context) => {
|
||||
const { argumentValue } = context.args.getArgument(parameterName);
|
||||
if (!pipeline) {
|
||||
return argumentValue;
|
||||
}
|
||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,59 @@
|
||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||
|
||||
export class WithParser extends RegexParser {
|
||||
protected readonly regex = new ExpressionRegexBuilder()
|
||||
// {{ with $parameterName }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('with')
|
||||
.expectOneOrMoreWhitespaces()
|
||||
.expectCharacters('$')
|
||||
.matchUntilFirstWhitespace() // First match: parameter name
|
||||
.expectExpressionEnd()
|
||||
// ...
|
||||
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||
// {{ end }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('end')
|
||||
.expectExpressionEnd()
|
||||
.buildRegExp();
|
||||
|
||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||
const parameterName = match[1];
|
||||
const scopeText = match[2];
|
||||
return {
|
||||
parameters: [ new FunctionParameter(parameterName, true) ],
|
||||
evaluator: (context) => {
|
||||
const argumentValue = context.args.hasArgument(parameterName) ?
|
||||
context.args.getArgument(parameterName).argumentValue
|
||||
: undefined;
|
||||
if (!argumentValue) {
|
||||
return '';
|
||||
}
|
||||
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
|
||||
if (!pipeline) {
|
||||
return argumentValue;
|
||||
}
|
||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
||||
// {{ . | pipeName }}
|
||||
protected readonly regex = new ExpressionRegexBuilder()
|
||||
// {{ with $parameterName }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('.')
|
||||
.matchPipeline() // First match: pipeline
|
||||
.expectCharacters('with')
|
||||
.expectOneOrMoreWhitespaces()
|
||||
.expectCharacters('$')
|
||||
.matchUntilFirstWhitespace() // First match: parameter name
|
||||
.expectExpressionEnd()
|
||||
// ...
|
||||
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||
// {{ end }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('end')
|
||||
.expectExpressionEnd()
|
||||
.buildRegExp();
|
||||
|
||||
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
||||
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets, but let pipeline compiler fail on those
|
||||
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1 ) => {
|
||||
return replacer(match1);
|
||||
});
|
||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
||||
const parameterName = match[1];
|
||||
const scopeText = match[2];
|
||||
return {
|
||||
parameters: [new FunctionParameter(parameterName, true)],
|
||||
evaluator: (context) => {
|
||||
const argumentValue = context.args.hasArgument(parameterName)
|
||||
? context.args.getArgument(parameterName).argumentValue
|
||||
: undefined;
|
||||
if (!argumentValue) {
|
||||
return '';
|
||||
}
|
||||
return replaceEachScopeSubstitution(scopeText, (pipeline) => {
|
||||
if (!pipeline) {
|
||||
return argumentValue;
|
||||
}
|
||||
return context.pipelineCompiler.compile(argumentValue, pipeline);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const ScopeSubstitutionRegEx = new ExpressionRegexBuilder()
|
||||
// {{ . | pipeName }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('.')
|
||||
.matchPipeline() // First match: pipeline
|
||||
.expectExpressionEnd()
|
||||
.buildRegExp();
|
||||
|
||||
function replaceEachScopeSubstitution(scopeText: string, replacer: (pipeline: string) => string) {
|
||||
// Not using /{{\s*.\s*(?:(\|\s*[^{}]*?)\s*)?}}/g for not matching brackets,
|
||||
// but instead letting the pipeline compiler to fail on those.
|
||||
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1) => {
|
||||
return replacer(match1);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user