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