Add support for pipes in templates #53

The goal is to be able to modify values of variables used in templates.
It enables future functionality such as escaping, inlining etc.

It adds support applying predefined pipes to variables. Pipes
can be applied to variable substitution in with and parameter
substitution expressions. They work in similar way to piping in Unix
where each pipe applied to the compiled result of pipe before.

It adds support for using pipes in `with` and parameter substitution
expressions. It also refactors how their regex is build to reuse more of
the logic by abstracting regex building into a new class.

Finally, it separates and extends documentation for templating.
This commit is contained in:
undergroundwires
2021-09-08 18:58:30 +01:00
parent 862914b06e
commit 4d7ff7edc5
30 changed files with 1112 additions and 207 deletions

View File

@@ -1,13 +1,28 @@
import { RegexParser, IPrimitiveExpression } from '../Parser/RegexParser';
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
export class ParameterSubstitutionParser extends RegexParser {
protected readonly regex = /{{\s*\$([^}| ]+)\s*}}/g;
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: (args) => args.getArgument(parameterName).argumentValue,
evaluator: (context) => {
const argumentValue = context.args.getArgument(parameterName).argumentValue;
if (!pipeline) {
return argumentValue;
}
return context.pipelineCompiler.compile(argumentValue, pipeline);
},
};
}
}

View File

@@ -1,24 +1,58 @@
import { RegexParser, IPrimitiveExpression } from '../Parser/RegexParser';
import { RegexParser, IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
export class WithParser extends RegexParser {
protected readonly regex = /{{\s*with\s+\$([^}| ]+)\s*}}\s*([^)]+?)\s*{{\s*end\s*}}/g;
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 innerText = match[2];
const scopeText = match[2];
return {
parameters: [ new FunctionParameter(parameterName, true) ],
evaluator: (args) => {
const argumentValue = args.hasArgument(parameterName) ?
args.getArgument(parameterName).argumentValue
evaluator: (context) => {
const argumentValue = context.args.hasArgument(parameterName) ?
context.args.getArgument(parameterName).argumentValue
: undefined;
if (!argumentValue) {
return '';
}
const substitutionRegex = /{{\s*.\s*}}/g;
const newText = innerText.replace(substitutionRegex, argumentValue);
return newText;
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 let pipeline compiler fail on those
return scopeText.replaceAll(ScopeSubstitutionRegEx, (_$, match1 ) => {
return replacer(match1);
});
}