Dartdoc snippet extension to inject full featured code snippets in to API docs. (#23281)

This creates a custom dartdoc tool that will generate snippet blocks in our API docs that allow the user to copy easily to the clipboard, and will also embed the snippet code into a template to show it in a larger context with an app.

This PR adds the snippet tool, a template, and a couple of HTML skeleton files, one for snippets that are designed to be in an application setting, and one where it simply puts a nice container around existing snippets, making them easier to copy to the clipboard.
diff --git a/dev/snippets/README.md b/dev/snippets/README.md
new file mode 100644
index 0000000..0606877
--- /dev/null
+++ b/dev/snippets/README.md
@@ -0,0 +1,57 @@
+## Snippet Tool
+
+This is a dartdoc extension tool that takes code snippets and expands how they
+are presented so that Flutter can have more interactive and useful code
+snippets.
+
+This takes code in dartdocs, like this:
+
+```dart
+/// The following is a skeleton of a stateless widget subclass called `GreenFrog`:
+/// {@tool snippet --template="stateless_widget"}
+/// class GreenFrog extends StatelessWidget {
+///   const GreenFrog({ Key key }) : super(key: key);
+///
+///   @override
+///   Widget build(BuildContext context) {
+///     return Container(color: const Color(0xFF2DBD3A));
+///   }
+/// }
+/// {@end-tool}
+```
+
+And converts it into something which has a nice visual presentation, and 
+a button to automatically copy the sample to the clipboard.
+
+It does this by processing the source input and emitting HTML for output,
+which dartdoc places back into the documentation. Any options given to the
+ `{@tool ...}` directive are passed on verbatim to the tool.
+
+To render the above, the snippets tool needs to render the code in a combination
+of markdown and HTML, using the `{@inject-html}` dartdoc directive.
+
+## Templates
+
+In order to support showing an entire app when you click on the right tab of
+the code snippet UI, we have to be able to insert the snippet into the template
+and instantiate the right parts.
+
+To do this, there is a [config/templates](config/templates) directory that
+contains a list of templates. These templates represent an entire app that the
+snippet can be placed into, basically a replacement for `lib/main.dart` in a
+flutter app package.
+
+## Skeletons
+
+A skeleton (in relation to this tool, in the [config/skeletons](config/skeletons)
+directory) is an HTML template into which the snippet Dart code and description
+are interpolated, in order to display it nicely.
+
+There is currently one skeleton for
+[application](config/skeletons/application.html) snippets and one for
+[sample](config/skeletons/sample.html)
+snippets, but there could be more. It uses moustache notation (e.g. `{{code}}`)
+to mark where the components to be interpolated into the template should go.
+
+(It doesn't actually use the moustache package, since the only things that need
+substituting are simple strings, but it uses the same syntax). 
\ No newline at end of file
diff --git a/dev/snippets/config/skeletons/application.html b/dev/snippets/config/skeletons/application.html
new file mode 100644
index 0000000..bbbed4f
--- /dev/null
+++ b/dev/snippets/config/skeletons/application.html
@@ -0,0 +1,34 @@
+{@inject-html}
+<div class="snippet-buttons">
+  <button id="shortSnippetButton" onclick="showSnippet(shortSnippet);" selected>Sample</button>
+  <button id="longSnippetButton" onclick="showSnippet(longSnippet);">Sample in an App</button>
+</div>
+<div class="snippet-container">
+  <div class="snippet" id="shortSnippet">
+    <div class="snippet-description">
+      {@end-inject-html}
+      {{description}}
+      {@inject-html}
+    </div>
+    <div class="copyable-container">
+      <button class="copy-button-overlay copy-button" title="Copy to clipboard"
+              onclick="copyTextToClipboard();">
+        <i class="material-icons copy-image">assignment</i>
+      </button>
+      <pre class="language-dart"><code class="language-dart">{{code}}</code></pre>
+    </div>
+  </div>
+  <div class="snippet" id="longSnippet" hidden>
+    <div class="snippet-description">To create a sample project with this code snippet, run:<br/>
+      <span class="snippet-create-command">flutter create --snippet={{id}} mysample</span>
+    </div>
+    <div class="copyable-container">
+      <button class="copy-button-overlay copy-button" title="Copy to clipboard"
+              onclick="copyTextToClipboard();">
+        <i class="material-icons copy-image">assignment</i>
+      </button>
+      <pre class="language-dart"><code class="language-dart">{{app}}</code></pre>
+    </div>
+  </div>
+</div>
+{@end-inject-html}
diff --git a/dev/snippets/config/skeletons/sample.html b/dev/snippets/config/skeletons/sample.html
new file mode 100644
index 0000000..9343a01
--- /dev/null
+++ b/dev/snippets/config/skeletons/sample.html
@@ -0,0 +1,20 @@
+{@inject-html}
+<div class="snippet-container">
+  <div class="snippet">
+    <div class="snippet-description">
+      {@end-inject-html}
+      {{description}}
+      {@inject-html}
+    </div>
+    <div class="copyable-container">
+      <button class="copy-button-overlay copy-button" title="Copy to clipboard"
+              onclick="copyTextToClipboard(findSiblingWithId(this, 'sample-code'));">
+        <i class="material-icons copy-image">assignment</i>
+      </button>
+      <pre class="language-dart" id="sample-code">
+        <code class="language-dart">{{code}}</code>
+      </pre>
+    </div>
+  </div>
+</div>
+{@end-inject-html}
diff --git a/dev/snippets/config/templates/README.md b/dev/snippets/config/templates/README.md
new file mode 100644
index 0000000..e5addd0
--- /dev/null
+++ b/dev/snippets/config/templates/README.md
@@ -0,0 +1,56 @@
+## Creating Code Snippets
+
+In general, creating application snippets can be accomplished with the following
+syntax inside of the dartdoc comment for a Flutter class/variable/enum/etc.:
+
+```dart
+/// {@tool snippet --template=stateful_widget}
+/// Any text outside of the code blocks will be accumulated and placed at the
+/// top of the snippet box as a description. Don't try and say "see the code
+/// above" or "see the code below", since the location of the description may
+/// change in the future. You can use dartdoc [Linking] in the description, and
+/// __Markdown__ too.
+/// ```dart preamble
+/// class Foo extends StatelessWidget {
+///   const Foo({this.value = ''});
+/// 
+///   String value; 
+/// 
+///   @override
+///   Widget build(BuildContext context) {
+///     return Text(value);
+///   }
+/// }
+/// ```
+/// This will get tacked on to the end of the description above, and shown above
+/// the snippet.  These two code blocks will be separated by `///...` in the
+/// short version of the snippet code sample.
+/// ```dart
+/// String myValue = 'Foo';
+/// 
+/// @override
+/// Widget build(BuildContext) {
+///   return const Foo(myValue);
+/// }
+/// ```
+/// {@end-tool}
+```
+
+This will result in the template having the section that's inside "```dart"
+interpolated into the template's stateful widget's state object body.
+
+All code within a code block in a snippet needs to be able to be run through
+dartfmt without errors, so it needs to be valid code (This shouldn't be an
+additional burden, since all code will also be compiled to be sure it compiles).
+
+## Available Templates
+
+The templates available for using as an argument to the snippets tool are as
+follows:
+
+- __`stateful_widget`__ : Takes a `preamble` in addition to the default code
+  block, which will be placed at the top level of the Dart file, so bare
+  function calls are not allowed in the preamble.  The default code block is
+  placed as the body of a stateful widget, so you will need to implement the
+  build() function, and any state variables.
+  
diff --git a/dev/snippets/config/templates/stateful_widget.tmpl b/dev/snippets/config/templates/stateful_widget.tmpl
new file mode 100644
index 0000000..aff8fbf
--- /dev/null
+++ b/dev/snippets/config/templates/stateful_widget.tmpl
@@ -0,0 +1,32 @@
+{{description}}
+
+import 'package:flutter/material.dart';
+
+void main() => runApp(new MyApp());
+
+class MyApp extends StatelessWidget {
+  // This widget is the root of your application.
+  @override
+  Widget build(BuildContext context) {
+    return new MaterialApp(
+      title: 'Flutter Code Sample for {{id}}',
+      theme: new ThemeData(
+        primarySwatch: Colors.blue,
+      ),
+      home: new MyHomePage(title: '{{id}} Sample'),
+    );
+  }
+}
+
+{{code-preamble}}
+
+class MyHomePage extends StatelessWidget {
+  MyHomePage({Key key}) : super(key: key);
+
+  @override
+  _MyHomePageState createState() => new _MyHomePageState();
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+  {{code}}
+}
diff --git a/dev/snippets/lib/configuration.dart b/dev/snippets/lib/configuration.dart
new file mode 100644
index 0000000..3f7f3eb
--- /dev/null
+++ b/dev/snippets/lib/configuration.dart
@@ -0,0 +1,72 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io' hide Platform;
+
+import 'package:meta/meta.dart';
+import 'package:platform/platform.dart';
+import 'package:path/path.dart' as path;
+
+/// What type of snippet to produce.
+enum SnippetType {
+  /// Produces a snippet that includes the code interpolated into an application
+  /// template.
+  application,
+  /// Produces a nicely formatted sample code, but no application.
+  sample,
+}
+
+/// Return the name of an enum item.
+String getEnumName(dynamic enumItem) {
+  final String name = '$enumItem';
+  final int index = name.indexOf('.');
+  return index == -1 ? name : name.substring(index + 1);
+}
+
+/// A class to compute the configuration of the snippets input and output
+/// locations based in the current location of the snippets main.dart.
+class Configuration {
+  const Configuration({Platform platform}) : platform = platform ?? const LocalPlatform();
+
+  final Platform platform;
+
+  /// This is the configuration directory for the snippets system, containing
+  /// the skeletons and templates.
+  @visibleForTesting
+  Directory getConfigDirectory(String kind) {
+    final String platformScriptPath = path.dirname(platform.script.toFilePath());
+    final String configPath =
+        path.canonicalize(path.join(platformScriptPath, '..', 'config', kind));
+    return Directory(configPath);
+  }
+
+  /// This is where the snippets themselves will be written, in order to be
+  /// uploaded to the docs site.
+  Directory get outputDirectory {
+    final String platformScriptPath = path.dirname(platform.script.toFilePath());
+    final String docsDirectory =
+      path.canonicalize(path.join(platformScriptPath, '..', '..', 'docs', 'doc', 'snippets'));
+    return Directory(docsDirectory);
+  }
+
+  /// This makes sure that the output directory exists.
+  void createOutputDirectory() {
+    if (!outputDirectory.existsSync()) {
+      outputDirectory.createSync(recursive: true);
+    }
+  }
+
+  /// The directory containing the HTML skeletons to be filled out with metadata
+  /// and returned to dartdoc for insertion in the output.
+  Directory get skeletonsDirectory => getConfigDirectory('skeletons');
+
+  /// The directory containing the code templates that can be referenced by the
+  /// dartdoc.
+  Directory get templatesDirectory => getConfigDirectory('templates');
+
+  /// Gets the skeleton file to use for the given [SnippetType].
+  File getHtmlSkeletonFile(SnippetType type) {
+    return File(path.join(skeletonsDirectory.path, '${getEnumName(type)}.html'));
+  }
+}
diff --git a/dev/snippets/lib/main.dart b/dev/snippets/lib/main.dart
new file mode 100644
index 0000000..3a84317
--- /dev/null
+++ b/dev/snippets/lib/main.dart
@@ -0,0 +1,122 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io' hide Platform;
+
+import 'package:args/args.dart';
+import 'package:platform/platform.dart';
+
+import 'configuration.dart';
+import 'snippets.dart';
+
+const String _kElementOption = 'element';
+const String _kInputOption = 'input';
+const String _kLibraryOption = 'library';
+const String _kPackageOption = 'package';
+const String _kTemplateOption = 'template';
+const String _kTypeOption = 'type';
+
+/// Generates snippet dartdoc output for a given input, and creates any sample
+/// applications needed by the snippet.
+void main(List<String> argList) {
+  const Platform platform = LocalPlatform();
+  final Map<String, String> environment = platform.environment;
+  final ArgParser parser = ArgParser();
+  final List<String> snippetTypes =
+      SnippetType.values.map<String>((SnippetType type) => getEnumName(type)).toList();
+  parser.addOption(
+    _kTypeOption,
+    defaultsTo: getEnumName(SnippetType.application),
+    allowed: snippetTypes,
+    allowedHelp: <String, String>{
+      getEnumName(SnippetType.application):
+          'Produce a code snippet complete with embedding the sample in an '
+          'application template.',
+      getEnumName(SnippetType.sample):
+          'Produce a nicely formatted piece of sample code. Does not embed the '
+          'sample into an application template.'
+    },
+    help: 'The type of snippet to produce.',
+  );
+  parser.addOption(
+    _kTemplateOption,
+    defaultsTo: null,
+    help: 'The name of the template to inject the code into.',
+  );
+  parser.addOption(
+    _kInputOption,
+    defaultsTo: environment['INPUT'],
+    help: 'The input file containing the snippet code to inject.',
+  );
+  parser.addOption(
+    _kPackageOption,
+    defaultsTo: environment['PACKAGE_NAME'],
+    help: 'The name of the package that this snippet belongs to.',
+  );
+  parser.addOption(
+    _kLibraryOption,
+    defaultsTo: environment['LIBRARY_NAME'],
+    help: 'The name of the library that this snippet belongs to.',
+  );
+  parser.addOption(
+    _kElementOption,
+    defaultsTo: environment['ELEMENT_NAME'],
+    help: 'The name of the element that this snippet belongs to.',
+  );
+
+  final ArgResults args = parser.parse(argList);
+
+  final SnippetType snippetType = SnippetType.values
+      .firstWhere((SnippetType type) => getEnumName(type) == args[_kTypeOption], orElse: () => null);
+  assert(snippetType != null, "Unable to find '${args[_kTypeOption]}' in SnippetType enum.");
+
+  if (args[_kInputOption] == null) {
+    stderr.writeln(parser.usage);
+    errorExit('The --$_kInputOption option must be specified, either on the command '
+        'line, or in the INPUT environment variable.');
+  }
+
+  final File input = File(args['input']);
+  if (!input.existsSync()) {
+    errorExit('The input file ${input.path} does not exist.');
+  }
+
+  String template;
+  if (snippetType == SnippetType.application) {
+    if (args[_kTemplateOption] == null || args[_kTemplateOption].isEmpty) {
+      stderr.writeln(parser.usage);
+      errorExit('The --$_kTemplateOption option must be specified on the command '
+          'line for application snippets.');
+    }
+    template = args[_kTemplateOption].toString().replaceAll(RegExp(r'.tmpl$'), '');
+  }
+
+  final List<String> id = <String>[];
+  if (args[_kPackageOption] != null &&
+      args[_kPackageOption].isNotEmpty &&
+      args[_kPackageOption] != 'flutter') {
+    id.add(args[_kPackageOption]);
+  }
+  if (args[_kLibraryOption] != null && args[_kLibraryOption].isNotEmpty) {
+    id.add(args[_kLibraryOption]);
+  }
+  if (args[_kElementOption] != null && args[_kElementOption].isNotEmpty) {
+    id.add(args[_kElementOption]);
+  }
+
+  if (id.isEmpty) {
+    errorExit('Unable to determine ID. At least one of --$_kPackageOption, '
+        '--$_kLibraryOption, --$_kElementOption, or the environment variables '
+        'PACKAGE_NAME, LIBRARY_NAME, or ELEMENT_NAME must be non-empty.');
+  }
+
+  final SnippetGenerator generator = SnippetGenerator();
+  stdout.write(generator.generate(
+    input,
+    snippetType,
+    template: template,
+    id: id.join('.'),
+  ));
+  exit(0);
+}
diff --git a/dev/snippets/lib/snippets.dart b/dev/snippets/lib/snippets.dart
new file mode 100644
index 0000000..c09f59c
--- /dev/null
+++ b/dev/snippets/lib/snippets.dart
@@ -0,0 +1,222 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+import 'package:dart_style/dart_style.dart';
+
+import 'configuration.dart';
+
+void errorExit(String message) {
+  stderr.writeln(message);
+  exit(1);
+}
+
+// A Tuple containing the name and contents associated with a code block in a
+// snippet.
+class _ComponentTuple {
+  _ComponentTuple(this.name, this.contents);
+  final String name;
+  final List<String> contents;
+  String get mergedContent => contents.join('\n').trim();
+}
+
+/// Generates the snippet HTML, as well as saving the output snippet main to
+/// the output directory.
+class SnippetGenerator {
+  SnippetGenerator({Configuration configuration})
+      : configuration = configuration ?? const Configuration() {
+    this.configuration.createOutputDirectory();
+  }
+
+  /// The configuration used to determine where to get/save data for the
+  /// snippet.
+  final Configuration configuration;
+
+  /// A Dart formatted used to format the snippet code and finished application
+  /// code.
+  static DartFormatter formatter = DartFormatter(pageWidth: 80, fixes: StyleFix.all);
+
+  /// This returns the output file for a given snippet ID. Only used for
+  /// [SnippetType.application] snippets.
+  File getOutputFile(String id) => File(path.join(configuration.outputDirectory.path, '$id.dart'));
+
+  /// Gets the path to the template file requested.
+  File getTemplatePath(String templateName, {Directory templatesDir}) {
+    final Directory templateDir = templatesDir ?? configuration.templatesDirectory;
+    final File templateFile = File(path.join(templateDir.path, '$templateName.tmpl'));
+    return templateFile.existsSync() ? templateFile : null;
+  }
+
+  /// Injects the [injections] into the [template], and turning the
+  /// "description" injection into a comment. Only used for
+  /// [SnippetType.application] snippets.
+  String interpolateTemplate(List<_ComponentTuple> injections, String template) {
+    final String injectionMatches =
+        injections.map<String>((_ComponentTuple tuple) => RegExp.escape(tuple.name)).join('|');
+    final RegExp moustacheRegExp = RegExp('{{($injectionMatches)}}');
+    return template.replaceAllMapped(moustacheRegExp, (Match match) {
+      if (match[1] == 'description') {
+        // Place the description into a comment.
+        final List<String> description = injections
+            .firstWhere((_ComponentTuple tuple) => tuple.name == match[1])
+            .contents
+            .map<String>((String line) => '// $line')
+            .toList();
+        // Remove any leading/trailing empty comment lines.
+        // We don't want to remove ALL empty comment lines, only the ones at the
+        // beginning and the end.
+        while (description.last == '// ') {
+          description.removeLast();
+        }
+        while (description.first == '// ') {
+          description.removeAt(0);
+        }
+        return description.join('\n').trim();
+      } else {
+        return injections
+            .firstWhere((_ComponentTuple tuple) => tuple.name == match[1])
+            .mergedContent;
+      }
+    }).trim();
+  }
+
+  /// Interpolates the [injections] into an HTML skeleton file.
+  ///
+  /// Similar to interpolateTemplate, but we are only looking for `code-`
+  /// components, and we care about the order of the injections.
+  ///
+  /// Takes into account the [type] and doesn't substitute in the id and the app
+  /// if not a [SnippetType.application] snippet.
+  String interpolateSkeleton(SnippetType type, List<_ComponentTuple> injections, String skeleton) {
+    final List<String> result = <String>[];
+    for (_ComponentTuple injection in injections) {
+      if (!injection.name.startsWith('code')) {
+        continue;
+      }
+      result.addAll(injection.contents);
+      result.addAll(<String>['', '// ...', '']);
+    }
+    if (result.length > 3) {
+      result.removeRange(result.length - 3, result.length);
+    }
+    String formattedCode;
+    try {
+      formattedCode = formatter.format(result.join('\n'));
+    } on FormatterException catch (exception) {
+      errorExit('Unable to format snippet code: $exception');
+    }
+    final Map<String, String> substitutions = <String, String>{
+      'description': injections
+          .firstWhere((_ComponentTuple tuple) => tuple.name == 'description')
+          .mergedContent,
+      'code': formattedCode,
+    }..addAll(type == SnippetType.application
+        ? <String, String>{
+            'id':
+                injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'id').mergedContent,
+            'app':
+                injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'app').mergedContent,
+          }
+        : <String, String>{'id': '', 'app': ''});
+    return skeleton.replaceAllMapped(RegExp(r'{{(code|app|id|description)}}'), (Match match) {
+      return substitutions[match[1]];
+    });
+  }
+
+  /// Parses the input for the various code and description segments, and
+  /// returns them in the order found.
+  List<_ComponentTuple> parseInput(String input) {
+    bool inSnippet = false;
+    input = input.trim();
+    final List<String> description = <String>[];
+    final List<_ComponentTuple> components = <_ComponentTuple>[];
+    String currentComponent;
+    for (String line in input.split('\n')) {
+      final Match match = RegExp(r'^\s*```(dart|dart (\w+))?\s*$').firstMatch(line);
+      if (match != null) {
+        inSnippet = !inSnippet;
+        if (match[1] != null) {
+          currentComponent = match[1];
+          if (match[2] != null) {
+            components.add(_ComponentTuple('code-${match[2]}', <String>[]));
+          } else {
+            components.add(_ComponentTuple('code', <String>[]));
+          }
+        } else {
+          currentComponent = null;
+        }
+        continue;
+      }
+      if (!inSnippet) {
+        description.add(line);
+      } else {
+        assert(currentComponent != null);
+        components.last.contents.add(line);
+      }
+    }
+    return <_ComponentTuple>[
+      _ComponentTuple('description', description),
+    ]..addAll(components);
+  }
+
+  String _loadFileAsUtf8(File file) {
+    return file.readAsStringSync(encoding: Encoding.getByName('utf-8'));
+  }
+
+  /// The main routine for generating snippets.
+  ///
+  /// The [input] is the file containing the dartdoc comments (minus the leading
+  /// comment markers).
+  ///
+  /// The [type] is the type of snippet to create: either a
+  /// [SnippetType.application] or a [SnippetType.sample].
+  ///
+  /// The [template] must not be null if the [type] is
+  /// [SnippetType.application], and specifies the name of the template to use
+  /// for the application code.
+  ///
+  /// The [id] is a string ID to use for the output file, and to tell the user
+  /// about in the `flutter create` hint. It must not be null if the [type] is
+  /// [SnippetType.application].
+  String generate(File input, SnippetType type, {String template, String id}) {
+    assert(template != null || type != SnippetType.application);
+    assert(id != null || type != SnippetType.application);
+    assert(input != null);
+    final List<_ComponentTuple> snippetData = parseInput(_loadFileAsUtf8(input));
+    switch (type) {
+      case SnippetType.application:
+        final Directory templatesDir = configuration.templatesDirectory;
+        if (templatesDir == null) {
+          stderr.writeln('Unable to find the templates directory.');
+          exit(1);
+        }
+        final File templateFile = getTemplatePath(template, templatesDir: templatesDir);
+        if (templateFile == null) {
+          stderr.writeln(
+              'The template $template was not found in the templates directory ${templatesDir.path}');
+          exit(1);
+        }
+        snippetData.add(_ComponentTuple('id', <String>[id]));
+        final String templateContents = _loadFileAsUtf8(templateFile);
+        String app = interpolateTemplate(snippetData, templateContents);
+
+        try {
+          app = formatter.format(app);
+        } on FormatterException catch (exception) {
+          errorExit('Unable to format snippet app template: $exception');
+        }
+
+        snippetData.add(_ComponentTuple('app', app.split('\n')));
+        getOutputFile(id).writeAsStringSync(app);
+        break;
+      case SnippetType.sample:
+        break;
+    }
+    final String skeleton = _loadFileAsUtf8(configuration.getHtmlSkeletonFile(type));
+    return interpolateSkeleton(type, snippetData, skeleton);
+  }
+}
diff --git a/dev/snippets/pubspec.yaml b/dev/snippets/pubspec.yaml
new file mode 100644
index 0000000..7d76fc3
--- /dev/null
+++ b/dev/snippets/pubspec.yaml
@@ -0,0 +1,101 @@
+name: snippets
+version: 0.1.0
+author: Flutter Team <flutter-dev@googlegroups.com>
+description: A code snippet dartdoc extension for Flutter API docs.
+homepage: https://github.com/flutter/flutter
+
+environment:
+  # The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite.
+  sdk: ">=2.0.0-dev.68.0 <3.0.0"
+
+dartdoc:
+  # Exclude this package from the hosted API docs (Ironically...).
+  nodoc: true
+
+dependencies:
+  args: 1.5.0
+  dart_style: 1.2.0
+  meta: 1.1.6
+  platform: 2.2.0
+
+  analyzer: 0.33.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  async: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  charcode: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  convert: 2.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  crypto: 2.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  csslib: 0.14.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  front_end: 0.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  glob: 1.1.7 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  html: 0.13.3+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  kernel: 0.3.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  logging: 0.11.3+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  package_config: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  path: 1.6.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  plugin: 0.2.0+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  source_span: 1.4.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  string_scanner: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  utf: 0.9.0+5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  watcher: 0.9.7+10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  yaml: 2.1.15 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+
+dev_dependencies:
+  test: 1.3.4
+
+  boolean_selector: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  http: 0.12.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  http_multi_server: 2.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  http_parser: 3.1.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  io: 0.3.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  js: 0.6.1+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  json_rpc_2: 2.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  matcher: 0.12.3+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  mime: 0.9.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  multi_server_socket: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  node_preamble: 1.4.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  package_resolver: 1.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  pool: 1.3.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  pub_semver: 1.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  shelf: 0.7.3+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  shelf_packages_handler: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  shelf_static: 0.2.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  shelf_web_socket: 0.2.2+4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  source_map_stack_trace: 1.1.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  source_maps: 0.10.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  stack_trace: 1.9.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  stream_channel: 1.6.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  term_glyph: 1.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  vm_service_client: 0.2.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  web_socket_channel: 1.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+
+executables:
+  snippets: null
+
+  boolean_selector: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  http: 0.12.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  http_multi_server: 2.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  http_parser: 3.1.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  io: 0.3.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  js: 0.6.1+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  json_rpc_2: 2.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  matcher: 0.12.3+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  mime: 0.9.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  multi_server_socket: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  node_preamble: 1.4.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  package_resolver: 1.0.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  pool: 1.3.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  pub_semver: 1.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  shelf: 0.7.3+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  shelf_packages_handler: 1.0.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  shelf_static: 0.2.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  shelf_web_socket: 0.2.2+4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  source_map_stack_trace: 1.1.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  source_maps: 0.10.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  stack_trace: 1.9.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  stream_channel: 1.6.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  term_glyph: 1.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  vm_service_client: 0.2.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+  web_socket_channel: 1.0.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
+
+# PUBSPEC CHECKSUM: f478
diff --git a/dev/snippets/test/configuration_test.dart b/dev/snippets/test/configuration_test.dart
new file mode 100644
index 0000000..8b2e567
--- /dev/null
+++ b/dev/snippets/test/configuration_test.dart
@@ -0,0 +1,45 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:platform/platform.dart' show FakePlatform;
+
+import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
+
+import 'package:snippets/configuration.dart';
+
+void main() {
+  group('Configuration', () {
+    FakePlatform fakePlatform;
+    Configuration config;
+
+    setUp(() {
+      fakePlatform = FakePlatform(
+          operatingSystem: 'linux',
+          script: Uri.parse('file:///flutter/dev/snippets/lib/configuration_test.dart'));
+      config = Configuration(platform: fakePlatform);
+    });
+    test('config directory is correct', () async {
+      expect(config.getConfigDirectory('foo').path,
+          matches(RegExp(r'[/\\]flutter[/\\]dev[/\\]snippets[/\\]config[/\\]foo')));
+    });
+    test('output directory is correct', () async {
+      expect(config.outputDirectory.path,
+          matches(RegExp(r'[/\\]flutter[/\\]dev[/\\]docs[/\\]doc[/\\]snippets')));
+    });
+    test('skeleton directory is correct', () async {
+      expect(config.skeletonsDirectory.path,
+          matches(RegExp(r'[/\\]flutter[/\\]dev[/\\]snippets[/\\]config[/\\]skeletons')));
+    });
+    test('templates directory is correct', () async {
+      expect(config.templatesDirectory.path,
+          matches(RegExp(r'[/\\]flutter[/\\]dev[/\\]snippets[/\\]config[/\\]templates')));
+    });
+    test('html skeleton file is correct', () async {
+      expect(
+          config.getHtmlSkeletonFile(SnippetType.application).path,
+          matches(RegExp(
+              r'[/\\]flutter[/\\]dev[/\\]snippets[/\\]config[/\\]skeletons[/\\]application.html')));
+    });
+  });
+}
diff --git a/dev/snippets/test/snippets_test.dart b/dev/snippets/test/snippets_test.dart
new file mode 100644
index 0000000..47bdc1a
--- /dev/null
+++ b/dev/snippets/test/snippets_test.dart
@@ -0,0 +1,118 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:io' hide Platform;
+import 'package:path/path.dart' as path;
+
+import 'package:platform/platform.dart' show FakePlatform;
+
+import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
+
+import 'package:snippets/configuration.dart';
+import 'package:snippets/snippets.dart';
+
+void main() {
+  group('Generator', () {
+    FakePlatform fakePlatform;
+    Configuration configuration;
+    SnippetGenerator generator;
+    Directory tmpDir;
+    File template;
+
+    setUp(() {
+      tmpDir = Directory.systemTemp.createTempSync('snippets_test');
+      fakePlatform = FakePlatform(
+          script: Uri.file(path.join(
+              tmpDir.absolute.path, 'flutter', 'dev', 'snippets', 'lib', 'snippets_test.dart')));
+      configuration = Configuration(platform: fakePlatform);
+      configuration.createOutputDirectory();
+      configuration.templatesDirectory.createSync(recursive: true);
+      configuration.skeletonsDirectory.createSync(recursive: true);
+      template = File(path.join(configuration.templatesDirectory.path, 'template.tmpl'));
+      template.writeAsStringSync('''
+
+{{description}}
+
+{{code-preamble}}
+
+main() {
+  {{code}}
+}
+''');
+      configuration.getHtmlSkeletonFile(SnippetType.application).writeAsStringSync('''
+<div>HTML Bits</div>
+{{description}}
+<pre>{{code}}</pre>
+<pre>{{app}}</pre>
+<div>More HTML Bits</div>
+''');
+      configuration.getHtmlSkeletonFile(SnippetType.sample).writeAsStringSync('''
+<div>HTML Bits</div>
+{{description}}
+<pre>{{code}}</pre>
+<div>More HTML Bits</div>
+''');
+      generator = SnippetGenerator(configuration: configuration);
+    });
+    tearDown(() {
+      tmpDir.deleteSync(recursive: true);
+    });
+
+    test('generates application snippets', () async {
+      final File inputFile = File(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
+        ..createSync(recursive: true)
+        ..writeAsStringSync('''
+A description of the snippet.
+
+On several lines.
+
+```dart preamble
+const String name = 'snippet';
+```
+
+```dart
+void main() {
+  print('The actual \$name.');
+}
+```
+''');
+
+      final String html =
+          generator.generate(inputFile, SnippetType.application, template: 'template', id: 'id');
+      expect(html, contains('<div>HTML Bits</div>'));
+      expect(html, contains('<div>More HTML Bits</div>'));
+      expect(html, contains("print('The actual \$name.');"));
+      expect(html, contains('A description of the snippet.\n'));
+      expect(
+          html,
+          contains('// A description of the snippet.\n'
+              '//\n'
+              '// On several lines.\n'));
+      expect(html, contains('void main() {'));
+    });
+
+    test('generates sample snippets', () async {
+      final File inputFile = File(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
+        ..createSync(recursive: true)
+        ..writeAsStringSync('''
+A description of the snippet.
+
+On several lines.
+
+```code
+void main() {
+  print('The actual \$name.');
+}
+```
+''');
+
+      final String html = generator.generate(inputFile, SnippetType.sample);
+      expect(html, contains('<div>HTML Bits</div>'));
+      expect(html, contains('<div>More HTML Bits</div>'));
+      expect(html, contains("print('The actual \$name.');"));
+      expect(html, contains('A description of the snippet.\n\nOn several lines.\n'));
+      expect(html, contains('main() {'));
+    });
+  });
+}