Skip to content

Commit 3494543

Browse files
gspencergoogjcollins-g
authored andcommitted
External tool support (#1756)
* Add tool support * Add tool support * Updating Docs * Updating Docs * Updating CONTRIBUTING.md * Updating CONTRIBUTING.md * Adding more complete tool options. * Adding more complete tool options. * Update docs on Linux again. * Update docs on Linux again. * Review changes and associated docs updates * Update README.md * Remove unnecessary type declarations. * Review Changes
1 parent b23e37d commit 3494543

File tree

452 files changed

+41451
-93
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

452 files changed

+41451
-93
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.22.0
2+
* Added the ability to run external tools on a section of documentation and
3+
replace it with the output of the tool.
4+
15
## 0.21.1
26
* Fix a problem where category ordering specified in categories option
37
was not obeyed. Reintroduce categoryOrder option to solve this problem.

CONTRIBUTING.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,16 @@ yet in the issue tracker, start by opening an issue. Thanks!
2525
2. When a change is user-facing, please add a new entry to the [changelog](https://github.com/dart-lang/dartdoc/blob/master/CHANGELOG.md)
2626
3. Please include a test for your change. `dartdoc` has both `package:test`-style unittests as well as integration tests. To run the unittests, use `dart test/all.dart`. Most changes can be tested via a unittest, but some require modifying the [test_package](https://github.com/dart-lang/dartdoc/tree/master/testing/test_package) and regenerating its docs via `grind update-test-package-docs`.
2727
4. For major changes, run `grind compare-sdk-warnings` and `grind compare-flutter-warnings`, and include the summary results in your pull request.
28-
5. Be sure to format your Dart code using `dartfmt -w`, otherwise travis will complain.
29-
6. Post your change via a pull request for review and integration!
28+
5. Be sure to format your Dart code using `dartfmt -w`, otherwise travis will complain.
29+
6. Because there are generated versions of the dartdoc docs for stable and development versions of Dart,
30+
you need to update the docs twice:
31+
- Download and install the latest STABLE version of dart from [the Dart website](https://www.dartlang.org/tools/sdk).
32+
(It's probably easiest to download a zip file and change your PATH to the extracted location's `bin` directory)
33+
- Run `pub run grinder update-test-package-docs` to update the stable docs.
34+
- Download and install the latest DEV version of dart from [the Dart website](https://www.dartlang.org/tools/sdk)
35+
(It's probably easiest to download a zip file and change your PATH to the extracted location's `bin` directory)
36+
- Run `pub run grinder update-test-package-docs` to update the dev docs.
37+
7. Post your change via a pull request for review and integration!
3038

3139
## Testing
3240

README.md

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,15 +221,15 @@ You can specify links to videos inline that will be handled with a simple HTML5
221221
You can specify "macros", i.e. reusable pieces of documentation. For that, first specify a template
222222
anywhere in the comments, like:
223223

224-
```
224+
```dart
225225
/// {@template template_name}
226226
/// Some shared docs
227227
/// {@endtemplate}
228228
```
229229

230230
and then you can insert it via `{@macro template_name}`, like
231231

232-
```
232+
```dart
233233
/// Some comment
234234
/// {@macro template_name}
235235
/// More comments
@@ -240,6 +240,60 @@ dartdoc is currently documenting. This can lead to inconsistent behavior betwee
240240
packages, especially if different command lines are used for dartdoc. It is recommended to use collision-resistant
241241
naming for any macros by including the package name and/or library it is defined in within the name.
242242

243+
### Tools
244+
245+
Dartdoc allows you to filter parts of the documentation through an external tool
246+
and then include the output of that tool in place of the given input.
247+
248+
First, you have to configure the tools that will be used in the `dartdoc_options.yaml` file:
249+
250+
```yaml
251+
dartdoc:
252+
tools:
253+
drill:
254+
command: ["bin/drill.dart"]
255+
description: "Puts holes in things."
256+
echo:
257+
macos: ['/bin/sh', '-c', 'echo']
258+
linux: ['/bin/sh', '-c', 'echo']
259+
windows: ['C:\\Windows\\System32\\cmd.exe', '/c', 'echo']
260+
description: 'Works on everything'
261+
```
262+
263+
The `command` tag is used to describe the command executable, and any options
264+
that are common among all executions. If the first element of this list is a
265+
filename that ends in `.dart`, then the dart executable will automatically be
266+
used to invoke that script. The `command` defined will be run on all platforms.
267+
268+
The `macos`, `linux`, and `windows` tags are used to describe the commands to
269+
be run on each of those platforms.
270+
271+
The `description` is just a short description of the tool for use as help text.
272+
273+
Only tools which are configured in the `dartdoc_options.yaml` file are able to
274+
be invoked.
275+
276+
To use the tools in comment documentation, use the `{@tool <name> [<options> ...] [$INPUT]}`
277+
directive to invoke the tool:
278+
279+
```dart
280+
/// {@tool drill --flag --option="value" $INPUT}
281+
/// This is the text that will be sent to the tool as input.
282+
/// {@end-tool}
283+
```
284+
285+
The `$INPUT` argument is a special token that will be replaced with the name of
286+
a temporary file that the tool needs to read from. It can appear anywhere in the
287+
options, and can appear multiple times.
288+
289+
If the example `drill` tool with those options is a tool that turns the content
290+
of its input file into a code-font heading, then the directive above would be
291+
the equivalent of having the following comment in the code:
292+
293+
```dart
294+
/// # `This is the text that will be sent to the tool as input.`
295+
```
296+
243297
### Auto including dependencies
244298

245299
If `--auto-include-dependencies` flag is provided, dartdoc tries to automatically add

lib/src/dartdoc_options.dart

Lines changed: 133 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ class DartdocFileMissing extends DartdocOptionError {
5656
DartdocFileMissing(String details) : super(details);
5757
}
5858

59+
/// Defines the attributes of a category in the options file, corresponding to
60+
/// the 'categories' keyword in the options file, and populated by the
61+
/// [CategoryConfiguration] class.
5962
class CategoryDefinition {
6063
/// Internal name of the category.
6164
final String name;
@@ -73,6 +76,8 @@ class CategoryDefinition {
7376
String get displayName => _displayName ?? name;
7477
}
7578

79+
/// A configuration class that can interpret category definitions from a YAML
80+
/// map.
7681
class CategoryConfiguration {
7782
/// A map of [CategoryDefinition.name] to [CategoryDefinition] objects.
7883
final Map<String, CategoryDefinition> categoryDefinitions;
@@ -110,6 +115,112 @@ class CategoryConfiguration {
110115
}
111116
}
112117

118+
/// Defines the attributes of a tool in the options file, corresponding to
119+
/// the 'tools' keyword in the options file, and populated by the
120+
/// [ToolConfiguration] class.
121+
class ToolDefinition {
122+
/// A list containing the command and options to be run for this tool. The
123+
/// first argument in the command is the tool executable. Must not be an empty
124+
/// list, or be null.
125+
final List<String> command;
126+
127+
/// A description of the defined tool. Must not be null.
128+
final String description;
129+
130+
ToolDefinition(this.command, this.description)
131+
: assert(command != null),
132+
assert(command.isNotEmpty),
133+
assert(description != null);
134+
135+
@override
136+
String toString() => '$runtimeType: "${command.join(' ')}" ($description)';
137+
}
138+
139+
/// A configuration class that can interpret [ToolDefinition]s from a YAML map.
140+
class ToolConfiguration {
141+
final Map<String, ToolDefinition> tools;
142+
143+
ToolConfiguration._(this.tools);
144+
145+
static ToolConfiguration get empty {
146+
return new ToolConfiguration._({});
147+
}
148+
149+
static ToolConfiguration fromYamlMap(
150+
YamlMap yamlMap, pathLib.Context pathContext) {
151+
var newToolDefinitions = <String, ToolDefinition>{};
152+
for (var entry in yamlMap.entries) {
153+
var name = entry.key.toString();
154+
var toolMap = entry.value;
155+
var description;
156+
List<String> command;
157+
if (toolMap is Map) {
158+
description = toolMap['description']?.toString();
159+
// If the command key is given, then it applies to all platforms.
160+
var commandFrom = toolMap.containsKey('command')
161+
? 'command'
162+
: Platform.operatingSystem;
163+
if (toolMap.containsKey(commandFrom)) {
164+
if (toolMap[commandFrom].value is String) {
165+
command = [toolMap[commandFrom].toString()];
166+
if (command[0].isEmpty) {
167+
throw new DartdocOptionError(
168+
'Tool commands must not be empty. Tool $name command entry '
169+
'"$commandFrom" must contain at least one path.');
170+
}
171+
} else if (toolMap[commandFrom] is YamlList) {
172+
command = (toolMap[commandFrom] as YamlList)
173+
.map<String>((node) => node.toString())
174+
.toList();
175+
if (command.isEmpty) {
176+
throw new DartdocOptionError(
177+
'Tool commands must not be empty. Tool $name command entry '
178+
'"$commandFrom" must contain at least one path.');
179+
}
180+
} else {
181+
throw new DartdocOptionError(
182+
'Tool commands must be a path to an executable, or a list of '
183+
'strings that starts with a path to an executable. '
184+
'The tool $name has a $commandFrom entry that is a '
185+
'${toolMap[commandFrom].runtimeType}');
186+
}
187+
}
188+
} else {
189+
throw new DartdocOptionError(
190+
'Tools must be defined as a map of tool names to definitions. Tool '
191+
'$name is not a map.');
192+
}
193+
if (command == null) {
194+
throw new DartdocOptionError(
195+
'At least one of "command" or "${Platform.operatingSystem}" must '
196+
'be defined for the tool $name.');
197+
}
198+
var executable = command.removeAt(0);
199+
executable = pathContext.canonicalize(executable);
200+
var executableFile = new File(executable);
201+
var exeStat = executableFile.statSync();
202+
if (exeStat.type == FileSystemEntityType.notFound) {
203+
throw new DartdocOptionError('Command executables must exist. '
204+
'The file "$executable" does not exist for tool $name.');
205+
}
206+
// Dart scripts don't need to be executable, because they'll be
207+
// executed with the Dart binary.
208+
bool isExecutable(int mode) {
209+
return (0x1 & ((mode >> 6) | (mode >> 3) | mode)) != 0;
210+
}
211+
212+
if (!executable.endsWith('.dart') && !isExecutable(exeStat.mode)) {
213+
throw new DartdocOptionError('Non-Dart commands must be '
214+
'executable. The file "$executable" for tool $name does not have '
215+
'executable permission.');
216+
}
217+
newToolDefinitions[name] =
218+
new ToolDefinition([executable] + command, description);
219+
}
220+
return new ToolConfiguration._(newToolDefinitions);
221+
}
222+
}
223+
113224
/// A container class to keep track of where our yaml data came from.
114225
class _YamlFileData {
115226
/// The map from the yaml file.
@@ -158,9 +269,10 @@ class _OptionValueWithContext<T> {
158269
return pathContext.canonicalize(resolveTildePath(value as String)) as T;
159270
} else if (value is Map<String, String>) {
160271
return (value as Map<String, String>)
161-
.map((String mapKey, String mapValue) => new MapEntry<String, String>(
162-
mapKey, pathContext.canonicalize(resolveTildePath(mapValue))))
163-
.cast<String, String>() as T;
272+
.map<String, String>((String key, String value) {
273+
return new MapEntry(
274+
key, pathContext.canonicalize(resolveTildePath(value)));
275+
}) as T;
164276
} else {
165277
throw new UnsupportedError('Type $T is not supported for resolvedValue');
166278
}
@@ -252,8 +364,7 @@ abstract class DartdocOption<T> {
252364
void _onMissing(
253365
_OptionValueWithContext valueWithContext, String missingFilename);
254366

255-
/// Call [_onMissing] for every path that does not exist. Returns true if
256-
/// all paths exist or [mustExist] == false.
367+
/// Call [_onMissing] for every path that does not exist.
257368
void _validatePaths(_OptionValueWithContext valueWithContext) {
258369
if (!mustExist) return;
259370
assert(isDir || isFile);
@@ -264,6 +375,9 @@ abstract class DartdocOption<T> {
264375
resolvedPaths = valueWithContext.resolvedValue.toList();
265376
} else if (valueWithContext.value is Map<String, String>) {
266377
resolvedPaths = valueWithContext.resolvedValue.values.toList();
378+
} else {
379+
assert(false, "Trying to ensure existence of unsupported type "
380+
"${valueWithContext.value.runtimeType}");
267381
}
268382
for (String path in resolvedPaths) {
269383
FileSystemEntity f = isDir ? new Directory(path) : new File(path);
@@ -1024,6 +1138,7 @@ class DartdocOptionContext {
10241138
List<String> get includeExternal =>
10251139
optionSet['includeExternal'].valueAt(context);
10261140
bool get includeSource => optionSet['includeSource'].valueAt(context);
1141+
ToolConfiguration get tools => optionSet['tools'].valueAt(context);
10271142

10281143
/// _input is only used to construct synthetic options.
10291144
// ignore: unused_element
@@ -1065,11 +1180,11 @@ Future<List<DartdocOption>> createDartdocOptions() async {
10651180
negatable: true),
10661181
new DartdocOptionArgFile<double>(
10671182
'ambiguousReexportScorerMinConfidence', 0.1,
1068-
help:
1069-
'Minimum scorer confidence to suppress warning on ambiguous reexport.'),
1183+
help: 'Minimum scorer confidence to suppress warning on ambiguous '
1184+
'reexport.'),
10701185
new DartdocOptionArgOnly<bool>('autoIncludeDependencies', false,
1071-
help:
1072-
'Include all the used libraries into the docs, even the ones not in the current package or "include-external"',
1186+
help: 'Include all the used libraries into the docs, even the ones not '
1187+
'in the current package or "include-external"',
10731188
negatable: true),
10741189
new DartdocOptionArgFile<List<String>>('categoryOrder', [],
10751190
help:
@@ -1078,8 +1193,8 @@ Future<List<DartdocOption>> createDartdocOptions() async {
10781193
new DartdocOptionFileOnly<CategoryConfiguration>(
10791194
'categories', CategoryConfiguration.empty,
10801195
convertYamlToType: CategoryConfiguration.fromYamlMap,
1081-
help:
1082-
"A list of all categories, their display names, and markdown documentation in the order they are to be displayed."),
1196+
help: 'A list of all categories, their display names, and markdown '
1197+
'documentation in the order they are to be displayed.'),
10831198
new DartdocOptionSyntheticOnly<List<String>>('dropTextFrom',
10841199
(DartdocSyntheticOption<List<String>> option, Directory dir) {
10851200
if (option.parent['hideSdkText'].valueAt(dir)) {
@@ -1249,5 +1364,12 @@ Future<List<DartdocOption>> createDartdocOptions() async {
12491364
new DartdocOptionArgOnly<bool>('verboseWarnings', true,
12501365
help: 'Display extra debugging information and help with warnings.',
12511366
negatable: true),
1367+
new DartdocOptionFileOnly<ToolConfiguration>(
1368+
'tools', ToolConfiguration.empty,
1369+
convertYamlToType: ToolConfiguration.fromYamlMap,
1370+
help: 'A map of tool names to executable paths. Each executable must '
1371+
'exist. Executables for different platforms are specified by '
1372+
'giving the platform name as a key, and a list of strings as the '
1373+
'command.'),
12521374
];
12531375
}

0 commit comments

Comments
 (0)