mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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:
parent
a3e0b0aee2
commit
65d3ddd5d1
9
dartdoc_options.yaml
Normal file
9
dartdoc_options.yaml
Normal 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."
|
@ -266,6 +266,7 @@ Future<void> _verifyNoTestPackageImports(String workingDirectory) async {
|
|||||||
if (path.split(file.path).contains('test_driver') ||
|
if (path.split(file.path).contains('test_driver') ||
|
||||||
name.startsWith('dev/missing_dependency_tests/') ||
|
name.startsWith('dev/missing_dependency_tests/') ||
|
||||||
name.startsWith('dev/automated_tests/') ||
|
name.startsWith('dev/automated_tests/') ||
|
||||||
|
name.startsWith('dev/snippets/') ||
|
||||||
name.startsWith('packages/flutter/test/engine/') ||
|
name.startsWith('packages/flutter/test/engine/') ||
|
||||||
name.startsWith('examples/layers/test/smoketests/raw/') ||
|
name.startsWith('examples/layers/test/smoketests/raw/') ||
|
||||||
name.startsWith('examples/layers/test/smoketests/rendering/') ||
|
name.startsWith('examples/layers/test/smoketests/rendering/') ||
|
||||||
|
@ -182,6 +182,7 @@ Future<void> _runTests() async {
|
|||||||
await _runFlutterTest(path.join(flutterRoot, 'packages', 'fuchsia_remote_debug_protocol'));
|
await _runFlutterTest(path.join(flutterRoot, 'packages', 'fuchsia_remote_debug_protocol'));
|
||||||
await _pubRunTest(path.join(flutterRoot, 'dev', 'bots'));
|
await _pubRunTest(path.join(flutterRoot, 'dev', 'bots'));
|
||||||
await _pubRunTest(path.join(flutterRoot, 'dev', 'devicelab'));
|
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', 'integration_tests', 'android_semantics_testing'));
|
||||||
await _runFlutterTest(path.join(flutterRoot, 'dev', 'manual_tests'));
|
await _runFlutterTest(path.join(flutterRoot, 'dev', 'manual_tests'));
|
||||||
await _runFlutterTest(path.join(flutterRoot, 'dev', 'tools', 'vitool'));
|
await _runFlutterTest(path.join(flutterRoot, 'dev', 'tools', 'vitool'));
|
||||||
|
139
dev/docs/assets/overrides.css
Normal file
139
dev/docs/assets/overrides.css
Normal 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;
|
||||||
|
}
|
108
dev/docs/assets/snippets.css
Normal file
108
dev/docs/assets/snippets.css
Normal 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;
|
||||||
|
}
|
93
dev/docs/assets/snippets.js
Normal file
93
dev/docs/assets/snippets.js
Normal 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
3
dev/docs/snippets.html
Normal 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>
|
@ -1,148 +1,10 @@
|
|||||||
<!-- style overrides for dartdoc -->
|
<!-- style overrides for dartdoc -->
|
||||||
<style>
|
<style>
|
||||||
@import 'https://fonts.googleapis.com/css?family=Roboto:500,400italic,300,400,100i';
|
@import 'https://fonts.googleapis.com/css?family=Roboto:500,400italic,300,400,100i';
|
||||||
|
@import 'https://fonts.googleapis.com/css?family=Material+Icons';
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<link href="../assets/overrides.css" rel="stylesheet" type="text/css">
|
||||||
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>
|
|
||||||
|
|
||||||
<!-- The following rules are from http://google.github.io/material-design-icons/ -->
|
<!-- The following rules are from http://google.github.io/material-design-icons/ -->
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
|
57
dev/snippets/README.md
Normal file
57
dev/snippets/README.md
Normal 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).
|
34
dev/snippets/config/skeletons/application.html
Normal file
34
dev/snippets/config/skeletons/application.html
Normal 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}
|
20
dev/snippets/config/skeletons/sample.html
Normal file
20
dev/snippets/config/skeletons/sample.html
Normal 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}
|
56
dev/snippets/config/templates/README.md
Normal file
56
dev/snippets/config/templates/README.md
Normal 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.
|
||||||
|
|
32
dev/snippets/config/templates/stateful_widget.tmpl
Normal file
32
dev/snippets/config/templates/stateful_widget.tmpl
Normal 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}}
|
||||||
|
}
|
72
dev/snippets/lib/configuration.dart
Normal file
72
dev/snippets/lib/configuration.dart
Normal 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
122
dev/snippets/lib/main.dart
Normal 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);
|
||||||
|
}
|
222
dev/snippets/lib/snippets.dart
Normal file
222
dev/snippets/lib/snippets.dart
Normal 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
101
dev/snippets/pubspec.yaml
Normal 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
|
45
dev/snippets/test/configuration_test.dart
Normal file
45
dev/snippets/test/configuration_test.dart
Normal 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')));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
118
dev/snippets/test/snippets_test.dart
Normal file
118
dev/snippets/test/snippets_test.dart
Normal 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() {'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -10,7 +10,8 @@ import 'package:args/args.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:path/path.dart' as path;
|
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
|
/// 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
|
/// 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('dependency_overrides:');
|
||||||
buf.writeln(' platform_integration:');
|
buf.writeln(' platform_integration:');
|
||||||
buf.writeln(' path: 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.
|
// Create the library file.
|
||||||
final Directory libDir = Directory('dev/docs/lib');
|
final Directory libDir = Directory('$kDocsRoot/lib');
|
||||||
libDir.createSync();
|
libDir.createSync();
|
||||||
|
|
||||||
final StringBuffer contents = StringBuffer('library temp_doc;\n\n');
|
final StringBuffer contents = StringBuffer('library temp_doc;\n\n');
|
||||||
for (String libraryRef in libraryRefs()) {
|
for (String libraryRef in libraryRefs()) {
|
||||||
contents.writeln('import \'package:$libraryRef\';');
|
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 String flutterRoot = Directory.current.path;
|
||||||
final Map<String, String> pubEnvironment = <String, String>{
|
final Map<String, String> pubEnvironment = <String, String>{
|
||||||
@ -86,7 +87,7 @@ Future<void> main(List<String> arguments) async {
|
|||||||
Process process = await Process.start(
|
Process process = await Process.start(
|
||||||
pubExecutable,
|
pubExecutable,
|
||||||
<String>['get'],
|
<String>['get'],
|
||||||
workingDirectory: 'dev/docs',
|
workingDirectory: kDocsRoot,
|
||||||
environment: pubEnvironment,
|
environment: pubEnvironment,
|
||||||
);
|
);
|
||||||
printStream(process.stdout, prefix: 'pub:stdout: ');
|
printStream(process.stdout, prefix: 'pub:stdout: ');
|
||||||
@ -95,7 +96,9 @@ Future<void> main(List<String> arguments) async {
|
|||||||
if (code != 0)
|
if (code != 0)
|
||||||
exit(code);
|
exit(code);
|
||||||
|
|
||||||
createFooter('dev/docs/lib/footer.html');
|
createFooter('$kDocsRoot/lib/footer.html');
|
||||||
|
copyAssets();
|
||||||
|
cleanOutSnippets();
|
||||||
|
|
||||||
final List<String> dartdocBaseArgs = <String>['global', 'run'];
|
final List<String> dartdocBaseArgs = <String>['global', 'run'];
|
||||||
if (args['checked']) {
|
if (args['checked']) {
|
||||||
@ -107,7 +110,7 @@ Future<void> main(List<String> arguments) async {
|
|||||||
final ProcessResult result = Process.runSync(
|
final ProcessResult result = Process.runSync(
|
||||||
pubExecutable,
|
pubExecutable,
|
||||||
<String>[]..addAll(dartdocBaseArgs)..add('--version'),
|
<String>[]..addAll(dartdocBaseArgs)..add('--version'),
|
||||||
workingDirectory: 'dev/docs',
|
workingDirectory: kDocsRoot,
|
||||||
environment: pubEnvironment,
|
environment: pubEnvironment,
|
||||||
);
|
);
|
||||||
print('\n${result.stdout}flutter version: $version\n');
|
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
|
// 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
|
// recursive dependencies of the package defined at dev/docs/pubspec.yaml
|
||||||
final List<String> dartdocArgs = <String>[]..addAll(dartdocBaseArgs)..addAll(<String>[
|
final List<String> dartdocArgs = <String>[]..addAll(dartdocBaseArgs)..addAll(<String>[
|
||||||
|
'--inject-html',
|
||||||
'--header', 'styles.html',
|
'--header', 'styles.html',
|
||||||
'--header', 'analytics.html',
|
'--header', 'analytics.html',
|
||||||
'--header', 'survey.html',
|
'--header', 'survey.html',
|
||||||
|
'--header', 'snippets.html',
|
||||||
'--footer-text', 'lib/footer.html',
|
'--footer-text', 'lib/footer.html',
|
||||||
'--exclude-packages',
|
'--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',
|
'--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',
|
'--favicon=favicon.ico',
|
||||||
'--package-order', 'flutter,Dart,flutter_test,flutter_driver',
|
'--package-order', 'flutter,Dart,flutter_test,flutter_driver',
|
||||||
'--auto-include-dependencies',
|
'--auto-include-dependencies',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
String quote(String arg) => arg.contains(' ') ? "'$arg'" : arg;
|
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(
|
process = await Process.start(
|
||||||
pubExecutable,
|
pubExecutable,
|
||||||
dartdocArgs,
|
dartdocArgs,
|
||||||
workingDirectory: 'dev/docs',
|
workingDirectory: kDocsRoot,
|
||||||
environment: pubEnvironment,
|
environment: pubEnvironment,
|
||||||
);
|
);
|
||||||
printStream(process.stdout, prefix: args['json'] ? '' : 'dartdoc:stdout: ',
|
printStream(process.stdout, prefix: args['json'] ? '' : 'dartdoc:stdout: ',
|
||||||
@ -211,16 +253,63 @@ void createFooter(String footerPath) {
|
|||||||
gitBranchOut].join(' '));
|
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() {
|
void sanityCheckDocs() {
|
||||||
final List<String> canaries = <String>[
|
final List<String> canaries = <String>[
|
||||||
'$kDocRoot/api/dart-io/File-class.html',
|
'$kPublishRoot/assets/overrides.css',
|
||||||
'$kDocRoot/api/dart-ui/Canvas-class.html',
|
'$kPublishRoot/api/dart-io/File-class.html',
|
||||||
'$kDocRoot/api/dart-ui/Canvas/drawRect.html',
|
'$kPublishRoot/api/dart-ui/Canvas-class.html',
|
||||||
'$kDocRoot/api/flutter_driver/FlutterDriver/FlutterDriver.connectedTo.html',
|
'$kPublishRoot/api/dart-ui/Canvas/drawRect.html',
|
||||||
'$kDocRoot/api/flutter_test/WidgetTester/pumpWidget.html',
|
'$kPublishRoot/api/flutter_driver/FlutterDriver/FlutterDriver.connectedTo.html',
|
||||||
'$kDocRoot/api/material/Material-class.html',
|
'$kPublishRoot/api/flutter_test/WidgetTester/pumpWidget.html',
|
||||||
'$kDocRoot/api/material/Tooltip-class.html',
|
'$kPublishRoot/api/material/Material-class.html',
|
||||||
'$kDocRoot/api/widgets/Widget-class.html',
|
'$kPublishRoot/api/material/Tooltip-class.html',
|
||||||
|
'$kPublishRoot/api/widgets/Widget-class.html',
|
||||||
];
|
];
|
||||||
for (String canary in canaries) {
|
for (String canary in canaries) {
|
||||||
if (!File(canary).existsSync())
|
if (!File(canary).existsSync())
|
||||||
@ -231,7 +320,7 @@ void sanityCheckDocs() {
|
|||||||
/// Creates a custom index.html because we try to maintain old
|
/// Creates a custom index.html because we try to maintain old
|
||||||
/// paths. Cleanup unused index.html files no longer needed.
|
/// paths. Cleanup unused index.html files no longer needed.
|
||||||
void createIndexAndCleanup() {
|
void createIndexAndCleanup() {
|
||||||
print('\nCreating a custom index.html in $kDocRoot/index.html');
|
print('\nCreating a custom index.html in $kPublishRoot/index.html');
|
||||||
removeOldFlutterDocsDir();
|
removeOldFlutterDocsDir();
|
||||||
renameApiDir();
|
renameApiDir();
|
||||||
copyIndexToRootOfDocs();
|
copyIndexToRootOfDocs();
|
||||||
@ -243,22 +332,22 @@ void createIndexAndCleanup() {
|
|||||||
|
|
||||||
void removeOldFlutterDocsDir() {
|
void removeOldFlutterDocsDir() {
|
||||||
try {
|
try {
|
||||||
Directory('$kDocRoot/flutter').deleteSync(recursive: true);
|
Directory('$kPublishRoot/flutter').deleteSync(recursive: true);
|
||||||
} on FileSystemException {
|
} on FileSystemException {
|
||||||
// If the directory does not exist, that's OK.
|
// If the directory does not exist, that's OK.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void renameApiDir() {
|
void renameApiDir() {
|
||||||
Directory('$kDocRoot/api').renameSync('$kDocRoot/flutter');
|
Directory('$kPublishRoot/api').renameSync('$kPublishRoot/flutter');
|
||||||
}
|
}
|
||||||
|
|
||||||
void copyIndexToRootOfDocs() {
|
void copyIndexToRootOfDocs() {
|
||||||
File('$kDocRoot/flutter/index.html').copySync('$kDocRoot/index.html');
|
File('$kPublishRoot/flutter/index.html').copySync('$kPublishRoot/index.html');
|
||||||
}
|
}
|
||||||
|
|
||||||
void changePackageToSdkInTitlebar() {
|
void changePackageToSdkInTitlebar() {
|
||||||
final File indexFile = File('$kDocRoot/index.html');
|
final File indexFile = File('$kPublishRoot/index.html');
|
||||||
String indexContents = indexFile.readAsStringSync();
|
String indexContents = indexFile.readAsStringSync();
|
||||||
indexContents = indexContents.replaceFirst(
|
indexContents = indexContents.replaceFirst(
|
||||||
'<li><a href="https://flutter.io">Flutter package</a></li>',
|
'<li><a href="https://flutter.io">Flutter package</a></li>',
|
||||||
@ -269,7 +358,7 @@ void changePackageToSdkInTitlebar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void addHtmlBaseToIndex() {
|
void addHtmlBaseToIndex() {
|
||||||
final File indexFile = File('$kDocRoot/index.html');
|
final File indexFile = File('$kPublishRoot/index.html');
|
||||||
String indexContents = indexFile.readAsStringSync();
|
String indexContents = indexFile.readAsStringSync();
|
||||||
indexContents = indexContents.replaceFirst(
|
indexContents = indexContents.replaceFirst(
|
||||||
'</title>\n',
|
'</title>\n',
|
||||||
@ -289,7 +378,7 @@ void addHtmlBaseToIndex() {
|
|||||||
|
|
||||||
void putRedirectInOldIndexLocation() {
|
void putRedirectInOldIndexLocation() {
|
||||||
const String metaTag = '<meta http-equiv="refresh" content="0;URL=../index.html">';
|
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() {
|
List<String> findPackageNames() {
|
||||||
|
@ -142,11 +142,13 @@ abstract class DeletableChipAttributes {
|
|||||||
///
|
///
|
||||||
/// The chip will not automatically remove itself: this just tells the app
|
/// 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
|
/// 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 {
|
/// class Actor {
|
||||||
/// const Actor(this.name, this.initials);
|
/// const Actor(this.name, this.initials);
|
||||||
/// final String name;
|
/// final String name;
|
||||||
@ -193,6 +195,14 @@ abstract class DeletableChipAttributes {
|
|||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// @override
|
||||||
|
/// Widget build(BuildContext context) {
|
||||||
|
/// return CastList();
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// {@end-tool}
|
||||||
VoidCallback get onDeleted;
|
VoidCallback get onDeleted;
|
||||||
|
|
||||||
/// The [Color] for the delete icon. The default is based on the ambient
|
/// 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
|
/// The [onSelected] and [TappableChipAttributes.onPressed] callbacks must not
|
||||||
/// both be specified at the same time.
|
/// both be specified at the same time.
|
||||||
///
|
///
|
||||||
/// ## Sample code
|
/// {@tool sample}
|
||||||
|
///
|
||||||
|
/// A [StatefulWidget] that illustrates use of onSelected in an [InputChip].
|
||||||
///
|
///
|
||||||
/// ```dart
|
/// ```dart
|
||||||
/// class Wood extends StatefulWidget {
|
/// class Wood extends StatefulWidget {
|
||||||
@ -272,6 +284,7 @@ abstract class SelectableChipAttributes {
|
|||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
/// {@end-tool}
|
||||||
ValueChanged<bool> get onSelected;
|
ValueChanged<bool> get onSelected;
|
||||||
|
|
||||||
/// Elevation to be applied on the chip during the press motion.
|
/// Elevation to be applied on the chip during the press motion.
|
||||||
|
Loading…
Reference in New Issue
Block a user