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:
undergroundwires
2022-10-02 20:12:49 +02:00
parent 7d3670c26d
commit e8d06e0f3e
6 changed files with 136 additions and 31 deletions

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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'],
},
);
});
});