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() {