[mustache_template] Initial import (#9944)
The `mustache_template` package has been transferred to the `flutter.dev` publisher, so this imports it into our repository and updates it to follow repository conventions:
- Fix/suppress analysis warnings
- Some via `dart fix`, some manual
- Add repo-level metadata
- Update license check command to recognize this package
- Add a temporary code-excerpt exclusion
- Update pubspec.yaml metadata
- Bump version to 2.0.1 to release the changes
`mustache_template` was itself a fork by a Flutter team member of the third-party `mustache` package, so the import is into `third_party/packages/`, updating the repo license check to recognize the license of this package.
The original code is quite old, and as a result still used a lot of implicit dynamic typing. Some of that that has been addressed in this import, but the trickier aspects have been left for future work, as converting from implicit dynamic typing to specified types has the potential for runtime regressions. (It's possible that even the more minor changes made here will cause regressions that will need fast-follow, but all the tests still pass, and the manual `test/all.dart` tests do not have any failures that were not present before making changes to the imported code).
Issues filed for future follow-up work:
https://github.com/flutter/flutter/issues/174721
https://github.com/flutter/flutter/issues/174722
https://github.com/flutter/flutter/issues/174742
## Pre-Review Checklist
[^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
diff --git a/CODEOWNERS b/CODEOWNERS
index 5eed388..664bd3d 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -46,6 +46,7 @@
third_party/packages/cupertino_icons/test/goldens/** @LongCatIsLooong
third_party/packages/flutter_svg/** @domesticmouse
third_party/packages/flutter_svg_test/** @domesticmouse
+third_party/packages/mustache_template/** @bkonyi @parlough
third_party/packages/path_parsing/** @domesticmouse
# Plugin platform implementation rules. These should stay last, since the last
diff --git a/README.md b/README.md
index a481acc..412f66a 100644
--- a/README.md
+++ b/README.md
@@ -67,6 +67,7 @@
| [plugin\_platform\_interface](./packages/plugin_platform_interface/) | [](https://pub.dev/packages/plugin_platform_interface) | [](https://pub.dev/packages/plugin_platform_interface/score) | [](https://pub.dev/packages/plugin_platform_interface/score) | [](https://github.com/flutter/flutter/labels/p%3A%20plugin_platform_interface) | [](https://github.com/flutter/packages/labels/p%3A%20plugin_platform_interface) |
| [quick\_actions](./packages/quick_actions/) | [](https://pub.dev/packages/quick_actions) | [](https://pub.dev/packages/quick_actions/score) | [](https://pub.dev/packages/quick_actions/score) | [](https://github.com/flutter/flutter/labels/p%3A%20quick_actions) | [](https://github.com/flutter/packages/labels/p%3A%20quick_actions) |
| [google\_identity\_services\_web](./packages/google_identity_services_web/) | [](https://pub.dev/packages/google_identity_services_web) | [](https://pub.dev/packages/google_identity_services_web/score) | [](https://pub.dev/packages/google_identity_services_web/score) | [](https://github.com/flutter/flutter/labels/p%3A%20google_identity_services_web) | [](https://github.com/flutter/packages/labels/p%3A%20google_identity_services_web) |
+| [mustache\_template](./third_party/packages/mustache_template/) | [](https://pub.dev/packages/mustache_template) | [](https://pub.dev/packages/mustache_template/score) | [](https://pub.dev/packages/mustache_template/score) | [](https://github.com/flutter/flutter/labels/p%3A%20mustache_template) | [](https://github.com/flutter/packages/labels/p%3A%20mustache_template) |
| [rfw](./packages/rfw/) | [](https://pub.dev/packages/rfw) | [](https://pub.dev/packages/rfw/score) | [](https://pub.dev/packages/rfw/score) | [](https://github.com/flutter/flutter/labels/p%3A%20rfw) | [](https://github.com/flutter/packages/labels/p%3A%20rfw) |
| [shared\_preferences](./packages/shared_preferences/) | [](https://pub.dev/packages/shared_preferences) | [](https://pub.dev/packages/shared_preferences/score) | [](https://pub.dev/packages/shared_preferences/score) | [](https://github.com/flutter/flutter/labels/p%3A%20shared_preferences) | [](https://github.com/flutter/packages/labels/p%3A%20shared_preferences) |
| [standard\_message\_codec](./packages/standard_message_codec/) | [](https://pub.dev/packages/standard_message_codec) | [](https://pub.dev/packages/standard_message_codec/score) | [](https://pub.dev/packages/standard_message_codec/score) | [](https://github.com/flutter/flutter/labels/p%3A%20standard_message_codec) | [](https://github.com/flutter/packages/labels/p%3A%20standard_message_codec) |
diff --git a/script/configs/temp_exclude_excerpt.yaml b/script/configs/temp_exclude_excerpt.yaml
index 3d39abc..2dd229b 100644
--- a/script/configs/temp_exclude_excerpt.yaml
+++ b/script/configs/temp_exclude_excerpt.yaml
@@ -7,5 +7,6 @@
# https://github.com/flutter/flutter/issues/102679
- espresso
- in_app_purchase/in_app_purchase
+- mustache_template
- pointer_interceptor
- quick_actions/quick_actions
diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart
index cc0bfb8..9ebeb61 100644
--- a/script/tool/lib/src/license_check_command.dart
+++ b/script/tool/lib/src/license_check_command.dart
@@ -53,6 +53,7 @@
// the package-level LICENSE file. Each entry must be a directory relative to
// third_party/packages, as that is the only directory where this is allowed.
const Set<String> _unannotatedFileThirdPartyDirectories = <String>{
+ 'mustache_template',
'path_parsing',
'flutter_svg',
'flutter_svg_test',
@@ -85,6 +86,13 @@
r'// Use of this source code is governed by a BSD-style license that can be\n'
r'// found in the LICENSE file\.\n',
),
+ // packages/third_party/mustache_template.
+ RegExp(
+ r'Copyright \(c\) 2013, Greg Lowe\n'
+ r'All rights reserved.\n\n'
+ r'Redistribution and use in source and binary forms, with or without '
+ r'modification, are permitted provided that the following conditions are met:\n',
+ ),
// packages/third_party/path_parsing.
RegExp(
r'Copyright \(c\) 2018 Dan Field\n\n'
diff --git a/third_party/packages/mustache_template/CHANGELOG.md b/third_party/packages/mustache_template/CHANGELOG.md
new file mode 100644
index 0000000..fd1da85
--- /dev/null
+++ b/third_party/packages/mustache_template/CHANGELOG.md
@@ -0,0 +1,71 @@
+## 2.0.1
+
+* Transfers the package source from https://github.com/jonahwilliams/mustache
+ to https://github.com/flutter/packages.
+* Updates minimum supported SDK version to Dart 3.7.
+* Updates code for new analysis options.
+
+## 2.0.0
+
+* Support for null safe dart added.
+
+## 1.0.0+1
+
+* Fixed regression where lookups from list did not work. Removed failing tests
+ that depend on reflection.
+
+## 1.0.0
+
+* Forked from original repo. Support for mirrors removed.
+
+## Fork
+
+## 1.1.1
+
+* Fixed error "boolean expression must not be null". Thanks Nico.
+
+## 1.1.0
+
+* Better support for class members in sections. Thanks to Janice Collins.
+* Set the SDK constraint to Dart 2+.
+
+## 1.0.2
+ Set the max SDK constraint to <3.0.0.
+
+## 0.2.5
+
+* Remove MustacheFormatException
+* Allow templates to specify default delimiters. Thanks to Joris Hermans.
+* Fix #24: renderString shrinks multiple newlines to just one (Thanks to John Ryan for the repro).
+
+## 0.2.4
+
+* Fix #23 failure if tag or comment contains "="
+
+## 0.2.3
+
+* Change handling of lenient sections to match python mustache implementation.
+
+## 0.2.2
+
+* Fix MirrorsUsed tag for using mirrors on dart2js.
+* Clean up dead code.
+
+## 0.2.1
+
+* Added new methods to LambdaContext.
+
+## 0.2
+
+* Deprecated parse() function - please update your code to use new Template(source).
+* Deprecated MustacheFormatException - please update your code to use TemplateException.
+* Breaking change: Template.render and Template.renderString methods no longer
+ take the optional lenient and htmlEscapeValues. These should now be passed to
+ the Template constructor.
+* Fully passing all mustache spec tests.
+* Added support for MirrorsUsed.
+* Implemented partials. #11
+* Implemented lambdas. #4
+* Implemented change delimiter tag.
+* Add template name parameter, and show this in error messages.
+* Allow whitespace at begining of tags. #10
diff --git a/third_party/packages/mustache_template/LICENSE b/third_party/packages/mustache_template/LICENSE
new file mode 100644
index 0000000..6bb39e4
--- /dev/null
+++ b/third_party/packages/mustache_template/LICENSE
@@ -0,0 +1,8 @@
+Copyright (c) 2013, Greg Lowe
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/third_party/packages/mustache_template/METADATA b/third_party/packages/mustache_template/METADATA
new file mode 100644
index 0000000..5025060
--- /dev/null
+++ b/third_party/packages/mustache_template/METADATA
@@ -0,0 +1,15 @@
+name: "mustache_template"
+description:
+ "Mustache template Dart library"
+
+third_party {
+ identifier {
+ type: "Git"
+ value: "https://github.com/jonahwilliams/mustache"
+ primary_source: true
+ version: "c4344e0dd45f6605758eb11aa4837859e2c055f0"
+ }
+ version: "c4344e0dd45f6605758eb11aa4837859e2c055f0"
+ last_upgrade_date { year: 2025 month: 9 day: 3 }
+ license_type: NOTICE
+}
diff --git a/third_party/packages/mustache_template/README.md b/third_party/packages/mustache_template/README.md
new file mode 100644
index 0000000..7602c27
--- /dev/null
+++ b/third_party/packages/mustache_template/README.md
@@ -0,0 +1,117 @@
+# Mustache templates
+
+A Dart library to parse and render [mustache templates](https://mustache.github.io/).
+
+See the [mustache manual](http://mustache.github.com/mustache.5.html) for detailed usage information.
+
+This library passes all [mustache specification](https://github.com/mustache/spec/tree/master/specs) tests.
+
+## Example usage
+```dart
+import 'package:mustache_template/mustache_template.dart';
+
+main() {
+ var source = '''
+ {{# names }}
+ <div>{{ lastname }}, {{ firstname }}</div>
+ {{/ names }}
+ {{^ names }}
+ <div>No names.</div>
+ {{/ names }}
+ {{! I am a comment. }}
+ ''';
+
+ var template = Template(source, name: 'template-filename.html');
+
+ var output = template.renderString({'names': [
+ {'firstname': 'Greg', 'lastname': 'Lowe'},
+ {'firstname': 'Bob', 'lastname': 'Johnson'}
+ ]});
+
+ print(output);
+}
+```
+
+A template is parsed when it is created, after parsing it can be rendered any number of times with different values. A TemplateException is thrown if there is a problem parsing or rendering the template.
+
+The Template contstructor allows passing a name, this name will be used in error messages. When working with a number of templates, it is important to pass a name so that the error messages specify which template caused the error.
+
+By default all output from `{{variable}}` tags is html escaped, this behaviour can be changed by passing htmlEscapeValues : false to the Template constructor. You can also use a `{{{triple mustache}}}` tag, or a unescaped variable tag `{{&unescaped}}`, the output from these tags is not escaped.
+
+## Differences between strict mode and lenient mode.
+
+### Strict mode (default)
+
+* Tag names may only contain the characters a-z, A-Z, 0-9, underscore, period and minus. Other characters in tags will cause a TemplateException to be thrown during parsing.
+
+* During rendering, if no map key or object member which matches the tag name is found, then a TemplateException will be thrown.
+
+### Lenient mode
+
+* Tag names may use any characters.
+* During rendering, if no map key or object member which matches the tag name is found, then silently ignore and output nothing.
+
+## Nested paths
+
+```dart
+ var t = Template('{{ author.name }}');
+ var output = template.renderString({'author': {'name': 'Greg Lowe'}});
+```
+
+## Partials - example usage
+
+```dart
+
+var partial = Template('{{ foo }}', name: 'partial');
+
+var resolver = (String name) {
+ if (name == 'partial-name') { // Name of partial tag.
+ return partial;
+ }
+};
+
+var t = Template('{{> partial-name }}', partialResolver: resolver);
+
+var output = t.renderString({'foo': 'bar'}); // bar
+
+```
+
+## Lambdas - example usage
+
+```dart
+var t = Template('{{# foo }}');
+var lambda = (_) => 'bar';
+t.renderString({'foo': lambda}); // bar
+```
+
+```dart
+var t = Template('{{# foo }}hidden{{/ foo }}');
+var lambda = (_) => 'shown';
+t.renderString('foo': lambda); // shown
+```
+
+```dart
+var t = Template('{{# foo }}oi{{/ foo }}');
+var lambda = (LambdaContext ctx) => '<b>${ctx.renderString().toUpperCase()}</b>';
+t.renderString({'foo': lambda}); // <b>OI</b>
+```
+
+```dart
+var t = Template('{{# foo }}{{bar}}{{/ foo }}');
+var lambda = (LambdaContext ctx) => '<b>${ctx.renderString().toUpperCase()}</b>';
+t.renderString({'foo': lambda, 'bar': 'pub'}); // <b>PUB</b>
+```
+
+```dart
+var t = Template('{{# foo }}{{bar}}{{/ foo }}');
+var lambda = (LambdaContext ctx) => '<b>${ctx.renderString().toUpperCase()}</b>';
+t.renderString({'foo': lambda, 'bar': 'pub'}); // <b>PUB</b>
+```
+
+In the following example `LambdaContext.renderSource(source)` re-parses the source string in the current context, this is the default behaviour in many mustache implementations. Since re-parsing the content is slow, and often not required, this library makes this step optional.
+
+```dart
+var t = Template('{{# foo }}{{bar}}{{/ foo }}');
+var lambda = (LambdaContext ctx) => ctx.renderSource(ctx.source + ' {{cmd}}');
+t.renderString({'foo': lambda, 'bar': 'pub', 'cmd': 'build'}); // pub build
+```
diff --git a/third_party/packages/mustache_template/dart_test.yaml b/third_party/packages/mustache_template/dart_test.yaml
new file mode 100644
index 0000000..cdb656d
--- /dev/null
+++ b/third_party/packages/mustache_template/dart_test.yaml
@@ -0,0 +1,6 @@
+# See https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#arguments
+override_platforms:
+ chrome:
+ settings:
+ executable: chrome
+ arguments: --no-sandbox
diff --git a/third_party/packages/mustache_template/lib/mustache.dart b/third_party/packages/mustache_template/lib/mustache.dart
new file mode 100644
index 0000000..678f5a0
--- /dev/null
+++ b/third_party/packages/mustache_template/lib/mustache.dart
@@ -0,0 +1,98 @@
+import 'src/template.dart' as t;
+
+/// A Template can be efficiently rendered multiple times with different
+/// values.
+abstract class Template {
+ /// The constructor parses the template source and throws [TemplateException]
+ /// if the syntax of the source is invalid.
+ /// Tag names may only contain characters a-z, A-Z, 0-9, underscore, and minus,
+ /// unless lenient mode is specified.
+ factory Template(
+ String source, {
+ bool lenient,
+ bool htmlEscapeValues,
+ String name,
+ PartialResolver? partialResolver,
+ String delimiters,
+ }) = t.Template.fromSource;
+
+ /// An optional name used to identify the template in error logging.
+ String? get name;
+
+ /// The template that should be filled when calling [render] or
+ /// [renderString].
+ String get source;
+
+ /// [values] can be a combination of Map, List, String. Any non-String object
+ /// will be converted using toString(). Null values will cause a
+ /// [TemplateException], unless lenient module is enabled.
+ String renderString(Object? values);
+
+ /// [values] can be a combination of Map, List, String. Any non-String object
+ /// will be converted using toString(). Null values will cause a
+ /// [TemplateException], unless lenient module is enabled.
+ void render(Object? values, StringSink sink);
+}
+
+// TODO(stuartmorgan): Remove this. See https://github.com/flutter/flutter/issues/174722.
+// ignore: public_member_api_docs
+typedef PartialResolver = Template? Function(String);
+
+// TODO(stuartmorgan): Remove this. See https://github.com/flutter/flutter/issues/174722.
+// ignore: public_member_api_docs
+typedef LambdaFunction = Object Function(LambdaContext context);
+
+/// Passed as an argument to a mustache lambda function. The methods on
+/// this object may only be called before the lambda function returns. If a
+/// method is called after it has returned an exception will be thrown.
+abstract class LambdaContext {
+ /// Render the current section tag in the current context and return the
+ /// result as a string. If provided, value will be added to the top of the
+ /// context's stack.
+ String renderString({Object? value});
+
+ /// Render and directly output the current section tag. If provided, value
+ /// will be added to the top of the context's stack.
+ void render({Object value});
+
+ /// Output a string. The output will not be html escaped, and will be written
+ /// before the output returned from the lambda.
+ void write(Object object);
+
+ /// Get the unevaluated template source for the current section tag.
+ String get source;
+
+ /// Evaluate the string as a mustache template using the current context. If
+ /// provided, value will be added to the top of the context's stack.
+ String renderSource(String source, {Object? value});
+
+ /// Lookup the value of a variable in the current context.
+ Object? lookup(String variableName);
+}
+
+/// [TemplateException] is used to obtain the line and column numbers
+/// of the token which caused parse or render to fail.
+abstract class TemplateException implements Exception {
+ /// A message describing the problem parsing or rendering the template.
+ String get message;
+
+ /// The name used to identify the template, as passed to the Template
+ /// constructor.
+ String? get templateName;
+
+ /// The 1-based line number of the token where formatting error was found.
+ int get line;
+
+ /// The 1-based column number of the token where formatting error was found.
+ int get column;
+
+ /// The character offset within the template source.
+ int? get offset;
+
+ /// The template source.
+ String? get source;
+
+ /// A short source substring of the source at the point the problem occurred
+ /// with parsing or rendering.
+ String get context;
+}
diff --git a/third_party/packages/mustache_template/lib/mustache_template.dart b/third_party/packages/mustache_template/lib/mustache_template.dart
new file mode 100644
index 0000000..299de3a
--- /dev/null
+++ b/third_party/packages/mustache_template/lib/mustache_template.dart
@@ -0,0 +1 @@
+export 'mustache.dart';
diff --git a/third_party/packages/mustache_template/lib/src/lambda_context.dart b/third_party/packages/mustache_template/lib/src/lambda_context.dart
new file mode 100644
index 0000000..1dcd568
--- /dev/null
+++ b/third_party/packages/mustache_template/lib/src/lambda_context.dart
@@ -0,0 +1,138 @@
+// TODO(stuartmorgan): Remove this. See https://github.com/flutter/flutter/issues/174722.
+// ignore_for_file: public_member_api_docs
+
+import '../mustache.dart' as m;
+
+import 'node.dart';
+import 'parser.dart' as parser;
+import 'renderer.dart';
+import 'template_exception.dart';
+
+/// Passed as an argument to a mustache lambda function.
+class LambdaContext implements m.LambdaContext {
+ LambdaContext(this._node, this._renderer);
+ final Node _node;
+ final Renderer _renderer;
+ bool _closed = false;
+
+ void close() {
+ _closed = true;
+ }
+
+ void _checkClosed() {
+ if (_closed) {
+ throw _error('LambdaContext accessed outside of callback.');
+ }
+ }
+
+ TemplateException _error(String msg) {
+ return TemplateException(
+ msg,
+ _renderer.templateName,
+ _renderer.source,
+ _node.start,
+ );
+ }
+
+ @override
+ String renderString({Object? value}) {
+ _checkClosed();
+ if (_node is! SectionNode) {
+ // TODO(stuartmorgan): Fix the lack of `throw` here, which looks like a
+ // bug in the original code.
+ _error(
+ 'LambdaContext.renderString() can only be called on section tags.',
+ );
+ }
+ final StringBuffer sink = StringBuffer();
+ _renderSubtree(sink, value);
+ return sink.toString();
+ }
+
+ void _renderSubtree(StringSink sink, Object? value) {
+ final Renderer renderer = Renderer.subtree(_renderer, sink);
+ final SectionNode section = _node as SectionNode;
+ if (value != null) {
+ renderer.push(value);
+ }
+ renderer.render(section.children);
+ }
+
+ @override
+ void render({Object? value}) {
+ _checkClosed();
+ if (_node is! SectionNode) {
+ // TODO(stuartmorgan): Fix the lack of `throw` here, which looks like a
+ // bug in the original code.
+ _error('LambdaContext.render() can only be called on section tags.');
+ }
+ _renderSubtree(_renderer.sink, value);
+ }
+
+ @override
+ void write(Object object) {
+ _checkClosed();
+ _renderer.write(object);
+ }
+
+ @override
+ String get source {
+ _checkClosed();
+
+ if (_node is! SectionNode) {
+ return '';
+ }
+
+ final SectionNode node = _node;
+ final List<Node> nodes = node.children;
+ if (nodes.isEmpty) {
+ return '';
+ }
+
+ if (nodes.length == 1 && nodes.first is TextNode) {
+ return (nodes.single as TextNode).text;
+ }
+
+ return _renderer.source.substring(node.contentStart, node.contentEnd);
+ }
+
+ @override
+ String renderSource(String source, {Object? value}) {
+ _checkClosed();
+ final StringBuffer sink = StringBuffer();
+
+ // Lambdas used for sections should parse with the current delimiters.
+ String delimiters = '{{ }}';
+ if (_node is SectionNode) {
+ final SectionNode node = _node;
+ delimiters = node.delimiters;
+ }
+
+ final List<Node> nodes = parser.parse(
+ source,
+ _renderer.lenient,
+ _renderer.templateName,
+ delimiters,
+ );
+
+ final Renderer renderer = Renderer.lambda(
+ _renderer,
+ source,
+ _renderer.indent,
+ sink,
+ );
+
+ if (value != null) {
+ renderer.push(value);
+ }
+ renderer.render(nodes);
+
+ return sink.toString();
+ }
+
+ @override
+ Object? lookup(String variableName) {
+ _checkClosed();
+ return _renderer.resolveValue(variableName);
+ }
+}
diff --git a/third_party/packages/mustache_template/lib/src/node.dart b/third_party/packages/mustache_template/lib/src/node.dart
new file mode 100644
index 0000000..bc06b57
--- /dev/null
+++ b/third_party/packages/mustache_template/lib/src/node.dart
@@ -0,0 +1,100 @@
+// TODO(stuartmorgan): Remove this. See https://github.com/flutter/flutter/issues/174722.
+// ignore_for_file: public_member_api_docs
+
+abstract class Node {
+ Node(this.start, this.end);
+
+ // The offset of the start of the token in the file. Unless this is a section
+ // or inverse section, then this stores the start of the content of the
+ // section.
+ final int start;
+ final int end;
+
+ void accept(Visitor visitor);
+ void visitChildren(Visitor visitor) {}
+}
+
+abstract class Visitor {
+ void visitText(TextNode node);
+ void visitVariable(VariableNode node);
+ void visitSection(SectionNode node);
+ void visitPartial(PartialNode node);
+}
+
+class TextNode extends Node {
+ TextNode(this.text, int start, int end) : super(start, end);
+
+ final String text;
+
+ @override
+ String toString() => '(TextNode "$_debugText" $start $end)';
+
+ String get _debugText {
+ final String t = text.replaceAll('\n', r'\n');
+ return t.length < 50 ? t : '${t.substring(0, 48)}...';
+ }
+
+ @override
+ void accept(Visitor visitor) => visitor.visitText(this);
+}
+
+class VariableNode extends Node {
+ VariableNode(this.name, int start, int end, {this.escape = true})
+ : super(start, end);
+
+ final String name;
+ final bool escape;
+
+ @override
+ void accept(Visitor visitor) => visitor.visitVariable(this);
+
+ @override
+ String toString() => '(VariableNode "$name" escape: $escape $start $end)';
+}
+
+class SectionNode extends Node {
+ SectionNode(
+ this.name,
+ int start,
+ int end,
+ this.delimiters, {
+ this.inverse = false,
+ }) : contentStart = end,
+ super(start, end);
+
+ final String name;
+ final String delimiters;
+ final bool inverse;
+ final int contentStart;
+ int? contentEnd; // Set in parser when close tag is parsed.
+ final List<Node> children = <Node>[];
+
+ @override
+ void accept(Visitor visitor) => visitor.visitSection(this);
+
+ @override
+ void visitChildren(Visitor visitor) {
+ for (final Node node in children) {
+ node.accept(visitor);
+ }
+ }
+
+ @override
+ String toString() => '(SectionNode $name inverse: $inverse $start $end)';
+}
+
+class PartialNode extends Node {
+ PartialNode(this.name, int start, int end, this.indent) : super(start, end);
+
+ final String name;
+
+ // Used to store the preceding whitespace before a partial tag, so that
+ // it's content can be correctly indented.
+ final String indent;
+
+ @override
+ void accept(Visitor visitor) => visitor.visitPartial(this);
+
+ @override
+ String toString() => '(PartialNode $name $start $end "$indent")';
+}
diff --git a/third_party/packages/mustache_template/lib/src/parser.dart b/third_party/packages/mustache_template/lib/src/parser.dart
new file mode 100644
index 0000000..b3d4f4b
--- /dev/null
+++ b/third_party/packages/mustache_template/lib/src/parser.dart
@@ -0,0 +1,428 @@
+// TODO(stuartmorgan): Remove this. See https://github.com/flutter/flutter/issues/174722.
+// ignore_for_file: public_member_api_docs
+
+import 'node.dart';
+import 'scanner.dart';
+import 'template_exception.dart';
+import 'token.dart';
+
+List<Node> parse(
+ String source,
+ bool lenient,
+ String? templateName,
+ String delimiters,
+) {
+ final Parser parser = Parser(
+ source,
+ templateName,
+ delimiters,
+ lenient: lenient,
+ );
+ return parser.parse();
+}
+
+class Tag {
+ Tag(this.type, this.name, this.start, this.end);
+ final TagType type;
+ final String name;
+ final int start;
+ final int end;
+}
+
+class TagType {
+ const TagType(this.name);
+ final String name;
+
+ static const TagType openSection = TagType('openSection');
+ static const TagType openInverseSection = TagType('openInverseSection');
+ static const TagType closeSection = TagType('closeSection');
+ static const TagType variable = TagType('variable');
+ static const TagType tripleMustache = TagType('tripleMustache');
+ static const TagType unescapedVariable = TagType('unescapedVariable');
+ static const TagType partial = TagType('partial');
+ static const TagType comment = TagType('comment');
+ static const TagType changeDelimiter = TagType('changeDelimiter');
+}
+
+class Parser {
+ Parser(
+ String source,
+ String? templateName,
+ String delimiters, {
+ bool lenient = false,
+ }) : _source = source,
+ _templateName = templateName,
+ _delimiters = delimiters,
+ _lenient = lenient,
+ _scanner = Scanner(source, templateName, delimiters);
+
+ final String _source;
+ final bool _lenient;
+ final String? _templateName;
+ final String _delimiters;
+ final Scanner _scanner;
+ final List<SectionNode> _stack = <SectionNode>[];
+ late List<Token> _tokens;
+ late String _currentDelimiters;
+ int _offset = 0;
+
+ List<Node> parse() {
+ _tokens = _scanner.scan();
+ _currentDelimiters = _delimiters;
+ _stack.clear();
+ _stack.add(SectionNode('root', 0, 0, _delimiters));
+
+ // Handle a standalone tag on first line, including special case where the
+ // first line is empty.
+ final Token? lineEnd = _readIf(TokenType.lineEnd, eofOk: true);
+ if (lineEnd != null) {
+ _appendTextToken(lineEnd);
+ }
+ _parseLine();
+
+ for (Token? token = _peek(); token != null; token = _peek()) {
+ switch (token.type) {
+ case TokenType.text:
+ case TokenType.whitespace:
+ _read();
+ _appendTextToken(token);
+
+ case TokenType.openDelimiter:
+ final Tag? tag = _readTag();
+ final Node? node = _createNodeFromTag(tag);
+ if (tag != null) {
+ _appendTag(tag, node);
+ }
+
+ case TokenType.changeDelimiter:
+ _read();
+ _currentDelimiters = token.value;
+
+ case TokenType.lineEnd:
+ _appendTextToken(_read()!);
+ _parseLine();
+
+ default:
+ // TODO(stuartmorgan): Convert to StateError.
+ throw Exception('Unreachable code.');
+ }
+ }
+
+ if (_stack.length != 1) {
+ throw TemplateException(
+ "Unclosed tag: '${_stack.last.name}'.",
+ _templateName,
+ _source,
+ _stack.last.start,
+ );
+ }
+
+ return _stack.last.children;
+ }
+
+ // Returns null on EOF.
+ Token? _peek() => _offset < _tokens.length ? _tokens[_offset] : null;
+
+ // Returns null on EOF.
+ Token? _read() {
+ Token? t;
+ if (_offset < _tokens.length) {
+ t = _tokens[_offset];
+ _offset++;
+ }
+ return t;
+ }
+
+ Token _expect(TokenType type) {
+ final Token? token = _read();
+ if (token == null) {
+ throw _errorEof();
+ }
+ if (token.type != type) {
+ throw _error('Expected: $type found: ${token.type}.', _offset);
+ }
+ return token;
+ }
+
+ Token? _readIf(TokenType type, {bool eofOk = false}) {
+ final Token? token = _peek();
+ if (!eofOk && token == null) {
+ throw _errorEof();
+ }
+ return token != null && token.type == type ? _read() : null;
+ }
+
+ TemplateException _errorEof() =>
+ _error('Unexpected end of input.', _source.length - 1);
+
+ TemplateException _error(String msg, int offset) =>
+ TemplateException(msg, _templateName, _source, offset);
+
+ // Add a text node to top most section on the stack and merge consecutive
+ // text nodes together.
+ void _appendTextToken(Token token) {
+ assert(
+ const <TokenType>[
+ TokenType.text,
+ TokenType.lineEnd,
+ TokenType.whitespace,
+ ].contains(token.type),
+ );
+ final List<Node> children = _stack.last.children;
+ if (children.isEmpty || children.last is! TextNode) {
+ children.add(TextNode(token.value, token.start, token.end));
+ } else {
+ final TextNode last = children.removeLast() as TextNode;
+ final TextNode node = TextNode(
+ last.text + token.value,
+ last.start,
+ token.end,
+ );
+ children.add(node);
+ }
+ }
+
+ // Add the node to top most section on the stack. If a section node then
+ // push it onto the stack, if a close section tag, then pop the stack.
+ void _appendTag(Tag tag, Node? node) {
+ switch (tag.type) {
+ // {{#...}} {{^...}}
+ case TagType.openSection:
+ case TagType.openInverseSection:
+ _stack.last.children.add(node!);
+ _stack.add(node as SectionNode);
+
+ // {{/...}}
+ case TagType.closeSection:
+ if (tag.name != _stack.last.name) {
+ throw TemplateException(
+ 'Mismatched tag, expected: '
+ "'${_stack.last.name}', was: '${tag.name}'",
+ _templateName,
+ _source,
+ tag.start,
+ );
+ }
+ final SectionNode node = _stack.removeLast();
+ node.contentEnd = tag.start;
+
+ // {{...}} {{&...}} {{{...}}}
+ case TagType.variable:
+ case TagType.unescapedVariable:
+ case TagType.tripleMustache:
+ case TagType.partial:
+ if (node != null) {
+ _stack.last.children.add(node);
+ }
+
+ case TagType.comment:
+ case TagType.changeDelimiter:
+ // Ignore.
+ break;
+
+ default:
+ // TODO(stuartmorgan): Convert to StateError.
+ throw Exception('Unreachable code.');
+ }
+ }
+
+ // Handle standalone tags and indented partials.
+ //
+ // A "standalone tag" in the spec is a tag one a line where the line only
+ // contains whitespace. During rendering the whitespace is omitted.
+ // Standalone partials also indent their content to match the tag during
+ // rendering.
+
+ // match:
+ // lineEnd whitespace openDelimiter any* closeDelimiter whitespace lineEnd
+ //
+ // Where lineEnd can also mean start/end of the source.
+ void _parseLine() {
+ // If first token is a newline append it.
+ final Token? t = _peek();
+ if (t != null && t.type == TokenType.lineEnd) {
+ _appendTextToken(t);
+ }
+
+ // Continue parsing standalone lines until we find one than isn't a
+ // standalone line.
+ while (_peek() != null) {
+ _readIf(TokenType.lineEnd, eofOk: true);
+ final Token? precedingWhitespace = _readIf(
+ TokenType.whitespace,
+ eofOk: true,
+ );
+ final String indent =
+ precedingWhitespace == null ? '' : precedingWhitespace.value;
+ final Tag? tag = _readTag();
+ final Node? tagNode = _createNodeFromTag(tag, partialIndent: indent);
+ final Token? followingWhitespace = _readIf(
+ TokenType.whitespace,
+ eofOk: true,
+ );
+
+ const List<TagType> standaloneTypes = <TagType>[
+ TagType.openSection,
+ TagType.closeSection,
+ TagType.openInverseSection,
+ TagType.partial,
+ TagType.comment,
+ TagType.changeDelimiter,
+ ];
+
+ if (tag != null &&
+ (_peek() == null || _peek()!.type == TokenType.lineEnd) &&
+ standaloneTypes.contains(tag.type)) {
+ // This is a tag on a "standalone line", so do not create text nodes
+ // for whitespace, or the following newline.
+ _appendTag(tag, tagNode);
+ // Now continue to loop and parse the next line.
+ } else {
+ // This is not a standalone line so add the whitespace to the ast.
+ if (precedingWhitespace != null) {
+ _appendTextToken(precedingWhitespace);
+ }
+ if (tag != null) {
+ _appendTag(tag, tagNode);
+ }
+ if (followingWhitespace != null) {
+ _appendTextToken(followingWhitespace);
+ }
+ // Done parsing standalone lines. Exit the loop.
+ break;
+ }
+ }
+ }
+
+ final RegExp _validIdentifier = RegExp(r'^[0-9a-zA-Z\_\-\.]+$');
+
+ static const Map<String, TagType> _tagTypeMap = <String, TagType>{
+ '#': TagType.openSection,
+ '^': TagType.openInverseSection,
+ '/': TagType.closeSection,
+ '&': TagType.unescapedVariable,
+ '>': TagType.partial,
+ '!': TagType.comment,
+ };
+
+ // If open delimiter, or change delimiter token then return a tag.
+ // If EOF or any another token then return null.
+ Tag? _readTag() {
+ final Token? t = _peek();
+ if (t == null ||
+ (t.type != TokenType.changeDelimiter &&
+ t.type != TokenType.openDelimiter)) {
+ return null;
+ } else if (t.type == TokenType.changeDelimiter) {
+ _read();
+ // Remember the current delimiters.
+ _currentDelimiters = t.value;
+
+ // Change delimiter tags are already parsed by the scanner.
+ // So just create a tag and return it.
+ return Tag(TagType.changeDelimiter, t.value, t.start, t.end);
+ }
+
+ // Start parsing a typical tag.
+
+ final Token open = _expect(TokenType.openDelimiter);
+
+ _readIf(TokenType.whitespace);
+
+ // A sigil is the character which identifies which sort of tag it is,
+ // i.e. '#', '/', or '>'.
+ // Variable tags and triple mustache tags don't have a sigil.
+ TagType? tagType;
+
+ if (open.value == '{{{') {
+ tagType = TagType.tripleMustache;
+ } else {
+ final Token? sigil = _readIf(TokenType.sigil);
+ tagType = sigil == null ? TagType.variable : _tagTypeMap[sigil.value];
+ }
+
+ _readIf(TokenType.whitespace);
+
+ // TODOsplit up names here instead of during render.
+ // Also check that they are valid token types.
+ // TODOsplit up names here instead of during render.
+ // Also check that they are valid token types.
+ final List<Token> list = <Token>[];
+ for (
+ Token? t = _peek();
+ t != null && t.type != TokenType.closeDelimiter;
+ t = _peek()
+ ) {
+ _read();
+ list.add(t);
+ }
+ final String name = list.map((Token t) => t.value).join().trim();
+ if (_peek() == null) {
+ throw _errorEof();
+ }
+
+ // Check to see if the tag name is valid.
+ if (tagType != TagType.comment) {
+ if (name == '') {
+ throw _error('Empty tag name.', open.start);
+ }
+ if (!_lenient) {
+ if (name.contains('\t') || name.contains('\n') || name.contains('\r')) {
+ throw _error('Tags may not contain newlines or tabs.', open.start);
+ }
+
+ if (!_validIdentifier.hasMatch(name)) {
+ throw _error(
+ 'Unless in lenient mode, tags may only contain the '
+ 'characters a-z, A-Z, minus, underscore and period.',
+ open.start,
+ );
+ }
+ }
+ }
+
+ final Token close = _expect(TokenType.closeDelimiter);
+
+ return Tag(tagType!, name, open.start, close.end);
+ }
+
+ Node? _createNodeFromTag(Tag? tag, {String partialIndent = ''}) {
+ // Handle EOF case.
+ if (tag == null) {
+ return null;
+ }
+
+ Node? node;
+ switch (tag.type) {
+ case TagType.openSection:
+ case TagType.openInverseSection:
+ final bool inverse = tag.type == TagType.openInverseSection;
+ node = SectionNode(
+ tag.name,
+ tag.start,
+ tag.end,
+ _currentDelimiters,
+ inverse: inverse,
+ );
+
+ case TagType.variable:
+ case TagType.unescapedVariable:
+ case TagType.tripleMustache:
+ final bool escape = tag.type == TagType.variable;
+ node = VariableNode(tag.name, tag.start, tag.end, escape: escape);
+
+ case TagType.partial:
+ node = PartialNode(tag.name, tag.start, tag.end, partialIndent);
+
+ case TagType.closeSection:
+ case TagType.comment:
+ case TagType.changeDelimiter:
+ node = null;
+
+ default:
+ // TODO(stuartmorgan): Convert to StateError.
+ throw Exception('Unreachable code');
+ }
+ return node;
+ }
+}
diff --git a/third_party/packages/mustache_template/lib/src/renderer.dart b/third_party/packages/mustache_template/lib/src/renderer.dart
new file mode 100644
index 0000000..09fdff6
--- /dev/null
+++ b/third_party/packages/mustache_template/lib/src/renderer.dart
@@ -0,0 +1,326 @@
+// TODO(stuartmorgan): Remove this. See https://github.com/flutter/flutter/issues/174722.
+// ignore_for_file: public_member_api_docs
+
+import '../mustache.dart' as m;
+import 'lambda_context.dart';
+import 'node.dart';
+import 'template.dart';
+import 'template_exception.dart';
+
+const Object noSuchProperty = Object();
+final RegExp _integerTag = RegExp(r'^[0-9]+$');
+
+class Renderer extends Visitor {
+ Renderer(
+ this.sink,
+ List<Object?> stack,
+ this.lenient,
+ this.htmlEscapeValues,
+ this.partialResolver,
+ this.templateName,
+ this.indent,
+ this.source,
+ ) : _stack = List<Object?>.from(stack);
+
+ Renderer.partial(Renderer ctx, Template partial, String indent)
+ : this(
+ ctx.sink,
+ ctx._stack,
+ ctx.lenient,
+ ctx.htmlEscapeValues,
+ ctx.partialResolver,
+ ctx.templateName,
+ ctx.indent + indent,
+ partial.source,
+ );
+
+ Renderer.subtree(Renderer ctx, StringSink sink)
+ : this(
+ sink,
+ ctx._stack,
+ ctx.lenient,
+ ctx.htmlEscapeValues,
+ ctx.partialResolver,
+ ctx.templateName,
+ ctx.indent,
+ ctx.source,
+ );
+
+ Renderer.lambda(Renderer ctx, String source, String indent, StringSink sink)
+ : this(
+ sink,
+ ctx._stack,
+ ctx.lenient,
+ ctx.htmlEscapeValues,
+ ctx.partialResolver,
+ ctx.templateName,
+ ctx.indent + indent,
+ source,
+ );
+
+ final StringSink sink;
+ final List<Object?> _stack;
+ final bool lenient;
+ final bool htmlEscapeValues;
+ final m.PartialResolver? partialResolver;
+ final String? templateName;
+ final String indent;
+ final String source;
+
+ void push(Object? value) => _stack.add(value);
+
+ Object? pop() => _stack.removeLast();
+
+ void write(Object output) => sink.write(output.toString());
+
+ void render(List<Node> nodes) {
+ if (indent == '') {
+ for (final Node n in nodes) {
+ n.accept(this);
+ }
+ } else if (nodes.isNotEmpty) {
+ // Special case to make sure there is not an extra indent after the last
+ // line in the partial file.
+ write(indent);
+
+ nodes.take(nodes.length - 1).forEach((Node n) => n.accept(this));
+
+ final Node node = nodes.last;
+ if (node is TextNode) {
+ visitText(node, lastNode: true);
+ } else {
+ node.accept(this);
+ }
+ }
+ }
+
+ @override
+ void visitText(TextNode node, {bool lastNode = false}) {
+ if (node.text == '') {
+ return;
+ }
+ if (indent == '') {
+ write(node.text);
+ } else if (lastNode && node.text.runes.last == _NEWLINE) {
+ // Don't indent after the last line in a template.
+ final String s = node.text.substring(0, node.text.length - 1);
+ write(s.replaceAll('\n', '\n$indent'));
+ write('\n');
+ } else {
+ write(node.text.replaceAll('\n', '\n$indent'));
+ }
+ }
+
+ @override
+ void visitVariable(VariableNode node) {
+ Object? value = resolveValue(node.name);
+
+ if (value is Function) {
+ final LambdaContext context = LambdaContext(node, this);
+ final Function valueFunction = value;
+ // TODO(stuartmorgan): Add function typing in a way that doesn't break
+ // backward compatibility.
+ // ignore: avoid_dynamic_calls
+ value = valueFunction(context);
+ context.close();
+ }
+
+ if (value == noSuchProperty) {
+ if (!lenient) {
+ throw error('Value was missing for variable tag: ${node.name}.', node);
+ }
+ } else {
+ final String valueString = (value == null) ? '' : value.toString();
+ final String output =
+ !node.escape || !htmlEscapeValues
+ ? valueString
+ : _htmlEscape(valueString);
+ write(output);
+ }
+ }
+
+ @override
+ void visitSection(SectionNode node) {
+ if (node.inverse) {
+ _renderInvSection(node);
+ } else {
+ _renderSection(node);
+ }
+ }
+
+ void _renderSection(SectionNode node) {
+ final Object? value = resolveValue(node.name);
+
+ if (value == null) {
+ // Do nothing.
+ } else if (value is Iterable) {
+ for (final Object? v in value) {
+ _renderWithValue(node, v);
+ }
+ } else if (value is Map) {
+ _renderWithValue(node, value);
+ } else if (value == true) {
+ _renderWithValue(node, value);
+ } else if (value == false) {
+ // Do nothing.
+ } else if (value == noSuchProperty) {
+ if (!lenient) {
+ throw error('Value was missing for section tag: ${node.name}.', node);
+ }
+ } else if (value is Function) {
+ final LambdaContext context = LambdaContext(node, this);
+ // TODO(stuartmorgan): Add function typing in a way that doesn't break
+ // backward compatibility.
+ // ignore: avoid_dynamic_calls
+ final Object? output = value(context);
+ context.close();
+ if (output != null) {
+ write(output);
+ }
+ } else {
+ // Assume the value might have accessible member values via mirrors.
+ _renderWithValue(node, value);
+ }
+ }
+
+ void _renderInvSection(SectionNode node) {
+ final Object? value = resolveValue(node.name);
+
+ if (value == null) {
+ _renderWithValue(node, null);
+ } else if ((value is Iterable && value.isEmpty) || value == false) {
+ _renderWithValue(node, node.name);
+ } else if (value == true || value is Map || value is Iterable) {
+ // Do nothing.
+ } else if (value == noSuchProperty) {
+ if (lenient) {
+ _renderWithValue(node, null);
+ } else {
+ throw error(
+ 'Value was missing for inverse section: ${node.name}.',
+ node,
+ );
+ }
+ } else if (value is Function) {
+ // Do nothing.
+ // TODO(stuartmorgan): Determine whether this should be an error in
+ // strict mode (per comment in initial source import).
+ } else if (lenient) {
+ // We consider all other values as 'true' in lenient mode. Since this
+ // is an inverted section, we do nothing.
+ } else {
+ throw error(
+ 'Invalid value type for inverse section, '
+ 'section: ${node.name}, '
+ 'type: ${value.runtimeType}.',
+ node,
+ );
+ }
+ }
+
+ void _renderWithValue(SectionNode node, Object? value) {
+ push(value);
+ node.visitChildren(this);
+ pop();
+ }
+
+ @override
+ void visitPartial(PartialNode node) {
+ final String partialName = node.name;
+ final Template? template =
+ partialResolver == null
+ ? null
+ : (partialResolver!(partialName) as Template?);
+ if (template != null) {
+ final Renderer renderer = Renderer.partial(this, template, node.indent);
+ final List<Node> nodes = getTemplateNodes(template);
+ renderer.render(nodes);
+ } else if (lenient) {
+ // do nothing
+ } else {
+ throw error('Partial not found: $partialName.', node);
+ }
+ }
+
+ // Walks up the stack looking for the variable.
+ // Handles dotted names of the form "a.b.c".
+ Object? resolveValue(String name) {
+ if (name == '.') {
+ return _stack.last;
+ }
+ final List<String> parts = name.split('.');
+ Object? object = noSuchProperty;
+ for (final Object? o in _stack.reversed) {
+ object = _getNamedProperty(o, parts[0]);
+ if (object != noSuchProperty) {
+ break;
+ }
+ }
+ for (int i = 1; i < parts.length; i++) {
+ if (object == noSuchProperty) {
+ return noSuchProperty;
+ }
+ object = _getNamedProperty(object, parts[i]);
+ }
+ return object;
+ }
+
+ // Returns the property of the given object by name. For a map,
+ // which contains the key name, this is object[name]. For other
+ // objects, this is object.name or object.name(). If no property
+ // by the given name exists, this method returns noSuchProperty.
+ Object? _getNamedProperty(dynamic object, String name) {
+ if (object is Map && object.containsKey(name)) {
+ return object[name];
+ }
+
+ if (object is List && _integerTag.hasMatch(name)) {
+ final int index = int.parse(name);
+ if (object.length > index) {
+ return object[index];
+ }
+ }
+ return noSuchProperty;
+ }
+
+ m.TemplateException error(String message, Node node) =>
+ TemplateException(message, templateName, source, node.start);
+
+ static const Map<int, String> _htmlEscapeMap = <int, String>{
+ _AMP: '&',
+ _LT: '<',
+ _GT: '>',
+ _QUOTE: '"',
+ _APOS: ''',
+ _FORWARD_SLASH: '/',
+ };
+
+ String _htmlEscape(String s) {
+ final StringBuffer buffer = StringBuffer();
+ int startIndex = 0;
+ int i = 0;
+ for (final int c in s.runes) {
+ if (c == _AMP ||
+ c == _LT ||
+ c == _GT ||
+ c == _QUOTE ||
+ c == _APOS ||
+ c == _FORWARD_SLASH) {
+ buffer.write(s.substring(startIndex, i));
+ buffer.write(_htmlEscapeMap[c]);
+ startIndex = i + 1;
+ }
+ i++;
+ }
+ buffer.write(s.substring(startIndex));
+ return buffer.toString();
+ }
+}
+
+const int _AMP = 38;
+const int _LT = 60;
+const int _GT = 62;
+const int _QUOTE = 34;
+const int _APOS = 39;
+const int _FORWARD_SLASH = 47;
+const int _NEWLINE = 10;
diff --git a/third_party/packages/mustache_template/lib/src/scanner.dart b/third_party/packages/mustache_template/lib/src/scanner.dart
new file mode 100644
index 0000000..b046c78
--- /dev/null
+++ b/third_party/packages/mustache_template/lib/src/scanner.dart
@@ -0,0 +1,390 @@
+// TODO(stuartmorgan): Remove this. See https://github.com/flutter/flutter/issues/174722.
+// ignore_for_file: public_member_api_docs
+
+import 'template_exception.dart';
+import 'token.dart';
+
+class Scanner {
+ Scanner(String source, this._templateName, String? delimiters)
+ : _source = source,
+ _itr = source.runes.iterator {
+ if (source == '') {
+ _c = _EOF;
+ } else {
+ _itr.moveNext();
+ _c = _itr.current;
+ }
+
+ if (delimiters == null) {
+ _openDelimiter = _openDelimiterInner = _OPEN_MUSTACHE;
+ _closeDelimiter = _closeDelimiterInner = _CLOSE_MUSTACHE;
+ } else if (delimiters.length == 3) {
+ _openDelimiter = delimiters.codeUnits[0];
+ _closeDelimiter = delimiters.codeUnits[2];
+ } else if (delimiters.length == 5) {
+ _openDelimiter = delimiters.codeUnits[0];
+ _openDelimiterInner = delimiters.codeUnits[1];
+ _closeDelimiterInner = delimiters.codeUnits[3];
+ _closeDelimiter = delimiters.codeUnits[4];
+ } else {
+ throw TemplateException(
+ 'Invalid delimiter string $delimiters',
+ null,
+ null,
+ null,
+ );
+ }
+ }
+
+ final String? _templateName;
+ final String _source;
+
+ final Iterator<int> _itr;
+ int _offset = 0;
+ int _c = 0;
+
+ final List<Token> _tokens = <Token>[];
+
+ // These can be changed by the change delimiter tag.
+ int? _openDelimiter;
+ int? _openDelimiterInner;
+ int? _closeDelimiterInner;
+ int? _closeDelimiter;
+
+ List<Token> scan() {
+ for (int c = _peek(); c != _EOF; c = _peek()) {
+ // Scan text tokens.
+ if (c != _openDelimiter) {
+ _scanText();
+ continue;
+ }
+
+ final int start = _offset;
+
+ // Read first open delimiter character.
+ _read();
+
+ // If only a single delimiter character then create a text token.
+ if (_openDelimiterInner != null && _peek() != _openDelimiterInner) {
+ final String value = String.fromCharCode(_openDelimiter!);
+ _append(TokenType.text, value, start, _offset);
+ continue;
+ }
+
+ if (_openDelimiterInner != null) {
+ _expect(_openDelimiterInner!);
+ }
+
+ // Handle triple mustache.
+ if (_openDelimiterInner == _OPEN_MUSTACHE &&
+ _openDelimiter == _OPEN_MUSTACHE &&
+ _peek() == _OPEN_MUSTACHE) {
+ _read();
+ _append(TokenType.openDelimiter, '{{{', start, _offset);
+ _scanTagContent();
+ _scanCloseTripleMustache();
+ } else {
+ // Check to see if this is a change delimiter tag. {{= | | =}}
+ // Need to skip whitespace and check for "=".
+ final int wsStart = _offset;
+ final String ws = _readWhile(_isWhitespace);
+
+ if (_peek() == _EQUAL) {
+ _parseChangeDelimiterTag(start);
+ } else {
+ // Scan standard mustache tag.
+ final String value = String.fromCharCodes(
+ _openDelimiterInner == null
+ ? <int>[_openDelimiter!]
+ : <int>[_openDelimiter!, _openDelimiterInner!],
+ );
+
+ _append(TokenType.openDelimiter, value, start, wsStart);
+
+ if (ws != '') {
+ _append(TokenType.whitespace, ws, wsStart, _offset);
+ }
+
+ _scanTagContent();
+ _scanCloseDelimiter();
+ }
+ }
+ }
+ return _tokens;
+ }
+
+ int _peek() => _c;
+
+ int _read() {
+ final int c = _c;
+ _offset++;
+ _c = _itr.moveNext() ? _itr.current : _EOF;
+ return c;
+ }
+
+ String _readWhile(bool Function(int charCode) test) {
+ if (_c == _EOF) {
+ return '';
+ }
+ final int start = _offset;
+ while (_peek() != _EOF && test(_peek())) {
+ _read();
+ }
+ final int end = _peek() == _EOF ? _source.length : _offset;
+ return _source.substring(start, end);
+ }
+
+ void _expect(int expectedCharCode) {
+ final int c = _read();
+
+ if (c == _EOF) {
+ throw TemplateException(
+ 'Unexpected end of input',
+ _templateName,
+ _source,
+ _offset - 1,
+ );
+ }
+ if (c != expectedCharCode) {
+ throw TemplateException(
+ 'Unexpected character, '
+ 'expected: ${String.fromCharCode(expectedCharCode)}, '
+ 'was: ${String.fromCharCode(c)}',
+ _templateName,
+ _source,
+ _offset - 1,
+ );
+ }
+ }
+
+ void _append(TokenType type, String value, int start, int end) =>
+ _tokens.add(Token(type, value, start, end));
+
+ bool _isWhitespace(int c) =>
+ const <int>[_SPACE, _TAB, _NEWLINE, _RETURN].contains(c);
+
+ // Scan text. This adds text tokens, line end tokens, and whitespace
+ // tokens for whitespace at the beginning of a line. This is because the
+ // mustache spec requires special handing of whitespace.
+ void _scanText() {
+ int start = 0;
+ TokenType token;
+ String value;
+
+ for (int c = _peek(); c != _EOF && c != _openDelimiter; c = _peek()) {
+ start = _offset;
+
+ switch (c) {
+ case _SPACE:
+ case _TAB:
+ value = _readWhile((int c) => c == _SPACE || c == _TAB);
+ token = TokenType.whitespace;
+
+ case _NEWLINE:
+ _read();
+ token = TokenType.lineEnd;
+ value = '\n';
+
+ case _RETURN:
+ _read();
+ if (_peek() == _NEWLINE) {
+ _read();
+ token = TokenType.lineEnd;
+ value = '\r\n';
+ } else {
+ token = TokenType.text;
+ value = '\r';
+ }
+
+ default:
+ value = _readWhile((int c) => c != _openDelimiter && c != _NEWLINE);
+ token = TokenType.text;
+ }
+
+ _append(token, value, start, _offset);
+ }
+ }
+
+ // Scan contents of a tag and the end delimiter token.
+ void _scanTagContent() {
+ int start;
+ TokenType token;
+ String value;
+
+ bool isCloseDelimiter(int c) =>
+ (_closeDelimiterInner == null && c == _closeDelimiter) ||
+ (_closeDelimiterInner != null && c == _closeDelimiterInner);
+
+ for (int c = _peek(); c != _EOF && !isCloseDelimiter(c); c = _peek()) {
+ start = _offset;
+
+ switch (c) {
+ case _HASH:
+ case _CARET:
+ case _FORWARD_SLASH:
+ case _GT:
+ case _AMP:
+ case _EXCLAIM:
+ _read();
+ token = TokenType.sigil;
+ value = String.fromCharCode(c);
+
+ case _SPACE:
+ case _TAB:
+ case _NEWLINE:
+ case _RETURN:
+ token = TokenType.whitespace;
+ value = _readWhile(_isWhitespace);
+
+ case _PERIOD:
+ _read();
+ token = TokenType.dot;
+ value = '.';
+
+ default:
+ // Identifier can be any other character in lenient mode.
+ token = TokenType.identifier;
+ value = _readWhile(
+ (int c) =>
+ !(const <int>[
+ _HASH,
+ _CARET,
+ _FORWARD_SLASH,
+ _GT,
+ _AMP,
+ _EXCLAIM,
+ _SPACE,
+ _TAB,
+ _NEWLINE,
+ _RETURN,
+ _PERIOD,
+ ].contains(c)) &&
+ c != _closeDelimiterInner &&
+ c != _closeDelimiter,
+ );
+ }
+ _append(token, value, start, _offset);
+ }
+ }
+
+ // Scan close delimiter token.
+ void _scanCloseDelimiter() {
+ if (_peek() != _EOF) {
+ final int start = _offset;
+
+ if (_closeDelimiterInner != null) {
+ _expect(_closeDelimiterInner!);
+ }
+ _expect(_closeDelimiter!);
+
+ final String value = String.fromCharCodes(
+ _closeDelimiterInner == null
+ ? <int>[_closeDelimiter!]
+ : <int>[_closeDelimiterInner!, _closeDelimiter!],
+ );
+
+ _append(TokenType.closeDelimiter, value, start, _offset);
+ }
+ }
+
+ // Scan close triple mustache delimiter token.
+ void _scanCloseTripleMustache() {
+ if (_peek() != _EOF) {
+ final int start = _offset;
+
+ _expect(_CLOSE_MUSTACHE);
+ _expect(_CLOSE_MUSTACHE);
+ _expect(_CLOSE_MUSTACHE);
+
+ _append(TokenType.closeDelimiter, '}}}', start, _offset);
+ }
+ }
+
+ // Open delimiter characters have already been read.
+ void _parseChangeDelimiterTag(int start) {
+ _expect(_EQUAL);
+
+ final int? delimiterInner = _closeDelimiterInner;
+ final int? delimiter = _closeDelimiter;
+
+ _readWhile(_isWhitespace);
+
+ int c;
+ c = _read();
+
+ if (c == _EQUAL) {
+ throw _error('Incorrect change delimiter tag.');
+ }
+ _openDelimiter = c;
+
+ c = _read();
+ if (_isWhitespace(c)) {
+ _openDelimiterInner = null;
+ } else {
+ _openDelimiterInner = c;
+ }
+
+ _readWhile(_isWhitespace);
+
+ c = _read();
+
+ if (_isWhitespace(c) || c == _EQUAL) {
+ throw _error('Incorrect change delimiter tag.');
+ }
+
+ if (_isWhitespace(_peek()) || _peek() == _EQUAL) {
+ _closeDelimiterInner = null;
+ _closeDelimiter = c;
+ } else {
+ _closeDelimiterInner = c;
+ _closeDelimiter = _read();
+ }
+
+ _readWhile(_isWhitespace);
+
+ _expect(_EQUAL);
+
+ _readWhile(_isWhitespace);
+
+ if (delimiterInner != null) {
+ _expect(delimiterInner);
+ }
+ _expect(delimiter!);
+
+ // Create delimiter string.
+ final StringBuffer buffer = StringBuffer();
+ buffer.writeCharCode(_openDelimiter!);
+ if (_openDelimiterInner != null) {
+ buffer.writeCharCode(_openDelimiterInner!);
+ }
+ buffer.write(' ');
+ if (_closeDelimiterInner != null) {
+ buffer.writeCharCode(_closeDelimiterInner!);
+ }
+ buffer.writeCharCode(_closeDelimiter!);
+ final String value = buffer.toString();
+
+ _append(TokenType.changeDelimiter, value, start, _offset);
+ }
+
+ TemplateException _error(String message) {
+ return TemplateException(message, _templateName, _source, _offset);
+ }
+}
+
+const int _EOF = -1;
+const int _TAB = 9;
+const int _NEWLINE = 10;
+const int _RETURN = 13;
+const int _SPACE = 32;
+const int _EXCLAIM = 33;
+const int _HASH = 35;
+const int _AMP = 38;
+const int _PERIOD = 46;
+const int _FORWARD_SLASH = 47;
+const int _EQUAL = 61;
+const int _GT = 62;
+const int _CARET = 94;
+
+const int _OPEN_MUSTACHE = 123;
+const int _CLOSE_MUSTACHE = 125;
diff --git a/third_party/packages/mustache_template/lib/src/template.dart b/third_party/packages/mustache_template/lib/src/template.dart
new file mode 100644
index 0000000..347c90d
--- /dev/null
+++ b/third_party/packages/mustache_template/lib/src/template.dart
@@ -0,0 +1,58 @@
+// TODO(stuartmorgan): Remove this. See https://github.com/flutter/flutter/issues/174722.
+// ignore_for_file: public_member_api_docs
+
+import '../mustache.dart' as m;
+import 'node.dart';
+import 'parser.dart' as parser;
+import 'renderer.dart';
+
+class Template implements m.Template {
+ Template.fromSource(
+ this.source, {
+ bool lenient = false,
+ bool htmlEscapeValues = true,
+ String? name,
+ m.PartialResolver? partialResolver,
+ String delimiters = '{{ }}',
+ }) : _nodes = parser.parse(source, lenient, name, delimiters),
+ _lenient = lenient,
+ _htmlEscapeValues = htmlEscapeValues,
+ _name = name,
+ _partialResolver = partialResolver;
+
+ @override
+ final String source;
+ final List<Node> _nodes;
+ final bool _lenient;
+ final bool _htmlEscapeValues;
+ final String? _name;
+ final m.PartialResolver? _partialResolver;
+
+ @override
+ String? get name => _name;
+
+ @override
+ String renderString(Object? values) {
+ final StringBuffer buf = StringBuffer();
+ render(values, buf);
+ return buf.toString();
+ }
+
+ @override
+ void render(Object? values, StringSink sink) {
+ final Renderer renderer = Renderer(
+ sink,
+ <dynamic>[values],
+ _lenient,
+ _htmlEscapeValues,
+ _partialResolver,
+ _name,
+ '',
+ source,
+ );
+ renderer.render(_nodes);
+ }
+}
+
+// Expose getter for nodes internally within this package.
+List<Node> getTemplateNodes(Template template) => template._nodes;
diff --git a/third_party/packages/mustache_template/lib/src/template_exception.dart b/third_party/packages/mustache_template/lib/src/template_exception.dart
new file mode 100644
index 0000000..e64ca70
--- /dev/null
+++ b/third_party/packages/mustache_template/lib/src/template_exception.dart
@@ -0,0 +1,124 @@
+// TODO(stuartmorgan): Remove this. See https://github.com/flutter/flutter/issues/174722.
+// ignore_for_file: public_member_api_docs
+
+import '../mustache.dart' as m;
+
+class TemplateException implements m.TemplateException {
+ TemplateException(this.message, this.templateName, this.source, this.offset);
+
+ @override
+ final String message;
+ @override
+ final String? templateName;
+ @override
+ final String? source;
+ @override
+ final int? offset;
+
+ bool _isUpdated = false;
+ late int _line;
+ late int _column;
+ late String _context;
+
+ @override
+ int get line {
+ _update();
+ return _line;
+ }
+
+ @override
+ int get column {
+ _update();
+ return _column;
+ }
+
+ @override
+ String get context {
+ _update();
+ return _context;
+ }
+
+ @override
+ String toString() {
+ final List<Object?> list = <Object?>[];
+ if (templateName != null) {
+ list.add(templateName);
+ }
+ list.add(line);
+ list.add(column);
+ final String location = list.isEmpty ? '' : ' (${list.join(':')})';
+ return '$message$location\n$context';
+ }
+
+ // This source code is a modified version of FormatException.toString().
+ void _update() {
+ if (_isUpdated) {
+ return;
+ }
+ _isUpdated = true;
+
+ if (source == null ||
+ offset == null ||
+ (offset! < 0 || offset! > source!.length)) {
+ return;
+ }
+
+ // Find line and character column.
+ int lineNum = 1;
+ int lineStart = 0;
+ bool lastWasCR = false;
+ for (int i = 0; i < offset!; i++) {
+ final int char = source!.codeUnitAt(i);
+ if (char == 0x0a) {
+ if (lineStart != i || !lastWasCR) {
+ lineNum += 1;
+ }
+ lineStart = i + 1;
+ lastWasCR = false;
+ } else if (char == 0x0d) {
+ lineNum++;
+ lineStart = i + 1;
+ lastWasCR = true;
+ }
+ }
+
+ _line = lineNum;
+ _column = offset! - lineStart + 1;
+
+ // Find context.
+ int lineEnd = source!.length;
+ for (int i = offset!; i < source!.length; i++) {
+ final int char = source!.codeUnitAt(i);
+ if (char == 0x0a || char == 0x0d) {
+ lineEnd = i;
+ break;
+ }
+ }
+ final int length = lineEnd - lineStart;
+ int start = lineStart;
+ int end = lineEnd;
+ String prefix = '';
+ String postfix = '';
+ if (length > 78) {
+ // Can't show entire line. Try to anchor at the nearest end, if
+ // one is within reach.
+ final int index = offset! - lineStart;
+ if (index < 75) {
+ end = start + 75;
+ postfix = '...';
+ } else if (end - offset! < 75) {
+ start = end - 75;
+ prefix = '...';
+ } else {
+ // Neither end is near, just pick an area around the offset.
+ start = offset! - 36;
+ end = offset! + 36;
+ prefix = postfix = '...';
+ }
+ }
+ final String slice = source!.substring(start, end);
+ final int markOffset = offset! - start + prefix.length;
+
+ _context = "$prefix$slice$postfix\n${" " * markOffset}^\n";
+ }
+}
diff --git a/third_party/packages/mustache_template/lib/src/token.dart b/third_party/packages/mustache_template/lib/src/token.dart
new file mode 100644
index 0000000..b93dd33
--- /dev/null
+++ b/third_party/packages/mustache_template/lib/src/token.dart
@@ -0,0 +1,38 @@
+// TODO(stuartmorgan): Remove this. See https://github.com/flutter/flutter/issues/174722.
+// ignore_for_file: public_member_api_docs
+
+class TokenType {
+ const TokenType(this.name);
+
+ final String name;
+
+ @override
+ String toString() => '(TokenType $name)';
+
+ static const TokenType text = TokenType('text');
+ static const TokenType openDelimiter = TokenType('openDelimiter');
+ static const TokenType closeDelimiter = TokenType('closeDelimiter');
+
+ // A sigil is the word commonly used to describe the special character at the
+ // start of mustache tag i.e. #, ^ or /.
+ static const TokenType sigil = TokenType('sigil');
+ static const TokenType identifier = TokenType('identifier');
+ static const TokenType dot = TokenType('dot');
+
+ static const TokenType changeDelimiter = TokenType('changeDelimiter');
+ static const TokenType whitespace = TokenType('whitespace');
+ static const TokenType lineEnd = TokenType('lineEnd');
+}
+
+class Token {
+ Token(this.type, this.value, this.start, this.end);
+
+ final TokenType type;
+ final String value;
+
+ final int start;
+ final int end;
+
+ @override
+ String toString() => '(Token ${type.name} "$value" $start $end)';
+}
diff --git a/third_party/packages/mustache_template/pubspec.yaml b/third_party/packages/mustache_template/pubspec.yaml
new file mode 100644
index 0000000..fd0de9f
--- /dev/null
+++ b/third_party/packages/mustache_template/pubspec.yaml
@@ -0,0 +1,14 @@
+name: mustache_template
+description: A templating library that implements the Mustache template specification
+repository: https://github.com/flutter/packages/tree/main/third_party/packages/mustache_template
+issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+mustache_template%22
+version: 2.0.1
+
+environment:
+ sdk: ^3.7.0
+
+dev_dependencies:
+ test: ^1.16.5
+
+topics:
+ - template
diff --git a/third_party/packages/mustache_template/test/all.dart b/third_party/packages/mustache_template/test/all.dart
new file mode 100644
index 0000000..0e7d0e4
--- /dev/null
+++ b/third_party/packages/mustache_template/test/all.dart
@@ -0,0 +1,9 @@
+import 'mustache_specs.dart' as specs;
+import 'mustache_test.dart' as test;
+import 'parser_test.dart' as parser;
+
+void main() {
+ specs.main();
+ test.main();
+ parser.main();
+}
diff --git a/third_party/packages/mustache_template/test/download-spec.sh b/third_party/packages/mustache_template/test/download-spec.sh
new file mode 100755
index 0000000..e36cb5b
--- /dev/null
+++ b/third_party/packages/mustache_template/test/download-spec.sh
@@ -0,0 +1 @@
+git clone https://github.com/mustache/spec.git
diff --git a/third_party/packages/mustache_template/test/mustache_specs.dart b/third_party/packages/mustache_template/test/mustache_specs.dart
new file mode 100644
index 0000000..ff4103c
--- /dev/null
+++ b/third_party/packages/mustache_template/test/mustache_specs.dart
@@ -0,0 +1,131 @@
+// Specification files can be downloaded here https://github.com/mustache/spec
+
+// Test implemented by Georgios Valotasios.
+// See: https://github.com/valotas/mustache4dart
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:mustache_template/mustache.dart';
+import 'package:test/test.dart';
+
+String render(
+ String source,
+ dynamic values, {
+ required String? Function(String) partial,
+}) {
+ late Template? Function(String) resolver;
+ resolver = (String name) {
+ final String? source = partial(name);
+ if (source == null) {
+ return null;
+ }
+ return Template(source, partialResolver: resolver, lenient: true);
+ };
+ final Template t = Template(source, partialResolver: resolver, lenient: true);
+ return t.renderString(values);
+}
+
+void main() {
+ defineTests();
+}
+
+void defineTests() {
+ final Directory specsDir = Directory('test/spec/specs');
+ specsDir.listSync().forEach((FileSystemEntity f) {
+ if (f is File) {
+ final String filename = f.path;
+ if (shouldRun(filename)) {
+ final String text = f.readAsStringSync();
+ _defineGroupFromFile(filename, text);
+ }
+ }
+ });
+}
+
+void _defineGroupFromFile(String filename, String text) {
+ final Map<String, Object?> jsondata =
+ (json.decode(text) as Map<dynamic, dynamic>).cast<String, Object?>();
+ final List<Map<String, Object?>> tests =
+ (jsondata['tests']! as List<dynamic>).cast<Map<String, Object?>>();
+ filename = filename.substring(filename.lastIndexOf('/') + 1);
+ group('Specs of $filename', () {
+ for (final Map<String, Object?> t in tests) {
+ final StringBuffer testDescription = StringBuffer(t['name']! as String);
+ testDescription.write(': ');
+ testDescription.write(t['desc']);
+ final String template = t['template']! as String;
+ final Object? data = t['data'];
+ final String templateOneline = template
+ .replaceAll('\n', r'\n')
+ .replaceAll('\r', r'\r');
+ final StringBuffer reason = StringBuffer(
+ "Could not render right '''$templateOneline'''",
+ );
+ final Object? expected = t['expected'];
+ final Map<String, Object?>? partials =
+ t['partials'] as Map<String, Object?>?;
+ String? partial(String name) {
+ if (partials == null) {
+ return null;
+ }
+ return partials[name] as String?;
+ }
+
+ //swap the data.lambda with a dart real function
+ if (data is Map && data['lambda'] != null) {
+ data['lambda'] = lambdas[t['name']];
+ }
+ reason.write(" with '$data'");
+ if (partials != null) {
+ reason.write(' and partial: $partials');
+ }
+ test(
+ testDescription.toString(),
+ () => expect(
+ render(template, data, partial: partial),
+ expected,
+ reason: reason.toString(),
+ ),
+ );
+ }
+ });
+}
+
+bool shouldRun(String filename) {
+ // filter out only .json files
+ if (!filename.endsWith('.json')) {
+ return false;
+ }
+ return true;
+}
+
+String Function(Object?) _dummyCallableWithState() {
+ int callCounter = 0;
+ return (Object? arg) {
+ callCounter++;
+ return callCounter.toString();
+ };
+}
+
+String Function(LambdaContext) wrapLambda(Object? Function(Object?) f) =>
+ (LambdaContext ctx) => ctx.renderSource(f(ctx.source).toString());
+
+Map<String, Function> lambdas = <String, Function>{
+ 'Interpolation': wrapLambda((Object? t) => 'world'),
+ 'Interpolation - Expansion': wrapLambda((Object? t) => '{{planet}}'),
+ 'Interpolation - Alternate Delimiters': wrapLambda(
+ (Object? t) => '|planet| => {{planet}}',
+ ),
+ 'Interpolation - Multiple Calls': wrapLambda(
+ _dummyCallableWithState(),
+ ), //function() { return (g=(function(){return this})()).calls=(g.calls||0)+1 }
+ 'Escaping': wrapLambda((Object? t) => '>'),
+ 'Section': wrapLambda((Object? txt) => txt == '{{x}}' ? 'yes' : 'no'),
+ 'Section - Expansion': wrapLambda((Object? txt) => '$txt{{planet}}$txt'),
+ 'Section - Alternate Delimiters': wrapLambda(
+ (Object? txt) => '$txt{{planet}} => |planet|$txt',
+ ),
+ 'Section - Multiple Calls': wrapLambda((Object? t) => '__${t}__'),
+ 'Inverted Section': wrapLambda((Object? txt) => false),
+};
diff --git a/third_party/packages/mustache_template/test/mustache_test.dart b/third_party/packages/mustache_template/test/mustache_test.dart
new file mode 100644
index 0000000..086247b
--- /dev/null
+++ b/third_party/packages/mustache_template/test/mustache_test.dart
@@ -0,0 +1,919 @@
+import 'package:mustache_template/mustache.dart';
+import 'package:test/test.dart';
+
+const String UNEXPECTED_EOF = 'Unexpected end of input';
+const String BAD_VALUE_INV_SECTION = 'Invalid value type for inverse section';
+const String BAD_TAG_NAME = 'Unless in lenient mode, tags may only contain';
+const String VALUE_MISSING = 'Value was missing';
+const String UNCLOSED_TAG = 'Unclosed tag';
+
+Template parse(String source, {bool lenient = false}) =>
+ Template(source, lenient: lenient);
+
+void main() {
+ group('Basic', () {
+ test('Variable', () {
+ final String output = parse(
+ '_{{var}}_',
+ ).renderString(<String, String>{'var': 'bob'});
+ expect(output, equals('_bob_'));
+ });
+ test('Comment', () {
+ final String output = parse(
+ '_{{! i am a\n comment ! }}_',
+ ).renderString(<dynamic, dynamic>{});
+ expect(output, equals('__'));
+ });
+ });
+ group('Section', () {
+ test('Map', () {
+ final String output = parse(
+ '{{#section}}_{{var}}_{{/section}}',
+ ).renderString(<String, Map<String, String>>{
+ 'section': <String, String>{'var': 'bob'},
+ });
+ expect(output, equals('_bob_'));
+ });
+ test('List', () {
+ final String output = parse(
+ '{{#section}}_{{var}}_{{/section}}',
+ ).renderString(<String, List<Map<String, String>>>{
+ 'section': <Map<String, String>>[
+ <String, String>{'var': 'bob'},
+ <String, String>{'var': 'jim'},
+ ],
+ });
+ expect(output, equals('_bob__jim_'));
+ });
+ test('Empty List', () {
+ final String output = parse(
+ '{{#section}}_{{var}}_{{/section}}',
+ ).renderString(<String, List<dynamic>>{'section': <dynamic>[]});
+ expect(output, equals(''));
+ });
+ test('False', () {
+ final String output = parse(
+ '{{#section}}_{{var}}_{{/section}}',
+ ).renderString(<String, bool>{'section': false});
+ expect(output, equals(''));
+ });
+ test('Invalid value', () {
+ final Exception? ex = renderFail(
+ '{{#section}}_{{var}}_{{/section}}',
+ <String, int>{'section': 42},
+ );
+ if (ex is TemplateException) {
+ expect(ex.message, startsWith(VALUE_MISSING));
+ } else {
+ fail('Unexpected type: $ex');
+ }
+ });
+ test('Invalid value - lenient mode', () {
+ final String output = parse(
+ '{{#var}}_{{var}}_{{/var}}',
+ lenient: true,
+ ).renderString(<String, int>{'var': 42});
+ expect(output, equals('_42_'));
+ });
+
+ test('True', () {
+ final String output = parse(
+ '{{#section}}_ok_{{/section}}',
+ ).renderString(<String, bool>{'section': true});
+ expect(output, equals('_ok_'));
+ });
+
+ test('Nested', () {
+ final String output = parse(
+ '{{#section}}.{{var}}.{{#nested}}_{{nestedvar}}_{{/nested}}.{{/section}}',
+ ).renderString(<String, Map<String, Object>>{
+ 'section': <String, Object>{
+ 'var': 'bob',
+ 'nested': <Map<String, String>>[
+ <String, String>{'nestedvar': 'jim'},
+ <String, String>{'nestedvar': 'sally'},
+ ],
+ },
+ });
+ expect(output, equals('.bob._jim__sally_.'));
+ });
+
+ test('Whitespace in section tags', () {
+ expect(
+ parse('{{#foo.bar}}oi{{/foo.bar}}').renderString(
+ <String, Map<String, bool>>{
+ 'foo': <String, bool>{'bar': true},
+ },
+ ),
+ equals('oi'),
+ );
+ expect(
+ parse('{{# foo.bar}}oi{{/foo.bar}}').renderString(
+ <String, Map<String, bool>>{
+ 'foo': <String, bool>{'bar': true},
+ },
+ ),
+ equals('oi'),
+ );
+ expect(
+ parse('{{#foo.bar }}oi{{/foo.bar}}').renderString(
+ <String, Map<String, bool>>{
+ 'foo': <String, bool>{'bar': true},
+ },
+ ),
+ equals('oi'),
+ );
+ expect(
+ parse('{{# foo.bar }}oi{{/foo.bar}}').renderString(
+ <String, Map<String, bool>>{
+ 'foo': <String, bool>{'bar': true},
+ },
+ ),
+ equals('oi'),
+ );
+ expect(
+ parse('{{#foo.bar}}oi{{/ foo.bar}}').renderString(
+ <String, Map<String, bool>>{
+ 'foo': <String, bool>{'bar': true},
+ },
+ ),
+ equals('oi'),
+ );
+ expect(
+ parse('{{#foo.bar}}oi{{/foo.bar }}').renderString(
+ <String, Map<String, bool>>{
+ 'foo': <String, bool>{'bar': true},
+ },
+ ),
+ equals('oi'),
+ );
+ expect(
+ parse('{{#foo.bar}}oi{{/ foo.bar }}').renderString(
+ <String, Map<String, bool>>{
+ 'foo': <String, bool>{'bar': true},
+ },
+ ),
+ equals('oi'),
+ );
+ expect(
+ parse('{{# foo.bar }}oi{{/ foo.bar }}').renderString(
+ <String, Map<String, bool>>{
+ 'foo': <String, bool>{'bar': true},
+ },
+ ),
+ equals('oi'),
+ );
+ });
+
+ test('Whitespace in variable tags', () {
+ expect(
+ parse('{{foo.bar}}').renderString(<String, Map<String, bool>>{
+ 'foo': <String, bool>{'bar': true},
+ }),
+ equals('true'),
+ );
+ expect(
+ parse('{{ foo.bar}}').renderString(<String, Map<String, bool>>{
+ 'foo': <String, bool>{'bar': true},
+ }),
+ equals('true'),
+ );
+ expect(
+ parse('{{foo.bar }}').renderString(<String, Map<String, bool>>{
+ 'foo': <String, bool>{'bar': true},
+ }),
+ equals('true'),
+ );
+ expect(
+ parse('{{ foo.bar }}').renderString(<String, Map<String, bool>>{
+ 'foo': <String, bool>{'bar': true},
+ }),
+ equals('true'),
+ );
+ });
+
+ test('Odd whitespace in tags', () {
+ void render(String source, dynamic values, dynamic output) => expect(
+ parse(source, lenient: true).renderString(values),
+ equals(output),
+ );
+
+ render('{{\t# foo}}oi{{\n/foo}}', <String, bool>{'foo': true}, 'oi');
+
+ render(
+ '{{ # # foo }} {{ oi }} {{ / # foo }}',
+ <String, List<Map<String, String>>>{
+ '# foo': <Map<String, String>>[
+ <String, String>{'oi': 'OI!'},
+ ],
+ },
+ ' OI! ',
+ );
+
+ render(
+ '{{ #foo }} {{ oi }} {{ /foo }}',
+ <String, List<Map<String, String>>>{
+ 'foo': <Map<String, String>>[
+ <String, String>{'oi': 'OI!'},
+ ],
+ },
+ ' OI! ',
+ );
+
+ render(
+ '{{\t#foo }} {{ oi }} {{ /foo }}',
+ <String, List<Map<String, String>>>{
+ 'foo': <Map<String, String>>[
+ <String, String>{'oi': 'OI!'},
+ ],
+ },
+ ' OI! ',
+ );
+
+ render('{{{ #foo }}} {{{ /foo }}}', <String, int>{
+ '#foo': 1,
+ '/foo': 2,
+ }, '1 2');
+
+ // Invalid - I'm ok with that for now.
+ // render(
+ // "{{{ { }}}",
+ // {'{': 1},
+ // '1');
+
+ render('{{\nfoo}}', <String, String>{'foo': 'bar'}, 'bar');
+
+ render('{{\tfoo}}', <String, String>{'foo': 'bar'}, 'bar');
+
+ render('{{\t# foo}}oi{{\n/foo}}', <String, bool>{'foo': true}, 'oi');
+
+ render('{{{\tfoo\t}}}', <String, bool>{'foo': true}, 'true');
+
+ // TODO(stuartmorgan): Fix and enable this test, which was commented out
+ // when the source was first imported.
+ // empty, or error in strict mode.
+ // render(
+ // "{{ > }}",
+ // {'>': 'oi'},
+ // '');
+ });
+
+ test('Empty source', () {
+ final Template t = Template('');
+ expect(t.renderString(<dynamic, dynamic>{}), equals(''));
+ });
+
+ test('Template name', () {
+ final Template t = Template('', name: 'foo');
+ expect(t.name, equals('foo'));
+ });
+
+ test('Bad tag', () {
+ expect(() => Template('{{{ foo }|'), throwsException);
+ });
+ });
+
+ group('Inverse Section', () {
+ test('Map', () {
+ final String output = parse(
+ '{{^section}}_{{var}}_{{/section}}',
+ ).renderString(<String, Map<String, String>>{
+ 'section': <String, String>{'var': 'bob'},
+ });
+ expect(output, equals(''));
+ });
+ test('List', () {
+ final String output = parse(
+ '{{^section}}_{{var}}_{{/section}}',
+ ).renderString(<String, List<Map<String, String>>>{
+ 'section': <Map<String, String>>[
+ <String, String>{'var': 'bob'},
+ <String, String>{'var': 'jim'},
+ ],
+ });
+ expect(output, equals(''));
+ });
+ test('Empty List', () {
+ final String output = parse(
+ '{{^section}}_ok_{{/section}}',
+ ).renderString(<String, List<dynamic>>{'section': <dynamic>[]});
+ expect(output, equals('_ok_'));
+ });
+ test('False', () {
+ final String output = parse(
+ '{{^section}}_ok_{{/section}}',
+ ).renderString(<String, bool>{'section': false});
+ expect(output, equals('_ok_'));
+ });
+ test('Invalid value', () {
+ final Exception? ex = renderFail(
+ '{{^section}}_{{var}}_{{/section}}',
+ <String, int>{'section': 42},
+ );
+ expect(ex is TemplateException, isTrue);
+ expect(
+ (ex! as TemplateException).message,
+ startsWith(BAD_VALUE_INV_SECTION),
+ );
+ });
+ test('Invalid value - lenient mode', () {
+ final String output = parse(
+ '{{^var}}_ok_{{/var}}',
+ lenient: true,
+ ).renderString(<String, int>{'var': 42});
+ expect(output, equals(''));
+ });
+ test('True', () {
+ final String output = parse(
+ '{{^section}}_ok_{{/section}}',
+ ).renderString(<String, bool>{'section': true});
+ expect(output, equals(''));
+ });
+ });
+
+ group('Html escape', () {
+ test('Escape at start', () {
+ final String output = parse(
+ '_{{var}}_',
+ ).renderString(<String, String>{'var': '&.'});
+ expect(output, equals('_&._'));
+ });
+
+ test('Escape at end', () {
+ final String output = parse(
+ '_{{var}}_',
+ ).renderString(<String, String>{'var': '.&'});
+ expect(output, equals('_.&_'));
+ });
+
+ test('&', () {
+ final String output = parse(
+ '_{{var}}_',
+ ).renderString(<String, String>{'var': '&'});
+ expect(output, equals('_&_'));
+ });
+
+ test('<', () {
+ final String output = parse(
+ '_{{var}}_',
+ ).renderString(<String, String>{'var': '<'});
+ expect(output, equals('_<_'));
+ });
+
+ test('>', () {
+ final String output = parse(
+ '_{{var}}_',
+ ).renderString(<String, String>{'var': '>'});
+ expect(output, equals('_>_'));
+ });
+
+ test('"', () {
+ final String output = parse(
+ '_{{var}}_',
+ ).renderString(<String, String>{'var': '"'});
+ expect(output, equals('_"_'));
+ });
+
+ test("'", () {
+ final String output = parse(
+ '_{{var}}_',
+ ).renderString(<String, String>{'var': "'"});
+ expect(output, equals('_'_'));
+ });
+
+ test('/', () {
+ final String output = parse(
+ '_{{var}}_',
+ ).renderString(<String, String>{'var': '/'});
+ expect(output, equals('_/_'));
+ });
+ });
+
+ group('Invalid format', () {
+ test('Mismatched tag', () {
+ const String source = '{{#section}}_{{var}}_{{/notsection}}';
+ final Exception? ex = renderFail(source, <String, Map<String, String>>{
+ 'section': <String, String>{'var': 'bob'},
+ });
+ expectFail(ex, 1, 22, 'Mismatched tag');
+ });
+
+ test('Unexpected EOF', () {
+ const String source = '{{#section}}_{{var}}_{{/section';
+ final Exception? ex = renderFail(source, <String, Map<String, String>>{
+ 'section': <String, String>{'var': 'bob'},
+ });
+ expectFail(ex, 1, 31, UNEXPECTED_EOF);
+ });
+
+ test('Bad tag name, open section', () {
+ const String source = r'{{#section$%$^%}}_{{var}}_{{/section}}';
+ final Exception? ex = renderFail(source, <String, Map<String, String>>{
+ 'section': <String, String>{'var': 'bob'},
+ });
+ expectFail(ex, null, null, BAD_TAG_NAME);
+ });
+
+ test('Bad tag name, close section', () {
+ const String source = r'{{#section}}_{{var}}_{{/section$%$^%}}';
+ final Exception? ex = renderFail(source, <String, Map<String, String>>{
+ 'section': <String, String>{'var': 'bob'},
+ });
+ expectFail(ex, null, null, BAD_TAG_NAME);
+ });
+
+ test('Bad tag name, variable', () {
+ const String source = r'{{#section}}_{{var$%$^%}}_{{/section}}';
+ final Exception? ex = renderFail(source, <String, Map<String, String>>{
+ 'section': <String, String>{'var': 'bob'},
+ });
+ expectFail(ex, null, null, BAD_TAG_NAME);
+ });
+
+ test('Missing variable', () {
+ const String source = r'{{#section}}_{{var}}_{{/section}}';
+ final Exception? ex = renderFail(source, <String, Map<dynamic, dynamic>>{
+ 'section': <dynamic, dynamic>{},
+ });
+ expectFail(ex, null, null, VALUE_MISSING);
+ });
+
+ // Null variables shouldn't be a problem.
+ test('Null variable', () {
+ final Template t = Template('{{#section}}_{{var}}_{{/section}}');
+ final String output = t.renderString(<String, Map<String, void>>{
+ 'section': <String, void>{'var': null},
+ });
+ expect(output, equals('__'));
+ });
+
+ test('Unclosed section', () {
+ const String source = r'{{#section}}foo';
+ final Exception? ex = renderFail(source, <String, Map<dynamic, dynamic>>{
+ 'section': <dynamic, dynamic>{},
+ });
+ expectFail(ex, null, null, UNCLOSED_TAG);
+ });
+ });
+
+ group('Lenient', () {
+ test('Odd section name', () {
+ final String output = parse(
+ r'{{#section$%$^%}}_{{var}}_{{/section$%$^%}}',
+ lenient: true,
+ ).renderString(<String, Map<String, String>>{
+ r'section$%$^%': <String, String>{'var': 'bob'},
+ });
+ expect(output, equals('_bob_'));
+ });
+
+ test('Odd variable name', () {
+ final String output = parse(
+ r'{{#section}}_{{var$%$^%}}_{{/section}}',
+ lenient: true,
+ ).renderString(<String, Map<String, String>>{
+ 'section': <String, String>{r'var$%$^%': 'bob'},
+ });
+ expect(output, equals('_bob_'));
+ });
+
+ test('Null variable', () {
+ final String output = parse(
+ r'{{#section}}_{{var}}_{{/section}}',
+ lenient: true,
+ ).renderString(<String, Map<String, void>>{
+ 'section': <String, void>{'var': null},
+ });
+ expect(output, equals('__'));
+ });
+
+ test('Null section', () {
+ final String output = parse(
+ '{{#section}}_{{var}}_{{/section}}',
+ lenient: true,
+ ).renderString(<String, void>{'section': null});
+ expect(output, equals(''));
+ });
+
+ // Known failure
+ // test('Null inverse section', () {
+ // var output = parse('{{^section}}_{{var}}_{{/section}}', lenient: true)
+ // .renderString({"section": null}, lenient: true);
+ // expect(output, equals(''));
+ // });
+ });
+
+ group('Escape tags', () {
+ test('{{{ ... }}}', () {
+ final String output = parse(
+ '{{{blah}}}',
+ ).renderString(<String, String>{'blah': '&'});
+ expect(output, equals('&'));
+ });
+ test('{{& ... }}', () {
+ final String output = parse(
+ '{{{blah}}}',
+ ).renderString(<String, String>{'blah': '&'});
+ expect(output, equals('&'));
+ });
+ });
+
+ group('Partial tag', () {
+ String partialTest(
+ Map<String, Object> values,
+ Map<String, Object> sources,
+ String renderTemplate, {
+ bool lenient = false,
+ }) {
+ final Map<String, Template> templates = <String, Template>{};
+ Template? resolver(String name) => templates[name];
+ for (final String k in sources.keys) {
+ templates[k] = Template(
+ sources[k]! as String,
+ name: k,
+ lenient: lenient,
+ partialResolver: resolver,
+ );
+ }
+ final Template? t = resolver(renderTemplate);
+ return t!.renderString(values);
+ }
+
+ test('basic', () {
+ final String output = partialTest(
+ <String, Object>{'foo': 'bar'},
+ <String, Object>{'root': '{{>partial}}', 'partial': '{{foo}}'},
+ 'root',
+ );
+ expect(output, 'bar');
+ });
+
+ test('missing partial strict', () {
+ bool threw = false;
+ try {
+ partialTest(
+ <String, Object>{'foo': 'bar'},
+ <String, Object>{'root': '{{>partial}}'},
+ 'root',
+ );
+ } on Exception catch (e) {
+ expect(e is TemplateException, isTrue);
+ threw = true;
+ }
+ expect(threw, isTrue);
+ });
+
+ test('missing partial lenient', () {
+ final String output = partialTest(
+ <String, Object>{'foo': 'bar'},
+ <String, Object>{'root': '{{>partial}}'},
+ 'root',
+ lenient: true,
+ );
+ expect(output, equals(''));
+ });
+
+ test('context', () {
+ final String output = partialTest(
+ <String, Object>{'text': 'content'},
+ <String, Object>{'root': '"{{>partial}}"', 'partial': '*{{text}}*'},
+ 'root',
+ lenient: true,
+ );
+ expect(output, equals('"*content*"'));
+ });
+
+ test('recursion', () {
+ final String output = partialTest(
+ <String, Object>{
+ 'content': 'X',
+ 'nodes': <Map<String, Object>>[
+ <String, Object>{'content': 'Y', 'nodes': <dynamic>[]},
+ ],
+ },
+ <String, Object>{
+ 'root': '{{>node}}',
+ 'node': '{{content}}<{{#nodes}}{{>node}}{{/nodes}}>',
+ },
+ 'root',
+ lenient: true,
+ );
+ expect(output, equals('X<Y<>>'));
+ });
+
+ test('standalone without previous', () {
+ final String output = partialTest(
+ <String, Object>{},
+ <String, Object>{'root': ' {{>partial}}\n>', 'partial': '>\n>'},
+ 'root',
+ lenient: true,
+ );
+ expect(output, equals(' >\n >>'));
+ });
+
+ test('standalone indentation', () {
+ final String output = partialTest(
+ <String, Object>{'content': '<\n->'},
+ <String, Object>{
+ 'root': '\\\n {{>partial}}\n/\n',
+ 'partial': '|\n{{{content}}}\n|\n',
+ },
+ 'root',
+ lenient: true,
+ );
+ expect(output, equals('\\\n |\n <\n->\n |\n/\n'));
+ });
+ });
+
+ group('Lambdas', () {
+ void lambdaTest({
+ required String template,
+ dynamic lambda,
+ dynamic output,
+ }) => expect(
+ parse(template).renderString(<String, dynamic>{'lambda': lambda}),
+ equals(output),
+ );
+
+ test('basic', () {
+ lambdaTest(
+ template: 'Hello, {{lambda}}!',
+ lambda: (_) => 'world',
+ output: 'Hello, world!',
+ );
+ });
+
+ test('escaping', () {
+ lambdaTest(
+ template: '<{{lambda}}{{{lambda}}}',
+ lambda: (_) => '>',
+ output: '<>>',
+ );
+ });
+
+ test('sections', () {
+ lambdaTest(
+ template: '{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}',
+ lambda: (LambdaContext ctx) => '__${ctx.renderString()}__',
+ output: '__FILE__ != __LINE__',
+ );
+ });
+
+ // TODO(stuartmorgan): Fix and re-enable this test, which was skipped when
+ // the package was first imported.
+ test('inverted sections truthy', () {
+ const String template = '<{{^lambda}}{{static}}{{/lambda}}>';
+ final Map<String, Object> values = <String, Object>{
+ 'lambda': (_) => false,
+ 'static': 'static',
+ };
+ const String output = '<>';
+ expect(parse(template).renderString(values), equals(output));
+ }, skip: 'skip test');
+
+ test("seth's use case", () {
+ const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
+ final Map<String, Object> values = <String, Object>{
+ 'markdown': (LambdaContext ctx) => ctx.renderString().toLowerCase(),
+ 'content': 'OI YOU!',
+ };
+ const String output = '<oi you!>';
+ expect(parse(template).renderString(values), equals(output));
+ });
+
+ test('Lambda v2', () {
+ const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
+ final Map<String, Object> values = <String, Object>{
+ 'markdown': (LambdaContext ctx) => ctx.source,
+ 'content': 'OI YOU!',
+ };
+ const String output = '<{{content}}>';
+ expect(parse(template).renderString(values), equals(output));
+ });
+
+ test('Lambda v2...', () {
+ const String template =
+ '<{{#markdown}}dsfsf dsfsdf dfsdfsd{{/markdown}}>';
+ final Map<String, dynamic Function(dynamic ctx)> values =
+ <String, dynamic Function(dynamic ctx)>{
+ // ignore: avoid_dynamic_calls
+ 'markdown': (dynamic ctx) => ctx.source,
+ };
+ const String output = '<dsfsf dsfsdf dfsdfsd>';
+ expect(parse(template).renderString(values), equals(output));
+ });
+
+ test('Alternate Delimiters', () {
+ // A lambda's return value should parse with the default delimiters.
+
+ const String template = '{{= | | =}}\nHello, (|&lambda|)!';
+
+ //function() { return "|planet| => {{planet}}" }
+ final Map<String, Object> values = <String, Object>{
+ 'planet': 'world',
+ 'lambda':
+ (LambdaContext ctx) => ctx.renderSource('|planet| => {{planet}}'),
+ };
+
+ const String output = 'Hello, (|planet| => world)!';
+
+ expect(parse(template).renderString(values), equals(output));
+ });
+
+ test('Alternate Delimiters 2', () {
+ // Lambdas used for sections should parse with the current delimiters.
+
+ const String template = '{{= | | =}}<|#lambda|-|/lambda|>';
+
+ //function() { return "|planet| => {{planet}}" }
+ final Map<String, Object> values = <String, Object>{
+ 'planet': 'Earth',
+ 'lambda': (LambdaContext ctx) {
+ final String txt = ctx.source;
+ return ctx.renderSource('$txt{{planet}} => |planet|$txt');
+ },
+ };
+
+ const String output = '<-{{planet}} => Earth->';
+
+ expect(parse(template).renderString(values), equals(output));
+ });
+
+ test('LambdaContext.lookup', () {
+ final Template t = Template('{{ foo }}');
+ final String s = t.renderString(<String, Object>{
+ 'foo': (LambdaContext lc) => lc.lookup('bar'),
+ 'bar': 'jim',
+ });
+ expect(s, equals('jim'));
+ });
+
+ test('LambdaContext.lookup closed', () {
+ final Template t = Template('{{ foo }}');
+ LambdaContext? lc2;
+ t.renderString(<String, Object>{
+ 'foo': (LambdaContext lc) => lc2 = lc,
+ 'bar': 'jim',
+ });
+ expect(lc2, isNotNull);
+ expect(() => lc2?.lookup('foo'), throwsException);
+ });
+ });
+
+ group('Other', () {
+ test('Standalone line', () {
+ final String val = parse(
+ '|\n{{#bob}}\n{{/bob}}\n|',
+ ).renderString(<String, List<dynamic>>{'bob': <dynamic>[]});
+ expect(val, equals('|\n|'));
+ });
+ });
+
+ group('Array indexing', () {
+ test('Basic', () {
+ final String val = parse('{{array.1}}').renderString(<String, List<int>>{
+ 'array': <int>[1, 2, 3],
+ });
+ expect(val, equals('2'));
+ });
+ test('RangeError', () {
+ final Exception? error = renderFail('{{array.5}}', <String, List<int>>{
+ 'array': <int>[1, 2, 3],
+ });
+ expect(error, isA<TemplateException>());
+ });
+ });
+
+ group('Delimiters', () {
+ test('Basic', () {
+ final String val = parse(
+ '{{=<% %>=}}(<%text%>)',
+ ).renderString(<String, String>{'text': 'Hey!'});
+ expect(val, equals('(Hey!)'));
+ });
+
+ test('Single delimiters', () {
+ final String val = parse(
+ '({{=[ ]=}}[text])',
+ ).renderString(<String, String>{'text': 'It worked!'});
+ expect(val, equals('(It worked!)'));
+ });
+ });
+
+ group('Template with custom delimiters', () {
+ test('Basic', () {
+ final Template t = Template('(<%text%>)', delimiters: '<% %>');
+ final String val = t.renderString(<String, String>{'text': 'Hey!'});
+ expect(val, equals('(Hey!)'));
+ });
+ });
+
+ group('Lambda context', () {
+ test('LambdaContext write', () {
+ const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
+ final Map<String, Null Function(LambdaContext ctx)> values =
+ <String, Null Function(LambdaContext ctx)>{
+ 'markdown': (LambdaContext ctx) {
+ ctx.write('foo');
+ },
+ };
+ const String output = '<foo>';
+ expect(parse(template).renderString(values), equals(output));
+ });
+
+ test('LambdaContext render', () {
+ const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
+ final Map<String, Object> values = <String, Object>{
+ 'content': 'bar',
+ 'markdown': (LambdaContext ctx) {
+ ctx.render();
+ },
+ };
+ const String output = '<bar>';
+ expect(parse(template).renderString(values), equals(output));
+ });
+
+ test('LambdaContext render with value', () {
+ const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
+ final Map<String, Null Function(LambdaContext ctx)> values =
+ <String, Null Function(LambdaContext ctx)>{
+ 'markdown': (LambdaContext ctx) {
+ ctx.render(value: <String, String>{'content': 'oi!'});
+ },
+ };
+ const String output = '<oi!>';
+ expect(parse(template).renderString(values), equals(output));
+ });
+
+ test('LambdaContext renderString with value', () {
+ const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
+ final Map<String, String Function(LambdaContext ctx)> values =
+ <String, String Function(LambdaContext ctx)>{
+ 'markdown': (LambdaContext ctx) {
+ return ctx.renderString(
+ value: <String, String>{'content': 'oi!'},
+ );
+ },
+ };
+ const String output = '<oi!>';
+ expect(parse(template).renderString(values), equals(output));
+ });
+
+ test('LambdaContext write and return', () {
+ const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
+ final Map<String, String Function(LambdaContext ctx)> values =
+ <String, String Function(LambdaContext ctx)>{
+ 'markdown': (LambdaContext ctx) {
+ ctx.write('foo');
+ return 'bar';
+ },
+ };
+ const String output = '<foobar>';
+ expect(parse(template).renderString(values), equals(output));
+ });
+
+ test('LambdaContext renderSource with value', () {
+ const String template = '<{{#markdown}}{{content}}{{/markdown}}>';
+ final Map<String, String Function(LambdaContext ctx)> values =
+ <String, String Function(LambdaContext ctx)>{
+ 'markdown': (LambdaContext ctx) {
+ return ctx.renderSource(
+ ctx.source,
+ value: <String, String>{'content': 'oi!'},
+ );
+ },
+ };
+ const String output = '<oi!>';
+ expect(parse(template).renderString(values), equals(output));
+ });
+ });
+}
+
+Exception? renderFail(String source, Object values) {
+ try {
+ parse(source).renderString(values);
+ return null;
+ } on Exception catch (e) {
+ return e;
+ }
+}
+
+void expectFail(
+ Exception? ex,
+ int? line,
+ int? column, [
+ String? msgStartsWith,
+]) {
+ if (ex is! TemplateException) {
+ fail('Unexpected type: $ex');
+ }
+ if (line != null) {
+ expect(ex.line, equals(line));
+ }
+ if (column != null) {
+ expect(ex.column, equals(column));
+ }
+ if (msgStartsWith != null) {
+ expect(ex.message, startsWith(msgStartsWith));
+ }
+}
diff --git a/third_party/packages/mustache_template/test/no_spec/whitespace.js b/third_party/packages/mustache_template/test/no_spec/whitespace.js
new file mode 100644
index 0000000..598ca1a
--- /dev/null
+++ b/third_party/packages/mustache_template/test/no_spec/whitespace.js
@@ -0,0 +1,52 @@
+var sys = require('sys');
+var mustache = require('mustache');
+
+function render(source, values) {
+ var output = mustache.to_html(source, values);
+ sys.puts(output);
+}
+
+render(
+ "{{ # # foo }} {{ oi }} {{ / # foo }}",
+ {'# foo': [{oi: 'OI!'}]}); // OI!
+
+render(
+ "{{ #foo }} {{ oi }} {{ /foo }}",
+ {'foo': [{oi: 'OI!'}]}); // OI!
+
+render(
+ "{{{ #foo }}} {{{ /foo }}}",
+ {'#foo': 1, '/foo': 2}); // 1 2
+
+render(
+ "{{{ { }}}",
+ {'{': 1}); // 1
+
+render(
+ "{{ > }}",
+ {'>': 'oi'}); // ''
+
+render(
+ "{{\nfoo}}",
+ {'foo': 'bar'}); // bar
+
+render(
+ "{{\tfoo}}",
+ {'foo': 'bar'}); // bar
+
+render(
+ "{{\t# foo}}oi{{\n/foo}}",
+ {foo: true}); // oi
+
+render(
+ "{{{\tfoo\t}}}",
+ {foo: true}); // oi
+
+
+//render(
+// "{{ { }}",
+// {'{': 1}); // ERROR unclosed tag
+
+//render(
+// "{{ { foo } }}",
+// {'foo': 1}); // ERROR unclosed tag
diff --git a/third_party/packages/mustache_template/test/no_spec/whitespace.py b/third_party/packages/mustache_template/test/no_spec/whitespace.py
new file mode 100644
index 0000000..7d40b31
--- /dev/null
+++ b/third_party/packages/mustache_template/test/no_spec/whitespace.py
@@ -0,0 +1,49 @@
+import pystache
+
+def render(source, values):
+ print pystache.render(source, values)
+
+render(
+ "{{ # # foo }} {{ oi }} {{ / # foo }}",
+ {'# foo': [{'oi': 'OI!'}]}) # OI!
+
+render(
+ "{{ #foo }} {{ oi }} {{ /foo }}",
+ {'foo': [{'oi': 'OI!'}]}) # OI!
+
+render(
+ "{{{ #foo }}} {{{ /foo }}}",
+ {'#foo': 1, '/foo': 2}) # 1 2
+
+render(
+ "{{{ { }}}",
+ {'{': 1}) # 1
+
+render(
+ "{{ > }}}",
+ {'>': 'oi'}) # "}" bug??
+
+render(
+ "{{\nfoo}}",
+ {'foo': 'bar'}) # // bar
+
+render(
+ "{{\tfoo}}",
+ {'foo': 'bar'}) # bar
+
+render(
+ "{{\t# foo}}oi{{\n/foo}}",
+ {'foo': True}) # oi
+
+render(
+ "{{{\tfoo\t}}}",
+ {'foo': True}) # oi
+
+# Don't work in mustache.js
+# render(
+# "{{ { }}",
+# {'{': 1}) # ERROR unclosed tag
+
+# render(
+# "{{ { foo } }}",
+# {'foo': 1}) # ERROR unclosed tag
diff --git a/third_party/packages/mustache_template/test/parser_test.dart b/third_party/packages/mustache_template/test/parser_test.dart
new file mode 100644
index 0000000..27ccadf
--- /dev/null
+++ b/third_party/packages/mustache_template/test/parser_test.dart
@@ -0,0 +1,442 @@
+// ignore_for_file: avoid_print
+
+import 'package:mustache_template/src/node.dart';
+import 'package:mustache_template/src/parser.dart';
+import 'package:mustache_template/src/scanner.dart';
+import 'package:mustache_template/src/template_exception.dart';
+import 'package:mustache_template/src/token.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('Scanner', () {
+ test('scan text', () {
+ const String source = 'abc';
+ final Scanner scanner = Scanner(source, 'foo', '{{ }}');
+ final List<Token> tokens = scanner.scan();
+ expectTokens(tokens, <Token>[Token(TokenType.text, 'abc', 0, 3)]);
+ });
+
+ test('scan tag', () {
+ const String source = 'abc{{foo}}def';
+ final Scanner scanner = Scanner(source, 'foo', '{{ }}');
+ final List<Token> tokens = scanner.scan();
+ expectTokens(tokens, <Token>[
+ Token(TokenType.text, 'abc', 0, 3),
+ Token(TokenType.openDelimiter, '{{', 3, 5),
+ Token(TokenType.identifier, 'foo', 5, 8),
+ Token(TokenType.closeDelimiter, '}}', 8, 10),
+ Token(TokenType.text, 'def', 10, 13),
+ ]);
+ });
+
+ test('scan tag whitespace', () {
+ const String source = 'abc{{ foo }}def';
+ final Scanner scanner = Scanner(source, 'foo', '{{ }}');
+ final List<Token> tokens = scanner.scan();
+ expectTokens(tokens, <Token>[
+ Token(TokenType.text, 'abc', 0, 3),
+ Token(TokenType.openDelimiter, '{{', 3, 5),
+ Token(TokenType.whitespace, ' ', 5, 6),
+ Token(TokenType.identifier, 'foo', 6, 9),
+ Token(TokenType.whitespace, ' ', 9, 10),
+ Token(TokenType.closeDelimiter, '}}', 10, 12),
+ Token(TokenType.text, 'def', 12, 15),
+ ]);
+ });
+
+ test('scan tag sigil', () {
+ const String source = 'abc{{ # foo }}def';
+ final Scanner scanner = Scanner(source, 'foo', '{{ }}');
+ final List<Token> tokens = scanner.scan();
+ expectTokens(tokens, <Token>[
+ Token(TokenType.text, 'abc', 0, 3),
+ Token(TokenType.openDelimiter, '{{', 3, 5),
+ Token(TokenType.whitespace, ' ', 5, 6),
+ Token(TokenType.sigil, '#', 6, 7),
+ Token(TokenType.whitespace, ' ', 7, 8),
+ Token(TokenType.identifier, 'foo', 8, 11),
+ Token(TokenType.whitespace, ' ', 11, 12),
+ Token(TokenType.closeDelimiter, '}}', 12, 14),
+ Token(TokenType.text, 'def', 14, 17),
+ ]);
+ });
+
+ test('scan tag dot', () {
+ const String source = 'abc{{ foo.bar }}def';
+ final Scanner scanner = Scanner(source, 'foo', '{{ }}');
+ final List<Token> tokens = scanner.scan();
+ expectTokens(tokens, <Token>[
+ Token(TokenType.text, 'abc', 0, 3),
+ Token(TokenType.openDelimiter, '{{', 3, 5),
+ Token(TokenType.whitespace, ' ', 5, 6),
+ Token(TokenType.identifier, 'foo', 6, 9),
+ Token(TokenType.dot, '.', 9, 10),
+ Token(TokenType.identifier, 'bar', 10, 13),
+ Token(TokenType.whitespace, ' ', 13, 14),
+ Token(TokenType.closeDelimiter, '}}', 14, 16),
+ Token(TokenType.text, 'def', 16, 19),
+ ]);
+ });
+
+ test('scan triple mustache', () {
+ const String source = 'abc{{{foo}}}def';
+ final Scanner scanner = Scanner(source, 'foo', '{{ }}');
+ final List<Token> tokens = scanner.scan();
+ expectTokens(tokens, <Token>[
+ Token(TokenType.text, 'abc', 0, 3),
+ Token(TokenType.openDelimiter, '{{{', 3, 6),
+ Token(TokenType.identifier, 'foo', 6, 9),
+ Token(TokenType.closeDelimiter, '}}}', 9, 12),
+ Token(TokenType.text, 'def', 12, 15),
+ ]);
+ });
+
+ test('scan triple mustache whitespace', () {
+ const String source = 'abc{{{ foo }}}def';
+ final Scanner scanner = Scanner(source, 'foo', '{{ }}');
+ final List<Token> tokens = scanner.scan();
+ expectTokens(tokens, <Token>[
+ Token(TokenType.text, 'abc', 0, 3),
+ Token(TokenType.openDelimiter, '{{{', 3, 6),
+ Token(TokenType.whitespace, ' ', 6, 7),
+ Token(TokenType.identifier, 'foo', 7, 10),
+ Token(TokenType.whitespace, ' ', 10, 11),
+ Token(TokenType.closeDelimiter, '}}}', 11, 14),
+ Token(TokenType.text, 'def', 14, 17),
+ ]);
+ });
+
+ test('scan tag with equals', () {
+ const String source = '{{foo=bar}}';
+ final Scanner scanner = Scanner(source, 'foo', '{{ }}');
+ final List<Token> tokens = scanner.scan();
+ expectTokens(tokens, <Token>[
+ Token(TokenType.openDelimiter, '{{', 0, 2),
+ Token(TokenType.identifier, 'foo=bar', 2, 9),
+ Token(TokenType.closeDelimiter, '}}', 9, 11),
+ ]);
+ });
+
+ test('scan comment with equals', () {
+ const String source = '{{!foo=bar}}';
+ final Scanner scanner = Scanner(source, 'foo', '{{ }}');
+ final List<Token> tokens = scanner.scan();
+ expectTokens(tokens, <Token>[
+ Token(TokenType.openDelimiter, '{{', 0, 2),
+ Token(TokenType.sigil, '!', 2, 3),
+ Token(TokenType.identifier, 'foo=bar', 3, 10),
+ Token(TokenType.closeDelimiter, '}}', 10, 12),
+ ]);
+ });
+ });
+
+ group('Parser', () {
+ test('parse variable', () {
+ const String source = 'abc{{foo}}def';
+ final Parser parser = Parser(source, 'foo', '{{ }}');
+ final List<Node> nodes = parser.parse();
+ expectNodes(nodes, <Node>[
+ TextNode('abc', 0, 3),
+ VariableNode('foo', 3, 10),
+ TextNode('def', 10, 13),
+ ]);
+ });
+
+ test('parse variable whitespace', () {
+ const String source = 'abc{{ foo }}def';
+ final Parser parser = Parser(source, 'foo', '{{ }}');
+ final List<Node> nodes = parser.parse();
+ expectNodes(nodes, <Node>[
+ TextNode('abc', 0, 3),
+ VariableNode('foo', 3, 12),
+ TextNode('def', 12, 15),
+ ]);
+ });
+
+ test('parse section', () {
+ const String source = 'abc{{#foo}}def{{/foo}}ghi';
+ final Parser parser = Parser(source, 'foo', '{{ }}');
+ final List<Node> nodes = parser.parse();
+ expectNodes(nodes, <Node>[
+ TextNode('abc', 0, 3),
+ SectionNode('foo', 3, 11, '{{ }}'),
+ TextNode('ghi', 22, 25),
+ ]);
+ expectNodes((nodes[1] as SectionNode).children, <Node>[
+ TextNode('def', 11, 14),
+ ]);
+ });
+
+ test('parse section standalone tag whitespace', () {
+ const String source = 'abc\n{{#foo}}\ndef\n{{/foo}}\nghi';
+ final Parser parser = Parser(source, 'foo', '{{ }}');
+ final List<Node> nodes = parser.parse();
+ expectNodes(nodes, <Node>[
+ TextNode('abc\n', 0, 4),
+ SectionNode('foo', 4, 12, '{{ }}'),
+ TextNode('ghi', 26, 29),
+ ]);
+ expectNodes((nodes[1] as SectionNode).children, <Node>[
+ TextNode('def\n', 13, 17),
+ ]);
+ });
+
+ test('parse section standalone tag whitespace consecutive', () {
+ const String source =
+ 'abc\n{{#foo}}\ndef\n{{/foo}}\n{{#foo}}\ndef\n{{/foo}}\nghi';
+ final Parser parser = Parser(source, 'foo', '{{ }}');
+ final List<Node> nodes = parser.parse();
+ expectNodes(nodes, <Node>[
+ TextNode('abc\n', 0, 4),
+ SectionNode('foo', 4, 12, '{{ }}'),
+ SectionNode('foo', 26, 34, '{{ }}'),
+ TextNode('ghi', 48, 51),
+ ]);
+ expectNodes((nodes[1] as SectionNode).children, <Node>[
+ TextNode('def\n', 13, 17),
+ ]);
+ });
+
+ test('parse section standalone tag whitespace on first line', () {
+ const String source = ' {{#foo}} \ndef\n{{/foo}}\nghi';
+ final Parser parser = Parser(source, 'foo', '{{ }}');
+ final List<Node> nodes = parser.parse();
+ expectNodes(nodes, <Node>[
+ SectionNode('foo', 2, 10, '{{ }}'),
+ TextNode('ghi', 26, 29),
+ ]);
+ expectNodes((nodes[0] as SectionNode).children, <Node>[
+ TextNode('def\n', 13, 17),
+ ]);
+ });
+
+ test('parse section standalone tag whitespace on last line', () {
+ const String source = '{{#foo}}def\n {{/foo}} ';
+ final Parser parser = Parser(source, 'foo', '{{ }}');
+ final List<Node> nodes = parser.parse();
+ expectNodes(nodes, <Node>[SectionNode('foo', 0, 8, '{{ }}')]);
+ expectNodes((nodes[0] as SectionNode).children, <Node>[
+ TextNode('def\n', 8, 12),
+ ]);
+ });
+
+ test('parse variable newline', () {
+ const String source = 'abc\n\n{{foo}}def';
+ final Parser parser = Parser(source, 'foo', '{{ }}');
+ final List<Node> nodes = parser.parse();
+ expectNodes(nodes, <Node>[
+ TextNode('abc\n\n', 0, 5),
+ VariableNode('foo', 5, 12),
+ TextNode('def', 12, 15),
+ ]);
+ });
+
+ test('parse section standalone tag whitespace v2', () {
+ const String source = 'abc\n\n{{#foo}}\ndef\n{{/foo}}\nghi';
+ final Parser parser = Parser(source, 'foo', '{{ }}');
+ final List<Node> nodes = parser.parse();
+ expectNodes(nodes, <Node>[
+ TextNode('abc\n\n', 0, 5),
+ SectionNode('foo', 5, 13, '{{ }}'),
+ TextNode('ghi', 27, 30),
+ ]);
+ expectNodes((nodes[1] as SectionNode).children, <Node>[
+ TextNode('def\n', 14, 18),
+ ]);
+ });
+
+ test('parse whitespace', () {
+ const String source = 'abc\n ';
+ final Parser parser = Parser(source, 'foo', '{{ }}');
+ final List<Node> nodes = parser.parse();
+ expectNodes(nodes, <Node>[TextNode('abc\n ', 0, 7)]);
+ });
+
+ test('parse partial', () {
+ const String source = 'abc\n {{>foo}}def';
+ final Parser parser = Parser(source, 'foo', '{{ }}');
+ final List<Node> nodes = parser.parse();
+ expectNodes(nodes, <Node>[
+ TextNode('abc\n ', 0, 7),
+ PartialNode('foo', 7, 15, ' '),
+ TextNode('def', 15, 18),
+ ]);
+ });
+
+ test('parse change delimiters', () {
+ const String source = '{{= | | =}}<|#lambda|-|/lambda|>';
+ final Parser parser = Parser(source, 'foo', '{{ }}');
+ final List<Node> nodes = parser.parse();
+ expectNodes(nodes, <Node>[
+ TextNode('<', 11, 12),
+ SectionNode('lambda', 12, 21, '| |'),
+ TextNode('>', 31, 32),
+ ]);
+ expect((nodes[1] as SectionNode).delimiters, equals('| |'));
+ expectNodes((nodes[1] as SectionNode).children, <Node>[
+ TextNode('-', 21, 22),
+ ]);
+ });
+
+ test('corner case strict', () {
+ const String source = '{{{ #foo }}} {{{ /foo }}}';
+ final Parser parser = Parser(source, 'foo', '{{ }}');
+ try {
+ parser.parse();
+ // TODO(stuartmorgan): Restructure test to use throwsA.
+ // ignore: use_test_throws_matchers
+ fail('Should fail.');
+ } on Exception catch (e) {
+ expect(e is TemplateException, isTrue);
+ }
+ });
+
+ test('corner case lenient', () {
+ const String source = '{{{ #foo }}} {{{ /foo }}}';
+ final Parser parser = Parser(source, 'foo', '{{ }}', lenient: true);
+ final List<Node> nodes = parser.parse();
+ expectNodes(nodes, <Node>[
+ VariableNode('#foo', 0, 12, escape: false),
+ TextNode(' ', 12, 13),
+ VariableNode('/foo', 13, 25, escape: false),
+ ]);
+ });
+
+ test('toString', () {
+ TextNode('foo', 1, 3).toString();
+ VariableNode('foo', 1, 3).toString();
+ PartialNode('foo', 1, 3, ' ').toString();
+ SectionNode('foo', 1, 3, '{{ }}').toString();
+ Token(TokenType.closeDelimiter, 'foo', 1, 3).toString();
+ TokenType.closeDelimiter.toString();
+ });
+
+ test('exception', () {
+ const String source =
+ "'{{ foo }} sdfffffffffffffffffffffffffffffffffffffffffffff "
+ 'dsfsdf sdfdsa fdsfads fsdfdsfadsf dsfasdfsdf sdfdsfsadf sdfadsfsdf ';
+ final TemplateException ex = TemplateException(
+ 'boom!',
+ 'foo.mustache',
+ source,
+ 2,
+ );
+ ex.toString();
+ });
+
+ Exception parseFail(String source) {
+ try {
+ final Parser parser = Parser(source, 'foo', '{{ }}');
+ parser.parse();
+ // TODO(stuartmorgan): Restructure test to use throwsA.
+ // ignore: use_test_throws_matchers
+ fail('Did not throw.');
+ } on Exception catch (ex, st) {
+ if (ex is! TemplateException) {
+ print(ex);
+ print(st);
+ }
+ return ex;
+ }
+ }
+
+ test('parse eof', () {
+ void expectTemplateEx(Exception ex) =>
+ expect(ex is TemplateException, isTrue);
+
+ expectTemplateEx(parseFail('{{#foo}}{{bar}}{{/foo}'));
+ expectTemplateEx(parseFail('{{#foo}}{{bar}}{{/foo'));
+ expectTemplateEx(parseFail('{{#foo}}{{bar}}{{/'));
+ expectTemplateEx(parseFail('{{#foo}}{{bar}}{{'));
+ expectTemplateEx(parseFail('{{#foo}}{{bar}}{'));
+ expectTemplateEx(parseFail('{{#foo}}{{bar}}'));
+ expectTemplateEx(parseFail('{{#foo}}{{bar}'));
+ expectTemplateEx(parseFail('{{#foo}}{{bar'));
+ expectTemplateEx(parseFail('{{#foo}}{{'));
+ expectTemplateEx(parseFail('{{#foo}}{'));
+ expectTemplateEx(parseFail('{{#foo}}'));
+ expectTemplateEx(parseFail('{{#foo}'));
+ expectTemplateEx(parseFail('{{#'));
+ expectTemplateEx(parseFail('{{'));
+ expectTemplateEx(parseFail('{{ # foo }}{{ bar }}{{ / foo }'));
+ expectTemplateEx(parseFail('{{ # foo }}{{ bar }}{{ / foo '));
+ expectTemplateEx(parseFail('{{ # foo }}{{ bar }}{{ / foo'));
+ expectTemplateEx(parseFail('{{ # foo }}{{ bar }}{{ / '));
+ expectTemplateEx(parseFail('{{ # foo }}{{ bar }}{{ /'));
+ expectTemplateEx(parseFail('{{ # foo }}{{ bar }}{{ '));
+ expectTemplateEx(parseFail('{{ # foo }}{{ bar }}{{'));
+ expectTemplateEx(parseFail('{{ # foo }}{{ bar }}{'));
+ expectTemplateEx(parseFail('{{ # foo }}{{ bar }}'));
+ expectTemplateEx(parseFail('{{ # foo }}{{ bar }'));
+ expectTemplateEx(parseFail('{{ # foo }}{{ bar '));
+ expectTemplateEx(parseFail('{{ # foo }}{{ bar'));
+ expectTemplateEx(parseFail('{{ # foo }}{{ '));
+ expectTemplateEx(parseFail('{{ # foo }}{{'));
+ expectTemplateEx(parseFail('{{ # foo }}{'));
+ expectTemplateEx(parseFail('{{ # foo }}'));
+ expectTemplateEx(parseFail('{{ # foo }'));
+ expectTemplateEx(parseFail('{{ # foo '));
+ expectTemplateEx(parseFail('{{ # foo'));
+ expectTemplateEx(parseFail('{{ # '));
+ expectTemplateEx(parseFail('{{ #'));
+ expectTemplateEx(parseFail('{{ '));
+ expectTemplateEx(parseFail('{{'));
+
+ expectTemplateEx(parseFail('{{= || || =}'));
+ expectTemplateEx(parseFail('{{= || || ='));
+ expectTemplateEx(parseFail('{{= || || '));
+ expectTemplateEx(parseFail('{{= || ||'));
+ expectTemplateEx(parseFail('{{= || |'));
+ expectTemplateEx(parseFail('{{= || '));
+ expectTemplateEx(parseFail('{{= ||'));
+ expectTemplateEx(parseFail('{{= |'));
+ expectTemplateEx(parseFail('{{= '));
+ expectTemplateEx(parseFail('{{='));
+ });
+ });
+}
+
+bool nodeEqual(Node a, Node b) {
+ if (a is TextNode) {
+ return b is TextNode &&
+ a.text == b.text &&
+ a.start == b.start &&
+ a.end == b.end;
+ } else if (a is VariableNode && b is VariableNode) {
+ return a.name == b.name &&
+ a.escape == b.escape &&
+ a.start == b.start &&
+ a.end == b.end;
+ } else if (a is SectionNode && b is SectionNode) {
+ return a.name == b.name &&
+ a.delimiters == b.delimiters &&
+ a.inverse == b.inverse &&
+ a.start == b.start &&
+ a.end == b.end;
+ } else if (a is PartialNode && b is PartialNode) {
+ return a.name == b.name && a.indent == b.indent;
+ } else {
+ return false;
+ }
+}
+
+bool tokenEqual(Token a, Token b) {
+ return a.type == b.type &&
+ a.value == b.value &&
+ a.start == b.start &&
+ a.end == b.end;
+}
+
+void expectTokens(List<Token> a, List<Token> b) {
+ expect(a.length, equals(b.length), reason: '$a != $b');
+ for (int i = 0; i < a.length; i++) {
+ expect(tokenEqual(a[i], b[i]), isTrue, reason: '$a != $b');
+ }
+}
+
+void expectNodes(List<Node> a, List<Node> b) {
+ expect(a.length, equals(b.length), reason: '$a != $b');
+ for (int i = 0; i < a.length; i++) {
+ expect(nodeEqual(a[i], b[i]), isTrue, reason: '$a != $b');
+ }
+}