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.
This commit is contained in:
Greg Spencer 2018-10-23 13:50:24 -07:00 committed by GitHub
parent a3e0b0aee2
commit 65d3ddd5d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1367 additions and 170 deletions

9
dartdoc_options.yaml Normal file
View file

@ -0,0 +1,9 @@
# This file is used by dartdoc when generating API documentation for Flutter.
dartdoc:
tools:
snippet:
command: ["dev/snippets/lib/main.dart", "--type=application"]
description: "Creates application sample code documentation output from embedded documentation samples."
sample:
command: ["dev/snippets/lib/main.dart", "--type=sample"]
description: "Creates sample code documentation output from embedded documentation samples."

View file

@ -266,6 +266,7 @@ Future<void> _verifyNoTestPackageImports(String workingDirectory) async {
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/') ||

View file

@ -182,6 +182,7 @@ Future<void> _runTests() async {
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'));

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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');
}

3
dev/docs/snippets.html Normal file
View file

@ -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>

View file

@ -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">

57
dev/snippets/README.md Normal file
View file

@ -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).

View file

@ -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}

View file

@ -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}

View file

@ -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.

View file

@ -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}}
}

View file

@ -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'));
}
}

122
dev/snippets/lib/main.dart Normal file
View file

@ -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);
}

View file

@ -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);
}
}

101
dev/snippets/pubspec.yaml Normal file
View file

@ -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

View file

@ -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')));
});
});
}

View file

@ -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() {'));
});
});
}

View file

@ -10,7 +10,8 @@ import 'package:args/args.dart';
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 @@ Future<void> main(List<String> arguments) async {
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 @@ Future<void> main(List<String> arguments) async {
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 @@ Future<void> main(List<String> arguments) async {
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 @@ Future<void> main(List<String> arguments) async {
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 @@ Future<void> main(List<String> arguments) async {
// 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 @@ void createFooter(String footerPath) {
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 @@ void sanityCheckDocs() {
/// 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 createIndexAndCleanup() {
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 changePackageToSdkInTitlebar() {
}
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 addHtmlBaseToIndex() {
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() {

View file

@ -142,11 +142,13 @@ abstract class DeletableChipAttributes {
///
/// The chip will not automatically remove itself: this just tells the app
/// that the user tapped the delete button. In order to delete the chip, you
/// have to do something like the following:
/// have to do something similar to the following sample:
///
/// ## Sample code
/// {@tool snippet --template=stateful_widget}
/// This sample shows how to use [onDeleted] to remove an entry when the
/// delete button is tapped.
///
/// ```dart
/// ```dart preamble
/// class Actor {
/// const Actor(this.name, this.initials);
/// final String name;
@ -193,6 +195,14 @@ abstract class DeletableChipAttributes {
/// }
/// }
/// ```
///
/// ```dart
/// @override
/// Widget build(BuildContext context) {
/// return CastList();
/// }
/// ```
/// {@end-tool}
VoidCallback get onDeleted;
/// The [Color] for the delete icon. The default is based on the ambient
@ -247,7 +257,9 @@ abstract class SelectableChipAttributes {
/// The [onSelected] and [TappableChipAttributes.onPressed] callbacks must not
/// both be specified at the same time.
///
/// ## Sample code
/// {@tool sample}
///
/// A [StatefulWidget] that illustrates use of onSelected in an [InputChip].
///
/// ```dart
/// class Wood extends StatefulWidget {
@ -272,6 +284,7 @@ abstract class SelectableChipAttributes {
/// }
/// }
/// ```
/// {@end-tool}
ValueChanged<bool> get onSelected;
/// Elevation to be applied on the chip during the press motion.