diff --git a/dartdoc_options.yaml b/dartdoc_options.yaml new file mode 100644 index 00000000000..d340a230628 --- /dev/null +++ b/dartdoc_options.yaml @@ -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." diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart index 1b911efdd8f..7b25c5a0c0f 100644 --- a/dev/bots/analyze.dart +++ b/dev/bots/analyze.dart @@ -266,6 +266,7 @@ Future _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/') || diff --git a/dev/bots/test.dart b/dev/bots/test.dart index 01c2c2e104e..a2b24831048 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -182,6 +182,7 @@ Future _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')); diff --git a/dev/docs/assets/overrides.css b/dev/docs/assets/overrides.css new file mode 100644 index 00000000000..4aa271620b4 --- /dev/null +++ b/dev/docs/assets/overrides.css @@ -0,0 +1,139 @@ +/* Overrides for dartdoc styles. */ +body { + font-size: 15px; + font-family: Roboto, sans-serif; + line-height: 1.5; + color: #111; + background-color: #fdfdfd; + font-weight: 300; + -webkit-font-smoothing: auto; +} + +header { + background-color: white; + color: #424242; +} + +nav.navbar { + min-height: 57px; + padding: 6px 0; +} + +header.header-fixed nav.navbar-fixed-top { + box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37); +} + +h1, h2 { + font-weight: 300; +} + +h3, h4, h5, h6 { + font-weight: 400; +} + +h1 { + font-size: 42px !important; + letter-spacing: -1px; +} + +header h1 { + font-weight: 300; +} + +h2 { + color: #111; + font-size: 24px; +} + +.markdown h2 { + font-size: 24px; +} + +section.summary h2 { + font-size: 24px; + color: inherit; + border-bottom: none; +} + +.sidebar ol, +.sidebar ol li.section-title { + font-size: inherit; +} + +@media screen and (max-width: 768px) { + .sidebar-offcanvas-left.active { + padding: 10px; + } +} + +.sidebar-offcanvas-left ol { + padding: 0 16px 16px 0; +} + +.sidebar-offcanvas-left h5 { + display: none; +} + +pre, +pre.prettyprint, +pre > code { + font-size: 14px; +} + +pre, +pre.prettyprint { + background: #f5f2f0; + margin: 0 0 15px 0; + padding: 8px 12px; + border: 1px solid #cccccc; + border-radius: 4px; +} + +code { + background-color: inherit; + font-size: 1em; /* browsers default to smaller font for code */ + font-weight: 300; + padding-left: 0; /* otherwise we get ragged left margins */ + padding-right: 0; +} + +#search-box { + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 2px; + font-family: inherit; + padding: 4px 6px; + font-size: 15px; +} + +input.form-control.typeahead { + padding: 4px 7px; + font-size: 15px; +} + +dl.dl-horizontal dt { + color: inherit; +} + +/* Line the material icons up with their labels */ +i.material-icons.md-36, +i.material-icons.md-48 { + vertical-align: bottom; +} + +/* thinify the inherited names in lists */ +li.inherited a { + font-weight: 100; +} + +/* address a style issue with the background of code sections */ +code.hljs { + background: inherit; +} + +footer { + font-size: 13px; + padding: 12px 20px; +} diff --git a/dev/docs/assets/snippets.css b/dev/docs/assets/snippets.css new file mode 100644 index 00000000000..65a7d338cbc --- /dev/null +++ b/dev/docs/assets/snippets.css @@ -0,0 +1,108 @@ +/* Styles for handling custom code snippets */ +.snippet-container { + background-color: #45aae8; + padding: 10px; + overflow: auto; +} + +.snippet-container pre { + max-height: 500px; + overflow: auto; + padding: 10px; + margin: 0px; +} + +.snippet-container ::-webkit-scrollbar { + width: 12px; +} + +.snippet-container ::-webkit-scrollbar-thumb { + width: 12px; + border-radius: 6px; +} + +.snippet { + position: relative; +} + +.snippet-description { + padding: 10px; + color: white; +} + +.snippet-buttons button { + background-color: #45aae8; + border-style: none; + color: white; + padding: 10px 24px; + cursor: pointer; + float: left; +} + +.snippet-buttons:after { + content: ""; + clear: both; + display: table; +} + +.snippet-buttons button:focus { outline: none; } + +.snippet-buttons button:hover { + opacity: 1.0; +} + +.snippet-buttons :not([selected]) { + opacity: 0.65; +} + +.snippet-buttons [selected] { + opacity: 1.0; +} + +.snippet-container [hidden] { + display: none; +} + +.snippet-create-command { + text-align: end; + font-size: smaller; + font-style: normal; + font-family: courier, lucidia; +} + +/* Styles for the copy-to-clipboard button */ +.copyable-container { + position: relative; +} + +.copy-button-overlay { + position: absolute; + top: 10px; + right: 14px; + height: 28px; + width: 28px; + transition: .3s ease; + background-color: #45aae8; +} + +.copy-button { + border-style: none; + background: none; + cursor: pointer; +} + +.copy-button :focus { + outline: 0px; +} + +.copy-button :hover { + transition: .3s ease; + color: #222; +} + +.copy-image { + opacity: 0.65; + color: #45aae8; + font-size: 28px; + padding-top: 4px; +} diff --git a/dev/docs/assets/snippets.js b/dev/docs/assets/snippets.js new file mode 100644 index 00000000000..b51c96e91ce --- /dev/null +++ b/dev/docs/assets/snippets.js @@ -0,0 +1,93 @@ +/** + * Scripting for handling custom code snippets + */ + +const shortSnippet = 'shortSnippet'; +const longSnippet = 'longSnippet'; +var visibleSnippet = shortSnippet; + +/** + * Shows the requested snippet. Values for "name" can be "shortSnippet" or + * "longSnippet". + */ +function showSnippet(name) { + if (visibleSnippet == name) return; + if (visibleSnippet != null) { + var shown = document.getElementById(visibleSnippet); + var attribute = document.createAttribute('hidden'); + if (shown != null) { + shown.setAttributeNode(attribute); + } + var button = document.getElementById(visibleSnippet + 'Button'); + if (button != null) { + button.removeAttribute('selected'); + } + } + if (name == null || name == '') { + visibleSnippet = null; + return; + } + var newlyVisible = document.getElementById(name); + if (newlyVisible != null) { + visibleSnippet = name; + newlyVisible.removeAttribute('hidden'); + } else { + visibleSnippet = null; + } + var button = document.getElementById(name + 'Button'); + var selectedAttribute = document.createAttribute('selected'); + if (button != null) { + button.setAttributeNode(selectedAttribute); + } +} + +// Finds a sibling to given element with the given id. +function findSiblingWithId(element, id) { + var siblings = element.parentNode.children; + var siblingWithId = null; + for (var i = siblings.length; i--;) { + if (siblings[i] == element) continue; + if (siblings[i].id == id) { + siblingWithId = siblings[i]; + break; + } + } + return siblingWithId; +}; + +// Returns true if the browser supports the "copy" command. +function supportsCopying() { + return !!document.queryCommandSupported && + !!document.queryCommandSupported('copy'); +} + +// Copies the text inside the currently visible snippet to the clipboard, or the +// given element, if any. +function copyTextToClipboard(element) { + if (element == null) { + var elementSelector = '#' + visibleSnippet + ' .language-dart'; + element = document.querySelector(elementSelector); + if (element == null) { + console.log( + 'copyTextToClipboard: Unable to find element for "' + + elementSelector + '"'); + return; + } + } + if (!supportsCopying()) { + alert('Unable to copy to clipboard (not supported by browser)'); + return; + } + + if (element.hasAttribute('contenteditable')) { + element.focus(); + } + + var selection = window.getSelection(); + var range = document.createRange(); + + range.selectNodeContents(element); + selection.removeAllRanges(); + selection.addRange(range); + document.execCommand('copy'); +} diff --git a/dev/docs/snippets.html b/dev/docs/snippets.html new file mode 100644 index 00000000000..dc04c6ee118 --- /dev/null +++ b/dev/docs/snippets.html @@ -0,0 +1,3 @@ + + + diff --git a/dev/docs/styles.html b/dev/docs/styles.html index 86713898d59..94579bb3147 100644 --- a/dev/docs/styles.html +++ b/dev/docs/styles.html @@ -1,148 +1,10 @@ - + diff --git a/dev/snippets/README.md b/dev/snippets/README.md new file mode 100644 index 00000000000..0606877c7b3 --- /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 00000000000..bbbed4f37ce --- /dev/null +++ b/dev/snippets/config/skeletons/application.html @@ -0,0 +1,34 @@ +{@inject-html} +
+ + +
+
+
+
+ {@end-inject-html} + {{description}} + {@inject-html} +
+
+ +
{{code}}
+
+
+ +
+{@end-inject-html} diff --git a/dev/snippets/config/skeletons/sample.html b/dev/snippets/config/skeletons/sample.html new file mode 100644 index 00000000000..9343a01f92e --- /dev/null +++ b/dev/snippets/config/skeletons/sample.html @@ -0,0 +1,20 @@ +{@inject-html} +
+
+
+ {@end-inject-html} + {{description}} + {@inject-html} +
+
+ +
+        {{code}}
+      
+
+
+
+{@end-inject-html} diff --git a/dev/snippets/config/templates/README.md b/dev/snippets/config/templates/README.md new file mode 100644 index 00000000000..e5addd0b828 --- /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 00000000000..aff8fbfcbe3 --- /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 { + {{code}} +} diff --git a/dev/snippets/lib/configuration.dart b/dev/snippets/lib/configuration.dart new file mode 100644 index 00000000000..3f7f3ebc438 --- /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 00000000000..3a84317a664 --- /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 argList) { + const Platform platform = LocalPlatform(); + final Map environment = platform.environment; + final ArgParser parser = ArgParser(); + final List snippetTypes = + SnippetType.values.map((SnippetType type) => getEnumName(type)).toList(); + parser.addOption( + _kTypeOption, + defaultsTo: getEnumName(SnippetType.application), + allowed: snippetTypes, + allowedHelp: { + 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 id = []; + 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 00000000000..c09f59c03ec --- /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 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((_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 description = injections + .firstWhere((_ComponentTuple tuple) => tuple.name == match[1]) + .contents + .map((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 result = []; + for (_ComponentTuple injection in injections) { + if (!injection.name.startsWith('code')) { + continue; + } + result.addAll(injection.contents); + result.addAll(['', '// ...', '']); + } + 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 substitutions = { + 'description': injections + .firstWhere((_ComponentTuple tuple) => tuple.name == 'description') + .mergedContent, + 'code': formattedCode, + }..addAll(type == SnippetType.application + ? { + 'id': + injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'id').mergedContent, + 'app': + injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'app').mergedContent, + } + : {'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 description = []; + 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]}', [])); + } else { + components.add(_ComponentTuple('code', [])); + } + } 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', [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 00000000000..7d76fc367e9 --- /dev/null +++ b/dev/snippets/pubspec.yaml @@ -0,0 +1,101 @@ +name: snippets +version: 0.1.0 +author: Flutter Team +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 00000000000..8b2e567743f --- /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 00000000000..47bdc1a96ff --- /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(''' +
HTML Bits
+{{description}} +
{{code}}
+
{{app}}
+
More HTML Bits
+'''); + configuration.getHtmlSkeletonFile(SnippetType.sample).writeAsStringSync(''' +
HTML Bits
+{{description}} +
{{code}}
+
More HTML Bits
+'''); + 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('
HTML Bits
')); + expect(html, contains('
More HTML Bits
')); + 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('
HTML Bits
')); + expect(html, contains('
More HTML Bits
')); + expect(html, contains("print('The actual \$name.');")); + expect(html, contains('A description of the snippet.\n\nOn several lines.\n')); + expect(html, contains('main() {')); + }); + }); +} diff --git a/dev/tools/dartdoc.dart b/dev/tools/dartdoc.dart index 29f52aff8d6..cda9793ed30 100644 --- a/dev/tools/dartdoc.dart +++ b/dev/tools/dartdoc.dart @@ -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 main(List 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 pubEnvironment = { @@ -86,7 +87,7 @@ Future main(List arguments) async { Process process = await Process.start( pubExecutable, ['get'], - workingDirectory: 'dev/docs', + workingDirectory: kDocsRoot, environment: pubEnvironment, ); printStream(process.stdout, prefix: 'pub:stdout: '); @@ -95,7 +96,9 @@ Future main(List arguments) async { if (code != 0) exit(code); - createFooter('dev/docs/lib/footer.html'); + createFooter('$kDocsRoot/lib/footer.html'); + copyAssets(); + cleanOutSnippets(); final List dartdocBaseArgs = ['global', 'run']; if (args['checked']) { @@ -107,7 +110,7 @@ Future main(List arguments) async { final ProcessResult result = Process.runSync( pubExecutable, []..addAll(dartdocBaseArgs)..add('--version'), - workingDirectory: 'dev/docs', + workingDirectory: kDocsRoot, environment: pubEnvironment, ); print('\n${result.stdout}flutter version: $version\n'); @@ -124,26 +127,65 @@ Future main(List 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 dartdocArgs = []..addAll(dartdocBaseArgs)..addAll([ + '--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', + [ + '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', + [ + '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(quote).join(' ')})'); + print('Executing: (cd $kDocsRoot ; $pubExecutable ${dartdocArgs.map(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 canaries = [ - '$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( '
  • Flutter package
  • ', @@ -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( '\n', @@ -289,7 +378,7 @@ void addHtmlBaseToIndex() { void putRedirectInOldIndexLocation() { const String metaTag = ''; - File('$kDocRoot/flutter/index.html').writeAsStringSync(metaTag); + File('$kPublishRoot/flutter/index.html').writeAsStringSync(metaTag); } List findPackageNames() { diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index e90235fd478..e78a23f0f34 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -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 get onSelected; /// Elevation to be applied on the chip during the press motion.