Add multiline support for with expression
Improve templating support for block rendering for `with` expression
that has multiline code. This improves templating support to render
multiline code conditionally.
This did not work before but works now:
```
{{ with $middleLine }}
first line
second line
{{ end }}
```
This commit is contained in:
@@ -56,9 +56,23 @@ A function can call other functions such as:
|
||||
|
||||
### with
|
||||
|
||||
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions. E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}`.
|
||||
Skips its "block" if the variable is absent or empty. Its "block" is between `with` start (`{{ with .. }}`) and end (`{{ end }`}) expressions.
|
||||
E.g. `{{ with $parameterName }} Hi, I'm a block! {{ end }}` would only output `Hi, I'm a block!` if `parameterName` has any value..
|
||||
|
||||
Binds its context (`.`) value of provided argument for the parameter if provided one. E.g. `{{ with $parameterName }} Parameter value is {{ . }} here {{ end }}`.
|
||||
It binds its context (value of the provided parameter value) as arbitrary `.` value. It allows you to use the argument value of the given parameter when it is provided and not empty such as:
|
||||
|
||||
```go
|
||||
{{ with $parameterName }}Parameter value is {{ . }} here {{ end }}
|
||||
```
|
||||
|
||||
It supports multiline text inside the block. You can have something like:
|
||||
|
||||
```go
|
||||
{{ with $argument }}
|
||||
First line
|
||||
Second line
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
💡 Declare parameters used for `with` condition as optional. Set `optional: true` for the argument if you use it like `{{ with $argument }} .. {{ end }}`.
|
||||
|
||||
|
||||
@@ -25,10 +25,10 @@ export class ExpressionRegexBuilder {
|
||||
.addRawRegex('([^|\\s]+)');
|
||||
}
|
||||
|
||||
public matchAnythingExceptSurroundingWhitespaces() {
|
||||
public matchMultilineAnythingExceptSurroundingWhitespaces() {
|
||||
return this
|
||||
.expectZeroOrMoreWhitespaces()
|
||||
.addRawRegex('(.+?)')
|
||||
.addRawRegex('([\\S\\s]+?)')
|
||||
.expectZeroOrMoreWhitespaces();
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export class WithParser extends RegexParser {
|
||||
.matchUntilFirstWhitespace() // First match: parameter name
|
||||
.expectExpressionEnd()
|
||||
// ...
|
||||
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||
.matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||
// {{ end }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('end')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'mocha';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { expect } from 'chai';
|
||||
import { ExpressionRegexBuilder } from '@/application/Parser/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder';
|
||||
|
||||
@@ -8,7 +9,7 @@ describe('ExpressionRegexBuilder', () => {
|
||||
const charactersToEscape = ['.', '$'];
|
||||
for (const character of charactersToEscape) {
|
||||
it(character, () => {
|
||||
runRegExTest(
|
||||
expectRegex(
|
||||
// act
|
||||
(act) => act.expectCharacters(character),
|
||||
// assert
|
||||
@@ -18,7 +19,7 @@ describe('ExpressionRegexBuilder', () => {
|
||||
}
|
||||
});
|
||||
it('escapes multiple as expected', () => {
|
||||
runRegExTest(
|
||||
expectRegex(
|
||||
// act
|
||||
(act) => act.expectCharacters('.I have no $$.'),
|
||||
// assert
|
||||
@@ -26,7 +27,7 @@ describe('ExpressionRegexBuilder', () => {
|
||||
);
|
||||
});
|
||||
it('adds as expected', () => {
|
||||
runRegExTest(
|
||||
expectRegex(
|
||||
// act
|
||||
(act) => act.expectCharacters('return as it is'),
|
||||
// assert
|
||||
@@ -35,7 +36,7 @@ describe('ExpressionRegexBuilder', () => {
|
||||
});
|
||||
});
|
||||
it('expectOneOrMoreWhitespaces', () => {
|
||||
runRegExTest(
|
||||
expectRegex(
|
||||
// act
|
||||
(act) => act.expectOneOrMoreWhitespaces(),
|
||||
// assert
|
||||
@@ -43,7 +44,7 @@ describe('ExpressionRegexBuilder', () => {
|
||||
);
|
||||
});
|
||||
it('matchPipeline', () => {
|
||||
runRegExTest(
|
||||
expectRegex(
|
||||
// act
|
||||
(act) => act.matchPipeline(),
|
||||
// assert
|
||||
@@ -51,23 +52,63 @@ describe('ExpressionRegexBuilder', () => {
|
||||
);
|
||||
});
|
||||
it('matchUntilFirstWhitespace', () => {
|
||||
runRegExTest(
|
||||
expectRegex(
|
||||
// act
|
||||
(act) => act.matchUntilFirstWhitespace(),
|
||||
// assert
|
||||
'([^|\\s]+)',
|
||||
);
|
||||
});
|
||||
it('matchAnythingExceptSurroundingWhitespaces', () => {
|
||||
runRegExTest(
|
||||
it('matches until first whitespace', () => expectMatch(
|
||||
// arrange
|
||||
'first second',
|
||||
// act
|
||||
(act) => act.matchAnythingExceptSurroundingWhitespaces(),
|
||||
(act) => act.matchUntilFirstWhitespace(),
|
||||
// assert
|
||||
'\\s*(.+?)\\s*',
|
||||
);
|
||||
'first',
|
||||
));
|
||||
});
|
||||
describe('matchMultilineAnythingExceptSurroundingWhitespaces', () => {
|
||||
it('returns expected regex', () => expectRegex(
|
||||
// act
|
||||
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
||||
// assert
|
||||
'\\s*([\\S\\s]+?)\\s*',
|
||||
));
|
||||
it('matches single line', () => expectMatch(
|
||||
// arrange
|
||||
'single line',
|
||||
// act
|
||||
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
||||
// assert
|
||||
'single line',
|
||||
));
|
||||
it('matches single line without surrounding whitespaces', () => expectMatch(
|
||||
// arrange
|
||||
' single line\t',
|
||||
// act
|
||||
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
||||
// assert
|
||||
'single line',
|
||||
));
|
||||
it('matches multiple lines', () => expectMatch(
|
||||
// arrange
|
||||
'first line\nsecond line',
|
||||
// act
|
||||
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
||||
// assert
|
||||
'first line\nsecond line',
|
||||
));
|
||||
it('matches multiple lines without surrounding whitespaces', () => expectMatch(
|
||||
// arrange
|
||||
' first line\nsecond line\t',
|
||||
// act
|
||||
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
|
||||
// assert
|
||||
'first line\nsecond line',
|
||||
));
|
||||
});
|
||||
it('expectExpressionStart', () => {
|
||||
runRegExTest(
|
||||
expectRegex(
|
||||
// act
|
||||
(act) => act.expectExpressionStart(),
|
||||
// assert
|
||||
@@ -75,7 +116,7 @@ describe('ExpressionRegexBuilder', () => {
|
||||
);
|
||||
});
|
||||
it('expectExpressionEnd', () => {
|
||||
runRegExTest(
|
||||
expectRegex(
|
||||
// act
|
||||
(act) => act.expectExpressionEnd(),
|
||||
// assert
|
||||
@@ -95,10 +136,10 @@ describe('ExpressionRegexBuilder', () => {
|
||||
});
|
||||
describe('can combine multiple parts', () => {
|
||||
it('with', () => {
|
||||
runRegExTest(
|
||||
expectRegex(
|
||||
(sut) => sut
|
||||
// act
|
||||
// {{ $with }}
|
||||
// {{ with $variable }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('with')
|
||||
.expectOneOrMoreWhitespaces()
|
||||
@@ -106,17 +147,17 @@ describe('ExpressionRegexBuilder', () => {
|
||||
.matchUntilFirstWhitespace()
|
||||
.expectExpressionEnd()
|
||||
// scope
|
||||
.matchAnythingExceptSurroundingWhitespaces()
|
||||
.matchMultilineAnythingExceptSurroundingWhitespaces()
|
||||
// {{ end }}
|
||||
.expectExpressionStart()
|
||||
.expectCharacters('end')
|
||||
.expectExpressionEnd(),
|
||||
// assert
|
||||
'{{\\s*with\\s+\\$([^|\\s]+)\\s*}}\\s*(.+?)\\s*{{\\s*end\\s*}}',
|
||||
'{{\\s*with\\s+\\$([^|\\s]+)\\s*}}\\s*([\\S\\s]+?)\\s*{{\\s*end\\s*}}',
|
||||
);
|
||||
});
|
||||
it('scoped substitution', () => {
|
||||
runRegExTest(
|
||||
expectRegex(
|
||||
(sut) => sut
|
||||
// act
|
||||
.expectExpressionStart().expectCharacters('.')
|
||||
@@ -127,7 +168,7 @@ describe('ExpressionRegexBuilder', () => {
|
||||
);
|
||||
});
|
||||
it('parameter substitution', () => {
|
||||
runRegExTest(
|
||||
expectRegex(
|
||||
(sut) => sut
|
||||
// act
|
||||
.expectExpressionStart().expectCharacters('$')
|
||||
@@ -142,7 +183,7 @@ describe('ExpressionRegexBuilder', () => {
|
||||
});
|
||||
});
|
||||
|
||||
function runRegExTest(
|
||||
function expectRegex(
|
||||
act: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder,
|
||||
expected: string,
|
||||
) {
|
||||
@@ -153,3 +194,25 @@ function runRegExTest(
|
||||
// assert
|
||||
expect(actual).to.equal(expected);
|
||||
}
|
||||
|
||||
function expectMatch(
|
||||
input: string,
|
||||
act: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder,
|
||||
expectedMatch: string,
|
||||
) {
|
||||
// arrange
|
||||
const [startMarker, endMarker] = [randomUUID(), randomUUID()];
|
||||
const markedInput = `${startMarker}${input}${endMarker}`;
|
||||
const builder = new ExpressionRegexBuilder()
|
||||
.expectCharacters(startMarker);
|
||||
act(builder);
|
||||
const markedRegex = builder.expectCharacters(endMarker).buildRegExp();
|
||||
// act
|
||||
const match = Array.from(markedInput.matchAll(markedRegex))
|
||||
.filter((matches) => matches.length > 1)
|
||||
.map((matches) => matches[1])
|
||||
.filter(Boolean)
|
||||
.join();
|
||||
// assert
|
||||
expect(match).to.equal(expectedMatch);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,14 @@ export class SyntaxParserTestsRunner {
|
||||
return this;
|
||||
}
|
||||
|
||||
public expectNoMatch(...testCases: INoMatchTestCase[]) {
|
||||
this.expectPosition(...testCases.map((testCase) => ({
|
||||
name: testCase.name,
|
||||
code: testCase.code,
|
||||
expected: [],
|
||||
})));
|
||||
}
|
||||
|
||||
public expectResults(...testCases: IExpectResultTestCase[]) {
|
||||
for (const testCase of testCases) {
|
||||
it(testCase.name, () => {
|
||||
@@ -104,6 +112,11 @@ interface IExpectPositionTestCase {
|
||||
expected: readonly ExpressionPosition[];
|
||||
}
|
||||
|
||||
interface INoMatchTestCase {
|
||||
name: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface IExpectPipeHitTestData {
|
||||
codeBuilder: (pipeline: string) => string;
|
||||
parameterName: string;
|
||||
|
||||
@@ -29,30 +29,31 @@ describe('WithParser', () => {
|
||||
code: 'no whitespaces {{with $parameter}}value: {{ . }}{{end}}',
|
||||
expected: [new ExpressionPosition(15, 55)],
|
||||
},
|
||||
{
|
||||
name: 'match multiline text',
|
||||
code: 'non related line\n{{ with $middleLine }}\nline before value\n{{ . }}\nline after value\n{{ end }}\nnon related line',
|
||||
expected: [new ExpressionPosition(17, 92)],
|
||||
},
|
||||
);
|
||||
});
|
||||
describe('ignores when syntax is wrong', () => {
|
||||
describe('ignores expression if "with" syntax is wrong', () => {
|
||||
runner.expectPosition(
|
||||
runner.expectNoMatch(
|
||||
{
|
||||
name: 'does not tolerate whitespace after with',
|
||||
code: '{{with $ parameter}}value: {{ . }}{{ end }}',
|
||||
expected: [],
|
||||
},
|
||||
{
|
||||
name: 'does not tolerate whitespace before dollar',
|
||||
code: '{{ with$parameter}}value: {{ . }}{{ end }}',
|
||||
expected: [],
|
||||
},
|
||||
{
|
||||
name: 'wrong text at scope end',
|
||||
code: '{{ with$parameter}}value: {{ . }}{{ fin }}',
|
||||
expected: [],
|
||||
},
|
||||
{
|
||||
name: 'wrong text at expression start',
|
||||
code: '{{ when $parameter}}value: {{ . }}{{ end }}',
|
||||
expected: [],
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -130,6 +131,20 @@ describe('WithParser', () => {
|
||||
.withArgument('letterL', 'l'),
|
||||
expected: ['Hello world!'],
|
||||
},
|
||||
{
|
||||
name: 'renders value in multi-lined text',
|
||||
code: '{{ with $middleLine }}line before value\n{{ . }}\nline after value{{ end }}',
|
||||
args: (args) => args
|
||||
.withArgument('middleLine', 'value line'),
|
||||
expected: ['line before value\nvalue line\nline after value'],
|
||||
},
|
||||
{
|
||||
name: 'renders value around whitespaces in multi-lined text',
|
||||
code: '{{ with $middleLine }}\nline before value\n{{ . }}\nline after value\t {{ end }}',
|
||||
args: (args) => args
|
||||
.withArgument('middleLine', 'value line'),
|
||||
expected: ['line before value\nvalue line\nline after value'],
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user