|  | // 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.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('{{([^}]+)}}'); | 
|  | 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.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; | 
|  | 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 ?? 'dart', | 
|  | '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*```([-\w]+|[-\w]+ ([-\w]+))?\s*$'); | 
|  | for (final 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 (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 != null && metadata['id'] != null); | 
|  | assert(input != 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 _ComponentTuple description = snippetData.firstWhere( | 
|  | (_ComponentTuple data) => data.name == 'description', | 
|  | orElse: () => null, | 
|  | ); | 
|  | metadata.addAll(<String, Object>{ | 
|  | 'file': path.basename(outputFile.path), | 
|  | 'description': description?.mergedContent, | 
|  | }); | 
|  | 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); | 
|  | } | 
|  | } |