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

* Update project.pbxproj files to say Flutter rather than Chromium Also, the templates now have an empty organization so that we don't cause people to give their apps a Flutter copyright. * Update the copyright notice checker to require a standard notice on all files * Update copyrights on Dart files. (This was a mechanical commit.) * Fix weird license headers on Dart files that deviate from our conventions; relicense Shrine. Some were already marked "The Flutter Authors", not clear why. Their dates have been normalized. Some were missing the blank line after the license. Some were randomly different in trivial ways for no apparent reason (e.g. missing the trailing period). * Clean up the copyrights in non-Dart files. (Manual edits.) Also, make sure templates don't have copyrights. * Fix some more ORGANIZATIONNAMEs
283 lines
11 KiB
Dart
283 lines
11 KiB
Dart
// Copyright 2014 The Flutter 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:dart_style/dart_style.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:path/path.dart' as path;
|
|
|
|
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, {String language}) : language = language ?? '';
|
|
final String name;
|
|
final List<String> contents;
|
|
final String language;
|
|
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 ??
|
|
// Flutter's root is four directories up from this script.
|
|
Configuration(flutterRoot: Directory(Platform.environment['FLUTTER_ROOT']
|
|
?? path.canonicalize(path.join(path.dirname(path.fromUri(Platform.script)), '..', '..', '..')))) {
|
|
this.configuration.createOutputDirectory();
|
|
}
|
|
|
|
/// The configuration used to determine where to get/save data for the
|
|
/// snippet.
|
|
final Configuration configuration;
|
|
|
|
static const JsonEncoder jsonEncoder = JsonEncoder.withIndent(' ');
|
|
|
|
/// 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, Map<String, Object> metadata) {
|
|
final RegExp moustacheRegExp = RegExp('{{([^}]+)}}');
|
|
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.isNotEmpty && description.last == '// ') {
|
|
description.removeLast();
|
|
}
|
|
while (description.isNotEmpty && description.first == '// ') {
|
|
description.removeAt(0);
|
|
}
|
|
return description.join('\n').trim();
|
|
} else {
|
|
// If the match isn't found in the injections, then just remove the
|
|
// mustache reference, since we want to allow the sections to be
|
|
// "optional" in the input: users shouldn't be forced to add an empty
|
|
// "```dart preamble" section if that section would be empty.
|
|
final _ComponentTuple result = injections
|
|
.firstWhere((_ComponentTuple tuple) => tuple.name == match[1], orElse: () => null);
|
|
return result?.mergedContent ?? (metadata[match[1]] ?? '').toString();
|
|
}
|
|
}).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, Map<String, Object> metadata) {
|
|
final List<String> result = <String>[];
|
|
const HtmlEscape htmlEscape = HtmlEscape();
|
|
String language;
|
|
for (_ComponentTuple injection in injections) {
|
|
if (!injection.name.startsWith('code')) {
|
|
continue;
|
|
}
|
|
result.addAll(injection.contents);
|
|
if (injection.language.isNotEmpty) {
|
|
language = injection.language;
|
|
}
|
|
result.addAll(<String>['', '// ...', '']);
|
|
}
|
|
if (result.length > 3) {
|
|
result.removeRange(result.length - 3, result.length);
|
|
}
|
|
// Only insert a div for the description if there actually is some text there.
|
|
// This means that the {{description}} marker in the skeleton needs to
|
|
// be inside of an {@inject-html} block.
|
|
String description = injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'description').mergedContent;
|
|
description = description.trim().isNotEmpty
|
|
? '<div class="snippet-description">{@end-inject-html}$description{@inject-html}</div>'
|
|
: '';
|
|
final Map<String, String> substitutions = <String, String>{
|
|
'description': description,
|
|
'code': htmlEscape.convert(result.join('\n')),
|
|
'language': language ?? 'dart',
|
|
'serial': '',
|
|
'id': metadata['id'],
|
|
'element': metadata['element'] ?? '',
|
|
'app': '',
|
|
};
|
|
if (type == SnippetType.application) {
|
|
substitutions
|
|
..['serial'] = metadata['serial']?.toString() ?? '0'
|
|
..['app'] = htmlEscape.convert(injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'app').mergedContent);
|
|
}
|
|
return skeleton.replaceAllMapped(RegExp('{{(${substitutions.keys.join('|')})}}'), (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 inCodeBlock = false;
|
|
input = input.trim();
|
|
final List<String> description = <String>[];
|
|
final List<_ComponentTuple> components = <_ComponentTuple>[];
|
|
String language;
|
|
final RegExp codeStartEnd = RegExp(r'^\s*```([-\w]+|[-\w]+ ([-\w]+))?\s*$');
|
|
for (String line in input.split('\n')) {
|
|
final Match match = codeStartEnd.firstMatch(line);
|
|
if (match != null) { // If we saw the start or end of a code block
|
|
inCodeBlock = !inCodeBlock;
|
|
if (match[1] != null) {
|
|
language = match[1];
|
|
if (match[2] != null) {
|
|
components.add(_ComponentTuple('code-${match[2]}', <String>[], language: language));
|
|
} else {
|
|
components.add(_ComponentTuple('code', <String>[], language: language));
|
|
}
|
|
} else {
|
|
language = null;
|
|
}
|
|
continue;
|
|
}
|
|
if (!inCodeBlock) {
|
|
description.add(line);
|
|
} else {
|
|
assert(language != null);
|
|
components.last.contents.add(line);
|
|
}
|
|
}
|
|
return <_ComponentTuple>[
|
|
_ComponentTuple('description', description),
|
|
...components,
|
|
];
|
|
}
|
|
|
|
String _loadFileAsUtf8(File file) {
|
|
return file.readAsStringSync(encoding: Encoding.getByName('utf-8'));
|
|
}
|
|
|
|
String _addLineNumbers(String app) {
|
|
final StringBuffer buffer = StringBuffer();
|
|
int count = 0;
|
|
for (String line in app.split('\n')) {
|
|
count++;
|
|
buffer.writeln('${count.toString().padLeft(5, ' ')}: $line');
|
|
}
|
|
return buffer.toString();
|
|
}
|
|
|
|
/// 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].
|
|
///
|
|
/// [showDartPad] indicates whether DartPad should be shown where possible.
|
|
/// Currently, this value only has an effect if [type] is
|
|
/// [SnippetType.application], in which case an alternate skeleton file is
|
|
/// used to create the final HTML output.
|
|
///
|
|
/// 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, {
|
|
bool showDartPad = false,
|
|
String template,
|
|
File output,
|
|
@required Map<String, Object> metadata,
|
|
}) {
|
|
assert(template != null || type != SnippetType.application);
|
|
assert(metadata != null && metadata['id'] != null);
|
|
assert(input != null);
|
|
assert(!showDartPad || type == SnippetType.application,
|
|
'Only application snippets work with dartpad.');
|
|
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);
|
|
}
|
|
final String templateContents = _loadFileAsUtf8(templateFile);
|
|
String app = interpolateTemplate(snippetData, templateContents, metadata);
|
|
|
|
try {
|
|
app = formatter.format(app);
|
|
} on FormatterException catch (exception) {
|
|
stderr.write('Code to format:\n${_addLineNumbers(app)}\n');
|
|
errorExit('Unable to format snippet app template: $exception');
|
|
}
|
|
|
|
snippetData.add(_ComponentTuple('app', app.split('\n')));
|
|
final File outputFile = output ?? getOutputFile(metadata['id']);
|
|
stderr.writeln('Writing to ${outputFile.absolute.path}');
|
|
outputFile.writeAsStringSync(app);
|
|
|
|
final File metadataFile = File(path.join(path.dirname(outputFile.path),
|
|
'${path.basenameWithoutExtension(outputFile.path)}.json'));
|
|
stderr.writeln('Writing metadata to ${metadataFile.absolute.path}');
|
|
final _ComponentTuple description = snippetData.firstWhere(
|
|
(_ComponentTuple data) => data.name == 'description',
|
|
orElse: () => null,
|
|
);
|
|
metadata ??= <String, Object>{};
|
|
metadata.addAll(<String, Object>{
|
|
'id': metadata['id'],
|
|
'file': path.basename(outputFile.path),
|
|
'description': description?.mergedContent,
|
|
});
|
|
metadataFile.writeAsStringSync(jsonEncoder.convert(metadata));
|
|
break;
|
|
case SnippetType.sample:
|
|
break;
|
|
}
|
|
final String skeleton =
|
|
_loadFileAsUtf8(configuration.getHtmlSkeletonFile(type, showDartPad: showDartPad));
|
|
return interpolateSkeleton(type, snippetData, skeleton, metadata);
|
|
}
|
|
}
|