diff --git a/docs/templating.md b/docs/templating.md index 5352edf1..96373fad 100644 --- a/docs/templating.md +++ b/docs/templating.md @@ -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 }}`. diff --git a/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder.ts b/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder.ts index 3a5965f8..abcf3c25 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder.ts @@ -25,10 +25,10 @@ export class ExpressionRegexBuilder { .addRawRegex('([^|\\s]+)'); } - public matchAnythingExceptSurroundingWhitespaces() { + public matchMultilineAnythingExceptSurroundingWhitespaces() { return this .expectZeroOrMoreWhitespaces() - .addRawRegex('(.+?)') + .addRawRegex('([\\S\\s]+?)') .expectZeroOrMoreWhitespaces(); } diff --git a/src/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts b/src/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts index 4d349d3e..b5208942 100644 --- a/src/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts +++ b/src/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts @@ -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') diff --git a/tests/unit/application/Parser/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder.spec.ts b/tests/unit/application/Parser/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder.spec.ts index 379a34a1..cc2f4d27 100644 --- a/tests/unit/application/Parser/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/Expressions/Parser/Regex/ExpressionRegexBuilder.spec.ts @@ -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); +} diff --git a/tests/unit/application/Parser/Script/Compiler/Expressions/SyntaxParsers/SyntaxParserTestsRunner.ts b/tests/unit/application/Parser/Script/Compiler/Expressions/SyntaxParsers/SyntaxParserTestsRunner.ts index d93573dd..66b3ab6b 100644 --- a/tests/unit/application/Parser/Script/Compiler/Expressions/SyntaxParsers/SyntaxParserTestsRunner.ts +++ b/tests/unit/application/Parser/Script/Compiler/Expressions/SyntaxParsers/SyntaxParserTestsRunner.ts @@ -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; diff --git a/tests/unit/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.spec.ts b/tests/unit/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.spec.ts index 6702bd07..ab16091c 100644 --- a/tests/unit/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.spec.ts @@ -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'], + }, ); }); });