mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

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.
223 lines
8.4 KiB
Dart
223 lines
8.4 KiB
Dart
// 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);
|
|
}
|
|
}
|