| // Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| // See the Mustachio README at tool/mustachio/README.md for high-level |
| // documentation. |
| |
| import 'package:meta/meta.dart'; |
| import 'package:source_span/source_span.dart'; |
| |
| import '../charcode.dart'; |
| |
| /// A [Mustache](https://mustache.github.io/mustache.5.html) parser for use by a |
| /// generated Mustachio renderer. |
| class MustachioParser { |
| /// The full content of a Mustache template (or Mustache partial). |
| final String template; |
| |
| /// 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, String path) |
| : _templateLength = template.length, |
| _sourceFile = SourceFile.fromString(template, url: path); |
| |
| /// Parses [template] into a sequence of [MustachioNode]s. |
| /// |
| /// In the returned nodes, no keys or partials have been resolved. There is |
| /// no guarantee that any key will resolve to a value during rendering. There |
| /// is no guarantee that any partial will resolve without errors during |
| /// rendering. The type of any [Section] node is not known until rendering. |
| List<MustachioNode> parse() { |
| assert(_index == 0); |
| var children = _parseBlock(); |
| return children; |
| } |
| |
| /// Parses a block of Mustache template content starting at [_index]. |
| /// |
| /// When an end tag is encountered: |
| /// * if [sectionKey] is non-null, and the end tag matches [sectionKey], the |
| /// block is complete. |
| /// * if [sectionKey] is non-null, and the end tag does not match |
| /// [sectionKey], the end tag is treated as plain text, not a tag. |
| /// * if [sectionKey] is null, the end tag is treated as plain text, not a |
| /// tag. |
| List<MustachioNode> _parseBlock({String? sectionKey}) { |
| var children = <MustachioNode>[]; |
| var textStartIndex = _index; |
| var textEndIndex = _index; |
| |
| void addTextNode(int startIndex, int endIndex) { |
| if (endIndex > startIndex) { |
| children.add(Text(template.substring(startIndex, endIndex), |
| span: _sourceFile.span(startIndex, endIndex))); |
| } |
| } |
| |
| /// Trims [textEndIndex] if it marks the end of a blank line. |
| /// |
| /// [textEndIndex] is reset back to the newline immediately preceding any |
| /// whitespace preceding [textEndIndex]. |
| void trimTextRight() { |
| var newEndIndex = textEndIndex; |
| while (true) { |
| if (newEndIndex == textStartIndex) { |
| // We walked all the way to [textStartIndex] without finding a |
| // newline; for example in `{{a}} {{b}}` we don't want to trim the |
| // singular space. |
| return; |
| } |
| var ch = template.codeUnitAt(newEndIndex - 1); |
| if (ch == $space || ch == $tab) { |
| newEndIndex--; |
| } else if (ch == $cr || ch == $lf) { |
| textEndIndex = newEndIndex - 1; |
| return; |
| } else { |
| // We walked back to some other character; [textEndIndex] does not |
| // mark the end of a blank line. |
| return; |
| } |
| } |
| } |
| |
| while (true) { |
| if (_nextAtEnd) { |
| addTextNode(textStartIndex, _templateLength); |
| break; |
| } |
| if (_thisChar == $lbrace && _nextChar == $lbrace) { |
| textEndIndex = _index; |
| _index += 2; |
| var result = _parseTag(); |
| if (result == _TagParseResult.endOfFile) { |
| _index = textEndIndex + 1; |
| continue; |
| } else if (result == _TagParseResult.notTag) { |
| _index = textEndIndex + 1; |
| continue; |
| } else if (result == _TagParseResult.commentTag) { |
| addTextNode(textStartIndex, textEndIndex); |
| textStartIndex = _index; |
| continue; |
| } else if (result.type == _TagParseResultType.parsedEndTag) { |
| if (sectionKey != null && sectionKey == result.endTagKey) { |
| trimTextRight(); |
| addTextNode(textStartIndex, textEndIndex); |
| break; |
| } else { |
| addTextNode(textStartIndex, _index); |
| textStartIndex = _index; |
| continue; |
| } |
| } else { |
| assert(result.type == _TagParseResultType.parsedTag); |
| if (result.node is Section) { |
| // Trim the right off of the preceding text node, only if a Section |
| // was just parsed. For other tags, like Variables or Partials, |
| // the whitespace may be important, as the tag itself will be |
| // rendered as text. |
| trimTextRight(); |
| } |
| addTextNode(textStartIndex, textEndIndex); |
| children.add(result.node!); |
| textStartIndex = _index; |
| continue; |
| } |
| } |
| _index++; |
| } |
| |
| return children; |
| } |
| |
| /// Tries to parse a tag at [_index]. |
| /// |
| /// [_index] should be at the character immediately following the open |
| /// delimiter `{{`. |
| _TagParseResult _parseTag() { |
| var tagStartIndex = _index - 2; |
| _walkPastWhitespace(); |
| if (_atEnd) { |
| return _TagParseResult.endOfFile; |
| } |
| var char = _thisChar; |
| if (char == $hash) { |
| _index++; |
| _walkPastWhitespace(); |
| return _parseSection(invert: false, tagStartIndex: tagStartIndex); |
| } else if (char == $caret) { |
| _index++; |
| _walkPastWhitespace(); |
| return _parseSection(invert: true, tagStartIndex: tagStartIndex); |
| } else if (char == $slash) { |
| _index++; |
| _walkPastWhitespace(); |
| return _parseEndSection(); |
| } else if (char == $gt) { |
| _index++; |
| _walkPastWhitespace(); |
| return _parsePartial(tagStartIndex: tagStartIndex); |
| } else if (char == $exclamation) { |
| _index++; |
| return _parseComment(); |
| } else { |
| return _parseVariable(tagStartIndex: tagStartIndex); |
| } |
| } |
| |
| /// Tries to parse a comment tag at [_index]. |
| /// |
| /// [_index] should be at the character immediately following the `!` |
| /// character which opens a possible comment tag. |
| _TagParseResult _parseComment() { |
| while (true) { |
| if (_nextAtEnd) { |
| return _TagParseResult.endOfFile; |
| } |
| if (_thisChar == $rbrace && _nextChar == $rbrace) { |
| break; |
| } |
| _index++; |
| } |
| |
| _index += 2; |
| return _TagParseResult.commentTag; |
| } |
| |
| /// Tries to parse a partial tag at [_index]. |
| /// |
| /// [_index] should be at the character immediately following the `>` |
| /// character which opens a possible partial tag. |
| _TagParseResult _parsePartial({required int tagStartIndex}) { |
| var startIndex = _index; |
| int endIndex; |
| while (true) { |
| if (_nextAtEnd) { |
| return _TagParseResult.endOfFile; |
| } |
| if (_thisChar == $space) { |
| endIndex = _index; |
| // Whitespace _must_ be at the end of the tag. |
| _walkPastWhitespace(); |
| if (_thisChar == $rbrace && _nextChar == $rbrace) { |
| break; |
| } else { |
| return _TagParseResult.notTag; |
| } |
| } |
| if (_thisChar == $rbrace && _nextChar == $rbrace) { |
| endIndex = _index; |
| break; |
| } |
| _index++; |
| } |
| _index += 2; |
| |
| var key = template.substring(startIndex, endIndex); |
| var keySpan = _sourceFile.span(startIndex, endIndex); |
| return _TagParseResult.ok(Partial(key, |
| span: _sourceFile.span(tagStartIndex, _index), keySpan: keySpan)); |
| } |
| |
| /// 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, required int tagStartIndex}) { |
| var parsedKey = _parseKey(); |
| if (parsedKey.type == _KeyParseResultType.notKey) { |
| return _TagParseResult.notTag; |
| } else if (parsedKey == _KeyParseResult.endOfFile) { |
| return _TagParseResult.endOfFile; |
| } |
| |
| var children = _parseBlock(sectionKey: parsedKey.joinedNames); |
| var span = _sourceFile.span(tagStartIndex, _index); |
| var parsedKeySpan = parsedKey.span!; |
| |
| if (parsedKey.names.length > 1) { |
| // Desugar section with dots into nested sections. |
| // |
| // Given a multi-name section like |
| // `{{#one.two.three}}...{/one.two.three}}`, "one" must be a non-Iterable, |
| // non-bool value. "two" must also be a non-Iterable, non-bool value. |
| // "three" may be any kind of value, resulting in a repeated section, |
| // optional section, or value section. The [children], the parsed AST |
| // 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 lastName = parsedKey.names.last; |
| var keySpanEndOffset = parsedKeySpan.end.offset; |
| var lastNameSpan = _sourceFile.span( |
| keySpanEndOffset - lastName.length, keySpanEndOffset); |
| var section = Section([lastName], children, |
| invert: invert, span: span, keySpan: lastNameSpan); |
| for (var i = parsedKey.names.length - 2; i >= 0; i--) { |
| var sectionKey = parsedKey.names[i]; |
| // To find the start offset of the ith name, take the length of all of |
| // the names 0 through `i - 1` re-joined with '.', and a final '.' at |
| // the end. |
| var sectionKeyStartOffset = parsedKeySpan.start.offset + |
| (i == 0 ? 0 : parsedKey.names.take(i).join('.').length + 1); |
| var keySpan = _sourceFile.span( |
| sectionKeyStartOffset, sectionKeyStartOffset + sectionKey.length); |
| section = Section([sectionKey], [section], |
| invert: false, span: span, keySpan: keySpan); |
| } |
| return _TagParseResult.ok(section); |
| } |
| |
| return _TagParseResult.ok(Section(parsedKey.names, children, |
| invert: invert, span: span, keySpan: parsedKeySpan)); |
| } |
| |
| /// Tries to parse an end tag at [_index]. |
| /// |
| /// [_index] should be at the character immediately following the `/` |
| /// character which opens a possible end tag. |
| _TagParseResult _parseEndSection() { |
| var parsedKey = _parseKey(); |
| if (parsedKey.type == _KeyParseResultType.notKey) { |
| return _TagParseResult.notTag; |
| } else if (parsedKey == _KeyParseResult.endOfFile) { |
| return _TagParseResult.endOfFile; |
| } |
| |
| return _TagParseResult.endTag(parsedKey.joinedNames); |
| } |
| |
| /// Tries to parse a variable tag at [_index]. |
| /// |
| /// [_index] should be at the character immediately following the `{{` |
| /// characters which open a possible variable tag. |
| _TagParseResult _parseVariable({required int tagStartIndex}) { |
| var escape = true; |
| if (_thisChar == $lbrace) { |
| escape = false; |
| _index++; |
| _walkPastWhitespace(); |
| } |
| var parsedKey = _parseKey(escape: escape); |
| if (parsedKey.type == _KeyParseResultType.notKey) { |
| return _TagParseResult.notTag; |
| } else if (parsedKey == _KeyParseResult.endOfFile) { |
| return _TagParseResult.endOfFile; |
| } |
| |
| var span = _sourceFile.span(tagStartIndex, _index); |
| return _TagParseResult.ok(Variable(parsedKey.names, |
| escape: escape, span: span, keySpan: parsedKey.span!)); |
| } |
| |
| /// Tries to parse a key at [_index]. |
| /// |
| /// [_index] should be at the first character which opens a possible key. |
| _KeyParseResult _parseKey({bool escape = true}) { |
| var startIndex = _index; |
| while (true) { |
| if (_atEnd) { |
| return _KeyParseResult.endOfFile; |
| } |
| var char = _thisChar; |
| if ((char >= $a && char <= $z) || |
| (char >= $A && char <= $Z) || |
| (char >= $0 && char <= $9) || |
| char == $underscore || |
| char == $dot || |
| char == $dollar) { |
| _index++; |
| continue; |
| } else { |
| break; |
| } |
| } |
| |
| if (_index == startIndex) { |
| return _KeyParseResult.notKey; |
| } |
| |
| var key = template.substring(startIndex, _index); |
| var span = _sourceFile.span(startIndex, _index); |
| |
| if (key.length > 1 && |
| (key.codeUnitAt(0) == $dot || key.codeUnitAt(key.length - 1) == $dot)) { |
| // A key cannot start or end with dot. |
| return _KeyParseResult.notKey; |
| } |
| |
| _walkPastWhitespace(); |
| if (_nextAtEnd) { |
| return _KeyParseResult.endOfFile; |
| } |
| |
| var char0 = _thisChar; |
| var char1 = _nextChar; |
| if (escape) { |
| if (char0 == $rbrace && char1 == $rbrace) { |
| _index += 2; |
| return _KeyParseResult(_KeyParseResultType.parsedKey, key, span: span); |
| } else { |
| return _KeyParseResult.notKey; |
| } |
| } else { |
| if (_nextNextAtEnd) { |
| return _KeyParseResult.endOfFile; |
| } |
| // Need one more right brace. |
| var char2 = _nextNextChar; |
| if (char0 == $rbrace && char1 == $rbrace && char2 == $rbrace) { |
| _index += 3; |
| return _KeyParseResult(_KeyParseResultType.parsedKey, key, span: span); |
| } else { |
| return _KeyParseResult.notKey; |
| } |
| } |
| } |
| |
| /// Walks past any whitespace characters. |
| /// |
| /// [_index] points to a non-whitespace character when this method returns. |
| /// |
| /// If EOF is reached, then [_index] points past the end of the template |
| /// content when this method returns. |
| void _walkPastWhitespace() { |
| while (true) { |
| if (_atEnd) { |
| return; |
| } |
| if (_thisChar == $space || |
| _thisChar == $tab || |
| _thisChar == $lf || |
| _thisChar == $cr) { |
| _index++; |
| continue; |
| } else { |
| return; |
| } |
| } |
| } |
| |
| bool get _atEnd => _index >= _templateLength; |
| |
| bool get _nextAtEnd => _index + 1 >= _templateLength; |
| |
| bool get _nextNextAtEnd => _index + 2 >= _templateLength; |
| |
| int get _thisChar => template.codeUnitAt(_index); |
| |
| int get _nextChar => template.codeUnitAt(_index + 1); |
| |
| int get _nextNextChar => template.codeUnitAt(_index + 2); |
| } |
| |
| /// An interface for various types of node in a Mustache template. |
| sealed class MustachioNode { |
| SourceSpan get span; |
| } |
| |
| /// A [MustachioNode] with a multi-named key. |
| mixin HasMultiNamedKey { |
| List<String> get key; |
| |
| SourceSpan get keySpan; |
| } |
| |
| /// A Text node, representing literal text. |
| @immutable |
| final class Text implements MustachioNode { |
| final String content; |
| |
| @override |
| final SourceSpan span; |
| |
| Text(this.content, {required this.span}); |
| |
| @override |
| String toString() => 'Text["$content"]'; |
| } |
| |
| /// A Variable node, representing a variable to be resolved. |
| @immutable |
| final class Variable with HasMultiNamedKey implements MustachioNode { |
| @override |
| final List<String> key; |
| |
| final bool escape; |
| |
| @override |
| final SourceSpan span; |
| |
| @override |
| final SourceSpan keySpan; |
| |
| Variable(this.key, |
| {required this.escape, required this.span, required this.keySpan}); |
| |
| @override |
| String toString() => 'Variable[$key, escape=$escape]'; |
| } |
| |
| /// A Section node, representing either a Conditional Section, a Repeated |
| /// Section, or a Value Section, possibly inverted. |
| @immutable |
| final class Section with HasMultiNamedKey implements MustachioNode { |
| @override |
| final List<String> key; |
| |
| final bool invert; |
| |
| final List<MustachioNode> children; |
| |
| @override |
| final SourceSpan span; |
| |
| @override |
| final SourceSpan keySpan; |
| |
| Section(this.key, this.children, |
| {required this.invert, required this.span, required this.keySpan}); |
| |
| @override |
| String toString() => 'Section[$key, invert=$invert]'; |
| } |
| |
| /// A Partial node, representing a partial to be resolved. |
| @immutable |
| final class Partial implements MustachioNode { |
| final String key; |
| |
| @override |
| final SourceSpan span; |
| |
| final SourceSpan keySpan; |
| |
| Partial(this.key, {required this.span, required this.keySpan}); |
| } |
| |
| /// An enumeration of types of tag parse results. |
| enum _TagParseResultType { |
| commentTag, |
| endOfFile, |
| notTag, |
| parsedTag, |
| parsedEndTag, |
| } |
| |
| /// The result of attempting to parse a Mustache tag. |
| @immutable |
| class _TagParseResult { |
| final _TagParseResultType type; |
| |
| /// The parsed Mustache node, if parsing was successful. |
| /// |
| /// This field is `null` if EOF was reached, or if a tag was not parsed |
| /// (perhaps it started out like a tag, but was malformed, and so is read as |
| /// text). |
| final MustachioNode? node; |
| |
| /// The key of an end tag, if an end tag was parsed. |
| final String? endTagKey; |
| |
| _TagParseResult(this.type, this.node, this.endTagKey); |
| |
| _TagParseResult.ok(this.node) |
| : type = _TagParseResultType.parsedTag, |
| endTagKey = null; |
| |
| _TagParseResult.endTag(this.endTagKey) |
| : type = _TagParseResultType.parsedEndTag, |
| node = null; |
| |
| /// A [_TagParseResult] representing that EOF was reached, without parsing a |
| /// tag. |
| static final _TagParseResult endOfFile = |
| _TagParseResult(_TagParseResultType.endOfFile, null, null); |
| |
| /// A [_TagParseResult] representing that a tag was not parsed. |
| static final _TagParseResult notTag = |
| _TagParseResult(_TagParseResultType.notTag, null, null); |
| |
| /// A [_TagParseResult] representing that a comment tag was parsed. |
| static final _TagParseResult commentTag = |
| _TagParseResult(_TagParseResultType.commentTag, null, null); |
| } |
| |
| /// An enumeration of types of key parse results. |
| enum _KeyParseResultType { |
| parsedKey, |
| notKey, |
| endOfFile, |
| } |
| |
| /// The result of attempting to parse a Mustache key. |
| @immutable |
| class _KeyParseResult { |
| final _KeyParseResultType type; |
| |
| final List<String> names; |
| |
| /// The source span from where this key was parsed, if this represents a |
| /// parsed key, otherwise `null`. |
| final SourceSpan? span; |
| |
| const _KeyParseResult._(this.type, this.names, {this.span}); |
| |
| factory _KeyParseResult(_KeyParseResultType type, String key, |
| {required SourceSpan span}) { |
| if (key == '.') { |
| return _KeyParseResult._(type, [key], span: span); |
| } else { |
| return _KeyParseResult._(type, key.split('.'), span: span); |
| } |
| } |
| |
| /// A [_KeyParseResult] representing that EOF was reached, without parsing a |
| /// key. |
| static const _KeyParseResult endOfFile = |
| _KeyParseResult._(_KeyParseResultType.endOfFile, []); |
| |
| /// A [_KeyParseResult] representing that a key was not parsed. |
| static const _KeyParseResult notKey = |
| _KeyParseResult._(_KeyParseResultType.notKey, []); |
| |
| /// The reconstituted key, with periods separating names. |
| String get joinedNames => names.join('.'); |
| |
| String get lastName => names.last; |
| } |