Prepare for 0.20.3 and fork mustache4dart into Dartdoc (#1734)

* Fork mustache4dart into dartdoc

* remove accidental submodule

* Prepare for 0.20.3 and fork mustache4dart into Dartdoc for 2.0 stable

* Fix up tests and tuple dependency in test package

* Add sdk dependency

* Use flutter repo's Dart when building flutter plugin docs
diff --git a/.travis.yml b/.travis.yml
index 111f9b8..ba30510 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,15 +2,16 @@
 sudo: false
 dart:
   - "dev/raw/latest"
+  - stable
 env:
   - DARTDOC_BOT=main
   - DARTDOC_BOT=packages
   - DARTDOC_BOT=flutter
   - DARTDOC_BOT=sdk-docs
 script: ./tool/travis.sh
+
 branches:
-  only:
-    - master
+  only: [master]
 cache:
   directories:
   - $HOME/.pub-cache
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0969f6..8d6713b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,11 @@
+## 0.20.3
+* Update dependencies and fork mustache4dart into dartdoc so dartdoc can resolve
+  dependencies on Dart 2.0 stable.
+
 ## 0.20.2
 * Fix void problems (#1724)
-* Fix crash building Angular docs and problems involving special objects (#1728,
-  #1554)
+* Fix crash building Angular docs and problems involving special objects
+  (#1728, #1554)
 * Run pub upgrade to get packages ready for 69.2.
 
 ## 0.20.1
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 83921c5..0f2ed2c 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -6,6 +6,7 @@
     - 'doc/**'
     - 'lib/templates/*.html'
     - 'pub.dartlang.org/**'
+    - 'third_party/**'
     - 'testing/**'
     - 'testing/test_package_flutter_plugin/**'
 linter:
diff --git a/lib/dartdoc.dart b/lib/dartdoc.dart
index 4d4311f..d01b1a9 100644
--- a/lib/dartdoc.dart
+++ b/lib/dartdoc.dart
@@ -37,7 +37,7 @@
 
 const String name = 'dartdoc';
 // Update when pubspec version changes.
-const String dartdocVersion = '0.20.3-dev';
+const String dartdocVersion = '0.20.3';
 
 /// Helper class to initialize the default generators since they require
 /// GeneratorContext.
diff --git a/pubspec.lock b/pubspec.lock
index 8770531..0f07e23 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -291,9 +291,9 @@
   mustache4dart:
     dependency: "direct main"
     description:
-      name: mustache4dart
-      url: "https://pub.dartlang.org"
-    source: hosted
+      path: "third_party/pkg/mustache4dart"
+      relative: true
+    source: path
     version: "2.1.2"
   node_preamble:
     dependency: transitive
@@ -499,4 +499,4 @@
     source: hosted
     version: "2.1.15"
 sdks:
-  dart: ">=2.0.0-dev.68 <=2.0.0-dev.69.3"
+  dart: ">=2.0.0-dev.68 <3.0.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 25b6b43..f402ea4 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
 name: dartdoc
 # Also update the `version` field in lib/dartdoc.dart.
-version: 0.20.3-dev
+version: 0.20.3
 author: Dart Team <misc@dartlang.org>
 description: A documentation generator for Dart.
 homepage: https://github.com/dart-lang/dartdoc
@@ -18,7 +18,10 @@
   http_parser: '>=3.0.3 <4.0.0'
   logging: ^0.11.3+1
   markdown: ^2.0.0
-  mustache4dart: ^2.1.2
+  # TODO(jcollins-g): return to using dependency when mustache4dart is
+  # supported on 2.0.
+  #mustache4dart: ^2.1.2
+  mustache4dart: {path: 'third_party/pkg/mustache4dart'}
   package_config: '>=0.1.5 <2.0.0'
   path: ^1.3.0
   pub_semver: ^1.3.7
diff --git a/test/compare_output_test.dart b/test/compare_output_test.dart
index b29a583..3abc771 100644
--- a/test/compare_output_test.dart
+++ b/test/compare_output_test.dart
@@ -83,7 +83,7 @@
         '--example-path-prefix',
         'examples',
         '--exclude-packages',
-        'Dart,meta,tuple,quiver_hashcode',
+        'Dart,matcher,meta,path,stack_trace,tuple,quiver',
         '--hide-sdk-text',
         '--no-include-source',
         '--output',
diff --git a/test/dartdoc_test.dart b/test/dartdoc_test.dart
index 691cfc1..423e206 100644
--- a/test/dartdoc_test.dart
+++ b/test/dartdoc_test.dart
@@ -90,7 +90,7 @@
       expect(
           useSomethingInAnotherPackage.modelType.linkedName,
           startsWith(
-              '<a href="https://pub.dartlang.org/documentation/tuple/1.0.1/tuple/Tuple2-class.html">Tuple2</a>'));
+              '<a href="https://pub.dartlang.org/documentation/tuple/1.0.2/tuple/Tuple2-class.html">Tuple2</a>'));
       RegExp stringLink = new RegExp(
           'https://api.dartlang.org/(dev|stable|edge|be)/${Platform.version.split(' ').first}/dart-core/String-class.html">String</a>');
       expect(useSomethingInAnotherPackage.modelType.linkedName,
diff --git a/test/model_test.dart b/test/model_test.dart
index ca93b77..7b2451b 100644
--- a/test/model_test.dart
+++ b/test/model_test.dart
@@ -227,7 +227,7 @@
       });
 
       test('multiple packages, sorted default', () {
-        expect(ginormousPackageGraph.localPackages, hasLength(6));
+        expect(ginormousPackageGraph.localPackages, hasLength(9));
         expect(ginormousPackageGraph.localPackages.first.name,
             equals('test_package'));
       });
diff --git a/testing/test_package/pubspec.yaml b/testing/test_package/pubspec.yaml
index b8e3ebc..3088301 100644
--- a/testing/test_package/pubspec.yaml
+++ b/testing/test_package/pubspec.yaml
@@ -4,6 +4,6 @@
 version: 0.0.1
 dependencies:
   meta: ^1.0.0
-  tuple: 1.0.1
+  tuple: 1.0.2
   test_package_imported:
     path: "../test_package_imported"
diff --git a/testing/test_package_docs/index.html b/testing/test_package_docs/index.html
index 5087dbc..112d171 100644
--- a/testing/test_package_docs/index.html
+++ b/testing/test_package_docs/index.html
@@ -4,7 +4,7 @@
   <meta charset="utf-8">
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <meta name="viewport" content="width=device-width, initial-scale=1">
-  <meta name="generator" content="made with love by dartdoc 0.20.3-dev">
+  <meta name="generator" content="made with love by dartdoc 0.20.3">
   <meta name="description" content="test_package API docs, for the Dart programming language.">
   <title>test_package - Dart API docs</title>
 
diff --git a/testing/test_package_imported/pubspec.lock b/testing/test_package_imported/pubspec.lock
index c427295..8f70af8 100644
--- a/testing/test_package_imported/pubspec.lock
+++ b/testing/test_package_imported/pubspec.lock
@@ -1,5 +1,5 @@
 # Generated by pub
-# See http://pub.dartlang.org/doc/glossary.html#lockfile
+# See https://www.dartlang.org/tools/pub/glossary#lockfile
 packages: {}
 sdks:
-  dart: any
+  dart: ">=2.0.0-dev.68 <3.0.0"
diff --git a/testing/test_package_imported/pubspec.yaml b/testing/test_package_imported/pubspec.yaml
index 79bc5a7..31338a7 100644
--- a/testing/test_package_imported/pubspec.yaml
+++ b/testing/test_package_imported/pubspec.yaml
@@ -1,3 +1,5 @@
 name: test_package_imported
 description: Doomed to be imported from other packages
 version: 0.0.1
+environment:
+  sdk: '>=2.0.0-dev.68 <3.0.0'
diff --git a/third_party/pkg/000-mustache4dart-pubspec.patch b/third_party/pkg/000-mustache4dart-pubspec.patch
new file mode 100644
index 0000000..3303030
--- /dev/null
+++ b/third_party/pkg/000-mustache4dart-pubspec.patch
@@ -0,0 +1,11 @@
+--- mustache4dart/pubspec.yaml.old	2018-08-06 10:02:45.152305202 -0700
++++ mustache4dart/pubspec.yaml	2018-08-06 10:02:53.036245297 -0700
+@@ -4,7 +4,7 @@
+ description: A mustache implementation for the Dart language
+ homepage: https://github.com/valotas/mustache4dart
+ environment:
+-  sdk: '>=0.8.10+6 <2.0.0'
++  sdk: '>=0.8.10+6 <3.0.0'
+ dev_dependencies:
+   test: '>=0.12.0 <0.12.23'
+   isolate: '>=1.0.0 <1.1.0'
diff --git a/third_party/pkg/mustache4dart/.gitignore b/third_party/pkg/mustache4dart/.gitignore
new file mode 100644
index 0000000..a1d6325
--- /dev/null
+++ b/third_party/pkg/mustache4dart/.gitignore
@@ -0,0 +1,8 @@
+.idea/
+bin/
+build/
+packages
+*.swp
+*~
+pubspec.lock
+.packages
diff --git a/third_party/pkg/mustache4dart/.gitmodules b/third_party/pkg/mustache4dart/.gitmodules
new file mode 100644
index 0000000..54b6158
--- /dev/null
+++ b/third_party/pkg/mustache4dart/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "spec"]
+	path = spec
+	url = https://github.com/mustache/spec.git
diff --git a/third_party/pkg/mustache4dart/.travis.yml b/third_party/pkg/mustache4dart/.travis.yml
new file mode 100644
index 0000000..5e95797
--- /dev/null
+++ b/third_party/pkg/mustache4dart/.travis.yml
@@ -0,0 +1,21 @@
+language: dart
+
+dart:
+  - dev
+  - stable
+  - 1.23.0
+  - 1.22.0
+  - 1.21.1
+  - 1.20.1
+  - 1.19.1
+
+before_script:
+  - export DISPLAY=:99.0
+  - sh -e /etc/init.d/xvfb start &
+  - sleep 3
+
+script: ./build.sh
+
+branches:
+  only:
+    - master
diff --git a/third_party/pkg/mustache4dart/CHANGELOG.md b/third_party/pkg/mustache4dart/CHANGELOG.md
new file mode 100644
index 0000000..886eb1d
--- /dev/null
+++ b/third_party/pkg/mustache4dart/CHANGELOG.md
@@ -0,0 +1,48 @@
+# CHANGELOG
+
+## 2.1.1 (2018-04-14)
+
+* Addressed Dart 2 analysis issues [#70](https://github.com/valotas/mustache4dart/pull/70)
+* Run tests against v2 vm of Dart
+
+## 2.1.0 (2017-06-21)
+
+* Mirrors improvements [#65](https://github.com/valotas/mustache4dart/pull/65)
+
+## 2.0.0 (2017-06-13)
+
+* Rework MustacheContext ([#64][pr-64])
+* Use dartanalyzer `--strong` mode ([#61](https://github.com/valotas/mustache4dart/issues/61))
+
+As part of the [MustacheContext rework][pr-64], a couple of simplifications have been made. Most
+notable one is the drop support of mirroring methods starting with `get` as it does not make any
+sense with dart. Use a getter instead.
+
+[pr-64]: https://github.com/valotas/mustache4dart/pull/64
+
+## 1.1.0 (2017-05-10)
+
+* Avoid trapping exceptions by using reflection ([#59](https://github.com/valotas/mustache4dart/pull/59))
+
+## 1.0.12 (2017-03-17)
+
+* Maintenance release
+
+## 1.0.11 (2017-03-02)
+
+* Fixed issue with default value of boolean arguments
+* Compile method now returns type annotation [issue](https://github.com/valotas/mustache4dart/issues/50)
+
+## 1.0.10 (2015-03-15)
+
+* Allow iterables to get queried for empty or isEmpty attribute [issue](https://github.com/valotas/mustache4dart/issues/44)
+
+## 1.0.9 (2015-02-13)
+
+* throw exception on missing property (helps debugging and tracking down errors) [issue](https://github.com/valotas/mustache4dart/issues/36)
+* introduced assumeNullNonExistingProperty (the difference between a null field and a non-existent field) [issue](https://github.com/valotas/mustache4dart/issues/41)
+* Provide lambdas with the current nested context when they have two parameters (lambdas can now render their contents when inside of a loop)  [issue](https://github.com/valotas/mustache4dart/issues/39)
+
+## 1.0.8 (2015-02-01)
+
+* Find property names in superclasses [issue](https://github.com/valotas/mustache4dart/issues/33)
diff --git a/third_party/pkg/mustache4dart/LICENSE.txt b/third_party/pkg/mustache4dart/LICENSE.txt
new file mode 100644
index 0000000..ca5ef50
--- /dev/null
+++ b/third_party/pkg/mustache4dart/LICENSE.txt
@@ -0,0 +1,13 @@
+Copyright (C) 2012 Georgios Valotasios. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/third_party/pkg/mustache4dart/README.md b/third_party/pkg/mustache4dart/README.md
new file mode 100644
index 0000000..79345ff
--- /dev/null
+++ b/third_party/pkg/mustache4dart/README.md
@@ -0,0 +1,145 @@
+# Mustache for Dart
+
+[![Build Status](https://travis-ci.org/valotas/mustache4dart.svg?branch=master)](https://travis-ci.org/valotas/mustache4dart)
+[![Coverage Status](https://coveralls.io/repos/github/valotas/mustache4dart/badge.svg?branch=master)](https://coveralls.io/github/valotas/mustache4dart?branch=master)
+
+A simple implementation of [Mustache][mustache] for the
+[Dart language][dartlang], which passes happily all the
+[mustache v1.1.2+λ specs][specs]. If you want to have a look at how it works,
+just check the [tests][tests]. For more info, just read further.
+
+Using it
+--------
+In order to use the library, just add it to your `pubspec.yaml` as a dependency
+
+	dependencies:
+	  mustache4dart: '>= 2.0.0 < 3.0.0'
+
+and then import the package
+
+```dart
+import 'package:mustache4dart/mustache4dart.dart';
+```
+
+and you are good to go. You can use the render toplevel function to render your
+template.
+
+For example:
+
+```dart
+var salutation = render('Hello {{name}}!', {'name': 'Bob'});
+print(salutation); //shoud print Hello Bob!
+```
+
+### Context objects
+mustache4dart will look at your given object for operators, fields or methods.
+For example, if you give the template `{{firstname}}` for rendering,
+mustache4dart will try the followings
+
+1. use the `[]` operator with `firstname` as the parameter
+2. search for a field named `firstname`
+3. search for a getter named `firstname`
+4. search for a method named `firstname` (see Lambdas support)
+
+in each case the first valid value will be used.
+
+#### @MirrorsUsed
+In order to do the stuff described above the mirror library is being used which
+could lead to big js files when compiling the library with dartjs. In order to
+preserve the type information you have to annotate the objects used as
+contextes with `@MirrorsUsed`. Have in mind though that [as documented][mirrorsused]
+this is experimental.
+
+In order to avoid the use of the mirrors package, make sure that you compile
+your library with `dart2js -DMIRRORS=false `. In that case though you must
+always make sure that your context object have a right implementation of the
+`[]` operator as no other checks on the object will be available.
+
+### Partials
+mustache4dart support partials but it needs somehow to know how to find a
+partial. You can do that by providing a function that returns a template
+given a name:
+
+```dart
+String partialProvider(String partialName) => "this is the partial with name: ${partialName}";
+expect(render('[{{>p}}]', null, partial: partialProvider), '[this is the partial with name: p]'));
+```
+
+### Compiling to functions
+If you have a template that you are going to reuse with different contexts,
+you can compile it to a function using the toplevel function compile:
+
+```dart
+var salut = compile('Hello {{name}}!');
+print(salut({'name': 'Alice'})); //should print Hello Alice!
+``` 
+
+### Lambdas support
+The library passes all the optional [lambda specs][lambda_specs] based on
+which lambdas must be treatable as arity 0 or 1 functions.
+As dart provides optional named parameters, you can pass to a given lambda
+function the `nestedContext`. In that case the current nested context will be
+given as parameter to the lambda function.
+
+
+Developing
+----------
+The project passes all the [Mustache specs][specs].  You have to make sure
+though that you've downloaded them. Just make sure that you have done the
+steps described below.
+
+```sh
+git clone git://github.com/valotas/mustache4dart.git
+git submodule init
+git submodule update
+pub get
+```
+
+If you are with Linux, you can use what [travis][travis] does:
+
+```sh
+./build.sh
+```
+
+Alternatively, if you have [Dart Test Runner][testrunner] installed you can
+just do:
+
+```
+pub global run test_runner
+```
+
+### Observatory
+
+To start the observatory after running test:
+
+```sh
+dart --pause-isolates-on-exit --enable-vm-service=NNNN ./test/mustache_all.dart
+```
+
+Then [`coverage`][coverage] can be used in order to collect and format data:
+
+```sh
+pub global run coverage:collect_coverage --uri=http://... -o /tmp/mustache4dart.coverage.json --resume-isolates
+pub global run coverage:format_coverage --packages=app_package/.packages -i /tmp/mustache4dart.coverage.json
+```
+
+Contributing
+------------
+If you found a bug, just create a [new issue][new_issue] or even better fork
+and issue a pull request with you fix.
+
+Versioning
+----------
+The library will follow a [semantic versioning][semver]
+
+[mustache]: http://mustache.github.com/
+[dartlang]: https://www.dartlang.org/
+[tests]: http://github.com/valotas/mustache4dart/blob/master/test/mustache_tests.dart
+[specs]: http://github.com/mustache/spec
+[lambda_specs]: https://github.com/mustache/spec/blob/master/specs/~lambdas.yml
+[new_issue]: https://github.com/valotas/mustache4dart/issues/new
+[semver]: http://semver.org/
+[mirrorsused]: https://api.dartlang.org/apidocs/channels/stable/#dart-mirrors.MirrorsUsed
+[testrunner]: https://pub.dartlang.org/packages/test_runner
+[travis]: https://travis-ci.org/valotas/mustache4dart
+[coverage]: https://pub.dartlang.org/packages/coverage
diff --git a/third_party/pkg/mustache4dart/build.sh b/third_party/pkg/mustache4dart/build.sh
new file mode 100755
index 0000000..342530d
--- /dev/null
+++ b/third_party/pkg/mustache4dart/build.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+# bail on error
+set -e
+
+echo "Analyzing with `dartanalyzer --version`"
+dartanalyzer="dartanalyzer --strong --fatal-warnings lib/*.dart test/*.dart"
+if [ "$TRAVIS_DART_VERSION" = "dev" ]; then
+  dartanalyzer="$dartanalyzer --preview-dart-2"
+fi
+$dartanalyzer
+
+pub deps
+
+# run the tests
+pub run test
+
+# Only run with the stable version of dart.
+if [ "$TRAVIS_DART_VERSION" = "stable" ]; then
+  pub global activate dart_style
+  dirty_code=$(pub global run dart_style:format --dry-run lib/ test/ example/)
+  if [[ -n "$dirty_code" ]]; then
+    echo Unformatted files:
+    echo "$dirty_code" | sed 's/^/    /'
+    exit 1
+  else
+    echo All Dart source files are formatted.
+  fi
+
+  # Install dart_coveralls; gather and send coverage data.
+  if [ "$COVERALLS_TOKEN" ]; then
+    pub global activate dart_coveralls
+    pub global run dart_coveralls report \
+      --retry 2 \
+      --exclude-test-files \
+      test/mustache_all.dart
+  fi
+
+
+  pub run test -p chrome,firefox
+fi
diff --git a/third_party/pkg/mustache4dart/example/simpleusage.dart b/third_party/pkg/mustache4dart/example/simpleusage.dart
new file mode 100644
index 0000000..9777c1e
--- /dev/null
+++ b/third_party/pkg/mustache4dart/example/simpleusage.dart
@@ -0,0 +1,26 @@
+library mustache_usage;
+
+import 'package:mustache4dart/mustache4dart.dart';
+
+void main() {
+  //Basic use of the library as you can find it at http://mustache.github.io/mustache.5.html
+  var template = '''Hello {{name}}
+You have just won \${{value}}!
+{{#in_ca}}
+Well, \${{taxed_value}}, after taxes.
+{{/in_ca}}''';
+
+  var obj = {
+    "name": "Chris",
+    "value": 10000,
+    "taxed_value": 10000 - (10000 * 0.4),
+    "in_ca": true
+  };
+
+  print(render(template, obj));
+
+  //Print something to a StringSink
+  var out = new StringBuffer();
+  render(template, obj, out: out);
+  print(out);
+}
diff --git a/third_party/pkg/mustache4dart/lib/mustache4dart.dart b/third_party/pkg/mustache4dart/lib/mustache4dart.dart
new file mode 100644
index 0000000..71692d9
--- /dev/null
+++ b/third_party/pkg/mustache4dart/lib/mustache4dart.dart
@@ -0,0 +1,19 @@
+library mustache4dart;
+
+import 'mustache_context.dart';
+
+part 'src/mustache.dart';
+part 'src/tokens.dart';
+part 'src/tmpl.dart';
+
+const EMPTY_STRING = '';
+const SPACE = ' ';
+const NL = '\n';
+const CRNL = '\r\n';
+
+/// Returns a StringSink, if one is passed in, otherwise returns
+/// a string of the rendered template.
+typedef dynamic TemplateRenderer(ctx,
+    {StringSink out,
+    bool errorOnMissingProperty,
+    bool assumeNullNonExistingProperty});
diff --git a/third_party/pkg/mustache4dart/lib/mustache_context.dart b/third_party/pkg/mustache4dart/lib/mustache_context.dart
new file mode 100644
index 0000000..150fc26
--- /dev/null
+++ b/third_party/pkg/mustache4dart/lib/mustache_context.dart
@@ -0,0 +1,204 @@
+library mustache_context;
+
+import 'dart:collection';
+
+import 'package:mustache4dart/src/mirrors.dart';
+
+const String DOT = '\.';
+
+typedef NoParamLambda();
+typedef OptionalParamLambda({nestedContext});
+typedef TwoParamLambda(String s, {nestedContext});
+
+abstract class MustacheContext {
+  factory MustacheContext(ctx,
+      {MustacheContext parent, assumeNullNonExistingProperty: true}) {
+    return _createMustacheContext(ctx,
+        parent: parent,
+        assumeNullNonExistingProperty: assumeNullNonExistingProperty);
+  }
+
+  get ctx;
+
+  value([arg]);
+
+  bool get isFalsey;
+
+  bool get isLambda;
+
+  MustacheContext field(String key);
+
+  MustacheContext _getMustacheContext(String key);
+}
+
+_createMustacheContext(obj,
+    {MustacheContext parent, bool assumeNullNonExistingProperty}) {
+  if (obj is Iterable) {
+    return new _IterableMustacheContextDecorator(obj,
+        parent: parent,
+        assumeNullNonExistingProperty: assumeNullNonExistingProperty);
+  }
+  if (obj == false) {
+    return falseyContext;
+  }
+  return new _MustacheContext(obj,
+      parent: parent,
+      assumeNullNonExistingProperty: assumeNullNonExistingProperty);
+}
+
+final falseyContext = new _MustacheContext(false);
+
+class _MustacheContext implements MustacheContext {
+  final ctx;
+  final _MustacheContext parent;
+  final bool assumeNullNonExistingProperty;
+  Reflection _ctxReflection;
+
+  _MustacheContext(this.ctx,
+      {_MustacheContext this.parent, this.assumeNullNonExistingProperty});
+
+  bool get isLambda => ctx is Function;
+
+  bool get isFalsey => ctx == null || ctx == false;
+
+  value([arg]) => isLambda ? callLambda(arg) : ctx.toString();
+
+  callLambda(arg) {
+    if (ctx is NoParamLambda) {
+      return ctx is OptionalParamLambda ? ctx(nestedContext: this) : ctx();
+    }
+    if (ctx is TwoParamLambda) {
+      return ctx(arg, nestedContext: this);
+    }
+    return ctx(arg);
+  }
+
+  MustacheContext field(String key) {
+    if (ctx == null) return null;
+    return _getInThisOrParent(key);
+  }
+
+  MustacheContext _getInThisOrParent(String key) {
+    var result = _getContextForKey(key);
+    //if the result is null, try the parent context
+
+    if (result == null) {
+      final hasSlot = ctxReflector.field(key).exists;
+      if (!assumeNullNonExistingProperty && !hasSlot && parent == null) {
+        throw new StateError('Could not find "$key" in given context');
+      }
+
+      //if the result is null, try the parent context
+      if (!hasSlot && parent != null) {
+        result = parent.field(key);
+        if (result != null) {
+          return _createChildMustacheContext(result.ctx);
+        }
+      }
+    }
+    return result;
+  }
+
+  MustacheContext _getContextForKey(String key) {
+    if (key == DOT) {
+      return this;
+    }
+    if (key.contains(DOT)) {
+      final Iterator<String> i = key.split(DOT).iterator;
+      MustacheContext val = this;
+      while (i.moveNext()) {
+        val = val._getMustacheContext(i.current);
+        if (val == null) {
+          return null;
+        }
+      }
+      return val;
+    }
+    //else
+    return _getMustacheContext(key);
+  }
+
+  MustacheContext _getMustacheContext(String fieldName) {
+    final v = ctxReflector.field(fieldName).val();
+    return _createChildMustacheContext(v);
+  }
+
+  _createChildMustacheContext(obj) {
+    if (obj == null) {
+      return null;
+    }
+    return _createMustacheContext(obj,
+        parent: this,
+        assumeNullNonExistingProperty: this.assumeNullNonExistingProperty);
+  }
+
+  Reflection get ctxReflector {
+    if (_ctxReflection == null) {
+      _ctxReflection = reflect(ctx);
+    }
+    return _ctxReflection;
+  }
+}
+
+class _IterableMustacheContextDecorator extends IterableBase<_MustacheContext>
+    implements MustacheContext {
+  final Iterable ctx;
+  final _MustacheContext parent;
+  final bool assumeNullNonExistingProperty;
+
+  _IterableMustacheContextDecorator(this.ctx,
+      {this.parent, this.assumeNullNonExistingProperty});
+
+  value([arg]) =>
+      throw new Exception('Iterable can not be called as a function');
+
+  Iterator<_MustacheContext> get iterator =>
+      new _MustacheContextIteratorDecorator(ctx.iterator,
+          parent: parent,
+          assumeNullNonExistingProperty: assumeNullNonExistingProperty);
+
+  int get length => ctx.length;
+
+  bool get isEmpty => ctx.isEmpty;
+
+  bool get isFalsey => isEmpty;
+
+  bool get isLambda => false;
+
+  field(String key) {
+    assert(key ==
+        DOT); // 'Iterable can only be iterated. No [] implementation is available'
+    return this;
+  }
+
+  _getMustacheContext(String key) {
+    // 'Iterable can only be asked for empty or isEmpty keys or be iterated'
+    assert(key == 'empty' || key == 'isEmpty');
+    return new _MustacheContext(isEmpty,
+        parent: parent,
+        assumeNullNonExistingProperty: assumeNullNonExistingProperty);
+  }
+}
+
+class _MustacheContextIteratorDecorator extends Iterator<_MustacheContext> {
+  final Iterator delegate;
+  final _MustacheContext parent;
+  final bool assumeNullNonExistingProperty;
+
+  _MustacheContext current;
+
+  _MustacheContextIteratorDecorator(this.delegate,
+      {this.parent, this.assumeNullNonExistingProperty});
+
+  bool moveNext() {
+    if (delegate.moveNext()) {
+      current = new _MustacheContext(delegate.current,
+          parent: parent,
+          assumeNullNonExistingProperty: assumeNullNonExistingProperty);
+      return true;
+    } else {
+      current = null;
+      return false;
+    }
+  }
+}
diff --git a/third_party/pkg/mustache4dart/lib/src/mirrors.dart b/third_party/pkg/mustache4dart/lib/src/mirrors.dart
new file mode 100644
index 0000000..d144ee7
--- /dev/null
+++ b/third_party/pkg/mustache4dart/lib/src/mirrors.dart
@@ -0,0 +1,141 @@
+import 'dart:mirrors' as mirrors;
+
+const USE_MIRRORS = const bool.fromEnvironment('MIRRORS', defaultValue: true);
+
+Reflection reflect(o, {bool useMirrors: USE_MIRRORS}) {
+  if (o is Map) {
+    return new MapReflection(o);
+  }
+  if (useMirrors && USE_MIRRORS) {
+    return new Mirror(o, mirrors.reflect(o));
+  }
+
+  // in any other case fallback to a mirrorless reflection
+  return new Reflection(o);
+}
+
+class Reflection {
+  final dynamic object;
+
+  Reflection(this.object);
+
+  Field field(String name) {
+    return new _BracketsField(object, name);
+  }
+}
+
+class Field {
+  bool get exists {
+    return false;
+  }
+
+  dynamic val() => null;
+}
+
+class MapReflection extends Reflection {
+  final Map map;
+
+  MapReflection(map)
+      : this.map = map,
+        super(map);
+
+  Field field(String name) {
+    if (map.containsKey(name)) {
+      return new _BracketsField(map, name, existingKey: true);
+    }
+    return _noField;
+  }
+}
+
+final _noField = new Field();
+
+final _bracketsOperator = new Symbol("[]");
+
+class Mirror extends Reflection {
+  final mirrors.InstanceMirror instanceMirror;
+
+  Mirror(object, this.instanceMirror) : super(object);
+
+  Field field(String name) {
+    final Map<Symbol, mirrors.MethodMirror> members =
+        _instanceMembers(instanceMirror);
+    if (_isStringAssignableToBracketsOperator(members)) {
+      return new _BracketsField(object, name);
+    }
+    final methodMirror = members[new Symbol(name)];
+    if (methodMirror == null) {
+      return _noField;
+    }
+    return new _MethodMirrorField(this.instanceMirror, methodMirror);
+  }
+}
+
+Map<Symbol, mirrors.MethodMirror> _instanceMembers(mirrors.InstanceMirror m) {
+  if (m != null && m.type != null) {
+    return m.type.instanceMembers;
+  }
+  return null;
+}
+
+_isStringAssignableToBracketsOperator(
+    Map<Symbol, mirrors.MethodMirror> members) {
+  if (!members.containsKey(_bracketsOperator)) {
+    return false;
+  }
+  try {
+    mirrors.MethodMirror m = members[_bracketsOperator];
+    return mirrors.reflectType(String).isAssignableTo(m.parameters[0].type);
+  } catch (e) {
+    return false;
+  }
+}
+
+class _MethodMirrorField extends Field {
+  final mirrors.InstanceMirror instance;
+  final mirrors.MethodMirror method;
+
+  _MethodMirrorField(this.instance, this.method);
+
+  bool get exists => isVariable || isGetter || isLambda;
+
+  bool get isGetter => method.isGetter;
+
+  bool get isVariable => method is mirrors.VariableMirror;
+
+  bool get isLambda => method.parameters.length >= 0;
+
+  val() {
+    if (!exists) {
+      return null;
+    }
+    final resultMirror = instance.getField(method.simpleName);
+    return resultMirror.reflectee;
+  }
+}
+
+const Object empty = const Object();
+
+class _BracketsField extends Field {
+  final dynamic objectWithBracketsOperator;
+  final String key;
+  final bool existingKey;
+  var value;
+
+  _BracketsField(this.objectWithBracketsOperator, this.key,
+      {this.existingKey: false}) {
+    this.value = empty;
+  }
+
+  bool get exists => existingKey || val() != null;
+
+  val() {
+    if (value == empty) {
+      try {
+        value = objectWithBracketsOperator[key];
+      } catch (e) {
+        value = null;
+      }
+    }
+    return value;
+  }
+}
diff --git a/third_party/pkg/mustache4dart/lib/src/mustache.dart b/third_party/pkg/mustache4dart/lib/src/mustache.dart
new file mode 100644
index 0000000..400c017
--- /dev/null
+++ b/third_party/pkg/mustache4dart/lib/src/mustache.dart
@@ -0,0 +1,24 @@
+part of mustache4dart;
+
+render(String template, Object context,
+    {Function partial,
+    Delimiter delimiter,
+    String ident: EMPTY_STRING,
+    StringSink out,
+    bool errorOnMissingProperty: false,
+    bool assumeNullNonExistingProperty: true}) {
+  return compile(template,
+          partial: partial, delimiter: delimiter, ident: ident)(context,
+      out: out,
+      errorOnMissingProperty: errorOnMissingProperty,
+      assumeNullNonExistingProperty: assumeNullNonExistingProperty);
+}
+
+TemplateRenderer compile(String template,
+    {Function partial, Delimiter delimiter, String ident: EMPTY_STRING}) {
+  if (delimiter == null) {
+    delimiter = new Delimiter('{{', '}}');
+  }
+  return new _Template(
+      template: template, delimiter: delimiter, ident: ident, partial: partial);
+}
diff --git a/third_party/pkg/mustache4dart/lib/src/tmpl.dart b/third_party/pkg/mustache4dart/lib/src/tmpl.dart
new file mode 100644
index 0000000..385ec4b
--- /dev/null
+++ b/third_party/pkg/mustache4dart/lib/src/tmpl.dart
@@ -0,0 +1,285 @@
+part of mustache4dart;
+
+class _Template {
+  final _TokenList list;
+
+  factory _Template(
+      {String template, Delimiter delimiter, String ident, Function partial}) {
+    if (template == null) {
+      throw new FormatException("The given template is null");
+    }
+    _TokenList tokens = new _TokenList(delimiter, ident);
+
+    bool searchForOpening = true;
+    for (int i = 0; i < template.length; i++) {
+      String char = template[i];
+      if (delimiter.isDelimiter(template, i, searchForOpening)) {
+        if (searchForOpening) {
+          //opening delimiter
+          tokens.addTokenWithBuffer(delimiter, ident, partial);
+          searchForOpening = false;
+        } else {
+          //closing delimiter
+          tokens.write(delimiter.closing); //add the closing delimiter
+          tokens.addTokenWithBuffer(delimiter, ident, partial);
+          i = i + delimiter.closingLength - 1;
+          delimiter = tokens.nextDelimiter; //get the next delimiter to use
+          searchForOpening = true;
+          continue;
+        }
+      } else if (isSingleCharToken(char, searchForOpening)) {
+        tokens.addTokenWithBuffer(delimiter, ident, partial);
+        tokens.addToken(char, delimiter, ident, partial);
+        continue;
+      } else if (isSpecialNewLine(template, i)) {
+        tokens.addTokenWithBuffer(delimiter, ident, partial);
+        tokens.addToken(CRNL, delimiter, ident, partial);
+        i++;
+        continue;
+      }
+      tokens.write(char);
+    }
+    tokens.addTokenWithBuffer(delimiter, ident, partial, last: true);
+
+    return new _Template._internal(tokens);
+  }
+
+  static bool isSingleCharToken(String char, bool opening) {
+    if (!opening) {
+      return false;
+    }
+    if (char == NL) {
+      return true;
+    }
+    if (char == SPACE) {
+      return true;
+    }
+    return false;
+  }
+
+  static bool isSpecialNewLine(String template, int position) {
+    if (position + 1 == template.length) {
+      return false;
+    }
+    var char = template[position];
+    var nextChar = template[position + 1];
+    return char == '\r' && nextChar == NL;
+  }
+
+  _Template._internal(this.list);
+
+  call(ctx,
+      {StringSink out: null,
+      bool errorOnMissingProperty: false,
+      bool assumeNullNonExistingProperty: true}) {
+    StringSink o = out == null ? new StringBuffer() : out;
+    _write(ctx, o,
+        assumeNullNonExistingProperty: assumeNullNonExistingProperty);
+
+    //If we provide a StringSink, write there and return it as
+    //the response of the function. Otherwise make our library
+    //easier to use by returning the string representation of
+    //the template
+    if (out == null) {
+      return o.toString();
+    }
+    return o;
+  }
+
+  void _write(ctx, StringSink out, {bool assumeNullNonExistingProperty}) {
+    if (list.head == null) {
+      return;
+    }
+    if (!(ctx is MustacheContext)) {
+      ctx = new MustacheContext(ctx,
+          assumeNullNonExistingProperty: assumeNullNonExistingProperty);
+    }
+
+    //Iterate the tokens and apply the context
+    Token token = list.head;
+    while (token != null) {
+      token = token(ctx, out);
+    }
+  }
+
+  String toString() {
+    return "Template($list)";
+  }
+}
+
+class _TokenList {
+  StringBuffer buffer;
+  Token head;
+  Token tail;
+  Delimiter _nextDelimiter;
+  Line line = new Line(null);
+  final List<_StartSectionToken> startingTokens = [];
+
+  _TokenList(Delimiter delimiter, String ident) {
+    //Our template should start as an empty string token
+    head = new _SpecialCharToken(EMPTY_STRING, ident);
+    tail = head;
+    _nextDelimiter = delimiter;
+    buffer = new StringBuffer();
+  }
+
+  void addTokenWithBuffer(Delimiter del, String ident, Function partial,
+      {last: false}) {
+    if (buffer.length > 0) {
+      addToken(buffer.toString(), del, ident, partial, last: last);
+      buffer = new StringBuffer();
+    }
+  }
+
+  void addToken(String str, Delimiter del, String ident, Function partial,
+      {last: false}) {
+    _add(new Token(str, partial, del, ident), last);
+  }
+
+  void _add(Token other, [bool last]) {
+    if (other == null) {
+      return;
+    }
+    if (other is _DelimiterToken) {
+      _nextDelimiter = other.newDelimiter;
+    } else if (other is _StartSectionToken) {
+      _addStartingToken(other);
+    } else if (other is _EndSectionToken) {
+      _addEndingToken(other);
+    }
+
+    _addToLine(other, last);
+
+    tail.next = other;
+    tail = other;
+  }
+
+  void _addStartingToken(_StartSectionToken t) {
+    startingTokens.add(t);
+  }
+
+  void _addEndingToken(_EndSectionToken t) {
+    var lastStarting = startingTokens.removeLast();
+    if (lastStarting.value != t.value) {
+      throw new FormatException(
+          "Expected {{/${lastStarting.value}}} but got {{/${t.value}}}");
+    } else {
+      lastStarting.endSection = t;
+    }
+  }
+
+  void _addToLine(Token t, [bool last]) {
+    line = line.add(t, last);
+  }
+
+  Delimiter get nextDelimiter => _nextDelimiter;
+
+  void write(String txt) {
+    buffer.write(txt);
+  }
+
+  String toString() {
+    StringBuffer str = new StringBuffer("TokenList(");
+    if (head == null) {
+      //Do not display anything
+    } else if (head == tail) {
+      str.write(head);
+    } else {
+      str.write("$head...$tail");
+    }
+    str.write(")");
+    return str.toString();
+  }
+}
+
+class Delimiter {
+  final String opening;
+  final String _closing;
+  String realClosingTag;
+
+  Delimiter(this.opening, this._closing);
+
+  bool isDelimiter(String template, int position, bool opening) {
+    String d = opening ? this.opening : this._closing;
+    if (d.length == 1) {
+      return d == template[position];
+    }
+    //else:
+    int endIndex = position + d.length;
+    if (endIndex >= template.length) {
+      return false;
+    }
+    String dd = template.substring(position, endIndex);
+    if (d != dd) {
+      return false;
+    }
+    //A hack to support tripple brackets
+    if (!opening && _closing == '}}' && template[endIndex] == '}') {
+      realClosingTag = '}}}';
+    } else {
+      realClosingTag = null;
+    }
+    return true;
+  }
+
+  String get closing => realClosingTag != null ? realClosingTag : _closing;
+
+  int get closingLength => closing.length;
+
+  int get openingLength => opening.length;
+
+  toString() => "Delimiter($opening, $closing)";
+}
+
+class Line {
+  final tokens = [];
+  bool full = false;
+  bool standAlone = true;
+  Line prev = null;
+
+  Line(Token t) {
+    if (t != null) {
+      add(t, false);
+    }
+  }
+
+  Line add(Token t, [eof = false]) {
+    if (full) {
+      throw new StateError("Line is full. Can not add $t to it.");
+    }
+    if (!_isStandAloneToken(t) && standAlone) {
+      standAlone = false;
+    }
+    tokens.add(t);
+    if (_isEndOfLine(t) || eof) {
+      return _eol();
+    }
+    //in any other case:
+    return this;
+  }
+
+  Line _eol() {
+    _markStandAloneLineTokens();
+    full = true;
+    Line newLine = new Line(null);
+    newLine.prev = this;
+    return newLine;
+  }
+
+  bool _isStandAloneToken(Token t) {
+    return t is StandAloneLineCapable;
+  }
+
+  bool _isEndOfLine(Token t) {
+    return t == NL || t == CRNL;
+  }
+
+  _markStandAloneLineTokens() {
+    if (tokens.length == 1) {
+      standAlone = false;
+    }
+    if (standAlone) {
+      tokens.forEach((t) => t.rendable = false);
+    }
+  }
+}
diff --git a/third_party/pkg/mustache4dart/lib/src/tokens.dart b/third_party/pkg/mustache4dart/lib/src/tokens.dart
new file mode 100644
index 0000000..1c1d639
--- /dev/null
+++ b/third_party/pkg/mustache4dart/lib/src/tokens.dart
@@ -0,0 +1,348 @@
+part of mustache4dart;
+
+/**
+ * This is the main class describing a compiled token.
+ */
+
+abstract class Token {
+  final String _source;
+
+  Token _next;
+  Token prev;
+  bool _rendable = true;
+
+  Token.withSource(this._source);
+
+  factory Token(String token, Function partial, Delimiter d, String ident) {
+    if (token == EMPTY_STRING) {
+      return null;
+    }
+    if (token.startsWith('{{{') && d.opening == '{{') {
+      return new _ExpressionToken(
+          token.substring(3, token.length - 3), false, token, partial, d);
+    } else if (token.startsWith(d.opening)) {
+      return new _ExpressionToken(
+          token.substring(d.openingLength, token.length - d.closingLength),
+          true,
+          token,
+          partial,
+          d);
+    } else if (token == SPACE || token == NL || token == CRNL) {
+      return new _SpecialCharToken(token, ident);
+    } else {
+      return new _StringToken(token);
+    }
+  }
+
+  Token call(MustacheContext context, StringSink out) {
+    if (out == null)
+      throw new Exception("Need an output to write the rendered result");
+    var string = apply(context);
+    if (rendable) {
+      out.write(string);
+    }
+    return next;
+  }
+
+  String apply(MustacheContext context);
+
+  void set next(Token n) {
+    _next = n;
+    n.prev = this;
+  }
+
+  Token get next => _next;
+
+  /**
+   * This describes the value of the token.
+   */
+  String get value;
+
+  void set rendable(bool rendable) {
+    _rendable = rendable;
+  }
+
+  bool get rendable => _rendable;
+
+  /**
+   * Two tokens are the same if their _val are the same.
+   */
+  bool operator ==(other) {
+    if (other is Token) {
+      return value == other.value;
+    }
+    if (other is String) {
+      return value == other;
+    }
+    return false;
+  }
+
+  int get hashCode => value.hashCode;
+}
+
+abstract class StandAloneLineCapable {}
+
+/**
+ * The simplest implementation of a token is the _StringToken which is any string that is not within
+ * an opening and closing mustache.
+ */
+class _StringToken extends Token {
+  _StringToken(_val) : super.withSource(_val);
+
+  apply(context) => value;
+
+  String get value => _source;
+
+  String toString() => "StringToken($value)";
+}
+
+class _SpecialCharToken extends _StringToken implements StandAloneLineCapable {
+  final String ident;
+
+  _SpecialCharToken(_val, [this.ident = EMPTY_STRING]) : super(_val);
+
+  apply(context) {
+    if (!rendable) {
+      return EMPTY_STRING;
+    }
+
+    if (next == null) {
+      return super.apply(context);
+    }
+    if (_isNewLineOrEmpty) {
+      return "${super.apply(context)}$ident";
+    }
+    return super.apply(context);
+  }
+
+  bool get _isNewLineOrEmpty => _isNewLine || value == EMPTY_STRING;
+
+  bool get _isNewLine => value == NL || value == CRNL;
+
+  String toString() {
+    var val = value.replaceAll('\r', '\\r').replaceAll(NL, '\\n');
+    return "SpecialCharToken($val)";
+  }
+}
+
+/**
+ * This is a token that represents a mustache expression. That is anything
+ * between an opening and closing mustache.
+ */
+class _ExpressionToken extends Token {
+  final String value;
+
+  factory _ExpressionToken(String val, bool escapeHtml, String source,
+      Function partial, Delimiter delimiter) {
+    val = val.trim();
+    if (escapeHtml && val.startsWith('&')) {
+      escapeHtml = false;
+      val = val.substring(1).trim();
+    }
+    if (!escapeHtml) {
+      return new _ExpressionToken.withSource(val, source);
+    }
+
+    String control = val.substring(0, 1);
+    String newVal = val.substring(1).trim();
+
+    if ('#' == control) {
+      return new _StartSectionToken(newVal, delimiter);
+    } else if ('/' == control) {
+      return new _EndSectionToken(newVal);
+    } else if ('^' == control) {
+      return new _InvertedSectionToken(newVal, delimiter);
+    } else if ('!' == control) {
+      return new _CommentToken();
+    } else if ('>' == control) {
+      return new _PartialToken(partial, newVal);
+    } else if ('=' == control) {
+      return new _DelimiterToken(newVal);
+    } else {
+      return new _EscapeHtmlToken(val, source);
+    }
+  }
+
+  _ExpressionToken.withSource(this.value, source) : super.withSource(source);
+
+  apply(MustacheContext ctx, {bool errorOnMissingProperty: false}) {
+    var field = ctx.field(value);
+    if (field == null) {
+      //TODO define an exception for such cases
+      if (errorOnMissingProperty) {
+        throw "Could not find '$value' property";
+      }
+      return EMPTY_STRING;
+    }
+    if (field.isLambda) {
+      //A lambda's return value should be parsed
+      return render(field.value(null), ctx);
+    }
+    return field.value();
+  }
+
+  String toString() => "ExpressionToken($value)";
+}
+
+class _DelimiterToken extends _ExpressionToken
+    implements StandAloneLineCapable {
+  _DelimiterToken(String val) : super.withSource(val, null);
+
+  apply(MustacheContext ctx, {bool errorOnMissingProperty: false}) =>
+      EMPTY_STRING;
+
+  bool get rendable => false;
+
+  Delimiter get newDelimiter {
+    List delimiters = value.substring(0, value.length - 1).split(SPACE);
+    return new Delimiter(delimiters[0], delimiters[1]);
+  }
+}
+
+class _PartialToken extends _ExpressionToken implements StandAloneLineCapable {
+  final Function partial;
+
+  _PartialToken(this.partial, String val) : super.withSource(val, null);
+
+  apply(MustacheContext ctx, {bool errorOnMissingProperty: false}) {
+    if (partial != null) {
+      var partialTemplate = partial(value);
+      if (partialTemplate != null) {
+        return render(partialTemplate, ctx, partial: partial, ident: _ident);
+      }
+    }
+    return EMPTY_STRING;
+  }
+
+  String get _ident {
+    StringBuffer ident = new StringBuffer();
+    Token p = this.prev;
+    while (p.value == SPACE) {
+      ident.write(SPACE);
+      p = p.prev;
+    }
+    if (p.value == NL || p.value == EMPTY_STRING) {
+      return ident.toString();
+    } else {
+      return EMPTY_STRING;
+    }
+  }
+
+  bool get rendable => true;
+}
+
+class _CommentToken extends _ExpressionToken implements StandAloneLineCapable {
+  _CommentToken() : super.withSource(EMPTY_STRING, EMPTY_STRING);
+
+  apply(MustacheContext ctx, {bool errorOnMissingProperty: false}) =>
+      EMPTY_STRING;
+
+  String toString() => "_CommentsToken()";
+}
+
+class _EscapeHtmlToken extends _ExpressionToken {
+  _EscapeHtmlToken(String val, String source) : super.withSource(val, source);
+
+  apply(MustacheContext ctx, {bool errorOnMissingProperty: false}) {
+    var val = super.apply(ctx);
+    if (!(val is String)) {
+      throw new Exception(
+          "Computed value ($val) is not a string. Can not apply it");
+    }
+
+    return val
+        .replaceAll("&", "&amp;")
+        .replaceAll("<", "&lt;")
+        .replaceAll(">", "&gt;")
+        .replaceAll('"', "&quot;")
+        .replaceAll("'", "&apos;");
+  }
+
+  String toString() => "EscapeHtmlToken($value)";
+}
+
+class _StartSectionToken extends _ExpressionToken
+    implements StandAloneLineCapable {
+  final Delimiter delimiter;
+  _EndSectionToken endSection;
+
+  _StartSectionToken(String val, this.delimiter) : super.withSource(val, null);
+
+  //Override the next getter
+  Token get next => endSection.next;
+
+  apply(MustacheContext ctx, {bool errorOnMissingProperty: false}) {
+    var field = ctx.field(value);
+    //TODO: remove null check by returning a falsey context
+    if (errorOnMissingProperty && field == null) {
+      throw "Could not find '$value' property";
+    }
+    if (field == null || field.isFalsey) {
+      return EMPTY_STRING;
+    }
+    StringBuffer str = new StringBuffer();
+    if (field is Iterable) {
+      (field as Iterable).forEach((v) {
+        forEachUntilEndSection((Token t) => str.write(t.apply(v)));
+      });
+      return str.toString();
+    }
+
+    if (field.isLambda) {
+      //apply the source to the given function
+      forEachUntilEndSection((Token t) => str.write(t._source));
+      //A lambda's return value should be parsed
+      return render(field.value(str.toString()), ctx, delimiter: delimiter);
+    }
+
+    //in any other case:
+    forEachUntilEndSection((Token t) => str.write(t.apply(field)));
+    return str.toString();
+  }
+
+  forEachUntilEndSection(void f(Token token)) {
+    if (f == null) {
+      throw new Exception('Can not apply a null function!');
+    }
+    Token n = super.next;
+    while (!identical(n, endSection)) {
+      f(n);
+      n = n.next;
+    }
+  }
+
+  //The token itself is always rendable
+  bool get rendable => true;
+
+  String toString() => "StartSectionToken($value)";
+}
+
+class _EndSectionToken extends _ExpressionToken
+    implements StandAloneLineCapable {
+  _EndSectionToken(String val) : super.withSource(val, null);
+
+  apply(MustacheContext ctx, {bool errorOnMissingProperty: false}) =>
+      EMPTY_STRING;
+  String toString() => "EndSectionToken($value)";
+}
+
+class _InvertedSectionToken extends _StartSectionToken {
+  _InvertedSectionToken(String val, Delimiter del) : super(val, del);
+
+  apply(MustacheContext ctx, {bool errorOnMissingProperty: false}) {
+    var field = ctx.field(value);
+    //TODO: remove null check. Always return a falsey context
+    if (field == null || field.isFalsey) {
+      StringBuffer buf = new StringBuffer();
+      forEachUntilEndSection((Token t) {
+        var val2 = t.apply(ctx);
+        buf.write(val2);
+      });
+      return buf.toString();
+    }
+    //else just return an empty string
+    return EMPTY_STRING;
+  }
+
+  String toString() => "InvertedSectionToken($value)";
+}
diff --git a/third_party/pkg/mustache4dart/pubspec.yaml b/third_party/pkg/mustache4dart/pubspec.yaml
new file mode 100644
index 0000000..62460fc
--- /dev/null
+++ b/third_party/pkg/mustache4dart/pubspec.yaml
@@ -0,0 +1,10 @@
+name: mustache4dart
+version: 2.1.2
+author: Georgios Valotasios <valotas@gmail.com>
+description: A mustache implementation for the Dart language
+homepage: https://github.com/valotas/mustache4dart
+environment:
+  sdk: '>=0.8.10+6 <3.0.0'
+dev_dependencies:
+  test: '>=0.12.0 <0.12.23'
+  isolate: '>=1.0.0 <1.1.0'
diff --git a/third_party/pkg/mustache4dart/test/lorem-ipsum.txt b/third_party/pkg/mustache4dart/test/lorem-ipsum.txt
new file mode 100644
index 0000000..033694b
--- /dev/null
+++ b/third_party/pkg/mustache4dart/test/lorem-ipsum.txt
@@ -0,0 +1,11 @@
+Lorem ipsum dolor sit amet, est vel euismod et dictum fermentum integer, tortor ipsam est. Sollicitudin imperdiet fringilla placerat eros luctus facilisi, pede nunc, congue nullam dolor. Nibh neque urna neque, praesent volutpat tincidunt maecenas, scelerisque est, vitae in scelerisque placerat lacus, eu vitae lectus malesuada. Vehicula suspendisse tincidunt, aenean ac eros varius lectus quis. Orci lacus luctus nullam arcu id risus, odio feugiat turpis nullam suspendisse metus suspendisse, ligula vehicula nec fermentum ligula, convallis ultrices cras nibh eu. Leo pede iaculis mus risus, proin aliquam phasellus dui pretium arcu, pede fringilla risus, tempus odio sem lobortis lectus sem enim, laoreet donec posuere. Tempus elit vel arcu. A iaculis. A amet felis, sed sit metus. Quis etiam vitae at ipsum, et nam a. In auctor molestie suscipit enim ultrices tincidunt, nullam mi, hendrerit integer quisque elementum nonummy, enim pede magna, dui at dignissim vestibulum molestie et. Vel mattis aliquet, nulla nostra laoreet et ultrices, luctus luctus tortor libero augue orci id.
+
+Ante proin aenean sit elit, vel at donec lobortis eget, erat curabitur sed tempus enim, et et mollis sed in ultricies ultricies, rutrum alias porttitor erat. Convallis in voluptatum a a. Nunc odio cras ut consequat neque, in dolor nec, suspendisse et a cras consectetuer ut, in sagittis in dis blandit sit nisl, eros consequat. Arcu enim pede mattis adipiscing wisi facilisi, adipiscing morbi integer ante auctor. Porttitor lectus scelerisque pellentesque torquent, viverra donec enim. Quam vehicula sed eu, viverra dolor et sed mollis in, vulputate integer tristique elit tellus laoreet aut, sed dis adipiscing fames integer turpis, curabitur tellus velit nonummy et nam.
+
+Elit dolor maecenas vestibulum sodales ut, molestie luctus proin sit dolor mauris. In a lectus arcu, ut dui, nec molestie wisi tempus urna. Mollis imperdiet in lectus justo, pede justo nec vulputate commodo eget tincidunt, sapien tellus volutpat lacinia lectus. Ut dis tellus neque ac at sapien, ultrices vivamus bibendum mattis. Dui quisque, sed vivamus accumsan pellentesque, est donec sem. Tincidunt dignissim, vivamus aliquam lacinia neque, est integer. Turpis habitant id tempus vestibulum. Purus massa feugiat at ut massa varius, vivamus diam hendrerit tincidunt.
+Vitae mi fermentum a nec vehicula imperdiet, ipsum potenti suspendisse ipsum porta turpis, nam maecenas. Velit nulla a consectetuer arcu accumsan vitae, sodales cras in vitae leo eu, aliquet pellentesque in tempus facilisis rutrum leo, lacus nunc pretium quisque egestas habitasse. Dolor porttitor ipsum, ullamcorper metus, amet etiam euismod vitae nulla orci. Id velit quam vel lectus, molestie commodo, turpis tellus odio rhoncus vehicula ut. Et nunc vestibulum tellus et vestibulum, nunc orci ut diam nunc quam, aliquam dignissim nunc donec, aliquam class, ipsum tempus. Vestibulum proin est, vel felis, lectus nec pharetra arcu, fringilla et et nisl magna leo, lorem facilisis erat.Lorem ipsum dolor sit amet, est vel euismod et dictum fermentum integer, tortor ipsam est. Sollicitudin imperdiet fringilla placerat eros luctus facilisi, pede nunc, congue nullam dolor. Nibh neque urna neque, praesent volutpat tincidunt maecenas, scelerisque est, vitae in scelerisque placerat lacus, eu vitae lectus malesuada. Vehicula suspendisse tincidunt, aenean ac eros varius lectus quis. Orci lacus luctus nullam arcu id risus, odio feugiat turpis nullam suspendisse metus suspendisse, ligula vehicula nec fermentum ligula, convallis ultrices cras nibh eu. Leo pede iaculis mus risus, proin aliquam phasellus dui pretium arcu, pede fringilla risus, tempus odio sem lobortis lectus sem enim, laoreet donec posuere. Tempus elit vel arcu. A iaculis. A amet felis, sed sit metus. Quis etiam vitae at ipsum, et nam a. In auctor molestie suscipit enim ultrices tincidunt, nullam mi, hendrerit integer quisque elementum nonummy, enim pede magna, dui at dignissim vestibulum molestie et. Vel mattis aliquet, nulla nostra laoreet et ultrices, luctus luctus tortor libero augue orci id.
+Ante proin aenean sit elit, vel at donec lobortis eget, erat curabitur sed tempus enim, et et mollis sed in ultricies ultricies, rutrum alias porttitor erat. Convallis in voluptatum a a. Nunc odio cras ut consequat neque, in dolor nec, suspendisse et a cras consectetuer ut, in sagittis in dis blandit sit nisl, eros consequat. Arcu enim pede mattis adipiscing wisi facilisi, adipiscing morbi integer ante auctor. Porttitor lectus scelerisque pellentesque torquent, viverra donec enim. Quam vehicula sed eu, viverra dolor et sed mollis in, vulputate integer tristique elit tellus laoreet aut, sed dis adipiscing fames integer turpis, curabitur tellus velit nonummy et nam.
+
+Elit dolor maecenas vestibulum sodales ut, molestie luctus proin sit dolor mauris. In a lectus arcu, ut dui, nec molestie wisi tempus urna. Mollis imperdiet in lectus justo, pede justo nec vulputate commodo eget tincidunt, sapien tellus volutpat lacinia lectus. Ut dis tellus neque ac at sapien, ultrices vivamus bibendum mattis. Dui quisque, sed vivamus accumsan pellentesque, est donec sem. Tincidunt dignissim, vivamus aliquam lacinia neque, est integer. Turpis habitant id tempus vestibulum. Purus massa feugiat at ut massa varius, vivamus diam hendrerit tincidunt.
+
+Vitae mi fermentum a nec vehicula imperdiet, ipsum potenti suspendisse ipsum porta turpis, nam maecenas. Velit nulla a consectetuer arcu accumsan vitae, sodales cras in vitae leo eu, aliquet pellentesque in tempus facilisis rutrum leo, lacus nunc pretium quisque egestas habitasse. Dolor porttitor ipsum, ullamcorper metus, amet etiam euismod vitae nulla orci. Id velit quam vel lectus, molestie commodo, turpis tellus odio rhoncus vehicula ut. Et nunc vestibulum tellus et vestibulum, nunc orci ut diam nunc quam, aliquam dignissim nunc donec, aliquam class, ipsum tempus. Vestibulum proin est, vel felis, lectus nec pharetra arcu, fringilla et et nisl magna leo, lorem facilisis erat.
diff --git a/third_party/pkg/mustache4dart/test/mustache_all.dart b/third_party/pkg/mustache4dart/test/mustache_all.dart
new file mode 100644
index 0000000..d155b53
--- /dev/null
+++ b/third_party/pkg/mustache4dart/test/mustache_all.dart
@@ -0,0 +1,15 @@
+import "mustache_context_test.dart" as context_test;
+import "mustache_issues_test.dart" as issues_test;
+import "mustache_line_test.dart" as line_test;
+import "mustache_specs_test.dart" as specs_test;
+import "mustache_test.dart" as general_test;
+import "mustache_context_reflect_test.dart" as reflect_test;
+
+void main() {
+  context_test.main();
+  issues_test.main();
+  line_test.main();
+  specs_test.main();
+  general_test.main();
+  reflect_test.main();
+}
diff --git a/third_party/pkg/mustache4dart/test/mustache_context_reflect_test.dart b/third_party/pkg/mustache4dart/test/mustache_context_reflect_test.dart
new file mode 100644
index 0000000..ce1f5b7
--- /dev/null
+++ b/third_party/pkg/mustache4dart/test/mustache_context_reflect_test.dart
@@ -0,0 +1,170 @@
+import 'dart:mirrors' as mirrors;
+import 'package:test/test.dart';
+import 'package:mustache4dart/src/mirrors.dart';
+
+@mirrors.MirrorsUsed()
+class Person {
+  final String name;
+  final String lastname;
+  final Person parent;
+
+  Person(this.name, {this.lastname: null, this.parent: null});
+
+  get fullname {
+    return "$name $lastname";
+  }
+
+  getFullnameWithInitial() {
+    final initial = this.parent.name[0].toUpperCase();
+    return "$name $initial. $lastname";
+  }
+}
+
+class ClassWithLambda {
+  final int num;
+
+  ClassWithLambda(this.num);
+
+  lambdaWithArity1(str) => "[[$str $num]]";
+}
+
+@mirrors.MirrorsUsed()
+class ClassWithBrackets {
+  operator [](String input) {
+    if (input == 'nullval') {
+      return null;
+    }
+    return new Person(input);
+  }
+}
+
+void main() {
+  group('reflect', () {
+    test('returns a mirror object', () {
+      final cat = new Person("cat");
+      expect(reflect(cat), isNotNull);
+    });
+
+    group('field([name])', () {
+      test('should return an object', () {
+        final cat = new Person("cat");
+
+        final actual = reflect(cat).field('name');
+
+        expect(actual, isNotNull);
+        expect(actual, new isInstanceOf<Field>());
+      });
+
+      group(".exists", () {
+        final cat = new Person("cat");
+
+        test('returns true if the field exists', () {
+          expect(reflect(cat).field('name').exists, isTrue);
+        });
+
+        test('returns true if the getter exists', () {
+          expect(reflect(cat).field('fullname').exists, isTrue);
+        });
+
+        test('returns false if the method does not exist', () {
+          expect(reflect(cat).field('fullnameWithInitial').exists, isFalse);
+        });
+
+        test('returns false if no field exists', () {
+          expect(reflect(cat).field('xyz').exists, isFalse);
+        });
+
+        test('returns false if [] operator returns a null value', () {
+          expect(reflect(new ClassWithBrackets()).field('nullval').exists,
+              isFalse);
+        });
+
+        test('returns true for map containing a field with a null value', () {
+          expect(reflect({'a': null}).field('a').exists, isTrue);
+        });
+      });
+
+      group(".val()", () {
+        test('returns the value of a field', () {
+          final cat = new Person("cat");
+
+          final actual = reflect(cat);
+
+          expect(actual.field('name').val(), "cat");
+        });
+
+        test('returns the value of a getter', () {
+          final george = new Person("George", lastname: "Valotasios");
+
+          final actual = reflect(george);
+
+          expect(actual.field('fullname').val(), "George Valotasios");
+        });
+
+        test('does not returns the value of a get methods', () {
+          final george = new Person("George",
+              lastname: "Valotasios", parent: new Person("Thomas"));
+
+          final actual = reflect(george);
+
+          expect(actual.field('fullnameWithInitial').exists, isFalse);
+        });
+
+        test('returns the value from a [] operator', () {
+          final object = new ClassWithBrackets();
+
+          final actual = reflect(object).field('xyz');
+
+          expect(actual.val(), isNotNull);
+          expect(actual.val(), new isInstanceOf<Person>());
+          expect(actual.val().name, 'xyz');
+        }, onPlatform: {
+          "js": new Skip("[] operator can not be reflected in javascript")
+        });
+
+        test('returns always a reference to the value', () {
+          final thomas = new Person("Thomas");
+          final george =
+              new Person("George", lastname: "Valotasios", parent: thomas);
+
+          final actual = reflect(george);
+
+          expect(actual.field('parent').val(), thomas);
+        });
+
+        test('returns a ref to the function if it has an arity of 1', () {
+          final labmbda = new ClassWithLambda(1);
+
+          final actual = reflect(labmbda).field('lambdaWithArity1');
+
+          expect(actual.val(), new isInstanceOf<Function>());
+          expect(actual.val()("-"), "[[- 1]]");
+        });
+      });
+    });
+
+    test('does not use reflection with Maps', () {
+      final reflection = reflect({'name': "g"});
+      expect(reflection, isNot(new isInstanceOf<Mirror>()));
+    });
+
+    group('with useMirrors = false', () {
+      test('should be disabled by default', () {
+        expect(USE_MIRRORS, true);
+      });
+
+      test('should return the result of the [] operator', () {
+        final reflection = reflect(new ClassWithBrackets(), useMirrors: false);
+        final value = reflection.field('George').val();
+        expect(value, new isInstanceOf<Person>());
+        expect(value.name, 'George');
+      });
+
+      test('should not be able to analyze classes with reflectioon', () {
+        final george = new Person('George');
+        final reflection = reflect(george, useMirrors: false);
+        expect(reflection.field('name').exists, isFalse);
+      });
+    });
+  });
+}
diff --git a/third_party/pkg/mustache4dart/test/mustache_context_test.dart b/third_party/pkg/mustache4dart/test/mustache_context_test.dart
new file mode 100644
index 0000000..7d0801c
--- /dev/null
+++ b/third_party/pkg/mustache4dart/test/mustache_context_test.dart
@@ -0,0 +1,218 @@
+import 'dart:mirrors';
+import 'package:test/test.dart';
+import 'package:mustache4dart/mustache_context.dart';
+
+void main() {
+  group('mustache_context lib', () {
+    test('Recursion of iterable contextes', () {
+      var contextY = {'content': 'Y', 'nodes': []};
+      var contextX = {
+        'content': 'X',
+        'nodes': [contextY]
+      };
+      var ctx = new MustacheContext(contextX);
+      expect(ctx.field('nodes'), isNotNull);
+      expect(ctx.field('nodes') is Iterable, isTrue);
+      expect((ctx.field('nodes') as Iterable).length, 1);
+      (ctx.field('nodes') as Iterable).forEach((n) {
+        expect(n.field('content').value(), 'Y');
+        expect(n.field('nodes').length, 0);
+      });
+    });
+
+    test('Direct interpolation', () {
+      var ctx = new MustacheContext({'n1': 1, 'n2': 2.0, 's': 'some string'});
+      expect(ctx.field('n1').field('.').value(), '1');
+      expect(ctx.field('n2').field('.').value(), '2.0');
+      expect(ctx.field('s').field('.').value(), 'some string');
+    }, testOn: "vm");
+
+    test('Direct list interpolation', () {
+      var list = [1, 'two', 'three', '4'];
+      var ctx = new MustacheContext(list);
+      expect(ctx.field('.') is Iterable, isTrue);
+    });
+  });
+
+  test('Simple context with map', () {
+    var ctx = new MustacheContext({'k1': 'value1', 'k2': 'value2'});
+    expect(ctx.field('k1').value(), 'value1');
+    expect(ctx.field('k2').value(), 'value2');
+    expect(ctx.field('k3'), null);
+  });
+
+  test('Simple context with object', () {
+    var ctx = new MustacheContext(new _Person('Γιώργος', 'Βαλοτάσιος'));
+    expect(ctx.field('name').value(), 'Γιώργος');
+    expect(ctx.field('lastname').value(), 'Βαλοτάσιος');
+    expect(ctx.field('last'), null);
+    expect(ctx.field('fullname').value(), 'Γιώργος Βαλοτάσιος');
+    expect(ctx.field('reversedName'), null);
+    expect(ctx.field('reversedLastName').value(), 'ςοισάτολαΒ');
+  });
+
+  test('Simple map with list of maps', () {
+    dynamic ctx = new MustacheContext({
+      'k': [
+        {'k1': 'item1'},
+        {'k2': 'item2'},
+        {
+          'k3': {'kk1': 'subitem1', 'kk2': 'subitem2'}
+        }
+      ]
+    });
+    expect(ctx.field('k').length, 3);
+  });
+
+  test('Map with list of lists', () {
+    var ctx = new MustacheContext({
+      'k': [
+        {'k1': 'item1'},
+        {
+          'k3': [
+            {'kk1': 'subitem1'},
+            {'kk2': 'subitem2'}
+          ]
+        }
+      ]
+    });
+    expect(ctx.field('k') is Iterable, isTrue);
+    expect((ctx.field('k') as Iterable).length, 2);
+    expect((ctx.field('k') as Iterable).last.field('k3').length, 2);
+  });
+
+  test('Object with iterables', () {
+    var p = new _Person('Νικόλας', 'Νικολάου');
+    p.contactInfos.add(new _ContactInfo('Address', {
+      'Street': 'Κολοκωτρόνη',
+      'Num': '31',
+      'Zip': '42100',
+      'Country': 'GR'
+    }));
+    p.contactInfos.add(new _ContactInfo('skype', 'some1'));
+    var ctx = new MustacheContext(p);
+    var contactInfos = ctx.field('contactInfos');
+    expect(contactInfos is Iterable, isTrue);
+    var iterableContactInfos = contactInfos as Iterable;
+    expect(iterableContactInfos.length, 2);
+    expect(
+        iterableContactInfos.first.field('value').field('Num').value(), '31');
+  });
+
+  test('Deep search with object', () {
+    //create our model:
+    _Person p = null;
+    for (int i = 10; i > 0; i--) {
+      p = new _Person("name$i", "lastname$i", p);
+    }
+
+    MustacheContext ctx = new MustacheContext(p);
+    expect(ctx.field('name').value(), 'name1');
+    expect(ctx.field('parent').field('lastname').value(), 'lastname2');
+    expect(ctx.field('parent').field('parent').field('fullname').value(),
+        'name3 lastname3');
+  });
+
+  test('simple MustacheFunction value', () {
+    var t = new _Transformer();
+    var ctx = new MustacheContext(t);
+    var f = ctx.field('transform');
+
+    expect(f.isLambda, true);
+    expect(f.value('123 456 777'), t.transform('123 456 777'));
+  });
+
+  test('MustacheFunction from anonymus function', () {
+    var map = {'transform': (String val) => "$val!"};
+    var ctx = new MustacheContext(map);
+    var f = ctx.field('transform');
+
+    expect(f.isLambda, true);
+    expect(f.value('woh'), 'woh!');
+  });
+
+  test('Dotted names', () {
+    var ctx =
+        new MustacheContext({'person': new _Person('George', 'Valotasios')});
+    expect(ctx.field('person.name').value(), 'George');
+  });
+
+  test('Context with another context', () {
+    var ctx = new MustacheContext(new _Person('George', 'Valotasios'),
+        parent: new MustacheContext({
+          'a': {'one': 1},
+          'b': {'two': 2}
+        }));
+    expect(ctx.field('name').value(), 'George');
+    expect(ctx.field('a').field('one').value(), '1');
+    expect(ctx.field('b.two').value(), '2');
+  });
+
+  test('Deep subcontext test', () {
+    var map = {
+      'a': {'one': 1},
+      'b': {'two': 2},
+      'c': {'three': 3}
+    };
+    var ctx = new MustacheContext({
+      'a': {'one': 1},
+      'b': {'two': 2},
+      'c': {'three': 3}
+    });
+    expect(ctx.field('a'), isNotNull,
+        reason: "a should exists when using $map");
+    expect(ctx.field('a').field('one').value(), '1');
+    expect(ctx.field('a').field('two'), isNull);
+    expect(ctx.field('a').field('b'), isNotNull,
+        reason: "a.b should exists when using $map");
+    expect(ctx.field('a').field('b').field('one').value(), '1',
+        reason: "a.b.one == a.own when using $map");
+    expect(ctx.field('a').field('b').field('two').value(), '2',
+        reason: "a.b.two == b.two when using $map");
+    expect(ctx.field('a').field('b').field('three'), isNull);
+    expect(ctx.field('a').field('b').field('c'), isNotNull,
+        reason: "a.b.c should not be null when using $map");
+
+    var abc = ctx.field('a').field('b').field('c');
+    expect(abc.field('one').value(), '1',
+        reason: "a.b.c.one == a.one when using $map");
+    expect(abc.field('two').value(), '2',
+        reason: "a.b.c.two == b.two when using $map");
+    expect(abc.field('three').value(), '3');
+  });
+}
+
+@MirrorsUsed()
+class _Person {
+  final name;
+  final lastname;
+  final _Person parent;
+  List<_ContactInfo> contactInfos = [];
+
+  _Person(this.name, this.lastname, [this.parent = null]);
+
+  get fullname => "$name $lastname";
+
+  static _reverse(String str) {
+    StringBuffer out = new StringBuffer();
+    for (int i = str.length; i > 0; i--) {
+      out.write(str[i - 1]);
+    }
+    return out.toString();
+  }
+
+  reversedLastName() => _reverse(lastname);
+}
+
+@MirrorsUsed()
+class _ContactInfo {
+  final String type;
+  final value;
+
+  _ContactInfo(this.type, this.value);
+}
+
+@MirrorsUsed()
+class _Transformer {
+  String transform(String val) => "<b>$val</b>";
+}
diff --git a/third_party/pkg/mustache4dart/test/mustache_issues_test.dart b/third_party/pkg/mustache4dart/test/mustache_issues_test.dart
new file mode 100644
index 0000000..79cc7f3
--- /dev/null
+++ b/third_party/pkg/mustache4dart/test/mustache_issues_test.dart
@@ -0,0 +1,173 @@
+import 'dart:io';
+import 'dart:convert';
+import 'package:test/test.dart';
+import 'package:mustache4dart/mustache4dart.dart';
+import 'package:mustache4dart/mustache_context.dart';
+
+class A {
+  final bar = 'bar';
+
+  String get foo => 'foo';
+}
+
+class B extends A {}
+
+class Parent {
+  String foo;
+}
+
+class Child extends Parent {
+  List<OtherChild> children = [];
+}
+
+class OtherChild extends Parent {}
+
+void main() {
+  group('mustache4dart issues', () {
+    test(
+        '#9: use empty strings for non existing variable',
+        () => expect(
+            render("{{#sec}}[{{variable}}]{{/sec}}", {'sec': 42}), '[]'));
+
+    test('#10',
+        () => expect(render('|\n{{#bob}}\n{{/bob}}\n|', {'bob': []}), '|\n|'));
+
+    test(
+        '#11',
+        () => expect(
+            () => render("{{#sec}}[{{var}}]{{/somethingelse}}", {'sec': 42}),
+            throwsFormatException));
+
+    test('#12: Write to a StringSink', () {
+      StringSink out = new StringBuffer();
+      StringSink outcome = render("{{name}}!", {'name': "George"}, out: out);
+      expect(out, outcome);
+      expect(out.toString(), "George!");
+    });
+
+    group('#16', () {
+      test('side effect',
+          () => expect(render('{{^x}}x{{/x}}!!!', null), 'x!!!'));
+
+      test(
+          'root cause: For null objects the value of any property should be null',
+          () {
+        var ctx = new MustacheContext(null);
+        expect(ctx.field('xxx'), null);
+        expect(ctx.field('123'), null);
+        expect(ctx.field(''), null);
+        expect(ctx.field(null), null);
+      });
+    });
+
+    group('#17', () {
+      test(
+          'side effect',
+          () => expect(
+              render('{{#a}}[{{{a}}}|{{b}}]{{/a}}', {'a': 'aa', 'b': 'bb'}),
+              '[aa|bb]'));
+
+      test('root cause: setting the same context as a subcontext', () {
+        final ctx = new MustacheContext({'a': 'aa', 'b': 'bb'});
+        expect(ctx, isNotNull);
+        expect(ctx.field('a').toString(), isNotNull);
+
+        //Here lies a problem if the subaa.other == suba
+        expect(ctx.field('a').field('a').toString(), isNotNull);
+      });
+    });
+
+    test('#20', () {
+      var currentPath = Directory.current.path;
+      if (!currentPath.endsWith('/test')) {
+        currentPath = "$currentPath/test";
+      }
+      final template = new File("$currentPath/lorem-ipsum.txt")
+          .readAsStringSync(encoding: UTF8);
+
+      final String out = render(template, {'ma': 'ma'});
+      expect(out, template);
+    }, onPlatform: {"js": new Skip("io is not available on a browser")});
+
+    test('#25', () {
+      var ctx = {
+        "parent_name": "John",
+        "children": [
+          {"name": "child"}
+        ]
+      };
+      expect(render('{{#children}}Parent: {{parent_name}}{{/children}}', ctx),
+          'Parent: John');
+    });
+
+    test('#28', () {
+      var model = {
+        "name": "God",
+        "hasChildren": true,
+        "children": [
+          {"name": "granpa", "hasChildren": true},
+          {"name": "granma", "hasChildren": false}
+        ]
+      };
+
+      expect(
+          render(
+              '{{#children}}{{name}}{{#hasChildren}} has children{{/hasChildren}},{{/children}}',
+              model),
+          'granpa has children,granma,');
+    });
+
+    test('#29', () {
+      var list = [1, 'two', 'three', '4'];
+      expect(render('{{#.}}{{.}},{{/.}}', list), '1,two,three,4,');
+    });
+
+    test('#30', () {
+      final txt = '''
+
+<div>
+  <h1>Hello World!</h1>
+</div>
+
+''';
+      expect(render(txt, null), txt);
+    });
+
+    test('#33', () {
+      final b = new B();
+      expect(render('{{b.foo}}', {'b': b}), 'foo');
+      expect(render('{{b.bar}}', {'b': b}), 'bar');
+    });
+
+    test(
+        '#41 do not look into parent context if current context has field but its value is null',
+        () {
+      var c = new Child()
+        ..foo = 'child'
+        ..children = [
+          new OtherChild()..foo = 'otherchild',
+          new OtherChild()..foo = null
+        ];
+
+      var template = '''
+{{foo}}
+{{#children}}{{foo}}!{{/children}}''';
+
+      var output = render(template, c, assumeNullNonExistingProperty: false);
+      var expected = "child\notherchild!!";
+
+      expect(output, expected);
+    });
+
+    test('#44 should provide a way to check for non empty lists', () {
+      final map = {
+        'list': [1, 2]
+      };
+      expect(
+          render(
+              '{{^list.empty}}<ul>{{#list}}<li>{{.}}</li>{{/list}}</ul>{{/list.empty}}',
+              map),
+          '<ul><li>1</li><li>2</li></ul>');
+    });
+  });
+}
diff --git a/third_party/pkg/mustache4dart/test/mustache_line_test.dart b/third_party/pkg/mustache4dart/test/mustache_line_test.dart
new file mode 100644
index 0000000..0a36a50
--- /dev/null
+++ b/third_party/pkg/mustache4dart/test/mustache_line_test.dart
@@ -0,0 +1,94 @@
+import 'package:test/test.dart';
+import 'package:mustache4dart/mustache4dart.dart';
+
+void main() {
+  group('mustache4dart Line', () {
+    var del = new Delimiter('{{', '}}');
+
+    newToken(String s) => new Token(s, null, del, null);
+
+    test('Should not accept more tokens when it is full', () {
+      var l = new Line(newToken('Random text'));
+      l.add(newToken('Random text2'));
+      l.add(newToken(NL));
+      expect(() => l.add(newToken('Some more random text')), throwsStateError);
+    });
+
+    test('add method should return the right line to add more stuff', () {
+      var l = new Line(newToken('some text'));
+      var l2 = l.add(newToken('some more text'));
+      expect(l2, same(l));
+      var nl = l.add(newToken(NL));
+      expect(nl, isNot(same(l)));
+    });
+
+    test('Should not be standalone if it contains a string token', () {
+      var l = new Line(newToken('Some text!'));
+      expect(l.standAlone, isFalse);
+    });
+
+    test("Expression tokens should be considered stand alone capable", () {
+      var l = new Line(newToken(' '));
+      l.add(newToken(' '));
+      l.add(newToken('{{/xxx}}'));
+      expect(l.standAlone, isTrue);
+    });
+
+    test("Last line with an expression only should be considered standalone",
+        () {
+      var l = new Line(newToken(' '));
+      l = l
+          .add(newToken(NL))
+          .add(newToken(' '))
+          .add(newToken(' '))
+          .add(newToken('{{/ending}}'));
+
+      expect(l.standAlone, isTrue);
+    });
+
+    test("Last line should not end with a new line", () {
+      var l = new Line(newToken('#')).add(newToken('{{#a}}'));
+      var l2 = l.add(newToken(NL)).add(newToken('/'));
+      var l3 = l2.add(newToken(NL)).add(newToken(' ')).add(newToken(' '));
+
+      expect(l2, isNot(same(l)));
+      expect(l3, isNot(same(l2)));
+      expect(l.standAlone, isFalse);
+      expect(l2.standAlone, isFalse);
+      expect(l3.standAlone, isTrue);
+    });
+
+    test("Stand empty line should not be considered standAlone", () {
+      //{{#a}}\n{{one}}\n{{/a}}\n\n{{b.two}}\n
+      var l_a = new Line(newToken('{{#a}}'));
+      var l_one = l_a.add(newToken(NL)).add(newToken('{{one}}'));
+      var l_a_end = l_one.add(newToken(NL)).add(newToken('{{/a}}'));
+      var l_empty = l_a_end.add(newToken(NL));
+      var l_b = l_empty.add(newToken(NL)).add(newToken('{{b.two}}'));
+      l_b.add(newToken(NL));
+
+      expect(l_a.standAlone, isTrue);
+      expect(l_one.standAlone, isFalse);
+      expect(l_a_end.standAlone, isTrue);
+
+      //Make sure that the empty line is actual an empty line. It only contains a NL char
+      expect(l_empty.tokens.length, 1);
+      expect(l_empty.tokens[0], newToken(NL));
+      expect(l_empty.standAlone, isFalse,
+          reason:
+              'empty line is part of the template and should not be considered as a standAlone one');
+
+      expect(l_b.standAlone, isFalse);
+    });
+
+    test(
+        "Should cosider partial tag followed by a newline as an standAlone line",
+        () {
+      var l = new Line(newToken('|'));
+      l = l.add(newToken(CRNL));
+      l = l.add(newToken('{{> p}}'));
+      l.add(newToken(CRNL)).add(newToken('|'));
+      expect(l.standAlone, isTrue);
+    });
+  });
+}
diff --git a/third_party/pkg/mustache4dart/test/mustache_specs_test.dart b/third_party/pkg/mustache4dart/test/mustache_specs_test.dart
new file mode 100644
index 0000000..b560fed
--- /dev/null
+++ b/third_party/pkg/mustache4dart/test/mustache_specs_test.dart
@@ -0,0 +1,98 @@
+@TestOn('vm')
+
+import 'dart:io';
+import 'dart:convert';
+import 'package:test/test.dart';
+import 'package:mustache4dart/mustache4dart.dart';
+
+main() {
+  var specs_dir = new Directory('spec/specs');
+  specs_dir.listSync().forEach((FileSystemEntity entity) {
+    var filename = entity.path;
+    if (entity is File && shouldRun(filename)) {
+      var text = entity.readAsStringSync(encoding: UTF8);
+      _defineGroupFromFile(filename, text);
+    }
+  });
+}
+
+_defineGroupFromFile(filename, text) {
+  var json = JSON.decode(text);
+  var tests = json['tests'];
+  filename = filename.substring(filename.lastIndexOf('/') + 1);
+  group("Specs of $filename", () {
+    //Make sure that we reset the state of the Interpolation - Multiple Calls test
+    //as for some reason dart can run the group more than once causing the test
+    //to fail the second time it runs
+    tearDown(() {
+      _DummyCallableWithState callable =
+          lambdas['Interpolation - Multiple Calls'];
+      callable.reset();
+    });
+
+    tests.forEach((t) {
+      var testDescription = new StringBuffer(t['name']);
+      testDescription.write(': ');
+      testDescription.write(t['desc']);
+      var template = t['template'];
+      var data = t['data'];
+      var templateOneline =
+          template.replaceAll('\n', '\\n').replaceAll('\r', '\\r');
+      var reason =
+          new StringBuffer("Could not render right '''$templateOneline'''");
+      var expected = t['expected'];
+      var partials = t['partials'];
+      var partial = (String name) {
+        if (partials == null) {
+          return null;
+        }
+        return partials[name];
+      };
+
+      //swap the data.lambda with a dart real function
+      if (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;
+}
+
+//Until we'll find a way to load a piece of code dynamically,
+//we provide the lambdas at the test here
+class _DummyCallableWithState {
+  var _callCounter = 0;
+
+  call(arg) => "${++_callCounter}";
+
+  reset() => _callCounter = 0;
+}
+
+dynamic lambdas = {
+  'Interpolation': (t) => 'world',
+  'Interpolation - Expansion': (t) => '{{planet}}',
+  'Interpolation - Alternate Delimiters': (t) => "|planet| => {{planet}}",
+  'Interpolation - Multiple Calls': new _DummyCallableWithState(),
+  //function() { return (g=(function(){return this})()).calls=(g.calls||0)+1 }
+  'Escaping': (t) => '>',
+  'Section': (txt) => txt == "{{x}}" ? "yes" : "no",
+  'Section - Expansion': (txt) => "$txt{{planet}}$txt",
+  'Section - Alternate Delimiters': (txt) => "$txt{{planet}} => |planet|$txt",
+  'Section - Multiple Calls': (t) => "__${t}__",
+  'Inverted Section': (txt) => false
+};
diff --git a/third_party/pkg/mustache4dart/test/mustache_test.dart b/third_party/pkg/mustache4dart/test/mustache_test.dart
new file mode 100644
index 0000000..0c1b050
--- /dev/null
+++ b/third_party/pkg/mustache4dart/test/mustache_test.dart
@@ -0,0 +1,189 @@
+import 'package:test/test.dart';
+import 'package:mustache4dart/mustache4dart.dart';
+
+class A {
+  String name;
+  A(this.name);
+}
+
+class B {
+  final map = {};
+
+  B(List<A> list) {
+    map['things'] = list;
+  }
+
+  lambda1(String s) => "1" + s + "1";
+
+  lambda2(String s, {nestedContext}) => "2" + render(s, nestedContext) + "2";
+
+  lambda3() => "[3]";
+
+  lambda4({nestedContext}) => "4${nestedContext != null}4";
+}
+
+void main() {
+  group('mustache4dart tests', () {
+    var salutTemplate = 'Hi {{name}}{{^name}}customer{{/name}}';
+    var salut = compile(salutTemplate);
+    test('Compiled function with existing context',
+        () => expect(salut({'name': 'George'}), 'Hi George'));
+    test('Compiled function with non existing context',
+        () => expect(salut({}), 'Hi customer'));
+    test(
+        'Compiled function with existing context same with render',
+        () => expect(salut({'name': 'George'}),
+            render(salutTemplate, {'name': 'George'})));
+    test('Compiled function with non existing context same with render',
+        () => expect(salut({}), render(salutTemplate, {})));
+
+    test('Contextless one letter template',
+        () => expect(render('!', null), '!'));
+    test('Template with string context after closing one',
+        () => expect(render('{{^x}}No x{{/x}}!!!', null), 'No x!!!'));
+
+    var map = {
+      'a': {'one': 1},
+      'b': {'two': 2},
+      'c': {'three': 3}
+    };
+    test('Simple context test',
+        () => expect(render('{{#a}}{{one}}{{/a}}', map), '1'));
+    test(
+        'Deeper context test',
+        () => expect(
+            render(
+                '{{#a}}{{one}}{{#b}}-{{one}}{{two}}{{#c}}-{{one}}{{two}}{{three}}{{/c}}{{/b}}{{/a}}',
+                map),
+            '1-12-123'));
+    test(
+        'Idented rendering',
+        () => expect(
+            render('Yeah!\nbaby!', null, ident: '--'), '--Yeah!\n--baby!'));
+    test('Standalone without new line',
+        () => expect(render('#{{#a}}\n/\n  {{/a}}', map), '#\n/\n'));
+    test(
+        'Should render emtpy lines',
+        () => expect(
+            render('{{#a}}\n{{one}}\n{{/a}}\n\n{{b.two}}\n', map), '1\n\n2\n'));
+  });
+
+  group('Performance tests', () {
+    test('Compiled templates should be at least 2 times faster', () {
+      var tmpl =
+          '{{#a}}{{one}}{{#b}}-{{one}}{{two}}{{#c}}-{{one}}{{two}}{{three}}{{#d}}-{{one}}{{two}}{{three}}{{four}}{{#e}}{{one}}{{two}}{{three}}{{four}}{{/e}}{{/d}}{{/c}}{{/b}}{{/a}}';
+      StringBuffer buf = new StringBuffer(tmpl);
+      for (int i = 0; i < 10; i++) {
+        buf.write(
+            'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd');
+        buf.write(tmpl);
+      }
+      tmpl = buf.toString();
+
+      var map = {
+        'a': {'one': 1},
+        'b': {'two': 2},
+        'c': {'three': 3},
+        'd': {'four': 4},
+        'e': false
+      };
+      var ctmpl = compile(tmpl);
+
+      var warmup = duration(100, () => "${ctmpl(map)}--${render(tmpl, map)}");
+      print(
+          "Warmup rendering of template with length ${tmpl.length} took ${warmup}millis");
+
+      var d = duration(100, () => render(tmpl, map));
+      print("100 iterations of uncompiled rendering took ${d}millis");
+
+      var d2 = duration(100, () => ctmpl(map));
+      print("100 iterations of compiled rendering tool ${d2}millis");
+      expect(d2 < (d / 2), isTrue);
+    });
+  }, skip: "Performance should not be part of unittest");
+
+  group('mustache4dart enhancements', () {
+    test('Throw exception on unknown tag', () {
+      try {
+        render('Hi {{name}}', {'namee': 'George'});
+      } catch (e) {
+        expect(e, "Could not find 'name' property in {namee: George}}");
+      }
+    });
+
+    test('Throw exception on unknown start tag', () {
+      try {
+        render('Hi {{#name}}man!{{/name}}', {'namee': 'George'});
+      } catch (e) {
+        expect(e, "Could not find 'name' property in {namee: George}}");
+      }
+    });
+
+    group('Lambdas with nested context (#39)', () {
+      test(
+          'Provide lambdas as a dynamic (String s, {nestedContext}) function within a map',
+          () {
+        var context = {
+          'map': {
+            'things': [new A('a'), new A('b')]
+          },
+          'lambda': (String s, {nestedContext}) =>
+              "[" + render(s, nestedContext) + "]"
+        };
+        var template = '''
+{{#map.things}}
+{{#lambda}}{{name}}{{/lambda}}|
+{{/map.things}}
+''';
+        expect(render(template, context), "[a]|\n[b]|\n");
+      });
+
+      test('Provide lambdas as a method(String s) within a class', () {
+        var context = new B([new A('a'), new A('b')]);
+
+        var template =
+            '''{{#map.things}}{{#lambda1}}{{name}}{{/lambda1}}|{{/map.things}}''';
+
+        expect(render(template, context), "1a1|1b1|");
+      });
+
+      test(
+          'Provide lambdas as a method(String s, {nestedContext}) within a class',
+          () {
+        var context = new B([new A('a'), new A('b')]);
+
+        var template =
+            '''{{#map.things}}{{#lambda2}}{{name}}{{/lambda2}}|{{/map.things}}''';
+
+        expect(render(template, context), "2a2|2b2|");
+      });
+
+      test('Provide lambdas as a method() within a class', () {
+        final context = new B([new A('a'), new A('b')]);
+
+        final template =
+            '''{{#map.things}}{{#lambda3}}{{name}}{{/lambda3}}|{{/map.things}}''';
+
+        expect(render(template, context), "[3]|[3]|");
+      });
+
+      test('Provide lambdas as a method({nestedContext}) within a class', () {
+        var context = new B([new A('a'), new A('b')]);
+
+        var template =
+            '''{{#map.things}}{{#lambda4}}{{name}}{{/lambda4}}|{{/map.things}}''';
+
+        expect(render(template, context), "4true4|4true4|");
+      });
+    }, onPlatform: {'js': new Skip("Broken mirrors, should be investigated")});
+  });
+}
+
+num duration(int reps, f()) {
+  var start = new DateTime.now();
+  for (int i = 0; i < reps; i++) {
+    f();
+  }
+  var end = new DateTime.now();
+  return end.millisecondsSinceEpoch - start.millisecondsSinceEpoch;
+}
diff --git a/tool/grind.dart b/tool/grind.dart
index 99d3b1a..e55920c 100644
--- a/tool/grind.dart
+++ b/tool/grind.dart
@@ -160,6 +160,41 @@
   ]);
 }
 
+// TODO(jcollins-g): make a library out of this
+final FilePath _pkgDir = new FilePath('third_party/pkg');
+final FilePath _mustache4dartDir =
+    new FilePath('third_party/pkg/mustache4dart');
+final RegExp _mustache4dartPatches =
+    new RegExp(r'^\d\d\d-mustache4dart-.*[.]patch$');
+@Task('Update third_party forks')
+updateThirdParty() async {
+  run('rm', arguments: ['-rf', _mustache4dartDir.path]);
+  new Directory(_pkgDir.path).createSync(recursive: true);
+  run('git', arguments: [
+    'clone',
+    '--branch',
+    'v2.1.2',
+    '--depth=1',
+    'git@github.com:valotas/mustache4dart',
+    _mustache4dartDir.path,
+  ]);
+  run('rm', arguments: ['-rf', pathLib.join(_mustache4dartDir.path, '.git')]);
+  for (String patchFileName in new Directory(_pkgDir.path)
+      .listSync()
+      .map((e) => pathLib.basename(e.path))
+      .where((String filename) => _mustache4dartPatches.hasMatch(filename))
+      .toList()
+        ..sort()) {
+    run('patch',
+        arguments: [
+          '-p0',
+          '-i',
+          patchFileName,
+        ],
+        workingDirectory: _pkgDir.path);
+  }
+}
+
 @Task('Analyze dartdoc to ensure there are no errors and warnings')
 analyze() async {
   await new SubprocessLauncher('analyze').runStreamed(
@@ -806,7 +841,7 @@
 
   return jsonMessageIterableToWarnings(
       await flutterRepo.launcher.runStreamed(
-          Platform.resolvedExecutable,
+          flutterRepo.cacheDart,
           [
             '--enable-asserts',
             pathLib.join(Directory.current.path, 'bin', 'dartdoc.dart'),
@@ -862,7 +897,7 @@
         '--example-path-prefix',
         'examples',
         '--exclude-packages',
-        'Dart,meta,tuple,quiver_hashcode',
+        'Dart,matcher,meta,path,stack_trace,tuple,quiver',
         '--hide-sdk-text',
         '--no-include-source',
         '--output',