Skip to content

Commit 619a128

Browse files
authored
Mustachio: allow mlti-name section keys (#2494)
1 parent f51278b commit 619a128

File tree

3 files changed

+85
-28
lines changed

3 files changed

+85
-28
lines changed

lib/src/mustachio/parser.dart

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -178,31 +178,50 @@ class MustachioParser {
178178
/// [_index] should be at the character immediately following the `#`
179179
/// character which opens a possible section tag.
180180
_TagParseResult _parseSection({@required invert}) {
181-
var result = _parseKey();
182-
if (result.type == _KeyParseResultType.notKey) {
181+
var parsedKey = _parseKey();
182+
if (parsedKey.type == _KeyParseResultType.notKey) {
183183
return _TagParseResult.notTag;
184-
} else if (result == _KeyParseResult.endOfFile) {
184+
} else if (parsedKey == _KeyParseResult.endOfFile) {
185185
return _TagParseResult.endOfFile;
186186
}
187187

188-
var children = _parseBlock(sectionKey: result.joinedNames);
188+
var children = _parseBlock(sectionKey: parsedKey.joinedNames);
189+
190+
if (parsedKey.names.length > 1) {
191+
// Desugar section with dots into nested sections.
192+
//
193+
// Given a multi-name section like
194+
// `{{#one.two.three}}...{/one.two.three}}`, "one" must be a non-Iterable,
195+
// non-bool value. "two" must also be a non-Iterable, non-bool value.
196+
// "three" may be any kind of value, resulting in a repeated section,
197+
// optional section, or value section. The [children], the parsed AST
198+
// inside the section, are the children of the [three] section. The
199+
// [three] section is the singular child node of the [two] section, and
200+
// the [two] section is the singular child of the [one] section.
201+
var section = Section([parsedKey.names.last], children, invert: invert);
202+
for (var sectionKey in parsedKey.names.reversed.skip(1)) {
203+
section = Section([sectionKey], [section], invert: false);
204+
}
205+
return _TagParseResult.ok(section);
206+
}
189207

190-
return _TagParseResult.ok(Section(result.names, children, invert: invert));
208+
return _TagParseResult.ok(
209+
Section(parsedKey.names, children, invert: invert));
191210
}
192211

193212
/// Tries to parse an end tag at [_index].
194213
///
195214
/// [_index] should be at the character immediately following the `/`
196215
/// character which opens a possible end tag.
197216
_TagParseResult _parseEndSection() {
198-
var result = _parseKey();
199-
if (result.type == _KeyParseResultType.notKey) {
217+
var parsedKey = _parseKey();
218+
if (parsedKey.type == _KeyParseResultType.notKey) {
200219
return _TagParseResult.notTag;
201-
} else if (result == _KeyParseResult.endOfFile) {
220+
} else if (parsedKey == _KeyParseResult.endOfFile) {
202221
return _TagParseResult.endOfFile;
203222
}
204223

205-
return _TagParseResult.endTag(result.joinedNames);
224+
return _TagParseResult.endTag(parsedKey.joinedNames);
206225
}
207226

208227
/// Tries to parse a variable tag at [_index].
@@ -216,14 +235,14 @@ class MustachioParser {
216235
_index++;
217236
_walkPastWhitespace();
218237
}
219-
var result = _parseKey(escape: escape);
220-
if (result.type == _KeyParseResultType.notKey) {
238+
var parsedKey = _parseKey(escape: escape);
239+
if (parsedKey.type == _KeyParseResultType.notKey) {
221240
return _TagParseResult.notTag;
222-
} else if (result == _KeyParseResult.endOfFile) {
241+
} else if (parsedKey == _KeyParseResult.endOfFile) {
223242
return _TagParseResult.endOfFile;
224243
}
225244

226-
return _TagParseResult.ok(Variable(result.names, escape: escape));
245+
return _TagParseResult.ok(Variable(parsedKey.names, escape: escape));
227246
}
228247

229248
/// Tries to parse a key at [_index].

test/mustachio/parser_test.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,35 @@ void main() {
227227
_expectVariable(section.children.single, equals(['two']));
228228
});
229229

230+
test('parses section with multi-name key', () {
231+
var parser =
232+
MustachioParser('Text {{#one.two.three}}Text{{/one.two.three}}');
233+
var ast = parser.parse();
234+
expect(ast, hasLength(2));
235+
_expectText(ast[0], equals('Text '));
236+
var sectionOne = ast[1] as Section;
237+
_expectSection(sectionOne, equals(['one']));
238+
expect(sectionOne.children, hasLength(1));
239+
var sectionTwo = sectionOne.children[0] as Section;
240+
_expectSection(sectionTwo, equals(['two']));
241+
var sectionThree = sectionTwo.children[0] as Section;
242+
_expectSection(sectionThree, equals(['three']));
243+
_expectText(sectionThree.children[0], equals('Text'));
244+
});
245+
246+
test('parses inverse section with multi-name key', () {
247+
var parser = MustachioParser('Text {{^one.two}}Text{{/one.two}}');
248+
var ast = parser.parse();
249+
expect(ast, hasLength(2));
250+
_expectText(ast[0], equals('Text '));
251+
var sectionOne = ast[1] as Section;
252+
_expectSection(sectionOne, equals(['one']));
253+
expect(sectionOne.children, hasLength(1));
254+
var sectionTwo = sectionOne.children[0] as Section;
255+
_expectSection(sectionTwo, equals(['two']), invert: true);
256+
_expectText(sectionTwo.children[0], equals('Text'));
257+
});
258+
230259
test('parses section with empty key as text', () {
231260
var parser = MustachioParser('Text {{#}}{{/key}}');
232261
var ast = parser.parse();

test/mustachio/renderer_test.dart

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,15 @@ void main() {
175175
expect(renderFoo(foo, fooTemplate), equals('Text Num 1, Num 2, Num 3, '));
176176
});
177177

178+
test('Renderer renders a repeated section node with a multi-name key',
179+
() async {
180+
var barTemplateFile = getFile('/project/bar.mustache')
181+
..writeAsStringSync('Text {{#foo.l1}}Num {{.}}, {{/foo.l1}}');
182+
var barTemplate = await Template.parse(barTemplateFile);
183+
var bar = Bar()..foo = (Foo()..l1 = [1, 2, 3]);
184+
expect(renderBar(bar, barTemplate), equals('Text Num 1, Num 2, Num 3, '));
185+
});
186+
178187
test('Renderer renders an empty repeated section node as blank', () async {
179188
var fooTemplateFile = getFile('/project/foo.mustache')
180189
..writeAsStringSync('Text {{#l1}}Num {{.}}, {{/l1}}');
@@ -200,11 +209,11 @@ void main() {
200209
});
201210

202211
test('Renderer renders a value section node', () async {
203-
var fooTemplateFile = getFile('/project/foo.mustache')
212+
var barTemplateFile = getFile('/project/bar.mustache')
204213
..writeAsStringSync('Text {{#foo}}Foo: {{s1}}{{/foo}}');
205-
var fooTemplate = await Template.parse(fooTemplateFile);
214+
var barTemplate = await Template.parse(barTemplateFile);
206215
var bar = Bar()..foo = (Foo()..s1 = 'hello');
207-
expect(renderBar(bar, fooTemplate), equals('Text Foo: hello'));
216+
expect(renderBar(bar, barTemplate), equals('Text Foo: hello'));
208217
});
209218

210219
test('Renderer renders a null value section node as blank', () async {
@@ -241,41 +250,41 @@ void main() {
241250

242251
test('Renderer resolves variable from outer context inside a value section',
243252
() async {
244-
var fooTemplateFile = getFile('/project/foo.mustache')
253+
var barTemplateFile = getFile('/project/foo.mustache')
245254
..writeAsStringSync('Text {{#foo}}{{s2}}{{/foo}}');
246-
var fooTemplate = await Template.parse(fooTemplateFile);
255+
var barTemplate = await Template.parse(barTemplateFile);
247256
var bar = Bar()
248257
..foo = (Foo()..s1 = 'hello')
249258
..s2 = 'goodbye';
250-
expect(renderBar(bar, fooTemplate), equals('Text goodbye'));
259+
expect(renderBar(bar, barTemplate), equals('Text goodbye'));
251260
});
252261

253262
test('Renderer resolves variable with key with multiple names', () async {
254-
var fooTemplateFile = getFile('/project/foo.mustache')
263+
var barTemplateFile = getFile('/project/foo.mustache')
255264
..writeAsStringSync('Text {{foo.s1}}');
256-
var fooTemplate = await Template.parse(fooTemplateFile);
265+
var barTemplate = await Template.parse(barTemplateFile);
257266
var bar = Bar()
258267
..foo = (Foo()..s1 = 'hello')
259268
..s2 = 'goodbye';
260-
expect(renderBar(bar, fooTemplate), equals('Text hello'));
269+
expect(renderBar(bar, barTemplate), equals('Text hello'));
261270
});
262271

263272
test('Renderer resolves outer variable with key with two names', () async {
264-
var fooTemplateFile = getFile('/project/foo.mustache')
273+
var barTemplateFile = getFile('/project/foo.mustache')
265274
..writeAsStringSync('Text {{#foo}}{{foo.s1}}{{/foo}}');
266-
var fooTemplate = await Template.parse(fooTemplateFile);
275+
var barTemplate = await Template.parse(barTemplateFile);
267276
var bar = Bar()
268277
..foo = (Foo()..s1 = 'hello')
269278
..s2 = 'goodbye';
270-
expect(renderBar(bar, fooTemplate), equals('Text hello'));
279+
expect(renderBar(bar, barTemplate), equals('Text hello'));
271280
});
272281

273282
test('Renderer resolves outer variable with key with three names', () async {
274-
var fooTemplateFile = getFile('/project/foo.mustache')
283+
var bazTemplateFile = getFile('/project/foo.mustache')
275284
..writeAsStringSync('Text {{#bar}}{{bar.foo.s1}}{{/bar}}');
276-
var fooTemplate = await Template.parse(fooTemplateFile);
285+
var bazTemplate = await Template.parse(bazTemplateFile);
277286
var baz = Baz()..bar = (Bar()..foo = (Foo()..s1 = 'hello'));
278-
expect(renderBaz(baz, fooTemplate), equals('Text hello'));
287+
expect(renderBaz(baz, bazTemplate), equals('Text hello'));
279288
});
280289

281290
test('Renderer resolves outer variable with key with more than three names',

0 commit comments

Comments
 (0)