blob: 6528a5ae77e3d62df3053143b4a64ffb42d8d70d [file] [log] [blame]
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/dart/element/type_provider.dart';
import 'package:analyzer/dart/element/type_system.dart';
import 'package:build/build.dart';
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
import 'package:dartdoc/src/mustachio/annotations.dart';
import 'package:dartdoc/src/mustachio/parser.dart';
import 'package:dartdoc/src/mustachio/renderer_base.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
/// Compiles all templates specified in [specs] into a Dart library containing
/// a renderer for each template.
Future<String> compileTemplatesToRenderers(
Set<RendererSpec> specs,
Uri sourceUri,
BuildStep buildStep,
TypeProvider typeProvider,
TypeSystem typeSystem,
TemplateFormat format,
) async {
var buildData = _BuildData(buildStep, typeProvider, typeSystem, format);
var rendererFunctions = <Method>[];
for (var spec in specs) {
var templateUri = spec.standardTemplateUris[format];
if (templateUri == null) continue;
var templateAsset = templateUri.isAbsolute
? AssetId.resolve(templateUri)
: AssetId.resolve(templateUri, from: buildStep.inputId);
var compiler = await _AotCompiler._readAndParse(
spec.contextType,
spec.name,
templateAsset,
buildData,
);
rendererFunctions.addAll(await compiler._compileToRenderer());
}
var library = Library((b) {
b.body.addAll(rendererFunctions);
b.body.add(Extension((b) => b
..on = refer('StringBuffer')
..methods.add(Method((b) => b
..returns = refer('void')
..name = 'writeEscaped'
..requiredParameters.add(Parameter((b) => b
..type = refer('String')
..name = 'value'))
..body = refer('write').call([
refer('htmlEscape', 'dart:convert')
.property('convert')
.call([refer('value')])
]).statement))));
});
return DartFormatter().format('''
// GENERATED CODE. DO NOT EDIT.
//
// To change the contents of this library, make changes to the builder source
// files in the tool/mustachio/ directory.
// Sometimes we enter a new section which triggers creating a new variable, but
// the variable is not used; generally when the section is checking if a
// non-bool, non-Iterable field is non-null.
// ignore_for_file: unused_local_variable
// ignore_for_file: non_constant_identifier_names, unnecessary_string_escapes
${library.accept(DartEmitter.scoped(orderDirectives: true))}
''');
}
/// A class which compiles a single template file into a renderer Dart function,
/// and possible support functions for partial templates.
class _AotCompiler {
/// The template to be compiled.
final AssetId _templateAssetId;
/// The context type which is to be rendered into the compiled template.
final InterfaceType _contextType;
/// The name of the renderer, which is either a public name (for top-level
/// renderers specified in a `@Renderer` annotation), or a private name for
/// a partial.
final String _rendererName;
final _BuildData _buildData;
/// The parsed syntax tree of the template at [_templateAssetId].
final List<MustachioNode> _syntaxTree;
/// The set of compilers for all referenced partials.
final Set<_AotCompiler> _partialCompilers = {};
/// The current stack of context objects (as variable lookups).
final List<_VariableLookup> _contextStack;
/// A counter for naming partial render functions.
///
/// Incrementing the counter keeps names unique.
int _partialCounter = 0;
/// A counter for naming context variables.
///
/// Incrementing the counter keeps names unique.
int _contextNameCounter;
/// Reads the template at [templateAssetId] and parses it into a syntax tree,
/// returning an [_AotCompiler] with the necessary information to be able to
/// compile the template into a renderer.
static Future<_AotCompiler> _readAndParse(
InterfaceType contextType,
String rendererName,
AssetId templateAssetId,
_BuildData buildData, {
List<_VariableLookup> contextStack,
}) async {
var template = await buildData._buildStep.readAsString(templateAssetId);
var syntaxTree = MustachioParser(template, templateAssetId.uri).parse();
return _AotCompiler._(
contextType, rendererName, templateAssetId, syntaxTree, buildData,
contextStack: contextStack);
}
_AotCompiler._(
this._contextType,
this._rendererName,
this._templateAssetId,
this._syntaxTree,
this._buildData, {
List<_VariableLookup> contextStack,
}) : _contextStack = _rename(contextStack ?? []),
_contextNameCounter = contextStack?.length ?? 0;
/// Returns a copy of [original], replacing each variable's name with
/// `context0` through `contextN` for `N` variables.
///
/// This ensures that each renderer accepts a simple list of context objects
/// with predictable names.
static List<_VariableLookup> _rename(List<_VariableLookup> original) {
var result = <_VariableLookup>[];
var index = original.length - 1;
for (var variable in original) {
result.push(_VariableLookup(variable.type, 'context$index'));
index--;
}
return [...result.reversed];
}
Future<List<Method>> _compileToRenderer() async {
if (_contextStack.isEmpty) {
var contextName = 'context0';
var contextVariable = _VariableLookup(_contextType, contextName);
_contextStack.push(contextVariable);
_contextNameCounter++;
}
// Get the type parameters of _each_ of the context types in the stack,
// including their bounds, concatenate them, and wrap them in angle
// brackets.
// TODO(srawlins): This will produce erroneous code if any two context types
// have type parameters with the same name. Something like:
// _renderFoo_partial_bar_1<T, T>(Baz<T> context1, Foo<T> context0)
// Rename type parameters to some predictable collision-free naming scheme;
// the body of the function should not reference the type parameters, so
// this should be perfectly possible.
var typeParameters = <TypeReference>[];
for (var context in _contextStack) {
for (var typeParameter in context.type.element.typeParameters) {
if (typeParameter.bound == null) {
typeParameters
.add(TypeReference((b) => b..symbol = typeParameter.name));
} else {
var bound = typeParameter.bound.element;
var boundUri = await _elementUri(bound);
typeParameters.add(TypeReference((b) => b
..symbol = typeParameter.name
..bound = refer(bound.name, boundUri)));
}
}
}
var blockCompiler = _BlockCompiler(this, _contextStack);
await blockCompiler._compile(_syntaxTree);
var rendererBody = blockCompiler._buffer.toString();
var parameters = <Parameter>[];
for (var context in _contextStack) {
var contextElement = context.type.element;
var contextElementUri = await _elementUri(contextElement);
parameters.add(Parameter((b) => b
..type = TypeReference((b) => b
..symbol = contextElement.displayName
..url = contextElementUri
..types.addAll(
contextElement.typeParameters.map((tp) => refer(tp.name))))
..name = context.name));
}
var renderFunction = Method((b) => b
..returns = refer('String')
..name = _rendererName
..types.addAll(typeParameters)
..requiredParameters.addAll(parameters)
..body = Code('''
final buffer = StringBuffer();
$rendererBody
return buffer.toString();
'''));
return [
renderFunction,
for (var partialRenderer in _partialCompilers)
...(await partialRenderer._compileToRenderer()),
];
}
/// Returns the URI of [element] for use in generated import directives.
Future<String> _elementUri(Element element) async {
if (element.library.isInSdk) {
return element.library.source.uri.toString();
}
var typeAssetId =
await _buildData._buildStep.resolver.assetIdForElement(element.library);
if (typeAssetId.path.startsWith('lib/')) {
return typeAssetId.uri.toString();
} else {
var entryAssetId = await _buildData._buildStep.resolver
.assetIdForElement(await _buildData._buildStep.inputLibrary);
return p.relative(typeAssetId.path, from: p.dirname(entryAssetId.path));
}
}
}
/// A class which can compile a Mustache block of nodes into Dart source for a
/// renderer.
class _BlockCompiler {
final _AotCompiler _templateCompiler;
final List<_VariableLookup> _contextStack;
final _buffer = StringBuffer();
_BlockCompiler(this._templateCompiler, this._contextStack);
void write(String text) => _buffer.write(text);
void writeln(String text) => _buffer.writeln(text);
InterfaceType get contextType => _contextStack.first.type;
String get contextName => _contextStack.first.name;
TemplateFormat get format => _templateCompiler._buildData._format;
TypeProvider get typeProvider => _templateCompiler._buildData._typeProvider;
TypeSystem get typeSystem => _templateCompiler._buildData._typeSystem;
/// Generates a new name for a context variable. Each context variable going
/// up the stack needs to be accessible, so they each need a unique variable
/// name.
String getNewContextName() {
var newContextName = 'context${_templateCompiler._contextNameCounter}';
_templateCompiler._contextNameCounter++;
return newContextName;
}
/// The base name of a partial rendering function.
String get partialBaseName => '_${_templateCompiler._rendererName}_partial';
Future<void> _compile(List<MustachioNode> syntaxTree) async {
for (var node in syntaxTree) {
if (node is Text) {
_writeText(node.content);
} else if (node is Variable) {
var variableLookup = _lookUpGetter(node);
_writeGetter(variableLookup, escape: node.escape);
} else if (node is Section) {
await _compileSection(node);
} else if (node is Partial) {
await _compilePartial(node);
}
}
}
/// Compiles [node] into a renderer's Dart source.
Future<void> _compilePartial(Partial node) async {
var extension = format == TemplateFormat.html ? 'html' : 'md';
var path = node.key.split('/');
var fileName = path.removeLast();
path.add('_$fileName.$extension');
var partialAssetId = AssetId.resolve(Uri.parse(path.join('/')),
from: _templateCompiler._templateAssetId);
var partialRenderer = _templateCompiler._partialCompilers.firstWhere(
(p) => p._templateAssetId == partialAssetId,
orElse: () => null);
if (partialRenderer == null) {
var sanitizedKey = node.key.replaceAll('.', '_').replaceAll('/', '_');
var name = '${partialBaseName}_'
'${sanitizedKey}_'
'${_templateCompiler._partialCounter}';
partialRenderer = await _AotCompiler._readAndParse(
contextType, name, partialAssetId, _templateCompiler._buildData,
contextStack: _contextStack);
// Add this partial renderer; to be written later.
_templateCompiler._partialCompilers.add(partialRenderer);
_templateCompiler._partialCounter++;
}
// Call the partial's renderer function here; the definition of the renderer
// function is written later.
write('buffer.write(');
writeln('${partialRenderer._rendererName}(');
writeln(_contextStack.map((context) => context.name).join(','));
writeln('));');
}
/// Compiles [node] into a renderer's Dart source.
Future<void> _compileSection(Section node) async {
var variableLookup = _lookUpGetter(node);
if (variableLookup == null) {
throw MustachioResolutionError(node.keySpan.message(
"Failed to resolve '${node.key}' as a property on the context type:"
'$contextType'));
}
if (variableLookup.type.isDartCoreBool) {
// Conditional block.
await _compileConditionalSection(variableLookup, node.children,
invert: node.invert);
} else if (typeSystem.isAssignableTo(
variableLookup.type, typeProvider.iterableDynamicType)) {
// Repeated block.
await _compileRepeatedSection(variableLookup, node.children,
invert: node.invert);
} else {
// Use accessor value as context.
await _compileValueSection(variableLookup, node.children,
invert: node.invert);
}
}
/// Compiles a conditional section containing [block] into a renderer's Dart
/// source.
Future<void> _compileConditionalSection(
_VariableLookup variableLookup, List<MustachioNode> block,
{bool invert = false}) async {
var variableAccess = variableLookup.name;
if (invert) {
writeln('if ($variableAccess != true) {');
} else {
writeln('if ($variableAccess == true) {');
}
await _compile(block);
writeln('}');
}
/// Compiles a repeated section containing [block] into a renderer's Dart
/// source.
Future<void> _compileRepeatedSection(
_VariableLookup variableLookup, List<MustachioNode> block,
{bool invert = false}) async {
var variableAccess = variableLookup.name;
if (invert) {
writeln('if ($variableAccess?.isEmpty ?? true) {');
await _compile(block);
writeln('}');
} else {
var variableAccessResult = getNewContextName();
writeln('var $variableAccessResult = $variableAccess;');
var newContextName = getNewContextName();
writeln('if ($variableAccessResult != null) {');
writeln(' for (var $newContextName in $variableAccessResult) {');
// If [loopType] is something like `C<int>` where
// `class C<T> implements Queue<Future<T>>`, we need the [ClassElement]
// for [Iterable], and then use [DartType.asInstanceOf] to ultimately
// determine that the inner type of the loop is, for example,
// `Future<int>`.
var iterableElement = typeProvider.iterableElement;
var iterableType = variableLookup.type.asInstanceOf(iterableElement);
var innerContextType = iterableType.typeArguments.first;
var innerContext = _VariableLookup(innerContextType, newContextName);
_contextStack.push(innerContext);
await _compile(block);
_contextStack.pop();
writeln(' }');
writeln('}');
}
}
/// Compiles a value section containing [block] into a renderer's Dart source.
Future<void> _compileValueSection(
_VariableLookup variableLookup, List<MustachioNode> block,
{bool invert = false}) async {
var variableAccess = variableLookup.name;
if (invert) {
writeln('if ($variableAccess == null) {');
await _compile(block);
writeln('}');
} else {
var innerContextName = getNewContextName();
writeln('var $innerContextName = $variableAccess;');
writeln('if ($innerContextName != null) {');
var innerContext = _VariableLookup(variableLookup.type, innerContextName);
_contextStack.push(innerContext);
await _compile(block);
_contextStack.pop();
writeln('}');
}
}
/// Returns a valid [_VariableLookup] on a Mustache node, [node] by resolving
/// its key.
_VariableLookup _lookUpGetter(HasMultiNamedKey node) {
var key = node.key;
// '.' is an entirely special case.
if (key.length == 1 && key[0] == '.') {
return _VariableLookup(contextType, contextName);
}
var primaryName = key[0];
for (var context in _contextStack) {
var getter =
context.type.lookUpGetter2(primaryName, contextType.element.library);
if (getter == null) {
continue;
}
var type = getter.returnType;
var contextChain = '${context.name}.$primaryName';
var remainingNames = [...key.skip(1)];
for (var secondaryKey in remainingNames) {
getter = (type as InterfaceType)
.lookUpGetter2(secondaryKey, type.element.library);
if (getter == null) {
throw MustachioResolutionError(node.keySpan.message(
"Failed to resolve '$secondaryKey' on ${context.type} while "
'resolving $remainingNames as a property chain on any types in '
'the context chain: $contextChain, after first resolving '
"'$primaryName' to a property on $type"));
}
type = getter.returnType;
contextChain = '$contextChain.$secondaryKey';
}
return _VariableLookup(type, contextChain);
}
var contextTypes = [
for (var c in _contextStack) c.type,
];
throw MustachioResolutionError(node.keySpan
.message("Failed to resolve '$key' as a property on any types in the "
'context chain: $contextTypes'));
}
/// Writes [content] to the generated render functions as text, properly
/// handling newlines, quotes, and other special characters.
void _writeText(String content) {
if (content.isEmpty) return;
if (content == '\n') {
// Blank lines happen a lot; just indicate them as such.
writeln('buffer.writeln();');
} else {
content = content
.replaceAll(r'\', r'\\')
.replaceAll("'", r"\'")
.replaceAll(r'$', r'\$');
if (_multipleWhitespacePattern.hasMatch(content)) {
write("buffer.write('");
write(content.replaceAll('\n', '\\n'));
writeln("');");
} else {
if (content[0] == '\n') {
write('buffer.writeln();');
}
write("buffer.write('''");
write(content);
writeln("''');");
}
}
}
/// A pattern for a String containing only space and newlines, more than one.
static final RegExp _multipleWhitespacePattern = RegExp('^[ \\n]+\$');
/// Writes a call to [variableLookup] to the renderer.
///
/// The result is HTML-escaped if [escape] is true.
void _writeGetter(_VariableLookup variableLookup, {bool escape = true}) {
var variableAccess = variableLookup.name;
if (escape) {
writeln('buffer.writeEscaped($variableAccess.toString());');
} else {
writeln('buffer.write($variableAccess.toString());');
}
}
}
/// Various static build data to be used for each renderer, including specified
/// renderers and template renderers.
@immutable
class _BuildData {
final BuildStep _buildStep;
final TypeProvider _typeProvider;
final TypeSystem _typeSystem;
final TemplateFormat _format;
_BuildData(
this._buildStep, this._typeProvider, this._typeSystem, this._format);
}
/// Represents a variable lookup via property access chain [name] which returns
/// an object of type [type].
@immutable
class _VariableLookup {
final InterfaceType type;
final String name;
_VariableLookup(this.type, this.name);
}
extension<T> on List<T> {
void push(T value) => insert(0, value);
T pop() => removeAt(0);
}