Switch document generation to use the snippets package (#87231)

Switch document generation to use the snippets package instead of the snippets code in the Flutter repo. In the process, some bugs in sample code analysis have been fixed, and I've fixed some more errors in the samples.

This will allow the snippets package to be developed separately from the Flutter repo, and reduce the code in the Flutter repo.

The snippets code is deleted in this PR.

I also converted some comments in the snippet templates to be regular comments instead of doc comments, because having a doc comment block before the imports causes the Dart import sorter to lose the comment. They should have been regular comments in the first place.

The snippets package resides in the assets-for-api-docs repo.

The sample analysis has also been converted to be run in parallel, and I've bumped the Dartdoc version to 1.0.2.
This commit is contained in:
Greg Spencer 2021-08-11 19:48:29 -07:00 committed by GitHub
parent 0340319a96
commit 10e4b04010
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 347 additions and 1378 deletions

View File

@ -138,7 +138,7 @@ task:
- name: docs-linux # linux-only
environment:
CPU: 4
MEMORY: 8G
MEMORY: 12G
only_if: "$CIRRUS_PR != ''"
script:
- ./dev/bots/docs.sh

View File

@ -1,16 +1,17 @@
# This file is used by dartdoc when generating API documentation for Flutter.
dartdoc:
# Before you can run dartdoc, the snippets tool needs to have a snapshot built.
# The dev/tools/dartdoc.dart script does this automatically.
# Before you can run dartdoc, the snippets tool needs to be
# activated with "pub global activate snippets <version>"
# The dev/bots/docs.sh script does this automatically.
tools:
snippet:
command: ["dev/snippets/lib/main.dart", "--type=snippet"]
command: ["bin/cache/dart-sdk/bin/pub", "global", "run", "snippets", "--output-directory=doc/snippets", "--type=snippet"]
description: "Creates sample code documentation output from embedded documentation samples."
sample:
command: ["dev/snippets/lib/main.dart", "--type=sample"]
command: ["bin/cache/dart-sdk/bin/pub", "global", "run", "snippets", "--output-directory=doc/snippets", "--type=sample"]
description: "Creates full application sample code documentation output from embedded documentation samples."
dartpad:
command: ["dev/snippets/lib/main.dart", "--type=sample", "--dartpad"]
command: ["bin/cache/dart-sdk/bin/pub", "global", "run", "snippets", "--output-directory=doc/snippets", "--type=dartpad"]
description: "Creates full application sample code documentation output from embedded documentation samples and displays it in an embedded DartPad."
errors:
# Default errors of dartdoc:

View File

@ -7,8 +7,10 @@
// To run this, from the root of the Flutter repository:
// bin/cache/dart-sdk/bin/dart dev/bots/analyze_sample_code.dart
// @dart= 2.12
// @dart= 2.14
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
@ -16,12 +18,22 @@ import 'package:args/args.dart';
import 'package:path/path.dart' as path;
import 'package:watcher/watcher.dart';
// If you update this version, also update it in dev/bots/docs.sh
const String _snippetsActivateVersion = '0.2.2';
final String _flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script))));
final String _defaultFlutterPackage = path.join(_flutterRoot, 'packages', 'flutter', 'lib');
final String _defaultDartUiLocation = path.join(_flutterRoot, 'bin', 'cache', 'pkg', 'sky_engine', 'lib', 'ui');
final String _flutter = path.join(_flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter');
void main(List<String> arguments) {
/// Finds the location of the pub executable, with the assumption that it is
/// in the same location as the Dart executable used to run this script.
String get _pubExecutable {
final File dartExecutable = File(Platform.resolvedExecutable);
return path.join(path.dirname(dartExecutable.absolute.path), Platform.isWindows ? 'pub.exe' : 'pub');
}
Future<void> main(List<String> arguments) async {
final ArgParser argParser = ArgParser();
argParser.addOption(
'temp',
@ -61,6 +73,13 @@ void main(List<String> arguments) {
abbr: 'i',
help: 'Analyzes the sample code in the specified file interactively.',
);
argParser.addFlag(
'global-activate-snippets',
defaultsTo: true,
negatable: true,
help: 'Whether or not to "pub global activate" the snippets package. If set, will '
'activate version $_snippetsActivateVersion',
);
final ArgResults parsedArguments = argParser.parse(arguments);
@ -107,8 +126,25 @@ void main(List<String> arguments) {
tempDirectory.createSync();
}
if (parsedArguments['global-activate-snippets']! as bool) {
try {
Process.runSync(
_pubExecutable,
<String>[
'global',
'activate',
'snippets',
_snippetsActivateVersion,
],
workingDirectory: _flutterRoot,
);
} on ProcessException catch (e) {
stderr.writeln('Unable to global activate snippets package at version $_snippetsActivateVersion: $e');
exit(1);
}
}
if (parsedArguments['interactive'] != null) {
_runInteractive(
await _runInteractive(
tempDir: tempDirectory,
flutterPackage: flutterPackage,
filePath: parsedArguments['interactive'] as String,
@ -116,7 +152,7 @@ void main(List<String> arguments) {
);
} else {
try {
exitCode = SampleChecker(
exitCode = await SampleChecker(
flutterPackage,
tempDirectory: tempDirectory,
verbose: parsedArguments['verbose'] as bool,
@ -129,6 +165,95 @@ void main(List<String> arguments) {
}
}
typedef TaskQueueClosure<T> = Future<T> Function();
class _TaskQueueItem<T> {
_TaskQueueItem(this._closure, this._completer, {this.onComplete});
final TaskQueueClosure<T> _closure;
final Completer<T> _completer;
void Function()? onComplete;
Future<void> run() async {
try {
_completer.complete(await _closure());
} catch (e) {
_completer.completeError(e);
} finally {
onComplete?.call();
}
}
}
/// A task queue of Futures to be completed in parallel, throttling
/// the number of simultaneous tasks.
///
/// The tasks return results of type T.
class TaskQueue<T> {
/// Creates a task queue with a maximum number of simultaneous jobs.
/// The [maxJobs] parameter defaults to the number of CPU cores on the
/// system.
TaskQueue({int? maxJobs})
: maxJobs = maxJobs ?? Platform.numberOfProcessors;
/// The maximum number of jobs that this queue will run simultaneously.
final int maxJobs;
final Queue<_TaskQueueItem<T>> _pendingTasks = Queue<_TaskQueueItem<T>>();
final Set<_TaskQueueItem<T>> _activeTasks = <_TaskQueueItem<T>>{};
final Set<Completer<void>> _completeListeners = <Completer<void>>{};
/// Returns a future that completes when all tasks in the [TaskQueue] are
/// complete.
Future<void> get tasksComplete {
// In case this is called when there are no tasks, we want it to
// signal complete immediately.
if (_activeTasks.isEmpty && _pendingTasks.isEmpty) {
return Future<void>.value();
}
final Completer<void> completer = Completer<void>();
_completeListeners.add(completer);
return completer.future;
}
/// Adds a single closure to the task queue, returning a future that
/// completes when the task completes.
Future<T> add(TaskQueueClosure<T> task) {
final Completer<T> completer = Completer<T>();
_pendingTasks.add(_TaskQueueItem<T>(task, completer));
if (_activeTasks.length < maxJobs) {
_processTask();
}
return completer.future;
}
// Process a single task.
void _processTask() {
if (_pendingTasks.isNotEmpty && _activeTasks.length <= maxJobs) {
final _TaskQueueItem<T> item = _pendingTasks.removeFirst();
_activeTasks.add(item);
item.onComplete = () {
_activeTasks.remove(item);
_processTask();
};
item.run();
} else {
_checkForCompletion();
}
}
void _checkForCompletion() {
if (_activeTasks.isEmpty && _pendingTasks.isEmpty) {
for (final Completer<void> completer in _completeListeners) {
if (!completer.isCompleted) {
completer.complete();
}
}
_completeListeners.clear();
}
}
}
class SampleCheckerException implements Exception {
SampleCheckerException(this.message, {this.file, this.line});
final String message;
@ -147,6 +272,12 @@ class SampleCheckerException implements Exception {
}
}
class AnalysisResult {
const AnalysisResult(this.exitCode, this.errors);
final int exitCode;
final Map<String, List<AnalysisError>> errors;
}
/// Checks samples and code snippets for analysis errors.
///
/// Extracts dartdoc content from flutter package source code, identifies code
@ -237,25 +368,6 @@ class SampleChecker {
/// generate them.
int _expressionId = 0;
/// The exit code from the analysis process.
int _exitCode = 0;
// Once the snippets tool has been precompiled by Dart, this contains the AOT
// snapshot.
String? _snippetsSnapshotPath;
/// Finds the location of the snippets script.
String get _snippetsExecutable {
final String platformScriptPath = path.dirname(path.fromUri(Platform.script));
return path.canonicalize(path.join(platformScriptPath, '..', 'snippets', 'lib', 'main.dart'));
}
/// Finds the location of the Dart executable.
String get _dartExecutable {
final File dartExecutable = File(Platform.resolvedExecutable);
return dartExecutable.absolute.path;
}
static List<File> _listDartFiles(Directory directory, {bool recursive = false}) {
return directory.listSync(recursive: recursive, followLinks: false).whereType<File>().where((File file) => path.extension(file.path) == '.dart').toList();
}
@ -282,9 +394,8 @@ class SampleChecker {
List<Line>? _headers;
/// Checks all the samples in the Dart files in [_flutterPackage] for errors.
int checkSamples() {
_exitCode = 0;
Map<String, List<AnalysisError>> errors = <String, List<AnalysisError>>{};
Future<int> checkSamples() async {
AnalysisResult? analysisResult;
try {
final Map<String, Section> sections = <String, Section>{};
final Map<String, Sample> snippets = <String, Sample>{};
@ -295,14 +406,14 @@ class SampleChecker {
..._listDartFiles(_flutterPackage, recursive: true),
if (_dartUiLocation != null && _dartUiLocation!.existsSync()) ... _listDartFiles(_dartUiLocation!, recursive: true),
];
_extractSamples(filesToAnalyze, sectionMap: sections, sampleMap: snippets);
errors = _analyze(_tempDirectory, sections, snippets);
await _extractSamples(filesToAnalyze, sectionMap: sections, sampleMap: snippets);
analysisResult = _analyze(_tempDirectory, sections, snippets);
} finally {
if (errors.isNotEmpty) {
for (final String filePath in errors.keys) {
errors[filePath]!.forEach(stderr.writeln);
if (analysisResult != null && analysisResult.errors.isNotEmpty) {
for (final String filePath in analysisResult.errors.keys) {
analysisResult.errors[filePath]!.forEach(stderr.writeln);
}
stderr.writeln('\nFound ${errors.length} sample code errors.');
stderr.writeln('\nFound ${analysisResult.errors.length} sample code errors.');
}
if (_keepTmp) {
print('Leaving temporary directory ${_tempDirectory.path} around for your perusal.');
@ -313,15 +424,8 @@ class SampleChecker {
stderr.writeln('Failed to delete ${_tempDirectory.path}: $e');
}
}
// If we made a snapshot, remove it (so as not to clutter up the tree).
if (_snippetsSnapshotPath != null) {
final File snapshot = File(_snippetsSnapshotPath!);
if (snapshot.existsSync()) {
snapshot.deleteSync();
}
}
}
return _exitCode;
return analysisResult.exitCode;
}
/// Creates a name for the snippets tool to use for the snippet ID from a
@ -333,37 +437,37 @@ class SampleChecker {
return sampleId;
}
// Precompiles the snippets tool if _snippetsSnapshotPath isn't set yet, and
// runs the precompiled version if it is set.
ProcessResult _runSnippetsScript(List<String> args) {
// The cached JSON Flutter version information from 'flutter --version --machine'.
String? _flutterVersion;
Future<ProcessResult> _runSnippetsScript(List<String> args) async {
final String workingDirectory = path.join(_flutterRoot, 'dev', 'docs');
if (_snippetsSnapshotPath == null) {
_snippetsSnapshotPath = '$_snippetsExecutable.snapshot';
return Process.runSync(
_dartExecutable,
<String>[
'--snapshot=$_snippetsSnapshotPath',
'--snapshot-kind=app-jit',
path.canonicalize(_snippetsExecutable),
...args,
],
workingDirectory: workingDirectory,
);
} else {
return Process.runSync(
_dartExecutable,
<String>[
path.canonicalize(_snippetsSnapshotPath!),
...args,
],
workingDirectory: workingDirectory,
);
if (_flutterVersion == null) {
// Capture the flutter version information once so that the snippets tool doesn't
// have to run it for every snippet.
final ProcessResult versionResult = Process.runSync(_flutter, <String>['--version', '--machine']);
_flutterVersion = versionResult.stdout as String? ?? '';
}
return Process.run(
_pubExecutable,
<String>[
'global',
'run',
'snippets',
...args,
],
workingDirectory: workingDirectory,
environment: <String, String>{
if (!Platform.environment.containsKey('FLUTTER_ROOT')) 'FLUTTER_ROOT': _flutterRoot,
if (_flutterVersion!.isNotEmpty) 'FLUTTER_VERSION': _flutterVersion!,
},
includeParentEnvironment: true,
);
}
/// Writes out the given sample to an output file in the [_tempDirectory] and
/// returns the output file.
File _writeSample(Sample sample) {
Future<File> _writeSample(Sample sample) async {
// Generate the snippet.
final String sampleId = _createNameFromSource('sample', sample.start.filename, sample.start.line);
final String inputName = '$sampleId.input';
@ -374,11 +478,14 @@ class SampleChecker {
final List<String> args = <String>[
'--output=${outputFile.absolute.path}',
'--input=${inputFile.absolute.path}',
// Formatting the output will fail on analysis errors, and we want it to fail
// here, not there.
'--no-format-output',
...sample.args,
];
if (verbose)
print('Generating sample for ${sample.start.filename}:${sample.start.line}');
final ProcessResult process = _runSnippetsScript(args);
final ProcessResult process = await _runSnippetsScript(args);
if (verbose)
stderr.write('${process.stderr}');
if (process.exitCode != 0) {
@ -394,12 +501,12 @@ class SampleChecker {
/// Extracts the samples from the Dart files in [files], writes them
/// to disk, and adds them to the appropriate [sectionMap] or [sampleMap].
void _extractSamples(
Future<void> _extractSamples(
List<File> files, {
required Map<String, Section> sectionMap,
required Map<String, Sample> sampleMap,
bool silent = false,
}) {
}) async {
final List<Section> sections = <Section>[];
final List<Sample> samples = <Sample>[];
int dartpadCount = 0;
@ -545,13 +652,19 @@ class SampleChecker {
if (sectionMap != null)
sectionMap[path] = section;
}
final TaskQueue<File> sampleQueue = TaskQueue<File>();
for (final Sample sample in samples) {
final File snippetFile = _writeSample(sample);
final Future<File> futureFile = sampleQueue.add(() => _writeSample(sample));
if (sampleMap != null) {
sample.contents = snippetFile.readAsLinesSync();
sampleQueue.add(() async {
final File snippetFile = await futureFile;
sample.contents = await snippetFile.readAsLines();
sampleMap[snippetFile.absolute.path] = sample;
return futureFile;
});
}
}
await sampleQueue.tasksComplete;
}
/// Helper to process arguments given as a (possibly quoted) string.
@ -596,7 +709,9 @@ class SampleChecker {
/// Creates the configuration files necessary for the analyzer to consider
/// the temporary directory a package, and sets which lint rules to enforce.
void _createConfigurationFiles(Directory directory) {
final File pubSpec = File(path.join(directory.path, 'pubspec.yaml'))..createSync(recursive: true);
final File pubSpec = File(path.join(directory.path, 'pubspec.yaml'));
if (!pubSpec.existsSync()) {
pubSpec.createSync(recursive: true);
pubSpec.writeAsStringSync('''
name: analyze_sample_code
@ -611,10 +726,12 @@ dependencies:
dev_dependencies:
flutter_lints: ^1.0.3
''');
}
// Import the analysis options from the Flutter root.
final File analysisOptions = File(path.join(directory.path, 'analysis_options.yaml'));
if (!analysisOptions.existsSync()) {
analysisOptions.createSync(recursive: true);
analysisOptions.writeAsStringSync('''
include: package:flutter_lints/flutter.yaml
@ -624,6 +741,7 @@ linter:
avoid_print: false
''');
}
}
/// Writes out a sample section to the disk and returns the file.
File _writeSection(Section section) {
@ -641,7 +759,7 @@ linter:
}
/// Invokes the analyzer on the given [directory] and returns the stdout.
List<String> _runAnalyzer(Directory directory, {bool silent = true}) {
int _runAnalyzer(Directory directory, {bool silent = true, required List<String> output}) {
if (!silent)
print('Starting analysis of code samples.');
_createConfigurationFiles(directory);
@ -654,7 +772,8 @@ linter:
final List<String> stdout = result.stdout.toString().trim().split('\n');
// Remove output from building the flutter tool.
stderr.removeWhere((String line) {
return line.startsWith('Building flutter tool...');
return line.startsWith('Building flutter tool...')
|| line.startsWith('Waiting for another flutter command to release the startup lock...');
});
// Check out the stderr to see if the analyzer had it's own issues.
if (stderr.isNotEmpty && stderr.first.contains(RegExp(r' issues? found\. \(ran in '))) {
@ -672,19 +791,21 @@ linter:
if (stdout.isNotEmpty && stdout.first.startsWith('Running "flutter pub get" in ')) {
stdout.removeAt(0);
}
_exitCode = result.exitCode;
return stdout;
output.addAll(stdout);
return result.exitCode;
}
/// Starts the analysis phase of checking the samples by invoking the analyzer
/// and parsing its output to create a map of filename to [AnalysisError]s.
Map<String, List<AnalysisError>> _analyze(
AnalysisResult _analyze(
Directory directory,
Map<String, Section> sections,
Map<String, Sample> samples, {
bool silent = false,
}) {
final List<String> errors = _runAnalyzer(directory, silent: silent);
final List<String> errors = <String>[];
int exitCode = _runAnalyzer(directory, silent: silent, output: errors);
final Map<String, List<AnalysisError>> analysisErrors = <String, List<AnalysisError>>{};
void addAnalysisError(File file, AnalysisError error) {
if (analysisErrors.containsKey(file.path)) {
@ -825,15 +946,15 @@ linter:
);
}
}
if (_exitCode == 1 && analysisErrors.isEmpty && !unknownAnalyzerErrors) {
_exitCode = 0;
if (exitCode == 1 && analysisErrors.isEmpty && !unknownAnalyzerErrors) {
exitCode = 0;
}
if (_exitCode == 0) {
if (exitCode == 0) {
if (!silent)
print('No analysis errors in samples!');
assert(analysisErrors.isEmpty);
}
return analysisErrors;
return AnalysisResult(exitCode, analysisErrors);
}
/// Process one block of sample code (the part inside of "```" markers).
@ -1070,17 +1191,17 @@ Future<void> _runInteractive({
}
print('Starting up in interactive mode on ${path.relative(filePath, from: _flutterRoot)} ...');
void analyze(SampleChecker checker, File file) {
Future<void> analyze(SampleChecker checker, File file) async {
final Map<String, Section> sections = <String, Section>{};
final Map<String, Sample> snippets = <String, Sample>{};
checker._extractSamples(<File>[file], silent: true, sectionMap: sections, sampleMap: snippets);
final Map<String, List<AnalysisError>> errors = checker._analyze(checker._tempDirectory, sections, snippets, silent: true);
await checker._extractSamples(<File>[file], silent: true, sectionMap: sections, sampleMap: snippets);
final AnalysisResult analysisResult = checker._analyze(checker._tempDirectory, sections, snippets, silent: true);
stderr.writeln('\u001B[2J\u001B[H'); // Clears the old results from the terminal.
if (errors.isNotEmpty) {
for (final String filePath in errors.keys) {
errors[filePath]!.forEach(stderr.writeln);
if (analysisResult.errors.isNotEmpty) {
for (final String filePath in analysisResult.errors.keys) {
analysisResult.errors[filePath]!.forEach(stderr.writeln);
}
stderr.writeln('\nFound ${errors.length} errors.');
stderr.writeln('\nFound ${analysisResult.errors.length} errors.');
} else {
stderr.writeln('\nNo issues found.');
}
@ -1088,7 +1209,7 @@ Future<void> _runInteractive({
final SampleChecker checker = SampleChecker(flutterPackage, tempDirectory: tempDir)
.._createConfigurationFiles(tempDir);
analyze(checker, file);
await analyze(checker, file);
print('Type "q" to quit, or "r" to delete temp dir and manually reload.');

View File

@ -13,14 +13,26 @@ function script_location() {
script_location="$(readlink "$script_location")"
[[ "$script_location" != /* ]] && script_location="$DIR/$script_location"
done
echo "$(cd -P "$(dirname "$script_location")" >/dev/null && pwd)"
cd -P "$(dirname "$script_location")" >/dev/null && pwd
}
function generate_docs() {
# Install and activate dartdoc.
# NOTE: When updating to a new dartdoc version, please also update
# `dartdoc_options.yaml` to include newly introduced error and warning types.
"$PUB" global activate dartdoc 1.0.0
"$PUB" global activate dartdoc 1.0.2
# Install and activate the snippets tool, which resides in the
# assets-for-api-docs repo:
# https://github.com/flutter/assets-for-api-docs/tree/master/packages/snippets
# >>> If you update this version, also update it in dev/bots/analyze_sample_code.dart <<<
"$PUB" global activate snippets 0.2.2
# Install and activate the snippets tool, which resides in the
# assets-for-api-docs repo:
# https://github.com/flutter/assets-for-api-docs/tree/master/packages/snippets
# >>> If you update this version, also update it in dev/bots/analyze_sample_code.dart <<<
"$PUB" global activate snippets 0.2.1
# This script generates a unified doc set, and creates
# a custom index.html, placing everything into dev/docs/doc.
@ -96,9 +108,9 @@ function move_offline_into_place() {
mv flutter.docs.zip doc/offline/flutter.docs.zip
du -sh doc/offline/flutter.docs.zip
if [[ "$LUCI_BRANCH" == "stable" ]]; then
echo -e "<entry>\n <version>${FLUTTER_VERSION}</version>\n <url>https://api.flutter.dev/offline/flutter.docset.tar.gz</url>\n</entry>" > doc/offline/flutter.xml
echo -e "<entry>\n <version>${FLUTTER_VERSION_STRING}</version>\n <url>https://api.flutter.dev/offline/flutter.docset.tar.gz</url>\n</entry>" > doc/offline/flutter.xml
else
echo -e "<entry>\n <version>${FLUTTER_VERSION}</version>\n <url>https://master-api.flutter.dev/offline/flutter.docset.tar.gz</url>\n</entry>" > doc/offline/flutter.xml
echo -e "<entry>\n <version>${FLUTTER_VERSION_STRING}</version>\n <url>https://master-api.flutter.dev/offline/flutter.docset.tar.gz</url>\n</entry>" > doc/offline/flutter.xml
fi
mv flutter.docset.tar.gz doc/offline/flutter.docset.tar.gz
du -sh doc/offline/flutter.docset.tar.gz
@ -125,9 +137,11 @@ DART="$DART_BIN/dart"
PUB="$DART_BIN/pub"
export PATH="$FLUTTER_BIN:$DART_BIN:$PATH"
# Make sure dart is installed by invoking flutter to download it.
"$FLUTTER" --version
FLUTTER_VERSION=$(cat "$FLUTTER_ROOT/version")
# Make sure dart is installed by invoking Flutter to download it.
# This also creates the 'version' file.
FLUTTER_VERSION=$("$FLUTTER" --version --machine)
export FLUTTER_VERSION
FLUTTER_VERSION_STRING=$(cat "$FLUTTER_ROOT/version")
# If the pub cache directory exists in the root, then use that.
FLUTTER_PUB_CACHE="$FLUTTER_ROOT/.pub-cache"

View File

@ -746,7 +746,6 @@ Future<void> _runFrameworkTests() async {
print('${green}Running package tests$reset for directories other than packages/flutter');
await _pubRunTest(path.join(flutterRoot, 'dev', 'bots'));
await _pubRunTest(path.join(flutterRoot, 'dev', 'devicelab'), ensurePrecompiledTool: false); // See https://github.com/flutter/flutter/issues/86209
await _pubRunTest(path.join(flutterRoot, 'dev', 'snippets'));
// TODO(fujino): Move this to its own test shard
await _pubRunTest(path.join(flutterRoot, 'dev', 'conductor'), forceSingleCore: true);
await _runFlutterTest(path.join(flutterRoot, 'dev', 'integration_tests', 'android_semantics_testing'));

View File

@ -11,11 +11,13 @@ library dart.ui;
/// Annotation used by Flutter's Dart compiler to indicate that an
/// [Object.toString] override should not be replaced with a supercall.
///
/// {@tool sample}
/// {@tool sample --template=stateless_widget_material}
/// A sample if using keepToString to prevent replacement by a supercall.
///
/// ```dart
/// class MyStringBuffer {
/// error;
///
/// StringBuffer _buffer = StringBuffer();
///
/// @keepToString

View File

@ -20,10 +20,9 @@ void main() {
<String>['analyze_sample_code.dart', '--no-include-dart-ui', 'test/analyze-sample-code-test-input'],
);
final List<String> stdoutLines = process.stdout.toString().split('\n');
final List<String> stderrLines = process.stderr.toString().split('\n')
..removeWhere((String line) => line.startsWith('Analyzer output:') || line.startsWith('Building flutter tool...'));
final List<String> stderrLines = process.stderr.toString().split('\n');
expect(process.exitCode, isNot(equals(0)));
expect(stderrLines, <String>[
expect(stderrLines, containsAll(<String>[
'In sample starting at dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:125: child: Text(title),',
">>> error: The final variable 'title' can't be read because it is potentially unassigned at this point (read_potentially_unassigned_final)",
'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:30:9: new Opacity(',
@ -38,32 +37,35 @@ void main() {
">>> error: A value of type 'Null' can't be assigned to a variable of type 'int' (invalid_assignment)",
'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:120:24: const SizedBox(),',
'>>> error: Unexpected comma at end of sample code. (missing_identifier)',
'',
'Found 2 sample code errors.',
''
]);
expect(stdoutLines, <String>[
]));
expect(stdoutLines, containsAll(<String>[
'Found 9 snippet code blocks, 0 sample code sections, and 2 dartpad sections.',
'Starting analysis of code samples.',
'',
]);
]));
});
test('Analyzes dart:ui code', () {
final ProcessResult process = Process.runSync(
'../../bin/cache/dart-sdk/bin/dart',
<String>[
'analyze_sample_code.dart',
'--dart-ui-location',
'test/analyze-sample-code-test-dart-ui',
'--dart-ui-location=test/analyze-sample-code-test-dart-ui',
'test/analyze-sample-code-test-input',
],
);
final List<String> stdoutLines = process.stdout.toString().split('\n');
final List<String> stderrLines = process.stderr.toString().split('\n');
expect(process.exitCode, isNot(equals(0)));
expect(stdoutLines, equals(<String>[
expect(stderrLines, containsAll(<String>[
'In sample starting at dev/bots/test/analyze-sample-code-test-dart-ui/ui.dart:15:class MyStatelessWidget extends StatelessWidget {',
">>> error: Missing concrete implementation of 'StatelessWidget.build' (non_abstract_class_inherits_abstract_member)",
'In sample starting at dev/bots/test/analyze-sample-code-test-dart-ui/ui.dart:15:class MyStringBuffer {',
">>> error: Classes can't be declared inside other classes (class_in_class)",
]));
expect(stdoutLines, containsAll(<String>[
// There is one sample code section in the test's dummy dart:ui code.
'Found 9 snippet code blocks, 1 sample code sections, and 2 dartpad sections.',
'',
'Starting analysis of code samples.',
]));
});
}

View File

@ -8,18 +8,6 @@
<i class="material-icons copy-image">link</i>
</a>
</div>
<div class="snippet-buttons">
<script>var visibleSnippet{{serial}} = "longSnippet{{serial}}";</script>
<button id="longSnippet{{serial}}Button"
onclick="visibleSnippet{{serial}} = showSnippet('longSnippet{{serial}}', visibleSnippet{{serial}});"
selected>
Interactive App
</button>
<button id="shortSnippet{{serial}}Button"
onclick="visibleSnippet{{serial}} = showSnippet('shortSnippet{{serial}}', visibleSnippet{{serial}});">
Sample code
</button>
</div>
<div class="snippet-container">
<div class="snippet" id="longSnippet{{serial}}">
{{description}}
@ -28,15 +16,5 @@
<span class="snippet-create-command">flutter create --sample={{id}} mysample</span>
</div>
</div>
<div class="snippet" id="shortSnippet{{serial}}" hidden>
{{description}}
<div class="copyable-container">
<button class="copy-button-overlay copy-button" title="Copy to clipboard"
onclick="copyTextToClipboard(visibleSnippet{{serial}});">
<i class="material-icons copy-image">assignment</i>
</button>
<pre class="language-{{language}}"><code class="language-{{language}}">{{code}}</code></pre>
</div>
</div>
</div>
{@end-inject-html}

View File

@ -8,9 +8,6 @@
<i class="material-icons copy-image">link</i>
</a>
</div>
<div class="snippet-buttons">
<button id="shortSnippet{{serial}}Button" selected>Sample</button>
</div>
<div class="snippet-container">
<div class="snippet">{{description}}
<div class="copyable-container">

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -35,5 +35,5 @@ class MyStatefulWidget extends StatefulWidget {
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -36,5 +36,5 @@ class MyStatefulWidget extends StatefulWidget {
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -39,5 +39,5 @@ class MyStatefulWidget extends StatefulWidget {
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -37,5 +37,5 @@ class MyStatefulWidget extends StatefulWidget {
/// This is the private State class that goes with MyStatefulWidget.
/// AnimationControllers can be created with `vsync: this` because of TickerProviderStateMixin.
class _MyStatefulWidgetState extends State<MyStatefulWidget> with TickerProviderStateMixin {
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -36,5 +36,5 @@ class MyStatefulWidget extends StatefulWidget {
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -37,5 +37,5 @@ class MyStatefulWidget extends StatefulWidget {
/// This is the private State class that goes with MyStatefulWidget.
/// AnimationControllers can be created with `vsync: this` because of TickerProviderStateMixin.
class _MyStatefulWidgetState extends State<MyStatefulWidget> with TickerProviderStateMixin {
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -39,5 +39,5 @@ class MyStatefulWidget extends StatefulWidget {
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -41,5 +41,5 @@ class MyStatefulWidget extends StatefulWidget {
/// This is the private State class that goes with MyStatefulWidget.
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -36,5 +36,5 @@ class MyStatefulWidget extends StatefulWidget {
/// This is the private State class that goes with MyStatefulWidget.
/// AnimationControllers can be created with `vsync: this` because of TickerProviderStateMixin.
class _MyStatefulWidgetState extends State<MyStatefulWidget> with TickerProviderStateMixin {
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -30,5 +30,5 @@ class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
@override
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -32,5 +32,5 @@ class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
@override
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -35,5 +35,5 @@ class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
@override
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -32,5 +32,5 @@ class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
@override
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -32,5 +32,5 @@ class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
@override
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -32,5 +32,5 @@ class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
@override
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -35,5 +35,5 @@ class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
@override
{{code}}
{{code}}
}

View File

@ -1,5 +1,5 @@
/// Flutter code sample for {{element}}
// Flutter code sample for {{element}}
//
{{description}}
{{code-dartImports}}
@ -37,5 +37,5 @@ class MyStatelessWidget extends StatelessWidget {
const MyStatelessWidget({Key? key}) : super(key: key);
@override
{{code}}
{{code}}
}

View File

@ -1,80 +0,0 @@
// 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:io' hide Platform;
import 'package:meta/meta.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.
sample,
/// Produces a nicely formatted sample code, but no application.
snippet,
}
/// 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 {
Configuration({required this.flutterRoot}) : assert(flutterRoot != null);
final Directory flutterRoot;
/// This is the configuration directory for the snippets system, containing
/// the skeletons and templates.
@visibleForTesting
Directory get configDirectory {
_configPath ??= Directory(
path.canonicalize(path.join(flutterRoot.absolute.path, 'dev', 'snippets', 'config')));
return _configPath!;
}
// Nullable so that we can use it as a lazy cache.
Directory? _configPath;
/// This is where the snippets themselves will be written, in order to be
/// uploaded to the docs site.
Directory get outputDirectory {
_docsDirectory ??= Directory(
path.canonicalize(path.join(flutterRoot.absolute.path, 'dev', 'docs', 'doc', 'snippets')));
return _docsDirectory!;
}
// Nullable so that we can use it as a lazy cache.
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 => Directory(path.join(configDirectory.path,'skeletons'));
/// The directory containing the code templates that can be referenced by the
/// dartdoc.
Directory get templatesDirectory => Directory(path.join(configDirectory.path, 'templates'));
/// Gets the skeleton file to use for the given [SnippetType] and DartPad preference.
File getHtmlSkeletonFile(SnippetType type, {bool showDartPad = false}) {
assert(!showDartPad || type == SnippetType.sample,
'Only application snippets work with dartpad.');
final String filename =
'${showDartPad ? 'dartpad-' : ''}${getEnumName(type)}.html';
return File(path.join(skeletonsDirectory.path, filename));
}
}

View File

@ -1,241 +0,0 @@
// 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:io' show exit, stderr, stdout, File, ProcessResult;
import 'package:args/args.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import 'configuration.dart';
import 'snippets.dart';
const String _kSerialOption = 'serial';
const String _kElementOption = 'element';
const String _kHelpOption = 'help';
const String _kInputOption = 'input';
const String _kLibraryOption = 'library';
const String _kOutputOption = 'output';
const String _kPackageOption = 'package';
const String _kTemplateOption = 'template';
const String _kTypeOption = 'type';
const String _kShowDartPad = 'dartpad';
class GitStatusFailed implements Exception {
GitStatusFailed(this.gitResult);
final ProcessResult gitResult;
@override
String toString() => 'git status exited with a non-zero exit code: ${gitResult.exitCode}:\n${gitResult.stderr}\n${gitResult.stdout}';
}
/// Get the name of the channel these docs are from.
///
/// First check env variable LUCI_BRANCH, then refer to the currently
/// checked out git branch.
String getChannelName({
@visibleForTesting
Platform platform = const LocalPlatform(),
@visibleForTesting
ProcessManager processManager = const LocalProcessManager(),
}) {
final String? envReleaseChannel = platform.environment['LUCI_BRANCH']?.trim();
if (<String>['master', 'stable'].contains(envReleaseChannel)) {
return envReleaseChannel!;
}
final RegExp gitBranchRegexp = RegExp(r'^## (?<branch>.*)');
final ProcessResult gitResult = processManager.runSync(<String>['git', 'status', '-b', '--porcelain'],
environment: <String, String>{
'GIT_TRACE': '2',
'GIT_TRACE_SETUP': '2'
},
includeParentEnvironment: true
);
if (gitResult.exitCode != 0) {
throw GitStatusFailed(gitResult);
}
final RegExpMatch? gitBranchMatch = gitBranchRegexp.firstMatch((gitResult.stdout as String).trim().split('\n').first);
return gitBranchMatch == null ? '<unknown>' : gitBranchMatch.namedGroup('branch')!.split('...').first;
}
// This is a hack to workaround the fact that git status inexplicably fails
// (random non-zero error code) about 2% of the time.
String getChannelNameWithRetries() {
int retryCount = 0;
while(retryCount < 2) {
try {
return getChannelName();
} on GitStatusFailed catch (e) {
retryCount += 1;
stderr.write('git status failed, retrying ($retryCount)\nError report:\n$e');
}
}
return getChannelName();
}
/// 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.sample),
allowed: snippetTypes,
allowedHelp: <String, String>{
getEnumName(SnippetType.sample):
'Produce a code sample application complete with embedding the sample in an '
'application template.',
getEnumName(SnippetType.snippet):
'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(
_kOutputOption,
defaultsTo: null,
help: 'The output path for the generated sample application. Overrides '
'the naming generated by the --package/--library/--element arguments. '
'Metadata will be written alongside in a .json file. '
'The basename of this argument is used as the ID',
);
parser.addOption(
_kInputOption,
defaultsTo: environment['INPUT'],
help: 'The input file containing the sample code to inject.',
);
parser.addOption(
_kPackageOption,
defaultsTo: environment['PACKAGE_NAME'],
help: 'The name of the package that this sample belongs to.',
);
parser.addOption(
_kLibraryOption,
defaultsTo: environment['LIBRARY_NAME'],
help: 'The name of the library that this sample belongs to.',
);
parser.addOption(
_kElementOption,
defaultsTo: environment['ELEMENT_NAME'],
help: 'The name of the element that this sample belongs to.',
);
parser.addOption(
_kSerialOption,
defaultsTo: environment['INVOCATION_INDEX'],
help: 'A unique serial number for this snippet tool invocation.',
);
parser.addFlag(
_kHelpOption,
defaultsTo: false,
negatable: false,
help: 'Prints help documentation for this command',
);
parser.addFlag(
_kShowDartPad,
defaultsTo: false,
negatable: false,
help: "Indicates whether DartPad should be included in the sample's "
'final HTML output. This flag only applies when the type parameter is '
'"sample".',
);
final ArgResults args = parser.parse(argList);
if (args[_kHelpOption] as bool) {
stderr.writeln(parser.usage);
exit(0);
}
final SnippetType snippetType = SnippetType.values
.firstWhere((SnippetType type) => getEnumName(type) == args[_kTypeOption]);
if (args[_kShowDartPad] == true && snippetType != SnippetType.sample) {
errorExit('${args[_kTypeOption]} was selected, but the --dartpad flag is only valid '
'for application sample code.');
}
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'] as String);
if (!input.existsSync()) {
errorExit('The input file ${input.path} does not exist.');
}
String? template;
if (snippetType == SnippetType.sample) {
final String templateArg = args[_kTemplateOption] as String;
if (templateArg == null || templateArg.isEmpty) {
stderr.writeln(parser.usage);
errorExit('The --$_kTemplateOption option must be specified on the command '
'line for application samples.');
}
template = templateArg.replaceAll(RegExp(r'.tmpl$'), '');
}
final String packageName = args[_kPackageOption] as String? ?? '';
final String libraryName = args[_kLibraryOption] as String? ?? '';
final String elementName = args[_kElementOption] as String? ?? '';
final String serial = args[_kSerialOption] as String? ?? '';
final List<String> id = <String>[];
if (args[_kOutputOption] != null) {
id.add(path.basename(path.basenameWithoutExtension(args[_kOutputOption] as String)));
} else {
if (packageName.isNotEmpty && packageName != 'flutter') {
id.add(packageName);
}
if (libraryName.isNotEmpty) {
id.add(libraryName);
}
if (elementName.isNotEmpty) {
id.add(elementName);
}
if (serial.isNotEmpty) {
id.add(serial);
}
if (id.isEmpty) {
errorExit('Unable to determine ID. At least one of --$_kPackageOption, '
'--$_kLibraryOption, --$_kElementOption, -$_kSerialOption, or the environment variables '
'PACKAGE_NAME, LIBRARY_NAME, ELEMENT_NAME, or INVOCATION_INDEX must be non-empty.');
}
}
final SnippetGenerator generator = SnippetGenerator();
stdout.write(generator.generate(
input,
snippetType,
showDartPad: args[_kShowDartPad] as bool,
template: template,
output: args[_kOutputOption] != null ? File(args[_kOutputOption] as String) : null,
metadata: <String, Object?>{
'sourcePath': environment['SOURCE_PATH'],
'sourceLine': environment['SOURCE_LINE'] != null
? int.tryParse(environment['SOURCE_LINE']!)
: null,
'id': id.join('.'),
'channel': getChannelNameWithRetries(),
'serial': serial,
'package': packageName,
'library': libraryName,
'element': elementName,
},
));
exit(0);
}

View File

@ -1,350 +0,0 @@
// 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: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, {this.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.sample] 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.sample] snippets.
String _interpolateTemplate(List<_ComponentTuple> injections, String template, Map<String, Object?> metadata) {
final RegExp moustacheRegExp = RegExp('{{([^}]+)}}');
final String interpolated = 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 int componentIndex = injections
.indexWhere((_ComponentTuple tuple) => tuple.name == match[1]);
if (componentIndex == -1) {
return (metadata[match[1]] ?? '').toString();
}
return injections[componentIndex].mergedContent;
}
}).trim();
return _sortImports(interpolated);
}
String _sortImports(String code) {
final List<String> result = <String>[];
final List<String> lines = code.split('\n');
final List<String> imports = <String>[];
int firstImport = -1;
int lineNumber =0;
for (final String line in lines) {
if (RegExp(r'^\s*import').matchAsPrefix(line) != null) {
if (firstImport < 0) {
firstImport = lineNumber;
}
imports.add(line);
} else {
result.add(line);
}
lineNumber++;
}
if (firstImport > 0) {
final List<String> dartImports = <String>[];
final List<String> packageImports = <String>[];
final List<String> otherImports = <String>[];
final RegExp typeRegExp = RegExp(r'''import\s+['"](?<type>\w+)''');
imports.sort();
for (final String importLine in imports) {
final RegExpMatch? match = typeRegExp.firstMatch(importLine);
if (match != null) {
switch (match.namedGroup('type')) {
case 'dart':
dartImports.add(importLine);
break;
case 'package':
packageImports.add(importLine);
break;
default:
otherImports.add(importLine);
break;
}
} else {
otherImports.add(importLine);
}
}
// Insert the sorted sections in the proper order, with a blank line in between
// sections.
result.insertAll(firstImport, <String>[
...dartImports,
if (dartImports.isNotEmpty) '',
...packageImports,
if (packageImports.isNotEmpty) '',
...otherImports,
]);
}
return result.join('\n');
}
/// 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.sample] 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 = 'dart';
for (final _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>'
: '';
// DartPad only supports stable or master as valid channels. Use master
// if not on stable so that local runs will work (although they will
// still take their sample code from the master docs server).
final String channel = metadata['channel'] == 'stable' ? 'stable' : 'master';
final Map<String, String> substitutions = <String, String>{
'description': description,
'code': htmlEscape.convert(result.join('\n')),
'language': language,
'serial': '',
'id': metadata['id']! as String,
'channel': channel,
'element': (metadata['element'] ?? '') as String,
'app': '',
};
if (type == SnippetType.sample) {
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*```(?<language>[-\w]+|[-\w]+ (?<section>[-\w]+))?\s*$');
for (final String line in input.split('\n')) {
final RegExpMatch? match = codeStartEnd.firstMatch(line);
if (match != null) { // If we saw the start or end of a code block
inCodeBlock = !inCodeBlock;
if (match.namedGroup('language') != null) {
language = match[1];
assert(language != null);
language = language!;
if (match.namedGroup('section') != null) {
components.add(_ComponentTuple('code-${match.namedGroup('section')}', <String>[], language: language));
} else {
components.add(_ComponentTuple('code', <String>[], language: language));
}
} else {
language = null;
}
continue;
}
if (!inCodeBlock) {
description.add(line);
} else {
assert(language != null);
if (components.isNotEmpty) {
components.last.contents.add(line);
}
}
}
return <_ComponentTuple>[
_ComponentTuple('description', description),
...components,
];
}
String _loadFileAsUtf8(File file) {
return file.readAsStringSync(encoding: utf8);
}
String _addLineNumbers(String app) {
final StringBuffer buffer = StringBuffer();
int count = 0;
for (final 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.sample] or a [SnippetType.snippet].
///
/// [showDartPad] indicates whether DartPad should be shown where possible.
/// Currently, this value only has an effect if [type] is
/// [SnippetType.sample], 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.sample], 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.sample].
String generate(
File input,
SnippetType type, {
bool showDartPad = false,
String? template,
File? output,
required Map<String, Object?> metadata,
}) {
assert(template != null || type != SnippetType.sample);
assert(metadata['id'] != null);
assert(!showDartPad || type == SnippetType.sample, 'Only application samples work with dartpad.');
final List<_ComponentTuple> snippetData = _parseInput(_loadFileAsUtf8(input));
switch (type) {
case SnippetType.sample:
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']! as String);
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 int descriptionIndex = snippetData.indexWhere(
(_ComponentTuple data) => data.name == 'description');
final String descriptionString = descriptionIndex == -1 ? '' : snippetData[descriptionIndex].mergedContent;
metadata.addAll(<String, Object>{
'file': path.basename(outputFile.path),
'description': descriptionString,
});
metadataFile.writeAsStringSync(jsonEncoder.convert(metadata));
break;
case SnippetType.snippet:
break;
}
final String skeleton =
_loadFileAsUtf8(configuration.getHtmlSkeletonFile(type, showDartPad: showDartPad));
return _interpolateSkeleton(type, snippetData, skeleton, metadata);
}
}

View File

@ -1,99 +0,0 @@
name: snippets
version: 0.1.0
description: A code snippet dartdoc extension for Flutter API docs.
homepage: https://github.com/flutter/flutter
environment:
sdk: ">=2.12.1 <3.0.0"
dartdoc:
# Exclude this package from the hosted API docs (Ironically...).
nodoc: true
dependencies:
args: 2.2.0
dart_style: 2.0.3
meta: 1.7.0
platform: 3.0.0
process: 4.2.3
_fe_analyzer_shared: 23.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
analyzer: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
async: 2.8.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
charcode: 1.3.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
cli_util: 0.3.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
collection: 1.15.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
convert: 3.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
crypto: 3.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
file: 6.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
glob: 2.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
package_config: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
path: 1.8.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pedantic: 1.11.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pub_semver: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
source_span: 1.8.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
string_scanner: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
term_glyph: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
typed_data: 1.3.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
watcher: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
yaml: 3.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
dev_dependencies:
test: 1.17.10
boolean_selector: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
coverage: 1.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
frontend_server_client: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http_multi_server: 3.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http_parser: 4.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
io: 1.0.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
js: 0.6.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
logging: 1.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
matcher: 0.12.10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
mime: 1.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
node_preamble: 2.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pool: 1.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf: 1.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf_packages_handler: 3.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf_static: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf_web_socket: 1.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
source_map_stack_trace: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
source_maps: 0.10.10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
stack_trace: 1.10.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
stream_channel: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
test_api: 0.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
test_core: 0.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
vm_service: 7.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
web_socket_channel: 2.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
webkit_inspection_protocol: 1.0.0 # 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: c51d

View File

@ -1,52 +0,0 @@
// 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:io';
import 'package:snippets/configuration.dart';
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
void main() {
group('Configuration', () {
late Configuration config;
setUp(() {
config = Configuration(flutterRoot: Directory('/flutter sdk'));
});
test('config directory is correct', () async {
expect(config.configDirectory.path,
matches(RegExp(r'[/\\]flutter sdk[/\\]dev[/\\]snippets[/\\]config')));
});
test('output directory is correct', () async {
expect(config.outputDirectory.path,
matches(RegExp(r'[/\\]flutter sdk[/\\]dev[/\\]docs[/\\]doc[/\\]snippets')));
});
test('skeleton directory is correct', () async {
expect(config.skeletonsDirectory.path,
matches(RegExp(r'[/\\]flutter sdk[/\\]dev[/\\]snippets[/\\]config[/\\]skeletons')));
});
test('templates directory is correct', () async {
expect(config.templatesDirectory.path,
matches(RegExp(r'[/\\]flutter sdk[/\\]dev[/\\]snippets[/\\]config[/\\]templates')));
});
test('html skeleton file for sample is correct', () async {
expect(
config.getHtmlSkeletonFile(SnippetType.snippet).path,
matches(RegExp(
r'[/\\]flutter sdk[/\\]dev[/\\]snippets[/\\]config[/\\]skeletons[/\\]snippet.html')));
});
test('html skeleton file for app with no dartpad is correct', () async {
expect(
config.getHtmlSkeletonFile(SnippetType.sample).path,
matches(RegExp(
r'[/\\]flutter sdk[/\\]dev[/\\]snippets[/\\]config[/\\]skeletons[/\\]sample.html')));
});
test('html skeleton file for app with dartpad is correct', () async {
expect(
config.getHtmlSkeletonFile(SnippetType.sample, showDartPad: true).path,
matches(RegExp(
r'[/\\]flutter sdk[/\\]dev[/\\]snippets[/\\]config[/\\]skeletons[/\\]dartpad-sample.html')));
});
});
}

View File

@ -1,334 +0,0 @@
// 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' show Directory, File, Process, ProcessResult, ProcessSignal, ProcessStartMode, SystemEncoding;
import 'package:path/path.dart' as path;
import 'package:platform/platform.dart';
import 'package:process/process.dart';
import 'package:snippets/configuration.dart';
import 'package:snippets/main.dart' show getChannelName;
import 'package:snippets/snippets.dart';
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
void main() {
group('Generator', () {
late Configuration configuration;
late SnippetGenerator generator;
late Directory tmpDir;
late File template;
setUp(() {
tmpDir = Directory.systemTemp.createTempSync('flutter_snippets_test.');
configuration = Configuration(flutterRoot: Directory(path.join(
tmpDir.absolute.path, 'flutter')));
configuration.createOutputDirectory();
configuration.templatesDirectory.createSync(recursive: true);
configuration.skeletonsDirectory.createSync(recursive: true);
template = File(path.join(configuration.templatesDirectory.path, 'template.tmpl'));
template.writeAsStringSync('''
// Flutter code sample for {{element}}
{{description}}
import 'package:flutter/material.dart';
import '../foo.dart';
{{code-imports}}
{{code-my-preamble}}
main() {
{{code}}
}
''');
configuration.getHtmlSkeletonFile(SnippetType.sample).writeAsStringSync('''
<div>HTML Bits</div>
{{description}}
<pre>{{code}}</pre>
<pre>{{app}}</pre>
<div>More HTML Bits</div>
''');
configuration.getHtmlSkeletonFile(SnippetType.snippet).writeAsStringSync('''
<div>HTML Bits</div>
{{description}}
<pre>{{code}}</pre>
<div>More HTML Bits</div>
''');
configuration.getHtmlSkeletonFile(SnippetType.sample, showDartPad: true).writeAsStringSync('''
<div>HTML Bits (DartPad-style)</div>
<iframe class="snippet-dartpad" src="https://dartpad.dev/embed-flutter.html?split=60&run=true&sample_id={{id}}&sample_channel={{channel}}"></iframe>
<div>More HTML Bits</div>
''');
generator = SnippetGenerator(configuration: configuration);
});
tearDown(() {
tmpDir.deleteSync(recursive: true);
});
test('generates samples', () async {
final File inputFile = File(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
..createSync(recursive: true)
..writeAsStringSync(r'''
A description of the snippet.
On several lines.
```dart imports
import 'dart:ui';
```
```my-dart_language my-preamble
const String name = 'snippet';
```
```dart
void main() {
print('The actual $name.');
}
```
''');
final File outputFile = File(path.join(tmpDir.absolute.path, 'snippet_out.txt'));
final String html = generator.generate(
inputFile,
SnippetType.sample,
template: 'template',
metadata: <String, Object>{
'id': 'id',
'channel': 'stable',
'element': 'MyElement',
},
output: outputFile,
);
expect(html, contains('<div>HTML Bits</div>'));
expect(html, contains('<div>More HTML Bits</div>'));
expect(html, contains(r'print(&#39;The actual $name.&#39;);'));
expect(html, contains('A description of the snippet.\n'));
expect(html, isNot(contains('sample_channel=stable')));
expect(
html,
contains('&#47;&#47; A description of the snippet.\n'
'&#47;&#47;\n'
'&#47;&#47; On several lines.\n'));
expect(html, contains('void main() {'));
final String outputContents = outputFile.readAsStringSync();
expect(outputContents, contains('// Flutter code sample for MyElement'));
expect(outputContents, contains('A description of the snippet.'));
expect(outputContents, contains('void main() {'));
expect(outputContents, contains("const String name = 'snippet';"));
final List<String> lines = outputContents.split('\n');
final int dartUiLine = lines.indexOf("import 'dart:ui';");
final int materialLine = lines.indexOf("import 'package:flutter/material.dart';");
final int otherLine = lines.indexOf("import '../foo.dart';");
expect(dartUiLine, lessThan(materialLine));
expect(materialLine, lessThan(otherLine));
});
test('generates snippets', () async {
final File inputFile = File(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
..createSync(recursive: true)
..writeAsStringSync(r'''
A description of the snippet.
On several lines.
```code
void main() {
print('The actual $name.');
}
```
''');
final String html = generator.generate(
inputFile,
SnippetType.snippet,
metadata: <String, Object>{'id': 'id'},
);
expect(html, contains('<div>HTML Bits</div>'));
expect(html, contains('<div>More HTML Bits</div>'));
expect(html, contains(r' print(&#39;The actual $name.&#39;);'));
expect(html, contains('<div class="snippet-description">{@end-inject-html}A description of the snippet.\n\n'
'On several lines.{@inject-html}</div>\n'));
expect(html, contains('main() {'));
});
test('generates dartpad samples', () async {
final File inputFile = File(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
..createSync(recursive: true)
..writeAsStringSync(r'''
A description of the snippet.
On several lines.
```code
void main() {
print('The actual $name.');
}
```
''');
final String html = generator.generate(
inputFile,
SnippetType.sample,
showDartPad: true,
template: 'template',
metadata: <String, Object>{'id': 'id', 'channel': 'stable'},
);
expect(html, contains('<div>HTML Bits (DartPad-style)</div>'));
expect(html, contains('<div>More HTML Bits</div>'));
expect(html, contains('<iframe class="snippet-dartpad" src="https://dartpad.dev/embed-flutter.html?split=60&run=true&sample_id=id&sample_channel=stable"></iframe>'));
});
test('generates sample metadata', () async {
final File inputFile = File(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
..createSync(recursive: true)
..writeAsStringSync(r'''
A description of the snippet.
On several lines.
```code
void main() {
print('The actual $name.');
}
```
''');
final File outputFile = File(path.join(tmpDir.absolute.path, 'snippet_out.dart'));
final File expectedMetadataFile = File(path.join(tmpDir.absolute.path, 'snippet_out.json'));
generator.generate(
inputFile,
SnippetType.sample,
template: 'template',
output: outputFile,
metadata: <String, Object>{'sourcePath': 'some/path.dart', 'id': 'id', 'channel': 'stable'},
);
expect(expectedMetadataFile.existsSync(), isTrue);
final Map<String, dynamic> json = jsonDecode(expectedMetadataFile.readAsStringSync()) as Map<String, dynamic>;
expect(json['id'], equals('id'));
expect(json['channel'], equals('stable'));
expect(json['file'], equals('snippet_out.dart'));
expect(json['description'], equals('A description of the snippet.\n\nOn several lines.'));
// Ensure any passed metadata is included in the output JSON too.
expect(json['sourcePath'], equals('some/path.dart'));
});
});
group('getChannelName()', () {
test('does not call git if LUCI_BRANCH env var provided', () {
const String branch = 'stable';
final FakePlatform platform = FakePlatform(
environment: <String, String>{'LUCI_BRANCH': branch},
);
final FakeProcessManager processManager = FakeProcessManager(<FakeCommand>[]);
expect(
getChannelName(
platform: platform,
processManager: processManager,
),
branch,
);
expect(processManager.hasRemainingExpectations, false);
});
test('calls git if LUCI_BRANCH env var is not provided', () {
const String branch = 'stable';
final FakePlatform platform = FakePlatform(
environment: <String, String>{},
);
final ProcessResult result = ProcessResult(0, 0, '## $branch...refs/heads/master', '');
final FakeProcessManager processManager = FakeProcessManager(
<FakeCommand>[FakeCommand('git status -b --porcelain', result)],
);
expect(
getChannelName(
platform: platform,
processManager: processManager,
),
branch,
);
expect(processManager.hasRemainingExpectations, false);
});
});
}
const SystemEncoding systemEncoding = SystemEncoding();
class FakeCommand {
FakeCommand(this.command, [ProcessResult? result]) : _result = result;
final String command;
final ProcessResult? _result;
ProcessResult get result => _result ?? ProcessResult(0, 0, '', '');
}
class FakeProcessManager implements ProcessManager {
FakeProcessManager(this.remainingExpectations);
final List<FakeCommand> remainingExpectations;
@override
bool canRun(dynamic command, {String? workingDirectory}) => true;
@override
Future<Process> start(
List<Object> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
ProcessStartMode mode = ProcessStartMode.normal,
}) {
throw Exception('not implemented');
}
@override
Future<ProcessResult> run(
List<Object> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
Encoding stdoutEncoding = systemEncoding,
Encoding stderrEncoding = systemEncoding,
}) {
throw Exception('not implemented');
}
@override
ProcessResult runSync(
List<Object> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
Encoding stdoutEncoding = systemEncoding,
Encoding stderrEncoding = systemEncoding,
}) {
if (remainingExpectations.isEmpty) {
fail(
'Called FakeProcessManager with $command when no further commands were expected!',
);
}
final FakeCommand expectedCommand = remainingExpectations.removeAt(0);
final String expectedName = expectedCommand.command;
final String actualName = command.join(' ');
if (expectedName != actualName) {
fail(
'FakeProcessManager expected the command $expectedName but received $actualName',
);
}
return expectedCommand.result;
}
bool get hasRemainingExpectations => remainingExpectations.isNotEmpty;
@override
bool killPid(int pid, [ProcessSignal signal = ProcessSignal.sigterm]) {
throw Exception('not implemented');
}
}

View File

@ -118,14 +118,25 @@ Future<void> main(List<String> arguments) async {
'dartdoc',
];
// Verify which version of dartdoc we're using.
final ProcessResult result = Process.runSync(
// Verify which version of snippets and dartdoc we're using.
final ProcessResult snippetsResult = Process.runSync(
pubExecutable,
<String>[...dartdocBaseArgs, '--version'],
<String>[
'global',
'list',
],
workingDirectory: kDocsRoot,
environment: pubEnvironment,
stdoutEncoding: utf8,
);
print('\n${result.stdout}flutter version: $version\n');
print('');
final Iterable<RegExpMatch> versionMatches = RegExp(r'^(?<name>snippets|dartdoc) (?<version>[^\s]+)', multiLine: true)
.allMatches(snippetsResult.stdout as String);
for (final RegExpMatch match in versionMatches) {
print('${match.namedGroup('name')} version: ${match.namedGroup('version')}');
}
print('flutter version: $version\n');
// Dartdoc warnings and errors in these packages are considered fatal.
// All packages owned by flutter should be in the list.

View File

@ -32,7 +32,7 @@ import 'theme.dart';
/// // Uncomment to change the background color
/// // backgroundColor: CupertinoColors.systemPink,
/// navigationBar: const CupertinoNavigationBar(
/// middle: const Text('Sample Code'),
/// middle: Text('Sample Code'),
/// ),
/// child: ListView(
/// children: <Widget>[

View File

@ -1410,7 +1410,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
/// child: SizedBox(
/// height: 20,
/// child: Center(
/// child: const Text('Scroll to see the SliverAppBar in effect.'),
/// child: Text('Scroll to see the SliverAppBar in effect.'),
/// ),
/// ),
/// ),

View File

@ -382,7 +382,7 @@ class DataCell {
/// child: DataTable(
/// columns: const <DataColumn>[
/// DataColumn(
/// label: const Text('Number'),
/// label: Text('Number'),
/// ),
/// ],
/// rows: List<DataRow>.generate(

View File

@ -2426,7 +2426,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
/// hintText: 'Hint Text',
/// helperText: 'Helper Text',
/// counterText: '0 characters',
/// border: const OutlineInputBorder(),
/// border: OutlineInputBorder(),
/// ),
/// );
/// }

View File

@ -281,7 +281,7 @@ class _LinearProgressIndicatorPainter extends CustomPainter {
/// children: <Widget>[
/// const Text(
/// 'Linear progress indicator with a fixed color',
/// style: const TextStyle(fontSize: 20),
/// style: TextStyle(fontSize: 20),
/// ),
/// LinearProgressIndicator(
/// value: controller.value,

View File

@ -339,10 +339,10 @@ abstract class Gradient {
/// Widget build(BuildContext context) {
/// return Container(
/// decoration: const BoxDecoration(
/// gradient: const LinearGradient(
/// gradient: LinearGradient(
/// begin: Alignment.topLeft,
/// end: Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
/// colors: const <Color>[Color(0xffee0000), Color(0xffeeee00)], // red to yellow
/// colors: <Color>[Color(0xffee0000), Color(0xffeeee00)], // red to yellow
/// tileMode: TileMode.repeated, // repeats the gradient over the canvas
/// ),
/// ),

View File

@ -444,7 +444,7 @@ typedef AutocompleteOptionToString<T extends Object> = String Function(T option)
/// ),
/// ElevatedButton(
/// onPressed: () {
/// FocusScope.of(context).requestFocus(new FocusNode());
/// FocusScope.of(context).unfocus();
/// if (!_formKey.currentState!.validate()) {
/// return;
/// }

View File

@ -901,7 +901,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// return const SizedBox(
/// width: 200,
/// child: Padding(
/// padding: const EdgeInsets.all(8.0),
/// padding: EdgeInsets.all(8.0),
/// child: TextField(
/// decoration: InputDecoration(border: OutlineInputBorder()),
/// ),

View File

@ -4433,7 +4433,7 @@ typedef ErrorWidgetBuilder = Widget Function(FlutterErrorDetails details);
/// alignment: Alignment.center,
/// child: const Text(
/// 'Error!',
/// style: const TextStyle(color: Colors.yellow),
/// style: TextStyle(color: Colors.yellow),
/// textDirection: TextDirection.ltr,
/// ),
/// );