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);
}
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();
}
class SnippetGenerator {
SnippetGenerator({Configuration configuration})
: configuration = configuration ??
Configuration(flutterRoot: Directory(Platform.environment['FLUTTER_ROOT']
?? path.canonicalize(path.join(path.dirname(path.fromUri(Platform.script)), '..', '..', '..')))) {
this.configuration.createOutputDirectory();
}
final Configuration configuration;
static const JsonEncoder jsonEncoder = JsonEncoder.withIndent(' ');
static DartFormatter formatter = DartFormatter(pageWidth: 80, fixes: StyleFix.all);
File getOutputFile(String id) => File(path.join(configuration.outputDirectory.path, '$id.dart'));
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;
}
String interpolateTemplate(List<_ComponentTuple> injections, String template) {
final RegExp moustacheRegExp = RegExp('{{([^}]+)}}');
return template.replaceAllMapped(moustacheRegExp, (Match match) {
if (match[1] == 'description') {
final List<String> description = injections
.firstWhere((_ComponentTuple tuple) => tuple.name == match[1])
.contents
.map<String>((String line) => '// $line')
.toList();
while (description.isNotEmpty && description.last == '// ') {
description.removeLast();
}
while (description.isNotEmpty && description.first == '// ') {
description.removeAt(0);
}
return description.join('\n').trim();
} else {
return injections
.firstWhere((_ComponentTuple tuple) => tuple.name == match[1], orElse: () => null)
?.mergedContent ?? '';
}
}).trim();
}
String interpolateSkeleton(SnippetType type, List<_ComponentTuple> injections, String skeleton, Map<String, Object> metadata) {
final List<String> result = <String>[];
const HtmlEscape htmlEscape = HtmlEscape();
String language;
for (_ComponentTuple injection in injections) {
if (!injection.name.startsWith('code')) {
continue;
}
result.addAll(injection.contents);
if (injection.language.isNotEmpty) {
language = injection.language;
}
result.addAll(<String>['', '// ...', '']);
}
if (result.length > 3) {
result.removeRange(result.length - 3, result.length);
}
String description = injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'description').mergedContent;
description = description.trim().isNotEmpty
? '<div class="snippet-description">{@end-inject-html}$description{@inject-html}</div>'
: '';
final Map<String, String> substitutions = <String, String>{
'description': description,
'code': htmlEscape.convert(result.join('\n')),
'language': language ?? 'dart',
'serial': '',
'id': metadata['id'],
'app': '',
};
if (type == SnippetType.application) {
substitutions
..['serial'] = metadata['serial']?.toString() ?? '0'
..['app'] = htmlEscape.convert(injections.firstWhere((_ComponentTuple tuple) => tuple.name == 'app').mergedContent);
}
return skeleton.replaceAllMapped(RegExp('{{(${substitutions.keys.join('|')})}}'), (Match match) {
return substitutions[match[1]];
});
}
List<_ComponentTuple> parseInput(String input) {
bool inCodeBlock = false;
input = input.trim();
final List<String> description = <String>[];
final List<_ComponentTuple> components = <_ComponentTuple>[];
String language;
final RegExp codeStartEnd = RegExp(r'^\s*```([-\w]+|[-\w]+ ([-\w]+))?\s*$');
for (String line in input.split('\n')) {
final Match match = codeStartEnd.firstMatch(line);
if (match != null) {
inCodeBlock = !inCodeBlock;
if (match[1] != null) {
language = match[1];
if (match[2] != null) {
components.add(_ComponentTuple('code-${match[2]}', <String>[], language: language));
} else {
components.add(_ComponentTuple('code', <String>[], language: language));
}
} else {
language = null;
}
continue;
}
if (!inCodeBlock) {
description.add(line);
} else {
assert(language != null);
components.last.contents.add(line);
}
}
return <_ComponentTuple>[
_ComponentTuple('description', description),
...components,
];
}
String _loadFileAsUtf8(File file) {
return file.readAsStringSync(encoding: Encoding.getByName('utf-8'));
}
String _addLineNumbers(String app) {
final StringBuffer buffer = StringBuffer();
int count = 0;
for (String line in app.split('\n')) {
count++;
buffer.writeln('${count.toString().padLeft(5, ' ')}: $line');
}
return buffer.toString();
}
String generate(
File input,
SnippetType type, {
String template,
File output,
@required Map<String, Object> metadata,
}) {
assert(template != null || type != SnippetType.application);
assert(metadata != null && metadata['id'] != null);
assert(input != null);
final List<_ComponentTuple> snippetData = parseInput(_loadFileAsUtf8(input));
switch (type) {
case SnippetType.application:
final Directory templatesDir = configuration.templatesDirectory;
if (templatesDir == null) {
stderr.writeln('Unable to find the templates directory.');
exit(1);
}
final File templateFile = getTemplatePath(template, templatesDir: templatesDir);
if (templateFile == null) {
stderr.writeln(
'The template $template was not found in the templates directory ${templatesDir.path}');
exit(1);
}
final String templateContents = _loadFileAsUtf8(templateFile);
String app = interpolateTemplate(snippetData, templateContents);
try {
app = formatter.format(app);
} on FormatterException catch (exception) {
stderr.write('Code to format:\n${_addLineNumbers(app)}\n');
errorExit('Unable to format snippet app template: $exception');
}
snippetData.add(_ComponentTuple('app', app.split('\n')));
final File outputFile = output ?? getOutputFile(metadata['id']);
stderr.writeln('Writing to ${outputFile.absolute.path}');
outputFile.writeAsStringSync(app);
final File metadataFile = File(path.join(path.dirname(outputFile.path),
'${path.basenameWithoutExtension(outputFile.path)}.json'));
stderr.writeln('Writing metadata to ${metadataFile.absolute.path}');
final _ComponentTuple description = snippetData.firstWhere(
(_ComponentTuple data) => data.name == 'description',
orElse: () => null,
);
metadata ??= <String, Object>{};
metadata.addAll(<String, Object>{
'id': metadata['id'],
'file': path.basename(outputFile.path),
'description': description?.mergedContent,
});
metadataFile.writeAsStringSync(jsonEncoder.convert(metadata));
break;
case SnippetType.sample:
break;
}
final String skeleton = _loadFileAsUtf8(configuration.getHtmlSkeletonFile(type));
return interpolateSkeleton(type, snippetData, skeleton, metadata);
}
}