| library mustache.node; |
| |
| import 'lambda_context.dart'; |
| import 'render_context.dart'; |
| import 'template.dart'; |
| |
| void renderWithContext(RenderContext ctx, List<Node> nodes) { |
| if (ctx.indent == null || ctx.indent == '') { |
| nodes.forEach((n) => n.render(ctx)); |
| |
| } else if (nodes.isNotEmpty) { |
| // Special case to make sure there is not an extra indent after the last |
| // line in the partial file. |
| |
| ctx.write(ctx.indent); |
| |
| for (var n in nodes.take(nodes.length - 1)) { |
| n.render(ctx); |
| } |
| |
| var node = nodes.last; |
| if (node is TextNode) { |
| node.render(ctx, lastNode: true); |
| } else { |
| node.render(ctx); |
| } |
| } |
| } |
| |
| abstract class Node { |
| |
| Node(this.start, this.end); |
| |
| void render(RenderContext renderer); |
| |
| // The offset of the start of the token in the file. Unless this is a section |
| // or inverse section, then this stores the start of the content of the |
| // section. |
| final int start; |
| final int end; |
| } |
| |
| |
| class TextNode extends Node { |
| |
| TextNode(this.text, int start, int end) : super(start, end); |
| |
| final String text; |
| |
| String toString() => '(TextNode "$_debugText" $start $end)'; |
| |
| String get _debugText { |
| var t = text.replaceAll('\n', '\\n'); |
| return t.length < 50 ? t : t.substring(0, 48) + '...'; |
| } |
| |
| // Remove me. |
| // Only used for testing. |
| bool operator ==(o) => o is TextNode |
| && text == o.text |
| && start == o.start |
| && end == o.end; |
| |
| // TODO hashcode. import quiver. |
| |
| void render(RenderContext ctx, {lastNode: false}) { |
| if (text == '') return; |
| if (ctx.indent == null || ctx.indent == '') { |
| ctx.write(text); |
| } else if (lastNode && text.runes.last == _NEWLINE) { |
| // Don't indent after the last line in a template. |
| var s = text.substring(0, text.length - 1); |
| ctx.write(s.replaceAll('\n', '\n${ctx.indent}')); |
| ctx.write('\n'); |
| } else { |
| ctx.write(text.replaceAll('\n', '\n${ctx.indent}')); |
| } |
| } |
| } |
| |
| class VariableNode extends Node { |
| |
| VariableNode(this.name, int start, int end, {this.escape: true}) |
| : super(start, end); |
| |
| final String name; |
| final bool escape; |
| |
| String toString() => '(VariableNode "$name" escape: $escape $start $end)'; |
| |
| // Only used for testing. |
| bool operator ==(o) => o is VariableNode |
| && name == o.name |
| && escape == o.escape |
| && start == o.start |
| && end == o.end; |
| |
| // TODO hashcode. import quiver. |
| |
| |
| void render(RenderContext ctx) { |
| |
| var value = ctx.resolveValue(name); |
| |
| if (value is Function) { |
| var context = new LambdaContext(this, ctx, isSection: false); |
| value = value(context); |
| context.close(); |
| } |
| |
| if (value == noSuchProperty) { |
| if (!ctx.lenient) |
| throw ctx.error('Value was missing for variable tag: ${name}.', this); |
| } else { |
| var valueString = (value == null) ? '' : value.toString(); |
| var output = !escape || !ctx.htmlEscapeValues |
| ? valueString |
| : _htmlEscape(valueString); |
| if (output != null) ctx.write(output); |
| } |
| } |
| |
| static const Map<String,String> _htmlEscapeMap = const { |
| _AMP: '&', |
| _LT: '<', |
| _GT: '>', |
| _QUOTE: '"', |
| _APOS: ''', |
| _FORWARD_SLASH: '/' |
| }; |
| |
| String _htmlEscape(String s) { |
| |
| var buffer = new StringBuffer(); |
| int startIndex = 0; |
| int i = 0; |
| for (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(); |
| } |
| } |
| |
| |
| class SectionNode extends Node { |
| |
| SectionNode(this.name, int start, int end, this.delimiters, |
| {this.inverse: false}) |
| : contentStart = end, |
| super(start, end); |
| |
| final String name; |
| final String delimiters; |
| final bool inverse; |
| final int contentStart; |
| int contentEnd; // Set in parser when close tag is parsed. |
| final List<Node> children = <Node>[]; |
| |
| toString() => '(SectionNode $name inverse: $inverse $start $end)'; |
| |
| // TODO Only used for testing. |
| //FIXME use deepequals in test for comparing children. |
| //Perhaps shift all of this == code into test. |
| bool operator ==(o) => o is SectionNode |
| && name == o.name |
| && delimiters == o.delimiters |
| && inverse == o.inverse |
| && start == o.start |
| && end == o.end; |
| |
| // TODO hashcode. import quiver. |
| |
| |
| //TODO can probably combine Inv and Normal to shorten. |
| void render(RenderContext ctx) => inverse |
| ? renderInv(ctx) |
| : renderNormal(ctx); |
| |
| void renderNormal(RenderContext renderer) { |
| var value = renderer.resolveValue(name); |
| |
| if (value == null) { |
| // Do nothing. |
| |
| } else if (value is Iterable) { |
| value.forEach((v) => _renderWithValue(renderer, v)); |
| |
| } else if (value is Map) { |
| _renderWithValue(renderer, value); |
| |
| } else if (value == true) { |
| _renderWithValue(renderer, value); |
| |
| } else if (value == false) { |
| // Do nothing. |
| |
| } else if (value == noSuchProperty) { |
| if (!renderer.lenient) |
| throw renderer.error('Value was missing for section tag: ${name}.', this); |
| |
| } else if (value is Function) { |
| var context = new LambdaContext(this, renderer, isSection: true); |
| var output = value(context); |
| context.close(); |
| if (output != null) renderer.write(output); |
| |
| } else { |
| throw renderer.error('Invalid value type for section, ' |
| 'section: ${name}, ' |
| 'type: ${value.runtimeType}.', this); |
| } |
| } |
| |
| void renderInv(RenderContext ctx) { |
| var value = ctx.resolveValue(name); |
| |
| if (value == null) { |
| _renderWithValue(ctx, null); |
| |
| } else if ((value is Iterable && value.isEmpty) || value == false) { |
| _renderWithValue(ctx, name); |
| |
| } else if (value == true || value is Map || value is Iterable) { |
| // Do nothing. |
| |
| } else if (value == noSuchProperty) { |
| if (ctx.lenient) { |
| _renderWithValue(ctx, null); |
| } else { |
| throw ctx.error('Value was missing for inverse section: ${name}.', this); |
| } |
| |
| } else if (value is Function) { |
| // Do nothing. |
| //TODO in strict mode should this be an error? |
| |
| } else { |
| throw ctx.error( |
| 'Invalid value type for inverse section, ' |
| 'section: $name, ' |
| 'type: ${value.runtimeType}.', this); |
| } |
| } |
| |
| void _renderWithValue(RenderContext ctx, value) { |
| ctx.push(value); |
| children.forEach((n) => n.render(ctx)); |
| ctx.pop(); |
| } |
| } |
| |
| class PartialNode extends Node { |
| |
| PartialNode(this.name, int start, int end, this.indent) |
| : super(start, end); |
| |
| final String name; |
| |
| // Used to store the preceding whitespace before a partial tag, so that |
| // it's content can be correctly indented. |
| final String indent; |
| |
| toString() => '(PartialNode $name $start $end "$indent")'; |
| |
| //TODO move to test. |
| bool operator ==(o) => o is PartialNode |
| && name == o.name |
| && indent == o.indent; |
| |
| // TODO hashcode. import quiver. |
| |
| |
| void render(RenderContext ctx) { |
| var partialName = name; |
| Template template = ctx.partialResolver == null |
| ? null |
| : ctx.partialResolver(partialName); |
| if (template != null) { |
| var partialCtx = new RenderContext.partial(ctx, template, this.indent); |
| renderWithContext(partialCtx, template.getNodes()); |
| } else if (ctx.lenient) { |
| // do nothing |
| } else { |
| throw ctx.error('Partial not found: $partialName.', this); |
| } |
| } |
| } |
| |
| 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; |