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
|
### 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 }}`.
|
💡 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]+)');
|
.addRawRegex('([^|\\s]+)');
|
||||||
}
|
}
|
||||||
|
|
||||||
public matchAnythingExceptSurroundingWhitespaces() {
|
public matchMultilineAnythingExceptSurroundingWhitespaces() {
|
||||||
return this
|
return this
|
||||||
.expectZeroOrMoreWhitespaces()
|
.expectZeroOrMoreWhitespaces()
|
||||||
.addRawRegex('(.+?)')
|
.addRawRegex('([\\S\\s]+?)')
|
||||||
.expectZeroOrMoreWhitespaces();
|
.expectZeroOrMoreWhitespaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class WithParser extends RegexParser {
|
|||||||
.matchUntilFirstWhitespace() // First match: parameter name
|
.matchUntilFirstWhitespace() // First match: parameter name
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
// ...
|
// ...
|
||||||
.matchAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
.matchMultilineAnythingExceptSurroundingWhitespaces() // Second match: Scope text
|
||||||
// {{ end }}
|
// {{ end }}
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('end')
|
.expectCharacters('end')
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { ExpressionRegexBuilder } from '@/application/Parser/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '@/application/Parser/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ describe('ExpressionRegexBuilder', () => {
|
|||||||
const charactersToEscape = ['.', '$'];
|
const charactersToEscape = ['.', '$'];
|
||||||
for (const character of charactersToEscape) {
|
for (const character of charactersToEscape) {
|
||||||
it(character, () => {
|
it(character, () => {
|
||||||
runRegExTest(
|
expectRegex(
|
||||||
// act
|
// act
|
||||||
(act) => act.expectCharacters(character),
|
(act) => act.expectCharacters(character),
|
||||||
// assert
|
// assert
|
||||||
@@ -18,7 +19,7 @@ describe('ExpressionRegexBuilder', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('escapes multiple as expected', () => {
|
it('escapes multiple as expected', () => {
|
||||||
runRegExTest(
|
expectRegex(
|
||||||
// act
|
// act
|
||||||
(act) => act.expectCharacters('.I have no $$.'),
|
(act) => act.expectCharacters('.I have no $$.'),
|
||||||
// assert
|
// assert
|
||||||
@@ -26,7 +27,7 @@ describe('ExpressionRegexBuilder', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('adds as expected', () => {
|
it('adds as expected', () => {
|
||||||
runRegExTest(
|
expectRegex(
|
||||||
// act
|
// act
|
||||||
(act) => act.expectCharacters('return as it is'),
|
(act) => act.expectCharacters('return as it is'),
|
||||||
// assert
|
// assert
|
||||||
@@ -35,7 +36,7 @@ describe('ExpressionRegexBuilder', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('expectOneOrMoreWhitespaces', () => {
|
it('expectOneOrMoreWhitespaces', () => {
|
||||||
runRegExTest(
|
expectRegex(
|
||||||
// act
|
// act
|
||||||
(act) => act.expectOneOrMoreWhitespaces(),
|
(act) => act.expectOneOrMoreWhitespaces(),
|
||||||
// assert
|
// assert
|
||||||
@@ -43,7 +44,7 @@ describe('ExpressionRegexBuilder', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('matchPipeline', () => {
|
it('matchPipeline', () => {
|
||||||
runRegExTest(
|
expectRegex(
|
||||||
// act
|
// act
|
||||||
(act) => act.matchPipeline(),
|
(act) => act.matchPipeline(),
|
||||||
// assert
|
// assert
|
||||||
@@ -51,23 +52,63 @@ describe('ExpressionRegexBuilder', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('matchUntilFirstWhitespace', () => {
|
it('matchUntilFirstWhitespace', () => {
|
||||||
runRegExTest(
|
expectRegex(
|
||||||
// act
|
// act
|
||||||
(act) => act.matchUntilFirstWhitespace(),
|
(act) => act.matchUntilFirstWhitespace(),
|
||||||
// assert
|
// assert
|
||||||
'([^|\\s]+)',
|
'([^|\\s]+)',
|
||||||
);
|
);
|
||||||
});
|
it('matches until first whitespace', () => expectMatch(
|
||||||
it('matchAnythingExceptSurroundingWhitespaces', () => {
|
// arrange
|
||||||
runRegExTest(
|
'first second',
|
||||||
// act
|
// act
|
||||||
(act) => act.matchAnythingExceptSurroundingWhitespaces(),
|
(act) => act.matchUntilFirstWhitespace(),
|
||||||
// assert
|
// 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', () => {
|
it('expectExpressionStart', () => {
|
||||||
runRegExTest(
|
expectRegex(
|
||||||
// act
|
// act
|
||||||
(act) => act.expectExpressionStart(),
|
(act) => act.expectExpressionStart(),
|
||||||
// assert
|
// assert
|
||||||
@@ -75,7 +116,7 @@ describe('ExpressionRegexBuilder', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('expectExpressionEnd', () => {
|
it('expectExpressionEnd', () => {
|
||||||
runRegExTest(
|
expectRegex(
|
||||||
// act
|
// act
|
||||||
(act) => act.expectExpressionEnd(),
|
(act) => act.expectExpressionEnd(),
|
||||||
// assert
|
// assert
|
||||||
@@ -95,10 +136,10 @@ describe('ExpressionRegexBuilder', () => {
|
|||||||
});
|
});
|
||||||
describe('can combine multiple parts', () => {
|
describe('can combine multiple parts', () => {
|
||||||
it('with', () => {
|
it('with', () => {
|
||||||
runRegExTest(
|
expectRegex(
|
||||||
(sut) => sut
|
(sut) => sut
|
||||||
// act
|
// act
|
||||||
// {{ $with }}
|
// {{ with $variable }}
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('with')
|
.expectCharacters('with')
|
||||||
.expectOneOrMoreWhitespaces()
|
.expectOneOrMoreWhitespaces()
|
||||||
@@ -106,17 +147,17 @@ describe('ExpressionRegexBuilder', () => {
|
|||||||
.matchUntilFirstWhitespace()
|
.matchUntilFirstWhitespace()
|
||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
// scope
|
// scope
|
||||||
.matchAnythingExceptSurroundingWhitespaces()
|
.matchMultilineAnythingExceptSurroundingWhitespaces()
|
||||||
// {{ end }}
|
// {{ end }}
|
||||||
.expectExpressionStart()
|
.expectExpressionStart()
|
||||||
.expectCharacters('end')
|
.expectCharacters('end')
|
||||||
.expectExpressionEnd(),
|
.expectExpressionEnd(),
|
||||||
// assert
|
// 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', () => {
|
it('scoped substitution', () => {
|
||||||
runRegExTest(
|
expectRegex(
|
||||||
(sut) => sut
|
(sut) => sut
|
||||||
// act
|
// act
|
||||||
.expectExpressionStart().expectCharacters('.')
|
.expectExpressionStart().expectCharacters('.')
|
||||||
@@ -127,7 +168,7 @@ describe('ExpressionRegexBuilder', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('parameter substitution', () => {
|
it('parameter substitution', () => {
|
||||||
runRegExTest(
|
expectRegex(
|
||||||
(sut) => sut
|
(sut) => sut
|
||||||
// act
|
// act
|
||||||
.expectExpressionStart().expectCharacters('$')
|
.expectExpressionStart().expectCharacters('$')
|
||||||
@@ -142,7 +183,7 @@ describe('ExpressionRegexBuilder', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function runRegExTest(
|
function expectRegex(
|
||||||
act: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder,
|
act: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder,
|
||||||
expected: string,
|
expected: string,
|
||||||
) {
|
) {
|
||||||
@@ -153,3 +194,25 @@ function runRegExTest(
|
|||||||
// assert
|
// assert
|
||||||
expect(actual).to.equal(expected);
|
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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public expectNoMatch(...testCases: INoMatchTestCase[]) {
|
||||||
|
this.expectPosition(...testCases.map((testCase) => ({
|
||||||
|
name: testCase.name,
|
||||||
|
code: testCase.code,
|
||||||
|
expected: [],
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
|
||||||
public expectResults(...testCases: IExpectResultTestCase[]) {
|
public expectResults(...testCases: IExpectResultTestCase[]) {
|
||||||
for (const testCase of testCases) {
|
for (const testCase of testCases) {
|
||||||
it(testCase.name, () => {
|
it(testCase.name, () => {
|
||||||
@@ -104,6 +112,11 @@ interface IExpectPositionTestCase {
|
|||||||
expected: readonly ExpressionPosition[];
|
expected: readonly ExpressionPosition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface INoMatchTestCase {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface IExpectPipeHitTestData {
|
interface IExpectPipeHitTestData {
|
||||||
codeBuilder: (pipeline: string) => string;
|
codeBuilder: (pipeline: string) => string;
|
||||||
parameterName: string;
|
parameterName: string;
|
||||||
|
|||||||
@@ -29,30 +29,31 @@ describe('WithParser', () => {
|
|||||||
code: 'no whitespaces {{with $parameter}}value: {{ . }}{{end}}',
|
code: 'no whitespaces {{with $parameter}}value: {{ . }}{{end}}',
|
||||||
expected: [new ExpressionPosition(15, 55)],
|
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 when syntax is wrong', () => {
|
||||||
describe('ignores expression if "with" syntax is wrong', () => {
|
describe('ignores expression if "with" syntax is wrong', () => {
|
||||||
runner.expectPosition(
|
runner.expectNoMatch(
|
||||||
{
|
{
|
||||||
name: 'does not tolerate whitespace after with',
|
name: 'does not tolerate whitespace after with',
|
||||||
code: '{{with $ parameter}}value: {{ . }}{{ end }}',
|
code: '{{with $ parameter}}value: {{ . }}{{ end }}',
|
||||||
expected: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'does not tolerate whitespace before dollar',
|
name: 'does not tolerate whitespace before dollar',
|
||||||
code: '{{ with$parameter}}value: {{ . }}{{ end }}',
|
code: '{{ with$parameter}}value: {{ . }}{{ end }}',
|
||||||
expected: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'wrong text at scope end',
|
name: 'wrong text at scope end',
|
||||||
code: '{{ with$parameter}}value: {{ . }}{{ fin }}',
|
code: '{{ with$parameter}}value: {{ . }}{{ fin }}',
|
||||||
expected: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'wrong text at expression start',
|
name: 'wrong text at expression start',
|
||||||
code: '{{ when $parameter}}value: {{ . }}{{ end }}',
|
code: '{{ when $parameter}}value: {{ . }}{{ end }}',
|
||||||
expected: [],
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -130,6 +131,20 @@ describe('WithParser', () => {
|
|||||||
.withArgument('letterL', 'l'),
|
.withArgument('letterL', 'l'),
|
||||||
expected: ['Hello world!'],
|
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