// TODO(stuartmorgan): Remove this. See https://github.com/flutter/flutter/issues/174722. // ignore_for_file: public_member_api_docs import '../mustache.dart' as m; import 'lambda_context.dart'; import 'node.dart'; import 'template.dart'; import 'template_exception.dart'; const Object noSuchProperty = Object(); final RegExp _integerTag = RegExp(r'^[0-9]+$'); class Renderer extends Visitor { Renderer( this.sink, List stack, this.lenient, this.htmlEscapeValues, this.partialResolver, this.templateName, this.indent, this.source, ) : _stack = List.from(stack); Renderer.partial(Renderer ctx, Template partial, String indent) : this( ctx.sink, ctx._stack, ctx.lenient, ctx.htmlEscapeValues, ctx.partialResolver, ctx.templateName, ctx.indent + indent, partial.source, ); Renderer.subtree(Renderer ctx, StringSink sink) : this( sink, ctx._stack, ctx.lenient, ctx.htmlEscapeValues, ctx.partialResolver, ctx.templateName, ctx.indent, ctx.source, ); Renderer.lambda(Renderer ctx, String source, String indent, StringSink sink) : this( sink, ctx._stack, ctx.lenient, ctx.htmlEscapeValues, ctx.partialResolver, ctx.templateName, ctx.indent + indent, source, ); final StringSink sink; final List _stack; final bool lenient; final bool htmlEscapeValues; final m.PartialResolver? partialResolver; final String? templateName; final String indent; final String source; void push(Object? value) => _stack.add(value); Object? pop() => _stack.removeLast(); void write(Object output) => sink.write(output.toString()); void render(List nodes) { if (indent == '') { for (final Node n in nodes) { n.accept(this); } } else if (nodes.isNotEmpty) { // Special case to make sure there is not an extra indent after the last // line in the partial file. write(indent); nodes.take(nodes.length - 1).forEach((Node n) => n.accept(this)); final Node node = nodes.last; if (node is TextNode) { visitText(node, lastNode: true); } else { node.accept(this); } } } @override void visitText(TextNode node, {bool lastNode = false}) { if (node.text == '') { return; } if (indent == '') { write(node.text); } else if (lastNode && node.text.runes.last == _NEWLINE) { // Don't indent after the last line in a template. final String s = node.text.substring(0, node.text.length - 1); write(s.replaceAll('\n', '\n$indent')); write('\n'); } else { write(node.text.replaceAll('\n', '\n$indent')); } } @override void visitVariable(VariableNode node) { Object? value = resolveValue(node.name); if (value is Function) { final LambdaContext context = LambdaContext(node, this); final Function valueFunction = value; // TODO(stuartmorgan): Add function typing in a way that doesn't break // backward compatibility. // ignore: avoid_dynamic_calls value = valueFunction(context); context.close(); } if (value == noSuchProperty) { if (!lenient) { throw error('Value was missing for variable tag: ${node.name}.', node); } } else { final String valueString = (value == null) ? '' : value.toString(); final String output = !node.escape || !htmlEscapeValues ? valueString : _htmlEscape(valueString); write(output); } } @override void visitSection(SectionNode node) { if (node.inverse) { _renderInvSection(node); } else { _renderSection(node); } } void _renderSection(SectionNode node) { final Object? value = resolveValue(node.name); if (value == null) { // Do nothing. } else if (value is Iterable) { for (final Object? v in value) { _renderWithValue(node, v); } } else if (value is Map) { _renderWithValue(node, value); } else if (value == true) { _renderWithValue(node, value); } else if (value == false) { // Do nothing. } else if (value == noSuchProperty) { if (!lenient) { throw error('Value was missing for section tag: ${node.name}.', node); } } else if (value is Function) { final LambdaContext context = LambdaContext(node, this); // TODO(stuartmorgan): Add function typing in a way that doesn't break // backward compatibility. // ignore: avoid_dynamic_calls final Object? output = value(context); context.close(); if (output != null) { write(output); } } else { // Assume the value might have accessible member values via mirrors. _renderWithValue(node, value); } } void _renderInvSection(SectionNode node) { final Object? value = resolveValue(node.name); if (value == null) { _renderWithValue(node, null); } else if ((value is Iterable && value.isEmpty) || value == false) { _renderWithValue(node, node.name); } else if (value == true || value is Map || value is Iterable) { // Do nothing. } else if (value == noSuchProperty) { if (lenient) { _renderWithValue(node, null); } else { throw error( 'Value was missing for inverse section: ${node.name}.', node, ); } } else if (value is Function) { // Do nothing. // TODO(stuartmorgan): Determine whether this should be an error in // strict mode (per comment in initial source import). } else if (lenient) { // We consider all other values as 'true' in lenient mode. Since this // is an inverted section, we do nothing. } else { throw error( 'Invalid value type for inverse section, ' 'section: ${node.name}, ' 'type: ${value.runtimeType}.', node, ); } } void _renderWithValue(SectionNode node, Object? value) { push(value); node.visitChildren(this); pop(); } @override void visitPartial(PartialNode node) { final String partialName = node.name; final Template? template = partialResolver == null ? null : (partialResolver!(partialName) as Template?); if (template != null) { final Renderer renderer = Renderer.partial(this, template, node.indent); final List nodes = getTemplateNodes(template); renderer.render(nodes); } else if (lenient) { // do nothing } else { throw error('Partial not found: $partialName.', node); } } // Walks up the stack looking for the variable. // Handles dotted names of the form "a.b.c". Object? resolveValue(String name) { if (name == '.') { return _stack.last; } final List parts = name.split('.'); Object? object = noSuchProperty; for (final Object? o in _stack.reversed) { object = _getNamedProperty(o, parts[0]); if (object != noSuchProperty) { break; } } for (int i = 1; i < parts.length; i++) { if (object == noSuchProperty) { return noSuchProperty; } object = _getNamedProperty(object, parts[i]); } return object; } // Returns the property of the given object by name. For a map, // which contains the key name, this is object[name]. For other // objects, this is object.name or object.name(). If no property // by the given name exists, this method returns noSuchProperty. Object? _getNamedProperty(dynamic object, String name) { if (object is Map && object.containsKey(name)) { return object[name]; } if (object is List && _integerTag.hasMatch(name)) { final int index = int.parse(name); if (object.length > index) { return object[index]; } } return noSuchProperty; } m.TemplateException error(String message, Node node) => TemplateException(message, templateName, source, node.start); static const Map _htmlEscapeMap = { _AMP: '&', _LT: '<', _GT: '>', _QUOTE: '"', _APOS: ''', _FORWARD_SLASH: '/', }; String _htmlEscape(String s) { final StringBuffer buffer = StringBuffer(); int startIndex = 0; int i = 0; for (final int c in s.runes) { if (c == _AMP || c == _LT || c == _GT || c == _QUOTE || c == _APOS || c == _FORWARD_SLASH) { buffer.write(s.substring(startIndex, i)); buffer.write(_htmlEscapeMap[c]); startIndex = i + 1; } i++; } buffer.write(s.substring(startIndex)); return buffer.toString(); } } const int _AMP = 38; const int _LT = 60; const int _GT = 62; const int _QUOTE = 34; const int _APOS = 39; const int _FORWARD_SLASH = 47; const int _NEWLINE = 10;