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/bots/analyze.dart b/dev/bots/analyze.dart
index 1b911ef..7b25c5a 100644
--- a/dev/bots/analyze.dart
+++ b/dev/bots/analyze.dart
@@ -266,6 +266,7 @@
         if (path.split(file.path).contains('test_driver') ||
             name.startsWith('dev/missing_dependency_tests/') ||
             name.startsWith('dev/automated_tests/') ||
+            name.startsWith('dev/snippets/') ||
             name.startsWith('packages/flutter/test/engine/') ||
             name.startsWith('examples/layers/test/smoketests/raw/') ||
             name.startsWith('examples/layers/test/smoketests/rendering/') ||
diff --git a/dev/bots/test.dart b/dev/bots/test.dart
index 01c2c2e..a2b2483 100644
--- a/dev/bots/test.dart
+++ b/dev/bots/test.dart
@@ -182,6 +182,7 @@
   await _runFlutterTest(path.join(flutterRoot, 'packages', 'fuchsia_remote_debug_protocol'));
   await _pubRunTest(path.join(flutterRoot, 'dev', 'bots'));
   await _pubRunTest(path.join(flutterRoot, 'dev', 'devicelab'));
+  await _pubRunTest(path.join(flutterRoot, 'dev', 'snippets'));
   await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'android_semantics_testing'));
   await _runFlutterTest(path.join(flutterRoot, 'dev', 'manual_tests'));
   await _runFlutterTest(path.join(flutterRoot, 'dev', 'tools', 'vitool'));
diff --git a/dev/docs/assets/overrides.css b/dev/docs/assets/overrides.css
new file mode 100644
index 0000000..4aa2716
--- /dev/null
+++ b/dev/docs/assets/overrides.css
@@ -0,0 +1,139 @@
+/* Overrides for dartdoc styles. */
+body {
+  font-size: 15px;
+  font-family: Roboto, sans-serif;
+  line-height: 1.5;
+  color: #111;
+  background-color: #fdfdfd;
+  font-weight: 300;
+  -webkit-font-smoothing: auto;
+}
+
+header {
+  background-color: white;
+  color: #424242;
+}
+
+nav.navbar {
+  min-height: 57px;
+  padding: 6px 0;
+}
+
+header.header-fixed nav.navbar-fixed-top {
+  box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37);
+}
+
+h1, h2 {
+  font-weight: 300;
+}
+
+h3, h4, h5, h6 {
+  font-weight: 400;
+}
+
+h1 {
+  font-size: 42px !important;
+  letter-spacing: -1px;
+}
+
+header h1 {
+  font-weight: 300;
+}
+
+h2 {
+  color: #111;
+  font-size: 24px;
+}
+
+.markdown h2 {
+  font-size: 24px;
+}
+
+section.summary h2 {
+  font-size: 24px;
+  color: inherit;
+  border-bottom: none;
+}
+
+.sidebar ol,
+.sidebar ol li.section-title {
+  font-size: inherit;
+}
+
+@media screen and (max-width: 768px) {
+  .sidebar-offcanvas-left.active {
+    padding: 10px;
+  }
+}
+
+.sidebar-offcanvas-left ol {
+  padding: 0 16px 16px 0;
+}
+
+.sidebar-offcanvas-left h5 {
+  display: none;
+}
+
+pre,
+pre.prettyprint,
+pre > code {
+  font-size: 14px;
+}
+
+pre,
+pre.prettyprint {
+  background: #f5f2f0;
+  margin: 0 0 15px 0;
+  padding: 8px 12px;
+  border: 1px solid #cccccc;
+  border-radius: 4px;
+}
+
+code {
+  background-color: inherit;
+  font-size: 1em; /* browsers default to smaller font for code */
+  font-weight: 300;
+  padding-left: 0; /* otherwise we get ragged left margins */
+  padding-right: 0;
+}
+
+#search-box {
+  color: #555;
+  background-color: #fff;
+  background-image: none;
+  border: 1px solid #ccc;
+  border-radius: 2px;
+  font-family: inherit;
+  padding: 4px 6px;
+  font-size: 15px;
+}
+
+input.form-control.typeahead {
+  padding: 4px 7px;
+  font-size: 15px;
+}
+
+dl.dl-horizontal dt {
+  color: inherit;
+}
+
+/* Line the material icons up with their labels */
+i.material-icons.md-36,
+i.material-icons.md-48 {
+  vertical-align: bottom;
+}
+
+/* thinify the inherited names in lists */
+li.inherited a {
+  font-weight: 100;
+}
+
+/* address a style issue with the background of code sections */
+code.hljs {
+  background: inherit;
+}
+
+footer {
+  font-size: 13px;
+  padding: 12px 20px;
+}
diff --git a/dev/docs/assets/snippets.css b/dev/docs/assets/snippets.css
new file mode 100644
index 0000000..65a7d33
--- /dev/null
+++ b/dev/docs/assets/snippets.css
@@ -0,0 +1,108 @@
+/* Styles for handling custom code snippets */
+.snippet-container {
+  background-color: #45aae8;
+  padding: 10px;
+  overflow: auto;
+}
+
+.snippet-container pre {
+  max-height: 500px;
+  overflow: auto;
+  padding: 10px;
+  margin: 0px;
+}
+
+.snippet-container ::-webkit-scrollbar {
+  width: 12px;
+}
+
+.snippet-container ::-webkit-scrollbar-thumb {
+  width: 12px;
+  border-radius: 6px;
+}
+
+.snippet {
+  position: relative;
+}
+
+.snippet-description {
+  padding: 10px;
+  color: white;
+}
+
+.snippet-buttons button {
+  background-color: #45aae8;
+  border-style: none;
+  color: white;
+  padding: 10px 24px;
+  cursor: pointer;
+  float: left;
+}
+
+.snippet-buttons:after {
+  content: "";
+  clear: both;
+  display: table;
+}
+
+.snippet-buttons button:focus { outline: none; }
+
+.snippet-buttons button:hover {
+  opacity: 1.0;
+}
+
+.snippet-buttons :not([selected]) {
+  opacity: 0.65;
+}
+
+.snippet-buttons [selected] {
+  opacity: 1.0;
+}
+
+.snippet-container [hidden] {
+  display: none;
+}
+
+.snippet-create-command {
+  text-align: end;
+  font-size: smaller;
+  font-style: normal;
+  font-family: courier, lucidia;
+}
+
+/* Styles for the copy-to-clipboard button */
+.copyable-container {
+  position: relative;
+}
+
+.copy-button-overlay {
+  position: absolute;
+  top: 10px;
+  right: 14px;
+  height: 28px;
+  width: 28px;
+  transition: .3s ease;
+  background-color: #45aae8;
+}
+
+.copy-button {
+  border-style: none;
+  background: none;
+  cursor: pointer;
+}
+
+.copy-button :focus {
+  outline: 0px;
+}
+
+.copy-button :hover {
+  transition: .3s ease;
+  color: #222;
+}
+
+.copy-image {
+  opacity: 0.65;
+  color: #45aae8;
+  font-size: 28px;
+  padding-top: 4px;
+}
diff --git a/dev/docs/assets/snippets.js b/dev/docs/assets/snippets.js
new file mode 100644
index 0000000..b51c96e
--- /dev/null
+++ b/dev/docs/assets/snippets.js
@@ -0,0 +1,93 @@
+/**
+ * Scripting for handling custom code snippets
+ */
+
+const shortSnippet = 'shortSnippet';
+const longSnippet = 'longSnippet';
+var visibleSnippet = shortSnippet;
+
+/**
+ * Shows the requested snippet. Values for "name" can be "shortSnippet" or
+ * "longSnippet".
+ */
+function showSnippet(name) {
+  if (visibleSnippet == name) return;
+  if (visibleSnippet != null) {
+    var shown = document.getElementById(visibleSnippet);
+    var attribute = document.createAttribute('hidden');
+    if (shown != null) {
+      shown.setAttributeNode(attribute);
+    }
+    var button = document.getElementById(visibleSnippet + 'Button');
+    if (button != null) {
+      button.removeAttribute('selected');
+    }
+  }
+  if (name == null || name == '') {
+    visibleSnippet = null;
+    return;
+  }
+  var newlyVisible = document.getElementById(name);
+  if (newlyVisible != null) {
+    visibleSnippet = name;
+    newlyVisible.removeAttribute('hidden');
+  } else {
+    visibleSnippet = null;
+  }
+  var button = document.getElementById(name + 'Button');
+  var selectedAttribute = document.createAttribute('selected');
+  if (button != null) {
+    button.setAttributeNode(selectedAttribute);
+  }
+}
+
+// Finds a sibling to given element with the given id.
+function findSiblingWithId(element, id) {
+  var siblings = element.parentNode.children;
+  var siblingWithId = null;
+  for (var i = siblings.length; i--;) {
+    if (siblings[i] == element) continue;
+    if (siblings[i].id == id) {
+      siblingWithId = siblings[i];
+      break;
+    }
+  }
+  return siblingWithId;
+};
+
+// Returns true if the browser supports the "copy" command.
+function supportsCopying() {
+  return !!document.queryCommandSupported &&
+      !!document.queryCommandSupported('copy');
+}
+
+// Copies the text inside the currently visible snippet to the clipboard, or the
+// given element, if any.
+function copyTextToClipboard(element) {
+  if (element == null) {
+    var elementSelector = '#' + visibleSnippet + ' .language-dart';
+    element = document.querySelector(elementSelector);
+    if (element == null) {
+      console.log(
+          'copyTextToClipboard: Unable to find element for "' +
+          elementSelector + '"');
+      return;
+    }
+  }
+  if (!supportsCopying()) {
+    alert('Unable to copy to clipboard (not supported by browser)');
+    return;
+  }
+
+  if (element.hasAttribute('contenteditable')) {
+    element.focus();
+  }
+
+  var selection = window.getSelection();
+  var range = document.createRange();
+
+  range.selectNodeContents(element);
+  selection.removeAllRanges();
+  selection.addRange(range);
+  document.execCommand('copy');
+}
diff --git a/dev/docs/snippets.html b/dev/docs/snippets.html
new file mode 100644
index 0000000..dc04c6e
--- /dev/null
+++ b/dev/docs/snippets.html
@@ -0,0 +1,3 @@
+<!-- Styles and scripting for handling custom code snippets -->
+<link href="../assets/snippets.css" rel="stylesheet" type="text/css">
+<script src="../assets/snippets.js"></script>
diff --git a/dev/docs/styles.html b/dev/docs/styles.html
index 8671389..94579bb 100644
--- a/dev/docs/styles.html
+++ b/dev/docs/styles.html
@@ -1,148 +1,10 @@
 <!-- style overrides for dartdoc -->
 <style>
 @import 'https://fonts.googleapis.com/css?family=Roboto:500,400italic,300,400,100i';
+@import 'https://fonts.googleapis.com/css?family=Material+Icons';
 </style>
 
-<style>
-  body {
-    font-size: 15px;
-    font-family: Roboto, sans-serif;
-    line-height: 1.5;
-    color: #111;
-    background-color: #fdfdfd;
-    font-weight: 300;
-    -webkit-font-smoothing: auto;
-  }
-
-  header {
-    background-color: white;
-    color: #424242;
-  }
-
-  nav.navbar {
-    min-height: 57px;
-    padding: 6px 0;
-  }
-
-  header.header-fixed nav.navbar-fixed-top {
-    box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37);
-  }
-
-  h1, h2 {
-    font-weight: 300;
-  }
-
-  h3, h4, h5, h6 {
-    font-weight: 400;
-  }
-
-  h1 {
-    font-size: 42px !important;
-    letter-spacing: -1px;
-  }
-
-  header h1 {
-    font-weight: 300;
-  }
-
-  h2 {
-    color: #111;
-    font-size: 24px;
-  }
-
-  .markdown h2 {
-    font-size: 24px;
-  }
-
-  section.summary h2 {
-    font-size: 24px;
-    color: inherit;
-    border-bottom: none;
-  }
-
-  .sidebar ol,
-  .sidebar ol li.section-title {
-    font-size: inherit;
-  }
-
-  @media screen and (max-width: 768px) {
-    .sidebar-offcanvas-left.active {
-      padding: 10px;
-    }
-  }
-
-  .sidebar-offcanvas-left ol {
-    padding: 0 16px 16px 0;
-  }
-
-  .sidebar-offcanvas-left h5 {
-    display: none;
-  }
-
-  pre,
-  pre.prettyprint,
-  pre > code {
-    font-size: 14px;
-  }
-
-  pre,
-  pre.prettyprint {
-    background: #f5f2f0;
-    margin: 0 0 15px 0;
-    padding: 8px 12px;
-    border: 1px solid #cccccc;
-    border-radius: 4px;
-  }
-
-  code {
-    background-color: inherit;
-    font-size: 1em; /* browsers default to smaller font for code */
-    font-weight: 300;
-    padding-left: 0; /* otherwise we get ragged left margins */
-    padding-right: 0;
-  }
-
-  #search-box {
-    color: #555;
-    background-color: #fff;
-    background-image: none;
-    border: 1px solid #ccc;
-    border-radius: 2px;
-    font-family: inherit;
-    padding: 4px 6px;
-    font-size: 15px;
-  }
-
-  input.form-control.typeahead {
-    padding: 4px 7px;
-    font-size: 15px;
-  }
-
-  dl.dl-horizontal dt {
-    color: inherit;
-  }
-
-  /* Line the material icons up with their labels */
-  i.material-icons.md-36,
-  i.material-icons.md-48 {
-    vertical-align: bottom;
-  }
-
-  /* thinify the inherited names in lists */
-  li.inherited a {
-    font-weight: 100;
-  }
-
-  /* address a style issue with the background of code sections */
-  code.hljs {
-    background: inherit;
-  }
-
-  footer {
-    font-size: 13px;
-    padding: 12px 20px;
-  }
-</style>
+<link href="../assets/overrides.css" rel="stylesheet" type="text/css">
 
 <!-- The following rules are from http://google.github.io/material-design-icons/ -->
 <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
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() {'));
+    });
+  });
+}
diff --git a/dev/tools/dartdoc.dart b/dev/tools/dartdoc.dart
index 29f52af..cda9793 100644
--- a/dev/tools/dartdoc.dart
+++ b/dev/tools/dartdoc.dart
@@ -10,7 +10,8 @@
 import 'package:intl/intl.dart';
 import 'package:path/path.dart' as path;
 
-const String kDocRoot = 'dev/docs/doc';
+const String kDocsRoot = 'dev/docs';
+const String kPublishRoot = '$kDocsRoot/doc';
 
 /// This script expects to run with the cwd as the root of the flutter repo. It
 /// will generate documentation for the packages in `//packages/` and write the
@@ -57,17 +58,17 @@
   buf.writeln('dependency_overrides:');
   buf.writeln('  platform_integration:');
   buf.writeln('    path: platform_integration');
-  File('dev/docs/pubspec.yaml').writeAsStringSync(buf.toString());
+  File('$kDocsRoot/pubspec.yaml').writeAsStringSync(buf.toString());
 
   // Create the library file.
-  final Directory libDir = Directory('dev/docs/lib');
+  final Directory libDir = Directory('$kDocsRoot/lib');
   libDir.createSync();
 
   final StringBuffer contents = StringBuffer('library temp_doc;\n\n');
   for (String libraryRef in libraryRefs()) {
     contents.writeln('import \'package:$libraryRef\';');
   }
-  File('dev/docs/lib/temp_doc.dart').writeAsStringSync(contents.toString());
+  File('$kDocsRoot/lib/temp_doc.dart').writeAsStringSync(contents.toString());
 
   final String flutterRoot = Directory.current.path;
   final Map<String, String> pubEnvironment = <String, String>{
@@ -86,7 +87,7 @@
   Process process = await Process.start(
     pubExecutable,
     <String>['get'],
-    workingDirectory: 'dev/docs',
+    workingDirectory: kDocsRoot,
     environment: pubEnvironment,
   );
   printStream(process.stdout, prefix: 'pub:stdout: ');
@@ -95,7 +96,9 @@
   if (code != 0)
     exit(code);
 
-  createFooter('dev/docs/lib/footer.html');
+  createFooter('$kDocsRoot/lib/footer.html');
+  copyAssets();
+  cleanOutSnippets();
 
   final List<String> dartdocBaseArgs = <String>['global', 'run'];
   if (args['checked']) {
@@ -107,7 +110,7 @@
   final ProcessResult result = Process.runSync(
     pubExecutable,
     <String>[]..addAll(dartdocBaseArgs)..add('--version'),
-    workingDirectory: 'dev/docs',
+    workingDirectory: kDocsRoot,
     environment: pubEnvironment,
   );
   print('\n${result.stdout}flutter version: $version\n');
@@ -124,26 +127,65 @@
   // We don't need to exclude flutter_tools in this list because it's not in the
   // recursive dependencies of the package defined at dev/docs/pubspec.yaml
   final List<String> dartdocArgs = <String>[]..addAll(dartdocBaseArgs)..addAll(<String>[
+    '--inject-html',
     '--header', 'styles.html',
     '--header', 'analytics.html',
     '--header', 'survey.html',
+    '--header', 'snippets.html',
     '--footer-text', 'lib/footer.html',
     '--exclude-packages',
-'analyzer,args,barback,cli_util,csslib,flutter_goldens,front_end,fuchsia_remote_debug_protocol,glob,html,http_multi_server,io,isolate,js,kernel,logging,mime,mockito,node_preamble,plugin,shelf,shelf_packages_handler,shelf_static,shelf_web_socket,utf,watcher,yaml',
+    <String>[
+      'analyzer',
+      'args',
+      'barback',
+      'cli_util',
+      'csslib',
+      'flutter_goldens',
+      'front_end',
+      'fuchsia_remote_debug_protocol',
+      'glob',
+      'html',
+      'http_multi_server',
+      'io',
+      'isolate',
+      'js',
+      'kernel',
+      'logging',
+      'mime',
+      'mockito',
+      'node_preamble',
+      'plugin',
+      'shelf',
+      'shelf_packages_handler',
+      'shelf_static',
+      'shelf_web_socket',
+      'utf',
+      'watcher',
+      'yaml',
+    ].join(','),
     '--exclude',
-  'package:Flutter/temp_doc.dart,package:http/browser_client.dart,package:intl/intl_browser.dart,package:matcher/mirror_matchers.dart,package:quiver/mirrors.dart,package:quiver/io.dart,package:vm_service_client/vm_service_client.dart,package:web_socket_channel/html.dart',
+    <String>[
+      'package:Flutter/temp_doc.dart',
+      'package:http/browser_client.dart',
+      'package:intl/intl_browser.dart',
+      'package:matcher/mirror_matchers.dart',
+      'package:quiver/io.dart',
+      'package:quiver/mirrors.dart',
+      'package:vm_service_client/vm_service_client.dart',
+      'package:web_socket_channel/html.dart',
+    ].join(','),
     '--favicon=favicon.ico',
     '--package-order', 'flutter,Dart,flutter_test,flutter_driver',
     '--auto-include-dependencies',
   ]);
 
   String quote(String arg) => arg.contains(' ') ? "'$arg'" : arg;
-  print('Executing: (cd dev/docs ; $pubExecutable ${dartdocArgs.map<String>(quote).join(' ')})');
+  print('Executing: (cd $kDocsRoot ; $pubExecutable ${dartdocArgs.map<String>(quote).join(' ')})');
 
   process = await Process.start(
     pubExecutable,
     dartdocArgs,
-    workingDirectory: 'dev/docs',
+    workingDirectory: kDocsRoot,
     environment: pubEnvironment,
   );
   printStream(process.stdout, prefix: args['json'] ? '' : 'dartdoc:stdout: ',
@@ -211,16 +253,63 @@
     gitBranchOut].join(' '));
 }
 
+/// Recursively copies `srcDir` to `destDir`, invoking [onFileCopied], if
+/// specified, for each source/destination file pair.
+///
+/// Creates `destDir` if needed.
+void copyDirectorySync(Directory srcDir, Directory destDir, [void onFileCopied(File srcFile, File destFile)]) {
+  if (!srcDir.existsSync())
+    throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy');
+
+  if (!destDir.existsSync())
+    destDir.createSync(recursive: true);
+
+  for (FileSystemEntity entity in srcDir.listSync()) {
+    final String newPath = path.join(destDir.path, path.basename(entity.path));
+    if (entity is File) {
+      final File newFile = File(newPath);
+      entity.copySync(newPath);
+      onFileCopied?.call(entity, newFile);
+    } else if (entity is Directory) {
+      copyDirectorySync(entity, Directory(newPath));
+    } else {
+      throw Exception('${entity.path} is neither File nor Directory');
+    }
+  }
+}
+
+void copyAssets() {
+  final Directory assetsDir = Directory(path.join(kPublishRoot, 'assets'));
+  if (assetsDir.existsSync()) {
+    assetsDir.deleteSync(recursive: true);
+  }
+  copyDirectorySync(
+      Directory(path.join(kDocsRoot, 'assets')),
+      Directory(path.join(kPublishRoot, 'assets')),
+          (File src, File dest) => print('Copied ${src.path} to ${dest.path}'));
+}
+
+
+void cleanOutSnippets() {
+  final Directory snippetsDir = Directory(path.join(kPublishRoot, 'snippets'));
+  if (snippetsDir.existsSync()) {
+    snippetsDir
+      ..deleteSync(recursive: true)
+      ..createSync(recursive: true);
+  }
+}
+
 void sanityCheckDocs() {
   final List<String> canaries = <String>[
-    '$kDocRoot/api/dart-io/File-class.html',
-    '$kDocRoot/api/dart-ui/Canvas-class.html',
-    '$kDocRoot/api/dart-ui/Canvas/drawRect.html',
-    '$kDocRoot/api/flutter_driver/FlutterDriver/FlutterDriver.connectedTo.html',
-    '$kDocRoot/api/flutter_test/WidgetTester/pumpWidget.html',
-    '$kDocRoot/api/material/Material-class.html',
-    '$kDocRoot/api/material/Tooltip-class.html',
-    '$kDocRoot/api/widgets/Widget-class.html',
+    '$kPublishRoot/assets/overrides.css',
+    '$kPublishRoot/api/dart-io/File-class.html',
+    '$kPublishRoot/api/dart-ui/Canvas-class.html',
+    '$kPublishRoot/api/dart-ui/Canvas/drawRect.html',
+    '$kPublishRoot/api/flutter_driver/FlutterDriver/FlutterDriver.connectedTo.html',
+    '$kPublishRoot/api/flutter_test/WidgetTester/pumpWidget.html',
+    '$kPublishRoot/api/material/Material-class.html',
+    '$kPublishRoot/api/material/Tooltip-class.html',
+    '$kPublishRoot/api/widgets/Widget-class.html',
   ];
   for (String canary in canaries) {
     if (!File(canary).existsSync())
@@ -231,7 +320,7 @@
 /// Creates a custom index.html because we try to maintain old
 /// paths. Cleanup unused index.html files no longer needed.
 void createIndexAndCleanup() {
-  print('\nCreating a custom index.html in $kDocRoot/index.html');
+  print('\nCreating a custom index.html in $kPublishRoot/index.html');
   removeOldFlutterDocsDir();
   renameApiDir();
   copyIndexToRootOfDocs();
@@ -243,22 +332,22 @@
 
 void removeOldFlutterDocsDir() {
   try {
-    Directory('$kDocRoot/flutter').deleteSync(recursive: true);
+    Directory('$kPublishRoot/flutter').deleteSync(recursive: true);
   } on FileSystemException {
     // If the directory does not exist, that's OK.
   }
 }
 
 void renameApiDir() {
-  Directory('$kDocRoot/api').renameSync('$kDocRoot/flutter');
+  Directory('$kPublishRoot/api').renameSync('$kPublishRoot/flutter');
 }
 
 void copyIndexToRootOfDocs() {
-  File('$kDocRoot/flutter/index.html').copySync('$kDocRoot/index.html');
+  File('$kPublishRoot/flutter/index.html').copySync('$kPublishRoot/index.html');
 }
 
 void changePackageToSdkInTitlebar() {
-  final File indexFile = File('$kDocRoot/index.html');
+  final File indexFile = File('$kPublishRoot/index.html');
   String indexContents = indexFile.readAsStringSync();
   indexContents = indexContents.replaceFirst(
     '<li><a href="https://flutter.io">Flutter package</a></li>',
@@ -269,7 +358,7 @@
 }
 
 void addHtmlBaseToIndex() {
-  final File indexFile = File('$kDocRoot/index.html');
+  final File indexFile = File('$kPublishRoot/index.html');
   String indexContents = indexFile.readAsStringSync();
   indexContents = indexContents.replaceFirst(
     '</title>\n',
@@ -289,7 +378,7 @@
 
 void putRedirectInOldIndexLocation() {
   const String metaTag = '<meta http-equiv="refresh" content="0;URL=../index.html">';
-  File('$kDocRoot/flutter/index.html').writeAsStringSync(metaTag);
+  File('$kPublishRoot/flutter/index.html').writeAsStringSync(metaTag);
 }
 
 List<String> findPackageNames() {