diff --git a/lib/src/mustachio/parser.dart b/lib/src/mustachio/parser.dart index 4ff9d78825..1374f02694 100644 --- a/lib/src/mustachio/parser.dart +++ b/lib/src/mustachio/parser.dart @@ -4,6 +4,7 @@ import 'package:charcode/charcode.dart'; import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; /// A [Mustache](https://mustache.github.io/mustache.5.html) parser for use by a /// generated Mustachio renderer. @@ -14,10 +15,14 @@ class MustachioParser { /// The length of the template, in code units. final int _templateLength; + final SourceFile _sourceFile; + /// The index of the character currently being parsed. int _index = 0; - MustachioParser(this.template) : _templateLength = template.length; + MustachioParser(this.template, Uri url) + : _templateLength = template.length, + _sourceFile = SourceFile.fromString(template, url: url); /// Parses [template] into a sequence of [MustachioNode]s. /// @@ -46,7 +51,8 @@ class MustachioParser { void addTextNode(int startIndex, int endIndex) { if (endIndex > startIndex) { - children.add(Text(template.substring(startIndex, endIndex))); + children.add(Text(template.substring(startIndex, endIndex), + span: _sourceFile.span(startIndex, endIndex))); } } @@ -96,6 +102,7 @@ class MustachioParser { /// [_index] should be at the character immediately following the open /// delimiter `{{`. _TagParseResult _parseTag() { + var tagStartIndex = _index - 2; _walkPastWhitespace(); if (_atEnd) { return _TagParseResult.endOfFile; @@ -103,21 +110,21 @@ class MustachioParser { var char = _thisChar; if (char == $hash) { _index++; - return _parseSection(invert: false); + return _parseSection(invert: false, tagStartIndex: tagStartIndex); } else if (char == $caret) { _index++; - return _parseSection(invert: true); + return _parseSection(invert: true, tagStartIndex: tagStartIndex); } else if (char == $slash) { _index++; return _parseEndSection(); } else if (char == $gt) { _index++; - return _parsePartial(); + return _parsePartial(tagStartIndex: tagStartIndex); } else if (char == $exclamation) { _index++; return _parseComment(); } else { - return _parseVariable(); + return _parseVariable(tagStartIndex: tagStartIndex); } } @@ -144,7 +151,7 @@ class MustachioParser { /// /// [_index] should be at the character immediately following the `>` /// character which opens a possible partial tag. - _TagParseResult _parsePartial() { + _TagParseResult _parsePartial({@required int tagStartIndex}) { var startIndex = _index; int endIndex; while (true) { @@ -170,14 +177,16 @@ class MustachioParser { _index += 2; var key = template.substring(startIndex, endIndex); - return _TagParseResult.ok(Partial(key)); + return _TagParseResult.ok( + Partial(key, span: _sourceFile.span(tagStartIndex, _index))); } /// Tries to parse a section tag at [_index]. /// /// [_index] should be at the character immediately following the `#` /// character which opens a possible section tag. - _TagParseResult _parseSection({@required bool invert}) { + _TagParseResult _parseSection( + {@required bool invert, @required int tagStartIndex}) { var parsedKey = _parseKey(); if (parsedKey.type == _KeyParseResultType.notKey) { return _TagParseResult.notTag; @@ -186,6 +195,7 @@ class MustachioParser { } var children = _parseBlock(sectionKey: parsedKey.joinedNames); + var span = _sourceFile.span(tagStartIndex, _index); if (parsedKey.names.length > 1) { // Desugar section with dots into nested sections. @@ -198,15 +208,20 @@ class MustachioParser { // inside the section, are the children of the [three] section. The // [three] section is the singular child node of the [two] section, and // the [two] section is the singular child of the [one] section. - var section = Section([parsedKey.names.last], children, invert: invert); + var section = + Section([parsedKey.names.last], children, invert: invert, span: span); for (var sectionKey in parsedKey.names.reversed.skip(1)) { - section = Section([sectionKey], [section], invert: false); + section = Section([sectionKey], [section], + invert: false, + // TODO(srawlins): It may not make sense to use [span] here; we + // might want to do the work to find the span of [sectionKey]. + span: span); } return _TagParseResult.ok(section); } return _TagParseResult.ok( - Section(parsedKey.names, children, invert: invert)); + Section(parsedKey.names, children, invert: invert, span: span)); } /// Tries to parse an end tag at [_index]. @@ -228,7 +243,7 @@ class MustachioParser { /// /// [_index] should be at the character immediately following the `{{` /// characters which open a possible variable tag. - _TagParseResult _parseVariable() { + _TagParseResult _parseVariable({@required int tagStartIndex}) { var escape = true; if (_thisChar == $lbrace) { escape = false; @@ -242,7 +257,8 @@ class MustachioParser { return _TagParseResult.endOfFile; } - return _TagParseResult.ok(Variable(parsedKey.names, escape: escape)); + return _TagParseResult.ok(Variable(parsedKey.names, + escape: escape, span: _sourceFile.span(tagStartIndex, _index))); } /// Tries to parse a key at [_index]. @@ -347,14 +363,19 @@ class MustachioParser { /// An interface for various types of node in a Mustache template. @sealed -abstract class MustachioNode {} +abstract class MustachioNode { + SourceSpan get span; +} /// A Text node, representing literal text. @immutable class Text implements MustachioNode { final String content; - Text(this.content); + @override + final SourceSpan span; + + Text(this.content, {@required this.span}); @override String toString() => 'Text["$content"]'; @@ -367,7 +388,10 @@ class Variable implements MustachioNode { final bool escape; - Variable(this.key, {@required this.escape}); + @override + final SourceSpan span; + + Variable(this.key, {@required this.escape, @required this.span}); @override String toString() => 'Variable[$key, escape=$escape]'; @@ -383,7 +407,11 @@ class Section implements MustachioNode { final List children; - Section(this.key, this.children, {@required this.invert}); + @override + final SourceSpan span; + + Section(this.key, this.children, + {@required this.invert, @required this.span}); @override String toString() => 'Section[$key, invert=$invert]'; @@ -394,7 +422,10 @@ class Section implements MustachioNode { class Partial implements MustachioNode { final String key; - Partial(this.key); + @override + final SourceSpan span; + + Partial(this.key, {@required this.span}); } /// An enumeration of types of tag parse results. diff --git a/lib/src/mustachio/renderer_base.dart b/lib/src/mustachio/renderer_base.dart index d5e4a9697c..768344ca33 100644 --- a/lib/src/mustachio/renderer_base.dart +++ b/lib/src/mustachio/renderer_base.dart @@ -89,7 +89,7 @@ class Template { // 2) In the case of a reference from a top-level template, user code has // called [Template.parse], and the user is responsible for handling the // exception. - var ast = MustachioParser(file.readAsStringSync()).parse(); + var ast = MustachioParser(file.readAsStringSync(), file.toUri()).parse(); var nodeQueue = Queue.of(ast); var partials = {}; @@ -115,9 +115,8 @@ class Template { partialTemplates: {...partialTemplates}); partialTemplates[partialFile] = partialTemplate; } on FileSystemException catch (e) { - throw MustachioResolutionError( - 'FileSystemException when reading partial "$key" found in ' - 'template "${file.path}": ${e.message}'); + throw MustachioResolutionError(node.span.message( + 'FileSystemException (${e.message}) when reading partial:')); } } } @@ -165,6 +164,8 @@ abstract class RendererBase { /// [names] may have multiple dot-separate names, and [names] may not be a /// valid property of _this_ context type, in which the [parent] renderer is /// referenced. + // TODO(srawlins): Accept the [MustachioNode] here, so that the various errors + // can use the span. String getFields(List names) { if (names.length == 1 && names.single == '.') { return context.toString(); @@ -213,6 +214,7 @@ abstract class RendererBase { var property = getProperty(key); if (property == null) { if (parent == null) { + // TODO(srawlins): use the span of the key of [node] when implemented. throw MustachioResolutionError( 'Failed to resolve $key as a property on any types in the current ' 'context'); diff --git a/pubspec.yaml b/pubspec.yaml index 2bb494c80c..cd670b0d6e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: package_config: '>=0.1.5 <2.0.0' path: ^1.3.0 pub_semver: ^1.3.7 + source_span: ^1.5.2 yaml: ^2.1.0 dev_dependencies: diff --git a/test/mustachio/foo.renderers.dart b/test/mustachio/foo.renderers.dart index 0e4dc642a6..a4029d68c5 100644 --- a/test/mustachio/foo.renderers.dart +++ b/test/mustachio/foo.renderers.dart @@ -3,7 +3,7 @@ // To change the contents of this library, make changes to the builder source // files in the tool/mustachio/ directory. -// ignore_for_file: camel_case_types, unnecessary_cast, unused_element, unused_import, non_constant_identifier_names +// ignore_for_file: camel_case_types, unnecessary_cast, unused_element, unused_import, non_constant_identifier_names, deprecated_member_use_from_same_package import 'package:analyzer/file_system/file_system.dart'; import 'package:dartdoc/dartdoc.dart'; import 'package:dartdoc/src/generator/template_data.dart'; diff --git a/test/mustachio/parser_test.dart b/test/mustachio/parser_test.dart index dc56440fe8..4ccffd42fc 100644 --- a/test/mustachio/parser_test.dart +++ b/test/mustachio/parser_test.dart @@ -1,316 +1,329 @@ import 'package:dartdoc/src/mustachio/parser.dart'; import 'package:test/test.dart'; +final _filePath = Uri.parse('file:///foo.dart'); + void main() { test('parses an empty template', () { - var parser = MustachioParser(''); + var parser = MustachioParser('', _filePath); var ast = parser.parse(); expect(ast, isEmpty); }); test('parses "{" as text', () { - var parser = MustachioParser('{'); + var parser = MustachioParser('{', _filePath); var ast = parser.parse(); expect(ast, hasLength(1)); - _expectText(ast.single, equals('{')); + _expectText(ast.single, equals('{'), spanStart: 0, spanEnd: 1); }); test('parses "{{" as text', () { - var parser = MustachioParser('{{'); + var parser = MustachioParser('{{', _filePath); var ast = parser.parse(); expect(ast, hasLength(1)); - _expectText(ast[0], equals('{{')); + _expectText(ast[0], equals('{{'), spanStart: 0, spanEnd: 2); }); test('parses "{{}}" as text', () { - var parser = MustachioParser('{{}}'); + var parser = MustachioParser('{{}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(1)); - _expectText(ast[0], equals('{{}}')); + _expectText(ast[0], equals('{{}}'), spanStart: 0, spanEnd: 4); }); test('parses "{{{}}" as text', () { - var parser = MustachioParser('{{{}}'); + var parser = MustachioParser('{{{}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(1)); - _expectText(ast[0], equals('{{{}}')); + _expectText(ast[0], equals('{{{}}'), spanStart: 0, spanEnd: 5); }); test('parses text as text', () { - var parser = MustachioParser('Words, punctuation, #^!>/ etc.'); + var parser = MustachioParser('Words, punctuation, #^!>/ etc.', _filePath); var ast = parser.parse(); expect(ast, hasLength(1)); - _expectText(ast.single, equals('Words, punctuation, #^!>/ etc.')); + _expectText(ast.single, equals('Words, punctuation, #^!>/ etc.'), + spanStart: 0, spanEnd: 30); }); test('drops comment, start of content', () { - var parser = MustachioParser('{{!comment}} Text'); + var parser = MustachioParser('{{!comment}} Text', _filePath); var ast = parser.parse(); expect(ast, hasLength(1)); - _expectText(ast.single, equals(' Text')); + _expectText(ast.single, equals(' Text'), spanStart: 12, spanEnd: 17); }); test('drops comment, end of content', () { - var parser = MustachioParser('Text {{!comment}}'); + var parser = MustachioParser('Text {{!comment}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(1)); - _expectText(ast.single, equals('Text ')); + _expectText(ast.single, equals('Text '), spanStart: 0, spanEnd: 5); }); test('drops comment, entire content', () { - var parser = MustachioParser('{{!comment}}'); + var parser = MustachioParser('{{!comment}}', _filePath); var ast = parser.parse(); expect(ast, isEmpty); }); test('drops comment with whitespace', () { - var parser = MustachioParser('Text {{ !comment }} Text'); + var parser = MustachioParser('Text {{ !comment }} Text', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); - _expectText(ast[0], equals('Text ')); - _expectText(ast[1], equals(' Text')); + _expectText(ast[0], equals('Text '), spanStart: 0, spanEnd: 5); + _expectText(ast[1], equals(' Text'), spanStart: 21, spanEnd: 26); }); test('drops comment with newlines', () { - var parser = MustachioParser('Text {{ \n !comment \n }} Text'); + var parser = MustachioParser('Text {{ \n !comment \n }} Text', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); - _expectText(ast[0], equals('Text ')); - _expectText(ast[1], equals(' Text')); + _expectText(ast[0], equals('Text '), spanStart: 0, spanEnd: 5); + _expectText(ast[1], equals(' Text'), spanStart: 23, spanEnd: 28); }); test('drops comment with various chars', () { - var parser = MustachioParser('Text {{!Text, punct. `!@#\$%^&*()-=+}} Text'); + var parser = MustachioParser( + 'Text {{!Text, punct. `!@#\$%^&*()-=+}} Text', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); - _expectText(ast[0], equals('Text ')); - _expectText(ast[1], equals(' Text')); + _expectText(ast[0], equals('Text '), spanStart: 0, spanEnd: 5); + _expectText(ast[1], equals(' Text'), spanStart: 37, spanEnd: 42); }); test('drops comment with newlines inside', () { - var parser = MustachioParser('Text {{!Text\nMore text}} Text'); + var parser = MustachioParser('Text {{!Text\nMore text}} Text', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); - _expectText(ast[0], equals('Text ')); - _expectText(ast[1], equals(' Text')); + _expectText(ast[0], equals('Text '), spanStart: 0, spanEnd: 5); + _expectText(ast[1], equals(' Text'), spanStart: 24, spanEnd: 29); }); test('parses variable', () { - var parser = MustachioParser('Text {{key}}'); + var parser = MustachioParser('Text {{key}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); - _expectText(ast[0], equals('Text ')); - _expectVariable(ast[1], equals(['key'])); + _expectText(ast[0], equals('Text '), spanStart: 0, spanEnd: 5); + _expectVariable(ast[1], equals(['key']), spanStart: 5, spanEnd: 12); }); test('parses variable with whitespace', () { - var parser = MustachioParser('Text {{ key }}'); + var parser = MustachioParser('Text {{ key }}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); - _expectVariable(ast[1], equals(['key'])); + _expectVariable(ast[1], equals(['key']), spanStart: 5, spanEnd: 16); }); test('parses variable with newlines', () { - var parser = MustachioParser('Text {{\n \nkey\n \n}}'); + var parser = MustachioParser('Text {{\n \nkey\n \n}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); - _expectVariable(ast[1], equals(['key'])); + _expectVariable(ast[1], equals(['key']), spanStart: 5, spanEnd: 20); }); test('parses variable with triple mustaches', () { - var parser = MustachioParser('Text {{{key}}}'); + var parser = MustachioParser('Text {{{key}}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); - _expectVariable(ast[1], equals(['key']), escape: false); + _expectVariable(ast[1], equals(['key']), + escape: false, spanStart: 5, spanEnd: 14); }); test('parses variable with triple mustaches, whitespace', () { - var parser = MustachioParser('Text {{{ key }}}'); + var parser = MustachioParser('Text {{{ key }}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); - _expectVariable(ast[1], equals(['key']), escape: false); + _expectVariable(ast[1], equals(['key']), + escape: false, spanStart: 5, spanEnd: 18); }); test('parses "." pseudo-variable', () { - var parser = MustachioParser('Text {{.}}'); + var parser = MustachioParser('Text {{.}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); - _expectVariable(ast[1], equals(['.'])); + _expectVariable(ast[1], equals(['.']), spanStart: 5, spanEnd: 10); }); test('parses "." pseudo-variable with whitespace', () { - var parser = MustachioParser('Text {{ . }}'); + var parser = MustachioParser('Text {{ . }}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); - _expectVariable(ast[1], equals(['.'])); + _expectVariable(ast[1], equals(['.']), spanStart: 5, spanEnd: 12); }); test('parses variable with multiple names', () { - var parser = MustachioParser('Text {{a.b}}'); + var parser = MustachioParser('Text {{a.b}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); - _expectVariable(ast[1], equals(['a', 'b'])); + _expectVariable(ast[1], equals(['a', 'b']), spanStart: 5, spanEnd: 12); }); test('parses variable with multiple names and whitespace', () { - var parser = MustachioParser('Text {{ a.b }}'); + var parser = MustachioParser('Text {{ a.b }}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); - _expectVariable(ast[1], equals(['a', 'b'])); + _expectVariable(ast[1], equals(['a', 'b']), spanStart: 5, spanEnd: 14); }); test('parses almost-variable with trailing "." as text', () { - var parser = MustachioParser('Text {{ a.b. }}'); + var parser = MustachioParser('Text {{ a.b. }}', _filePath); var ast = parser.parse(); expect(ast, hasLength(1)); - _expectText(ast[0], equals('Text {{ a.b. }}')); + _expectText(ast[0], equals('Text {{ a.b. }}'), spanStart: 0, spanEnd: 15); }); test('parses almost-variable missing one "}" as text', () { - var parser = MustachioParser('Text {{ a.b }'); + var parser = MustachioParser('Text {{ a.b }', _filePath); var ast = parser.parse(); expect(ast, hasLength(1)); - _expectText(ast[0], equals('Text {{ a.b }')); + _expectText(ast[0], equals('Text {{ a.b }'), spanStart: 0, spanEnd: 13); }); test('parses almost-variable missing one "{" as text', () { - var parser = MustachioParser('Text { a.b }}'); + var parser = MustachioParser('Text { a.b }}', _filePath); var ast = parser.parse(); expect(ast, hasLength(1)); - _expectText(ast.single, equals('Text { a.b }}')); + _expectText(ast.single, equals('Text { a.b }}'), spanStart: 0, spanEnd: 13); }); test('parses variable with extra "{"', () { - var parser = MustachioParser('Text {{{ a.b }}'); + var parser = MustachioParser('Text {{{ a.b }}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); - _expectText(ast[0], equals('Text {')); - _expectVariable(ast[1], equals(['a', 'b'])); + _expectText(ast[0], equals('Text {'), spanStart: 0, spanEnd: 6); + _expectVariable(ast[1], equals(['a', 'b']), spanStart: 6, spanEnd: 15); }); test('parses section', () { - var parser = MustachioParser('Text {{#key}}Section text{{/key}}'); + var parser = + MustachioParser('Text {{#key}}Section text{{/key}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); var section = ast[1] as Section; - _expectSection(section, equals(['key'])); + _expectSection(section, equals(['key']), spanStart: 5, spanEnd: 33); expect(section.children, hasLength(1)); - _expectText(section.children.single, equals('Section text')); + _expectText(section.children.single, equals('Section text'), + spanStart: 13, spanEnd: 25); }); test('parses empty section', () { - var parser = MustachioParser('Text {{#key}}{{/key}}'); + var parser = MustachioParser('Text {{#key}}{{/key}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); var section = ast[1] as Section; - _expectSection(section, equals(['key'])); + _expectSection(section, equals(['key']), spanStart: 5, spanEnd: 21); expect(section.children, isEmpty); }); test('parses section with variable tag inside', () { - var parser = MustachioParser('Text {{#key}}{{two}}{{/key}}'); + var parser = MustachioParser('Text {{#key}}{{two}}{{/key}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); var section = ast[1] as Section; - _expectSection(section, equals(['key'])); + _expectSection(section, equals(['key']), spanStart: 5, spanEnd: 28); expect(section.children, hasLength(1)); - _expectVariable(section.children.single, equals(['two'])); + _expectVariable(section.children.single, equals(['two']), + spanStart: 13, spanEnd: 20); }); test('parses section with multi-name key', () { - var parser = - MustachioParser('Text {{#one.two.three}}Text{{/one.two.three}}'); + var parser = MustachioParser( + 'Text {{#one.two.three}}Text{{/one.two.three}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); var sectionOne = ast[1] as Section; - _expectSection(sectionOne, equals(['one'])); + _expectSection(sectionOne, equals(['one']), spanStart: 5, spanEnd: 45); expect(sectionOne.children, hasLength(1)); var sectionTwo = sectionOne.children[0] as Section; - _expectSection(sectionTwo, equals(['two'])); + _expectSection(sectionTwo, equals(['two']), spanStart: 5, spanEnd: 45); var sectionThree = sectionTwo.children[0] as Section; _expectSection(sectionThree, equals(['three'])); - _expectText(sectionThree.children[0], equals('Text')); + _expectText(sectionThree.children[0], equals('Text'), + spanStart: 23, spanEnd: 27); }); test('parses inverse section with multi-name key', () { - var parser = MustachioParser('Text {{^one.two}}Text{{/one.two}}'); + var parser = + MustachioParser('Text {{^one.two}}Text{{/one.two}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); var sectionOne = ast[1] as Section; - _expectSection(sectionOne, equals(['one'])); + _expectSection(sectionOne, equals(['one']), spanStart: 5, spanEnd: 33); expect(sectionOne.children, hasLength(1)); var sectionTwo = sectionOne.children[0] as Section; - _expectSection(sectionTwo, equals(['two']), invert: true); + _expectSection(sectionTwo, equals(['two']), + invert: true, spanStart: 5, spanEnd: 33); _expectText(sectionTwo.children[0], equals('Text')); }); test('parses section with empty key as text', () { - var parser = MustachioParser('Text {{#}}{{/key}}'); + var parser = MustachioParser('Text {{#}}{{/key}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(1)); _expectText(ast[0], equals('Text {{#}}{{/key}}')); }); test('parses section with missing closing tag as text', () { - var parser = MustachioParser('Text {{#}}{{/key}'); + var parser = MustachioParser('Text {{#}}{{/key}', _filePath); var ast = parser.parse(); expect(ast, hasLength(1)); _expectText(ast[0], equals('Text {{#}}{{/key}')); }); test('parses section with other closing tag', () { - var parser = MustachioParser('Text {{#key}}{{/other}}{{/key}}'); + var parser = MustachioParser('Text {{#key}}{{/other}}{{/key}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); var section = ast[1] as Section; - _expectSection(section, equals(['key'])); + _expectSection(section, equals(['key']), spanStart: 5, spanEnd: 31); expect(section.children, hasLength(1)); - _expectText(section.children.single, equals('{{/other}}')); + _expectText(section.children.single, equals('{{/other}}'), + spanStart: 13, spanEnd: 23); }); test('parses empty closing tag as text', () { - var parser = MustachioParser('Text {{#key}}{{/}}{{/key}}'); + var parser = MustachioParser('Text {{#key}}{{/}}{{/key}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); var section = ast[1] as Section; - _expectSection(section, equals(['key'])); + _expectSection(section, equals(['key']), spanStart: 5, spanEnd: 26); expect(section.children, hasLength(1)); _expectText(section.children.single, equals('{{/}}')); }); test('parses nested sections', () { var parser = MustachioParser( - 'Text {{#key1}} AA {{#key2}} BB {{/key2}} CC {{/key1}}'); + 'Text {{#key1}} AA {{#key2}} BB {{/key2}} CC {{/key1}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); var section = ast[1] as Section; - _expectSection(section, equals(['key1'])); + _expectSection(section, equals(['key1']), spanStart: 5, spanEnd: 53); expect(section.children, hasLength(3)); _expectText(section.children[0], equals(' AA ')); var innerSection = section.children[1] as Section; - _expectSection(innerSection, equals(['key2'])); + _expectSection(innerSection, equals(['key2']), spanStart: 18, spanEnd: 40); expect(innerSection.children, hasLength(1)); _expectText(innerSection.children[0], equals(' BB ')); }); test('parses nested sections with the same key', () { - var parser = - MustachioParser('Text {{#key}} AA {{#key}} BB {{/key}} CC {{/key}}'); + var parser = MustachioParser( + 'Text {{#key}} AA {{#key}} BB {{/key}} CC {{/key}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); @@ -325,60 +338,113 @@ void main() { }); test('parses inverted section', () { - var parser = MustachioParser('Text {{^key}} AA {{/key}}'); + var parser = MustachioParser('Text {{^key}} AA {{/key}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); var section = ast[1] as Section; - _expectSection(section, equals(['key']), invert: true); + _expectSection(section, equals(['key']), + invert: true, spanStart: 5, spanEnd: 25); expect(section.children, hasLength(1)); _expectText(section.children[0], equals(' AA ')); }); test('parses section with empty key as text', () { - var parser = MustachioParser('Text {{^}}{{/key}}'); + var parser = MustachioParser('Text {{^}}{{/key}}', _filePath); var ast = parser.parse(); expect(ast, hasLength(1)); _expectText(ast[0], equals('Text {{^}}{{/key}}')); }); test('parses partial', () { - var parser = MustachioParser('Text {{ >partial }}'); + var parser = MustachioParser('Text {{ >partial }}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); - _expectPartial(ast[1], equals('partial')); + _expectPartial(ast[1], equals('partial'), spanStart: 5, spanEnd: 19); }); test('parses partial with various chars', () { - var parser = MustachioParser('Text {{ >Text,punct.`!@#\$%^&*()-=+ }}'); + var parser = + MustachioParser('Text {{ >Text,punct.`!@#\$%^&*()-=+ }}', _filePath); var ast = parser.parse(); expect(ast, hasLength(2)); _expectText(ast[0], equals('Text ')); - _expectPartial(ast[1], equals('Text,punct.`!@#\$%^&*()-=+')); + _expectPartial(ast[1], equals('Text,punct.`!@#\$%^&*()-=+'), + spanStart: 5, spanEnd: 37); }); } -void _expectText(MustachioNode node, Object matcher) { +void _expectText(MustachioNode node, Object matcher, + {int spanStart, int spanEnd}) { expect(node, isA().having((e) => e.content, 'content', matcher)); + if (spanStart != null) { + expect( + node, + isA() + .having((e) => e.span.start.offset, 'span.start', spanStart)); + } + if (spanEnd != null) { + expect(node, + isA().having((e) => e.span.end.offset, 'span.end', spanEnd)); + } } -void _expectVariable(MustachioNode node, Object matcher, {bool escape = true}) { +void _expectVariable(MustachioNode node, Object matcher, + {bool escape = true, int spanStart, int spanEnd}) { expect( node, isA() .having((e) => e.key, 'key', matcher) .having((e) => e.escape, 'escape', escape)); + if (spanStart != null) { + var actualSpanStart = (node as Variable).span.start.offset; + expect(actualSpanStart, spanStart, + reason: 'Variable span start offset expected to be $spanStart but was ' + '$actualSpanStart'); + } + if (spanEnd != null) { + var actualSpanEnd = (node as Variable).span.end.offset; + expect(actualSpanEnd, spanEnd, + reason: 'Variable span end offset expected to be $spanEnd but was ' + '$actualSpanEnd'); + } } -void _expectSection(MustachioNode node, Object matcher, {bool invert = false}) { +void _expectSection(MustachioNode node, Object matcher, + {bool invert = false, int spanStart, int spanEnd}) { expect( node, isA
() .having((e) => e.key, 'key', matcher) .having((e) => e.invert, 'invert', invert)); + if (spanStart != null) { + var actualSpanStart = (node as Section).span.start.offset; + expect(actualSpanStart, spanStart, + reason: 'Section span start offset expected to be $spanStart but was ' + '$actualSpanStart'); + } + if (spanEnd != null) { + var actualSpanEnd = (node as Section).span.end.offset; + expect(actualSpanEnd, spanEnd, + reason: 'Section span end offset expected to be $spanEnd but was ' + '$actualSpanEnd'); + } } -void _expectPartial(MustachioNode node, Object matcher) { +void _expectPartial(MustachioNode node, Object matcher, + {int spanStart, int spanEnd}) { expect(node, isA().having((e) => e.key, 'key', matcher)); + if (spanStart != null) { + var actualSpanStart = (node as Partial).span.start.offset; + expect(actualSpanStart, spanStart, + reason: 'Partial span start offset expected to be $spanStart but was ' + '$actualSpanStart'); + } + if (spanEnd != null) { + var actualSpanEnd = (node as Partial).span.end.offset; + expect(actualSpanEnd, spanEnd, + reason: 'Partial span end offset expected to be $spanEnd but was ' + '$actualSpanEnd'); + } } diff --git a/test/mustachio/renderer_test.dart b/test/mustachio/renderer_test.dart index a8a660c481..def48d434e 100644 --- a/test/mustachio/renderer_test.dart +++ b/test/mustachio/renderer_test.dart @@ -483,13 +483,15 @@ void main() { test('Template parser throws when it cannot read a partial', () async { var barTemplateFile = getFile('/project/src/bar.mustache') ..writeAsStringSync('Text {{#foo}}{{>missing.mustache}}{{/foo}}'); + var missingTemplateFile = getFile('/project/src/missing.mustache'); expect( () async => await Template.parse(barTemplateFile), - throwsA(const TypeMatcher().having( - (e) => e.message, - 'message', - contains( - 'FileSystemException when reading partial "missing.mustache" ' - 'found in template "${barTemplateFile.path}"')))); + throwsA(const TypeMatcher() + .having((e) => e.message, 'message', contains(''' +line 1, column 14 of ${barTemplateFile.path}: FileSystemException (File "${missingTemplateFile.path}" does not exist.) when reading partial: + ╷ +1 │ Text {{#foo}}{{>missing.mustache}}{{/foo}} + │ ^^^^^^^^^^^^^^^^^^^^^ +''')))); }); }