Skip to content

Commit

Permalink
Add multiline support for with expression
Browse files Browse the repository at this point in the history
Improve 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 }}
```
  • Loading branch information
undergroundwires committed Oct 2, 2022
1 parent 7d3670c commit 4e82a69
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 30 deletions.
18 changes: 16 additions & 2 deletions docs/templating.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ export class ExpressionRegexBuilder {
.addRawRegex('([^|\\s]+)');
}

public matchAnythingExceptSurroundingWhitespaces() {
public matchMultilineAnythingExceptSurroundingWhitespaces() {
return this
.expectZeroOrMoreWhitespaces()
.addRawRegex('(.+?)')
.addRawRegex('([\\S\\s]+?)')
.expectZeroOrMoreWhitespaces();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -8,7 +9,7 @@ describe('ExpressionRegexBuilder', () => {
const charactersToEscape = ['.', '$'];
for (const character of charactersToEscape) {
it(character, () => {
runRegExTest(
expectRegex(
// act
(act) => act.expectCharacters(character),
// assert
Expand All @@ -18,15 +19,15 @@ describe('ExpressionRegexBuilder', () => {
}
});
it('escapes multiple as expected', () => {
runRegExTest(
expectRegex(
// act
(act) => act.expectCharacters('.I have no $$.'),
// assert
'\\.I have no \\$\\$\\.',
);
});
it('adds as expected', () => {
runRegExTest(
expectRegex(
// act
(act) => act.expectCharacters('return as it is'),
// assert
Expand All @@ -35,47 +36,87 @@ describe('ExpressionRegexBuilder', () => {
});
});
it('expectOneOrMoreWhitespaces', () => {
runRegExTest(
expectRegex(
// act
(act) => act.expectOneOrMoreWhitespaces(),
// assert
'\\s+',
);
});
it('matchPipeline', () => {
runRegExTest(
expectRegex(
// act
(act) => act.matchPipeline(),
// assert
'\\s*(\\|\\s*.+?)?',
);
});
it('matchUntilFirstWhitespace', () => {
runRegExTest(
expectRegex(
// act
(act) => act.matchUntilFirstWhitespace(),
// assert
'([^|\\s]+)',
);
it('matches until first whitespace', () => expectMatch(
// arrange
'first second',
// act
(act) => act.matchUntilFirstWhitespace(),
// assert
'first',
));
});
it('matchAnythingExceptSurroundingWhitespaces', () => {
runRegExTest(
describe('matchMultilineAnythingExceptSurroundingWhitespaces', () => {
it('returns expected regex', () => expectRegex(
// act
(act) => act.matchAnythingExceptSurroundingWhitespaces(),
(act) => act.matchMultilineAnythingExceptSurroundingWhitespaces(),
// assert
'\\s*(.+?)\\s*',
);
'\\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
'{{\\s*',
);
});
it('expectExpressionEnd', () => {
runRegExTest(
expectRegex(
// act
(act) => act.expectExpressionEnd(),
// assert
Expand All @@ -95,28 +136,28 @@ describe('ExpressionRegexBuilder', () => {
});
describe('can combine multiple parts', () => {
it('with', () => {
runRegExTest(
expectRegex(
(sut) => sut
// act
// {{ $with }}
// {{ with $variable }}
.expectExpressionStart()
.expectCharacters('with')
.expectOneOrMoreWhitespaces()
.expectCharacters('$')
.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('.')
Expand All @@ -127,7 +168,7 @@ describe('ExpressionRegexBuilder', () => {
);
});
it('parameter substitution', () => {
runRegExTest(
expectRegex(
(sut) => sut
// act
.expectExpressionStart().expectCharacters('$')
Expand All @@ -142,7 +183,7 @@ describe('ExpressionRegexBuilder', () => {
});
});

function runRegExTest(
function expectRegex(
act: (sut: ExpressionRegexBuilder) => ExpressionRegexBuilder,
expected: string,
) {
Expand All @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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, () => {
Expand Down Expand Up @@ -104,6 +112,11 @@ interface IExpectPositionTestCase {
expected: readonly ExpressionPosition[];
}

interface INoMatchTestCase {
name: string;
code: string;
}

interface IExpectPipeHitTestData {
codeBuilder: (pipeline: string) => string;
parameterName: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
},
);
});
Expand Down Expand Up @@ -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'],
},
);
});
});
Expand Down

0 comments on commit 4e82a69

Please sign in to comment.