From f1287f10768ad3920b5ced1033edc8496a142895 Mon Sep 17 00:00:00 2001 From: Olzhas Suleimen Date: Thu, 19 Sep 2024 19:12:16 +0500 Subject: [PATCH 1/7] 0.6.1-dev.3 --- CHANGELOG.md | 89 +++++++++--------- README.md | 8 +- lib/reflection.dart | 4 +- lib/src/compiler.dart | 11 ++- lib/src/context.dart | 90 +----------------- lib/src/defaults.dart | 5 +- lib/src/environment.dart | 87 ++++++----------- lib/src/filters.dart | 2 +- lib/src/namespace.dart | 50 +--------- lib/src/nodes.dart | 12 ++- lib/src/nodes/statements.dart | 1 + lib/src/optimizer.dart | 2 +- lib/src/parser.dart | 13 +-- lib/src/renderer.dart | 171 ++++++++++------------------------ lib/src/runtime.dart | 158 +++++++++++++++++++++++++++++++ lib/src/tests.dart | 1 + lib/src/utils.dart | 2 +- pubspec.yaml | 2 +- test/api_test.dart | 2 +- test/check.dart | 2 +- test/functions_test.dart | 2 +- 21 files changed, 322 insertions(+), 392 deletions(-) create mode 100644 lib/src/runtime.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 479d8a8..92b8ec3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ -## 0.6.1-dev.2 ([diff](https://github.com/ykmnkmi/jinja.dart/compare/88996f8..main)) -- added: +## 0.6.1-dev.3 ([diff](https://github.com/ykmnkmi/jinja.dart/compare/88996f8..main)) +- Added: - `UndefinedError` exception - `UndefinedFactory` typedef - `Environment`: @@ -7,44 +7,46 @@ - `undefined` field - `Template`: - `Template({undefined})` argument + - Filters: + - `null` (`none` alias) ## 0.6.0 ([diff](https://github.com/ykmnkmi/jinja.dart/compare/c12244e6..88996f8)) -- bump SDK version to 3.3.0. -- update dependencies. -- internal changes. +- Bump SDK version to 3.3.0. +- Update dependencies. +- Internal changes. - `chrome` platform tests. -- added: - - statements: +- Added: + - Statements: - `import` - `from` - `Template`: - `Template.fromNode({globals})` argument - `globals` field -- restored: - - conditional and variable `extends` statement variants - - choice, ignore missing and variable `include` statement variants -- changed: +- Restored: + - Conditional and variable `extends` statement variants + - Choice, ignore missing and variable `include` statement variants +- Changed: - `Environment`: - `Environment.lex()` return from `List` to `Iterable` - `Environment.scan(tokens)` argument type from `List` to `Iterable` -- removed: - - exceptions: +- Removed: + - Exceptions: - `FilterArgumentError` - `*args` and `**kwargs` support ## 0.5.0 -- minimal SDK version: 3.0.0. -- internal changes. -- added: +- Minimal SDK version: 3.0.0. +- Internal changes. +- Added: - `Template`: - `Template.fromNode(Environment environment, {String? path, required Node body})` constructor - - statements: + - Statements: - `macro` - `call` - - filters: + - Filters: - `items` - `title` -- changed: +- Changed: - `Environment`: - `Environment({modifiers})` argument type from `List` to `List` - `modifiers` type from `List` to `List` @@ -52,34 +54,33 @@ - `parse(...)` return type from `List` to `Node` - `Template`: - `Template({modifiers})` argument type from `List` to `List` - - filters: + - Filters: - `truncate` arguments are now positional -- removed: +- Removed: - `Template`: - `Template.fromNodes(...)` constructor - - statements: + - Statements: - `autoescape` - - filters: + - Filters: - `forceescape` - `safe` - `unsafe` - - tests: + - Tests: - `escaped` ## 0.4.2 -- internal changes. +- Internal changes. ## 0.4.1 -- update links. +- Update links. ## 0.4.0 -- minimal SDK version: 2.18.0. -- added: +- Minimal SDK version: 2.18.0. +- Added: - `passContext` and `passEnvironment` functions - `print` to globals `{{ do print(name) }}` - `Environment` - - `Environment({lineCommentPrefix, lineStatementPrefix, newLine, autoReload, modifiers, templates})` - constructor arguments + - `Environment({lineCommentPrefix, lineStatementPrefix, newLine, autoReload, modifiers, templates})` constructor arguments - `autoReload` field - `lexer` field - `lineCommentPrefix` field @@ -91,10 +92,9 @@ - `scan` method - `parse` method - `Template` - - `Template({path, lineCommentPrefix, lineStatementPrefix, newLine, modifiers, templates})` - constructor arguments + - `Template({path, lineCommentPrefix, lineStatementPrefix, newLine, modifiers, templates})` constructor arguments - `renderTo` method - - exceptions are public now: + - Exceptions (was internal): - `TemplateError` - `TemplateSyntaxError` - `TemplateAssertionError` @@ -102,10 +102,10 @@ - `TemplatesNotFound` - `TemplateRuntimeError` - `FilterArgumentError` - - statements: + - Statements: - `do` - `with` - - filters: + - Filters: - `dictsort` - `replace` - `reverse` @@ -118,11 +118,11 @@ - `item` - `map` - `tojson` - - test: + - Test: - `filter` - `test` -- changed: - - `FieldGetter` typedef renamed to `AttributeGetter` +- Changed: + - `FieldGetter` type definition renamed to `AttributeGetter` - `default` filter compare values with `null`, no boolean argument - `defined` and `undefined` tests compare values with `null` - `Environment` @@ -135,11 +135,11 @@ - `Loader.listSources` method renamed to `listTemplates` - `MapLoader.mapping` field renamed to `sources` - `FileSystemLoader` - - `FileSystemLoader({paths})` argument now non-nullable, defaults to `['templates']` + - `FileSystemLoader({paths})` argument is now non-nullable, defaults to `['templates']` - moved to `package:jinja/loaders.dart` library - `package:jinja/get_field.dart` library renamed to `package:jinja/reflection.dart` - `getField` function renamed to `getAttribute` -- removed: +- Removed: - `Undefined` type and `missing` object - `Environment.undefined` method - `Template.render` method @@ -147,8 +147,7 @@ - `FileSystemLoader({path, autoReload})` arguments - `autoReload` field - `directory` field - - slices and negative indexes - - conditional and variable `extends` statement variants - - choice, ignore missing and variable `include` statement variants -- internal changes -- _work in progress_ \ No newline at end of file + - Slices and negative indexes + - Conditional and variable `extends` statement variants + - Choice, ignore missing and variable `include` statement variants +- Internal changes \ No newline at end of file diff --git a/README.md b/README.md index f5b71a9..8b82866 100644 --- a/README.md +++ b/README.md @@ -57,10 +57,13 @@ See also examples with [conduit][conduit_example] and ## Status: ### TODO: +- Informative error messages + - Template name 🔥 + - Source span 🔥 - `Template` class: - `generate` method - `stream` method -- Relative template Paths +- Relative template paths - Async Support - Expressions - Dart Methods and Properties @@ -127,6 +130,7 @@ See also examples with [conduit][conduit_example] and - `('tuple', 'of', 'values')` - `{'dict': 'of', 'key': 'and', 'value': 'pairs'}` - `true` / `false` + - `null` - Math - `+` - `-` @@ -157,7 +161,7 @@ See also examples with [conduit][conduit_example] and - If Expression - `{{ list.last if list }}` - `{{ user.name if user else 'Guest' }}` - - Dart Methods and Properties (with reflection) + - Dart Methods and Properties (if reflection is on) - `{{ string.toUpperCase() }}` - `{{ list.add(item) }}` - List of Global Functions diff --git a/lib/reflection.dart b/lib/reflection.dart index 7ee3f6b..3204f2b 100644 --- a/lib/reflection.dart +++ b/lib/reflection.dart @@ -5,6 +5,6 @@ import 'dart:mirrors' show MirrorSystem, reflect; /// Reflection based object attribute getter. Object? getAttribute(String field, Object? object) { var symbol = MirrorSystem.getSymbol(field); - var mirror = reflect(object).getField(symbol); - return mirror.reflectee; + var fieldMirror = reflect(object).getField(symbol); + return fieldMirror.reflectee; } diff --git a/lib/src/compiler.dart b/lib/src/compiler.dart index db1c5cc..11ca430 100644 --- a/lib/src/compiler.dart +++ b/lib/src/compiler.dart @@ -346,9 +346,8 @@ class RuntimeCompiler implements Visitor { for (var (name, alias) in node.names) { _macroses.add(alias ?? name); } - return node.copyWith( - template: visitNode(node.template, context), - ); + + return node.copyWith(template: visitNode(node.template, context)); } @override @@ -356,18 +355,19 @@ class RuntimeCompiler implements Visitor { return node.copyWith( test: visitNode(node.test, context), body: visitNode(node.body, context), + orElse: visitNode(node.orElse, context), ); } @override Import visitImport(Import node, void context) { _imports.add(node.target); - return node; + return node.copyWith(template: visitNode(node.template, context)); } @override Include visitInclude(Include node, void context) { - return node; + return node.copyWith(template: visitNode(node.template, context)); } @override @@ -428,6 +428,7 @@ class RuntimeCompiler implements Visitor { TemplateNode visitTemplateNode(TemplateNode node, void context) { return node.copyWith( blocks: visitNodes(node.blocks, context), + macros: visitNodes(node.macros, context), body: visitNode(node.body, context), ); } diff --git a/lib/src/context.dart b/lib/src/context.dart index 855aa48..f931542 100644 --- a/lib/src/context.dart +++ b/lib/src/context.dart @@ -1,88 +1,4 @@ -import 'dart:collection'; +@Deprecated('Use `package:jinja/src/runtime.dart` instead.') +library; -import 'package:jinja/src/environment.dart'; - -base class Context { - Context( - this.environment, { - this.parent = const {}, - Map? data, - }) : context = HashMap() { - if (data != null) { - context.addAll(data); - } - } - - final Environment environment; - - final Map parent; - - final Map context; - - Object? call( - dynamic object, [ - List positional = const [], - Map named = const {}, - ]) { - Function function; - - if (object is Function) { - function = object; - } else { - // TODO(dynamic): dynamic invocation - // ignore: avoid_dynamic_calls - function = object.call as Function; - } - - return environment.callCommon(function, positional, named, this); - } - - Context derived({Map? data}) { - var parent = HashMap.from(this.parent)..addAll(context); - return Context(environment, parent: parent, data: data); - } - - bool has(String key) { - if (context.containsKey(key)) { - return true; - } - - return parent.containsKey(key); - } - - Object? resolve(String name) { - if (context.containsKey(name)) { - return context[name]; - } - - if (parent.containsKey(name)) { - return parent[name]; - } - - return environment.undefined(name); - } - - Object? attribute(String name, Object? value) { - return environment.getAttribute(name, value); - } - - Object? item(Object? name, Object? value) { - return environment.getItem(name, value); - } - - Object? filter( - String name, [ - List positional = const [], - Map named = const {}, - ]) { - return environment.callFilter(name, positional, named, this); - } - - bool test( - String name, [ - List positional = const [], - Map named = const {}, - ]) { - return environment.callTest(name, positional, named, this); - } -} +export 'package:jinja/src/runtime.dart' show Context; diff --git a/lib/src/defaults.dart b/lib/src/defaults.dart index 377f784..179570f 100644 --- a/lib/src/defaults.dart +++ b/lib/src/defaults.dart @@ -1,5 +1,4 @@ -import 'package:jinja/src/context.dart'; -import 'package:jinja/src/namespace.dart'; +import 'package:jinja/src/runtime.dart'; import 'package:jinja/src/utils.dart'; export 'package:jinja/src/filters.dart' show filters; @@ -30,6 +29,6 @@ Object? getItem(Object? item, dynamic object) { } } -Object? undefined(String name) { +Object? undefined(String name, [String? template]) { return null; } diff --git a/lib/src/environment.dart b/lib/src/environment.dart index 05f26a5..656d09f 100644 --- a/lib/src/environment.dart +++ b/lib/src/environment.dart @@ -1,8 +1,6 @@ -import 'dart:collection' show HashMap; import 'dart:math' show Random; import 'package:jinja/src/compiler.dart'; -import 'package:jinja/src/context.dart'; import 'package:jinja/src/defaults.dart' as defaults; import 'package:jinja/src/exceptions.dart'; import 'package:jinja/src/lexer.dart'; @@ -11,6 +9,7 @@ import 'package:jinja/src/nodes.dart'; import 'package:jinja/src/optimizer.dart'; import 'package:jinja/src/parser.dart'; import 'package:jinja/src/renderer.dart'; +import 'package:jinja/src/runtime.dart'; import 'package:jinja/src/utils.dart'; import 'package:meta/meta.dart'; @@ -43,43 +42,11 @@ typedef AttributeGetter = Object? Function(String attribute, Object? object); /// Used by `object['item']` expression. typedef ItemGetter = Object? Function(Object? key, Object? object); -/// A function that returns an undefined object or throws an error if -/// the variable is not found. +/// A function that returns a value or throws an error if the variable is not +/// found. /// /// Used by `{{ user.field }}` expression when `user` not found. -/// -/// Possible usage to throw an error when accessing properties of an undefined -/// variable: -/// ```dart -/// Object? undefined(String key) { -/// return Undefined(key); -/// } -/// -/// final class Undefined { -/// const Undefined(this.name); -/// -/// final String name; -/// -/// Object? operator [](String key) { -/// throw UndefinedError('$name has no property $key'); -/// } -/// -/// @override -/// dynamic noSuchMethod(Invocation invocation) { -/// var symbol = '${invocation.memberName}'; // Symbol('name') -/// var memberName = symbol.substring(8, symbol.length - 2); -/// throw UndefinedError('$name has no property $memberName'); -/// } -/// -/// @override -/// String toString() { -/// return 'Undefined($name)'; -/// } -/// } -/// -/// var environment = Environment(undefined: undefined); -/// ``` -typedef UndefinedFactory = Object? Function(String key); +typedef UndefinedCallback = Object? Function(String name, [String? template]); /// Pass the [Context] as the first argument to the applied function when /// called while rendering a template. @@ -136,11 +103,11 @@ base class Environment { this.getItem = defaults.getItem, this.undefined = defaults.undefined, }) : finalize = wrapFinalizer(finalize), - globals = HashMap.of(defaults.globals), - filters = HashMap.of(defaults.filters), - tests = HashMap.of(defaults.tests), + globals = {...defaults.globals}, + filters = {...defaults.filters}, + tests = {...defaults.tests}, modifiers = [], - templates = HashMap(), + templates = {}, random = random ?? Random(), getAttribute = wrapGetAttribute(getAttribute, getItem) { if (newLine != '\r' && newLine != '\n' && newLine != '\r\n') { @@ -268,7 +235,7 @@ base class Environment { /// Get an undefined object or throw an error if the variable is not found. /// /// Default implementation throws [UndefinedError]. - final UndefinedFactory undefined; + final UndefinedCallback undefined; @override int get hashCode { @@ -387,11 +354,7 @@ base class Environment { String? path, Map? globals, }) { - if (globals == null) { - globals = HashMap.of(this.globals); - } else { - globals = HashMap.of(this.globals)..addAll(globals); - } + globals = {...this.globals, ...?globals}; var body = parse(source, path: path); @@ -400,14 +363,16 @@ base class Environment { } if (optimize) { - body = body.accept(const Optimizer(), Context(this)); + body = body.accept(const Optimizer(), Context(this, template: path)); } + body = body.accept(RuntimeCompiler(), null); + return Template.fromNode( this, path: path, globals: globals, - body: body.accept(RuntimeCompiler(), null), + body: body, ); } @@ -469,19 +434,19 @@ base class Environment { /// @nodoc @protected static ContextFinalizer wrapFinalizer(Function function) { - if (function case ContextFinalizer contextFinalizer) { - return contextFinalizer; + if (function is ContextFinalizer) { + return function; } - if (function case EnvironmentFinalizer environmentFinalizer) { + if (function is EnvironmentFinalizer) { return (context, value) { - return environmentFinalizer(context.environment, value); + return function(context.environment, value); }; } - if (function case Finalizer finalizer) { + if (function is Finalizer) { return (context, value) { - return finalizer(value); + return function(value); }; } @@ -541,7 +506,7 @@ base class Template { Random? random, AttributeGetter? getAttribute, ItemGetter getItem = defaults.getItem, - UndefinedFactory undefined = defaults.undefined, + UndefinedCallback undefined = defaults.undefined, }) { if (environment == null) { return Environment( @@ -581,8 +546,8 @@ base class Template { this.environment, { this.path, this.globals = const {}, - required this.body, - }); + required Node body, + }) : body = body is TemplateNode ? body : TemplateNode(body: body); /// The environment used to parse and render template. final Environment environment; @@ -594,13 +559,14 @@ base class Template { final Map globals; /// Template body node. - final Node body; + @internal + final TemplateNode body; /// If no arguments are given the context will be empty. String render([Map? data]) { var buffer = StringBuffer(); renderTo(buffer, data); - return '$buffer'; + return buffer.toString(); } /// If no arguments are given the context will be empty. @@ -608,6 +574,7 @@ base class Template { var context = StringSinkRenderContext( environment, sink, + template: path, parent: globals, data: data, ); diff --git a/lib/src/filters.dart b/lib/src/filters.dart index 2cd3c1e..575b338 100644 --- a/lib/src/filters.dart +++ b/lib/src/filters.dart @@ -1,8 +1,8 @@ import 'dart:convert' show LineSplitter; import 'dart:math' as math; -import 'package:jinja/src/context.dart'; import 'package:jinja/src/environment.dart'; +import 'package:jinja/src/runtime.dart'; import 'package:jinja/src/utils.dart' as utils; import 'package:textwrap/textwrap.dart' show TextWrapper; import 'package:textwrap/utils.dart'; diff --git a/lib/src/namespace.dart b/lib/src/namespace.dart index cb6fedf..f708e43 100644 --- a/lib/src/namespace.dart +++ b/lib/src/namespace.dart @@ -1,48 +1,4 @@ -import 'dart:collection'; +@Deprecated('Use `package:jinja/src/runtime.dart` instead.') +library; -class Namespace extends MapView { - Namespace([Map? context]) - : super(HashMap()) { - if (context != null) { - addAll(context); - } - } - - @override - String toString() { - var values = entries.map((entry) => '${entry.key}: ${entry.value}'); - return 'Namespace(${values.join(', ')})'; - } - - static Namespace factory([List? datas]) { - var namespace = Namespace(); - - if (datas == null) { - return namespace; - } - - for (var data in datas) { - if (data is! Map) { - // TODO(namespace): update error - throw TypeError(); - } - - namespace.addAll(data.cast()); - } - - return namespace; - } -} - -class NamespaceValue { - NamespaceValue(this.name, this.item); - - String name; - - String item; - - @override - String toString() { - return 'NamespaceValue($name, $item)'; - } -} +export 'package:jinja/src/runtime.dart' show Namespace, NamespaceValue; diff --git a/lib/src/nodes.dart b/lib/src/nodes.dart index 6d6384c..a05b4b3 100644 --- a/lib/src/nodes.dart +++ b/lib/src/nodes.dart @@ -155,8 +155,16 @@ final class TemplateNode extends Node { } @override - TemplateNode copyWith({List? blocks, Node? body}) { - return TemplateNode(blocks: blocks ?? this.blocks, body: body ?? this.body); + TemplateNode copyWith({ + List? blocks, + List? macros, + Node? body, + }) { + return TemplateNode( + blocks: blocks ?? this.blocks, + macros: macros ?? this.macros, + body: body ?? this.body, + ); } @override diff --git a/lib/src/nodes/statements.dart b/lib/src/nodes/statements.dart index 2b6b1fe..edc5f77 100644 --- a/lib/src/nodes/statements.dart +++ b/lib/src/nodes/statements.dart @@ -547,6 +547,7 @@ final class Block extends Statement { Map toJson() { return { 'class': 'Block', + 'name': name, if (scoped) 'scoped': scoped, if (required) 'required': required, 'body': body.toJson(), diff --git a/lib/src/optimizer.dart b/lib/src/optimizer.dart index ad2fe03..30b9d9d 100644 --- a/lib/src/optimizer.dart +++ b/lib/src/optimizer.dart @@ -1,7 +1,7 @@ import 'dart:math' as math; -import 'package:jinja/src/context.dart'; import 'package:jinja/src/nodes.dart'; +import 'package:jinja/src/runtime.dart'; import 'package:jinja/src/utils.dart'; import 'package:jinja/src/visitor.dart'; import 'package:meta/meta.dart'; diff --git a/lib/src/parser.dart b/lib/src/parser.dart index c7c823b..7786e2e 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -331,16 +331,9 @@ final class Parser { var required = reader.skipIf('name', 'required'); var body = parseStatements(reader, endBlock, true); - if (required) { - switch (body) { - case Data data when data.isLeaf: - case Output(nodes: []): - break; - - default: - fail('Required blocks can only contain comments or whitespace.', - token.line); - } + if (required && (body is! Data || body.isLeaf)) { + fail('Required blocks can only contain comments or whitespace.', + token.line); } var maybeName = reader.current; diff --git a/lib/src/renderer.dart b/lib/src/renderer.dart index 212a384..19e85e5 100644 --- a/lib/src/renderer.dart +++ b/lib/src/renderer.dart @@ -1,12 +1,10 @@ -import 'dart:collection'; import 'dart:math' as math; -import 'package:jinja/src/context.dart'; import 'package:jinja/src/environment.dart'; import 'package:jinja/src/exceptions.dart'; import 'package:jinja/src/loop.dart'; -import 'package:jinja/src/namespace.dart'; import 'package:jinja/src/nodes.dart'; +import 'package:jinja/src/runtime.dart'; import 'package:jinja/src/tests.dart'; import 'package:jinja/src/utils.dart'; import 'package:jinja/src/visitor.dart'; @@ -15,72 +13,47 @@ import 'package:meta/dart2js.dart'; abstract base class RenderContext extends Context { RenderContext( super.environment, { - Map>? blocks, + super.template, + super.blocks, super.parent, super.data, - }) : blocks = blocks ?? HashMap>(); - - final Map> blocks; - - @override - RenderContext derived({ - Map>? blocks, - Map? data, }); - void set(String key, Object? value) { - context[key] = value; - } - - bool remove(String name) { - if (context.containsKey(name)) { - context.remove(name); - return true; - } - - return false; - } + @override + RenderContext derived({String? template, Map? data}); Object? finalize(Object? object) { return environment.finalize(this, object); } void assignTargets(Object? target, Object? current) { - if (target case String string) { - set(string, current); - return; - } - - if (target case List strings) { + if (target is String) { + set(target, current); + } else if (target is List) { var values = list(current); - if (values.length < strings.length) { + if (values.length < target.length) { throw StateError('Not enough values to unpack.'); } - if (values.length > strings.length) { + if (values.length > target.length) { throw StateError('Too many values to unpack.'); } - for (var i = 0; i < strings.length; i++) { - set(strings[i], values[i]); + for (var i = 0; i < target.length; i++) { + set(target[i], values[i]); } + } else if (target is NamespaceValue) { + var value = resolve(target.name); - return; - } - - if (target case NamespaceValue namespaceValue) { - var value = resolve(namespaceValue.name); - - if (value case Namespace namespace) { - namespace[target.item] = current; - return; + if (value is Namespace) { + value[target.item] = current; + } else { + throw TemplateRuntimeError('Non-namespace object'); } - - throw TemplateRuntimeError('Non-namespace object'); + } else { + throw TypeError(); } - - throw TypeError(); } } @@ -88,6 +61,7 @@ base class StringSinkRenderContext extends RenderContext { StringSinkRenderContext( super.environment, this.sink, { + super.template, super.blocks, super.parent, super.data, @@ -98,14 +72,14 @@ base class StringSinkRenderContext extends RenderContext { @override StringSinkRenderContext derived({ StringSink? sink, - Map>? blocks, + String? template, Map? data, bool withContext = true, }) { Map parent; if (withContext) { - parent = HashMap.of(this.parent)..addAll(context); + parent = {...this.parent, ...context}; } else { parent = this.parent; } @@ -113,7 +87,8 @@ base class StringSinkRenderContext extends RenderContext { return StringSinkRenderContext( environment, sink ?? this.sink, - blocks: blocks ?? this.blocks, + template: template, + blocks: blocks, parent: parent, data: data, ); @@ -432,37 +407,7 @@ base class StringSinkRenderer @override void visitBlock(Block node, StringSinkRenderContext context) { - var blocks = context.blocks[node.name]; - - if (blocks == null || blocks.isEmpty) { - node.body.accept(this, context); - } else { - if (node.required) { - if (blocks.length == 1) { - throw TemplateRuntimeError( - "Required block '${node.name}' not found."); - } - } - - var first = blocks[0]; - var index = 0; - - // TODO(renderer): move to context - String parent() { - if (index < blocks.length - 1) { - var parentBlock = blocks[index += 1]; - parentBlock.body.accept(this, context); - return ''; - } - - // TODO(renderer): add error message - throw TemplateRuntimeError(); - } - - context.set('super', parent); - first.body.accept(this, context); - context.remove('super'); - } + context.blocks[node.name]![0](context); } @override @@ -583,40 +528,30 @@ base class StringSinkRenderer for (var (name, alias) in node.names) { String macro(List positional, Map named) { - Macro targetMacro; + Macro? targetMacro; - if (template.body case Macro macro) { - if (macro.name != name) { - throw TemplateRuntimeError( - "The '${template.path}' does not export the requested name."); + for (var macro in template.body.macros) { + if (macro.name == name) { + targetMacro = macro; + break; } + } - targetMacro = macro; - } else if (template.body case TemplateNode body) { - found: - { - for (var macro in body.macros) { - if (macro.name == name) { - targetMacro = macro; - break found; - } - } - - throw TemplateRuntimeError( - "The '${template.path}' does not export the requested name."); - } - } else { - throw TemplateRuntimeError('Non-macro object.'); + if (targetMacro == null) { + throw TemplateRuntimeError( + "The '${template.path}' does not export the requested name."); } + MacroFunction function; + if (node.withContext) { - var function = getMacroFunction(targetMacro, context); - return function(positional, named.cast()); + function = getMacroFunction(targetMacro, context); } else { var newContext = context.derived(withContext: false); - var function = getMacroFunction(targetMacro, newContext); - return function(positional, named.cast()); + function = getMacroFunction(targetMacro, newContext); } + + return function(positional, named.cast()); } context.set(alias ?? name, macro); @@ -642,20 +577,9 @@ base class StringSinkRenderer Object? value => throw ArgumentError.value(value, 'template'), }; - List macros; - - if (template.body case Macro macro) { - macros = [macro]; - } else if (template.body case TemplateNode body) { - macros = body.macros; - } else { - // TODO(renderer): update error message - throw TemplateRuntimeError('Non-macro object.'); - } - var namespace = Namespace(); - for (var macro in macros) { + for (var macro in template.body.macros) { if (node.withContext) { namespace[macro.name] = getMacroFunction(macro, context); } else { @@ -720,11 +644,14 @@ base class StringSinkRenderer var self = Namespace(); for (var block in node.blocks) { - var blocks = context.blocks[block.name] ??= []; - blocks.add(block); - String render() { - block.accept(this, context); + var blocks = context.blocks[block.name]; + + if (blocks == null || blocks.isEmpty) { + throw UndefinedError("Block '${block.name}' is not defined."); + } + + blocks.last(context); return ''; } diff --git a/lib/src/runtime.dart b/lib/src/runtime.dart new file mode 100644 index 0000000..07ac9a9 --- /dev/null +++ b/lib/src/runtime.dart @@ -0,0 +1,158 @@ +import 'package:jinja/src/environment.dart'; + +typedef ContextCallback = void Function(Context context); + +base class Context { + Context( + this.environment, { + this.template, + Map>? blocks, + this.parent = const {}, + Map? data, + }) : blocks = blocks ?? >{}, + context = {...?data}; + + final Environment environment; + + String? template; + + final Map> blocks; + + final Map parent; + + final Map context; + + Object? call( + dynamic object, [ + List positional = const [], + Map named = const {}, + ]) { + Function function; + + if (object is Function) { + function = object; + } else { + // TODO(dynamic): dynamic invocation + // ignore: avoid_dynamic_calls + function = object.call as Function; + } + + return environment.callCommon(function, positional, named, this); + } + + Context derived({ + String? template, + Map? data, + }) { + return Context( + environment, + template: template ?? this.template, + blocks: blocks, + parent: parent, + data: data, + ); + } + + bool has(String key) { + if (context.containsKey(key)) { + return true; + } + + return parent.containsKey(key); + } + + Object? resolve(String name) { + if (context.containsKey(name)) { + return context[name]; + } + + if (parent.containsKey(name)) { + return parent[name]; + } + + return environment.undefined(name, template); + } + + void set(String key, Object? value) { + context[key] = value; + } + + bool remove(String name) { + if (context.containsKey(name)) { + context.remove(name); + return true; + } + + return false; + } + + Object? undefined(String name, [String? template]) { + return environment.undefined(name, template); + } + + Object? attribute(String name, Object? value) { + return environment.getAttribute(name, value); + } + + Object? item(Object? name, Object? value) { + return environment.getItem(name, value); + } + + Object? filter( + String name, [ + List positional = const [], + Map named = const {}, + ]) { + return environment.callFilter(name, positional, named, this); + } + + bool test( + String name, [ + List positional = const [], + Map named = const {}, + ]) { + return environment.callTest(name, positional, named, this); + } +} + +base class Namespace { + Namespace([Map? data]) + : context = {...?data}; + + final Map context; + + Object? operator [](String name) { + return context[name]; + } + + void operator []=(String name, Object? value) { + context[name] = value; + } + + static Namespace factory([List? datas]) { + var namespace = Namespace(); + + if (datas == null) { + return namespace; + } + + for (var data in datas) { + if (data is! Map) { + // TODO(namespace): update error + throw TypeError(); + } + + namespace.context.addAll(data.cast()); + } + + return namespace; + } +} + +final class NamespaceValue { + NamespaceValue(this.name, this.item); + + final String name; + + final String item; +} diff --git a/lib/src/tests.dart b/lib/src/tests.dart index 485bfb5..a0953b0 100644 --- a/lib/src/tests.dart +++ b/lib/src/tests.dart @@ -189,6 +189,7 @@ final Map tests = { 'filter': passEnvironment(isFilter), 'test': passEnvironment(isTest), 'none': isNull, + 'null': isNull, 'boolean': isBoolean, 'false': isFalse, 'true': isTrue, diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 8322b32..6b688db 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,8 +1,8 @@ import 'dart:convert'; import 'package:html_unescape/html_unescape.dart'; -import 'package:jinja/src/context.dart'; import 'package:jinja/src/environment.dart'; +import 'package:jinja/src/runtime.dart'; import 'package:textwrap/utils.dart'; final RegExp _tagsRe = RegExp('(|<[^>]*>)'); diff --git a/pubspec.yaml b/pubspec.yaml index b8202f5..cc80e53 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: jinja -version: 0.6.1-dev.2 +version: 0.6.1-dev.3 description: >- Jinja2 template engine for Dart. Variables, expressions, control structures and template inheritance. diff --git a/test/api_test.dart b/test/api_test.dart index 500d392..f1b4f34 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -2,8 +2,8 @@ library; import 'package:jinja/jinja.dart'; -import 'package:jinja/src/context.dart'; import 'package:jinja/src/loop.dart'; +import 'package:jinja/src/runtime.dart'; import 'package:test/test.dart'; void main() { diff --git a/test/check.dart b/test/check.dart index a703291..d0a540f 100644 --- a/test/check.dart +++ b/test/check.dart @@ -5,10 +5,10 @@ import 'dart:io'; import 'dart:math' show max; import 'package:jinja/src/compiler.dart'; -import 'package:jinja/src/context.dart'; import 'package:jinja/src/environment.dart'; import 'package:jinja/src/exceptions.dart'; import 'package:jinja/src/optimizer.dart'; +import 'package:jinja/src/runtime.dart'; const JsonEncoder jsonEncoder = JsonEncoder.withIndent(' '); diff --git a/test/functions_test.dart b/test/functions_test.dart index ee719fc..d0f403c 100644 --- a/test/functions_test.dart +++ b/test/functions_test.dart @@ -3,7 +3,7 @@ library; import 'package:jinja/jinja.dart'; import 'package:jinja/reflection.dart'; -import 'package:jinja/src/context.dart'; +import 'package:jinja/src/runtime.dart'; import 'package:test/test.dart'; Object? func({String named = 'default'}) { From de5106d92ce6ab00a2ddf7579da61cb4ec12e718 Mon Sep 17 00:00:00 2001 From: Olzhas Suleimen Date: Fri, 20 Sep 2024 18:37:49 +0500 Subject: [PATCH 2/7] 0.6.1-dev.4 --- CHANGELOG.md | 2 +- lib/src/environment.dart | 18 ++++++++++++------ lib/src/parser.dart | 10 +++++++--- lib/src/renderer.dart | 22 +++++++++++++++++++++- pubspec.yaml | 2 +- test/syntax_test.dart | 3 ++- test/test.dart | 19 +++++++++++++++++++ 7 files changed, 63 insertions(+), 13 deletions(-) create mode 100644 test/test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b8ec3..e124b45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.6.1-dev.3 ([diff](https://github.com/ykmnkmi/jinja.dart/compare/88996f8..main)) +## 0.6.1-dev.4 ([diff](https://github.com/ykmnkmi/jinja.dart/compare/88996f8..main)) - Added: - `UndefinedError` exception - `UndefinedFactory` typedef diff --git a/lib/src/environment.dart b/lib/src/environment.dart index 656d09f..0b5c921 100644 --- a/lib/src/environment.dart +++ b/lib/src/environment.dart @@ -439,15 +439,19 @@ base class Environment { } if (function is EnvironmentFinalizer) { - return (context, value) { + Object? finalizer(Context context, Object? value) { return function(context.environment, value); - }; + } + + return finalizer; } if (function is Finalizer) { - return (context, value) { + Object? finalizer(Context context, Object? value) { return function(value); - }; + } + + return finalizer; } // Dart doesn't support union types, so we have to throw an error here. @@ -464,13 +468,15 @@ base class Environment { return itemGetter; } - return (attribute, object) { + Object? getter(String attribute, Object? object) { try { return attributeGetter(attribute, object); } on NoSuchMethodError { return itemGetter(attribute, object); } - }; + } + + return getter; } } diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 7786e2e..96734b9 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -177,8 +177,12 @@ final class Parser { reader.next(); } - if (nodes case [Node node]) { - return node; + if (nodes.isEmpty) { + return Data(data: ''); + } + + if (nodes.length == 1) { + return nodes[0]; } return Output(nodes: nodes); @@ -331,7 +335,7 @@ final class Parser { var required = reader.skipIf('name', 'required'); var body = parseStatements(reader, endBlock, true); - if (required && (body is! Data || body.isLeaf)) { + if (required && (body is! Data || !body.isLeaf)) { fail('Required blocks can only contain comments or whitespace.', token.line); } diff --git a/lib/src/renderer.dart b/lib/src/renderer.dart index 19e85e5..ca0a9e3 100644 --- a/lib/src/renderer.dart +++ b/lib/src/renderer.dart @@ -180,7 +180,7 @@ base class StringSinkRenderer } node.body.accept(this, derived); - return '$buffer'; + return buffer.toString(); } return macro; @@ -523,6 +523,7 @@ base class StringSinkRenderer var template = switch (templateOrParth) { String path => context.environment.getTemplate(path), Template template => template, + // TODO(renderer): add error message Object? value => throw ArgumentError.value(value, 'template'), }; @@ -602,6 +603,7 @@ base class StringSinkRenderer String path => context.environment.getTemplate(path), Template template => template, List paths => context.environment.selectTemplate(paths), + // TODO(renderer): add error message Object? value => throw ArgumentError.value(value, 'template'), }; } on TemplateNotFound { @@ -656,6 +658,24 @@ base class StringSinkRenderer } self[block.name] = render; + + var blocks = context.blocks[block.name] ??= []; + + if (block.required) { + String callback(Context context) { + throw TemplateRuntimeError( + "Required block '${block.name}' not found."); + } + + blocks.add(callback); + } else { + String callback(Context context) { + block.body.accept(this, context); + return ''; + } + + blocks.add(callback); + } } context.set('self', self); diff --git a/pubspec.yaml b/pubspec.yaml index cc80e53..db1ede8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: jinja -version: 0.6.1-dev.3 +version: 0.6.1-dev.4 description: >- Jinja2 template engine for Dart. Variables, expressions, control structures and template inheritance. diff --git a/test/syntax_test.dart b/test/syntax_test.dart index d103556..5223e4f 100644 --- a/test/syntax_test.dart +++ b/test/syntax_test.dart @@ -306,8 +306,9 @@ void main() { test('neg filter priority', () { var tmpl = env.fromString('{{ -1|foo }}'); + var body = tmpl.body.body as Interpolation; - expect((tmpl.body as Interpolation).value, predicate((filter) { + expect(body.value, predicate((filter) { var argument = filter.calling.arguments[0]; return argument is Constant && argument.value == -1; })); diff --git a/test/test.dart b/test/test.dart new file mode 100644 index 0000000..8102a69 --- /dev/null +++ b/test/test.dart @@ -0,0 +1,19 @@ +// ignore_for_file: avoid_print + +import 'dart:convert'; + +import 'package:jinja/jinja.dart'; + +const Map sources = { + 'include': '{% macro test(foo) %}[{{ foo }}]{% endmacro %}', +}; + +void main() { + var loader = MapLoader(sources); + var env = Environment(loader: loader); + var tmpl = env.fromString(''' +{% from "include" import test -%} +{{ test("foo") }}'''); + print(JsonEncoder.withIndent(' ').convert(env.getTemplate('include').body.toJson())); + print(tmpl.render()); +} From 71a9e3ce3e1a234a1bb619c313dc71aa72e0bd5e Mon Sep 17 00:00:00 2001 From: Olzhas Suleimen Date: Fri, 27 Sep 2024 11:59:23 +0500 Subject: [PATCH 3/7] 0.6.1-dev.5 --- CHANGELOG.md | 6 +- README.md | 18 ++- lib/src/compiler.dart | 42 ++++--- lib/src/environment.dart | 6 +- lib/src/loop.dart | 200 +-------------------------------- lib/src/nodes.dart | 10 +- lib/src/nodes/statements.dart | 2 +- lib/src/optimizer.dart | 5 +- lib/src/parser.dart | 28 +---- lib/src/renderer.dart | 81 +++++++++----- lib/src/runtime.dart | 204 +++++++++++++++++++++++++++++++++- lib/src/visitor.dart | 72 ++++++------ pubspec.yaml | 2 +- test/api_test.dart | 1 - test/statements/set_test.dart | 4 +- test/test.dart | 19 ---- 16 files changed, 357 insertions(+), 343 deletions(-) delete mode 100644 test/test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index e124b45..b0e267c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,10 @@ - `UndefinedError` exception - `UndefinedFactory` typedef - `Environment`: - - `Environment({undefined})` argument - - `undefined` field + - `Environment({UndefinedFactory undefined})` argument + - `UndefinedFactory undefined` field - `Template`: - - `Template({undefined})` argument + - `Template({UndefinedFactory undefined})` argument - Filters: - `null` (`none` alias) diff --git a/README.md b/README.md index 8b82866..92ca039 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,22 @@ See also examples with [conduit][conduit_example] and - `stream` method - Relative template paths - Async Support +- Template Inheritance + - Super Blocks + - `{{ super.super() }}` +- List of Control Structures + - Extends + - Execute non-`block` statements and expressions + ```jinja + {% extends 'base.html' %} + {% set title = 'Index' %} + {% macro header() %} +

{{ title }}

+ {% endmacro %} ``` + {% block body %} + {{ header() }} + {% endblock %} + ``` - Expressions - Dart Methods and Properties - `!.`/`?.` @@ -73,7 +89,6 @@ See also examples with [conduit][conduit_example] and - ... - List of Global Functions - `lipsum` - - `dict` - `cycler` - `joiner` - Extensions @@ -165,6 +180,7 @@ See also examples with [conduit][conduit_example] and - `{{ string.toUpperCase() }}` - `{{ list.add(item) }}` - List of Global Functions + - ~~`dict`~~ - `print` - `range` - `list` diff --git a/lib/src/compiler.dart b/lib/src/compiler.dart index 11ca430..08476c4 100644 --- a/lib/src/compiler.dart +++ b/lib/src/compiler.dart @@ -3,16 +3,18 @@ import 'package:jinja/src/nodes.dart'; import 'package:jinja/src/visitor.dart'; import 'package:meta/meta.dart'; +// TODO(renderer): Rename to `StringSinkRendererCompiler` +// and move to `renderer.dart`. Add `ContextNode` for `ContextCallback`s. @doNotStore class RuntimeCompiler implements Visitor { RuntimeCompiler() : _imports = {}, - _macroses = {}, + _macros = {}, _inMacro = false; final Set _imports; - final Set _macroses; + final Set _macros; bool _inMacro; @@ -46,8 +48,8 @@ class RuntimeCompiler implements Visitor { @override Call visitCall(Call node, void context) { - // Modifies Template AST from `loop.cycle(first, second, *list)` - // to `loop['cycle']([first, second], list)`, which matches + // Modifies Template AST from `loop.cycle(first, second)` + // to `loop['cycle']([first, second])`, which matches // [LoopContext.cycle] definition. // // TODO(compiler): check name arguments @@ -59,7 +61,7 @@ class RuntimeCompiler implements Visitor { : [Array(values: calling.arguments)]; return node.copyWith( - value: visitNode(node.value, context), + value: Item(key: Constant(value: 'cycle'), value: value), calling: Calling(arguments: visitNodes(arguments, context)), ); } @@ -94,7 +96,7 @@ class RuntimeCompiler implements Visitor { ); } - if (_inMacro && name == 'caller' || _macroses.contains(name)) { + if (_inMacro && name == 'caller' || _macros.contains(name)) { var calling = visitNode(node.calling, context); var arguments = calling.arguments; var keywords = calling.keywords; @@ -344,7 +346,7 @@ class RuntimeCompiler implements Visitor { @override FromImport visitFromImport(FromImport node, void context) { for (var (name, alias) in node.names) { - _macroses.add(alias ?? name); + _macros.add(alias ?? name); } return node.copyWith(template: visitNode(node.template, context)); @@ -378,7 +380,7 @@ class RuntimeCompiler implements Visitor { @override Macro visitMacro(Macro node, void context) { _inMacro = true; - _macroses.add(node.name); + _macros.add(node.name); var positional = []; var named = <(Expression, Expression)>[]; @@ -420,17 +422,29 @@ class RuntimeCompiler implements Visitor { } @override - Output visitOutput(Output node, void context) { + Node visitOutput(Output node, void context) { + if (node.nodes.isEmpty) { + return Data(); + } + + if (node.nodes.length == 1) { + return visitNode(node.nodes[0], context); + } + return node.copyWith(nodes: visitNodes(node.nodes, context)); } @override TemplateNode visitTemplateNode(TemplateNode node, void context) { - return node.copyWith( - blocks: visitNodes(node.blocks, context), - macros: visitNodes(node.macros, context), - body: visitNode(node.body, context), - ); + var body = visitNode(node.body, context) as Node; + var blocks = [if (body is Block) body, ...body.findAll()]; + var macros = [if (body is Macro) body, ...body.findAll()]; + + if (body is Output && body.nodes.first is Extends) { + body = body.nodes.first; + } + + return node.copyWith(blocks: blocks, macros: macros, body: body); } @override diff --git a/lib/src/environment.dart b/lib/src/environment.dart index 0b5c921..d34cd65 100644 --- a/lib/src/environment.dart +++ b/lib/src/environment.dart @@ -342,10 +342,8 @@ base class Environment { } /// Parse the source code and return the AST node. - /// - /// This can be useful for debugging or to extract information from templates. Node parse(String source, {String? path}) { - return scan(lex(source), path: path); + return Parser(this, path: path).parse(source); } /// Load a template from a source string without using [loader]. @@ -564,7 +562,7 @@ base class Template { /// The global variables for this template. final Map globals; - /// Template body node. + // @nodoc @internal final TemplateNode body; diff --git a/lib/src/loop.dart b/lib/src/loop.dart index b529809..b504e10 100644 --- a/lib/src/loop.dart +++ b/lib/src/loop.dart @@ -1,197 +1,5 @@ -final class LoopContext extends Iterable { - LoopContext(this.values, this.depth0, this.recurse) - : length = values.length, - index0 = -1; +@Deprecated('Use `package:jinja/src/runtime.dart` instead') +library; - final List values; - - @override - final int length; - - final int depth0; - - final String Function(Object? data, [int depth]) recurse; - - int index0; - - @override - LoopIterator get iterator { - return LoopIterator(this); - } - - int get index { - return index0 + 1; - } - - int get depth { - return depth0 + 1; - } - - int get revindex0 { - return length - index; - } - - int get revindex { - return length - index0; - } - - @override - bool get first { - return index0 == 0; - } - - @override - bool get last { - return index == length; - } - - Object? get next { - if (last) { - return null; - } - - return values[index0 + 1]; - } - - Object? get prev { - if (first) { - return null; - } - - return values[index0 - 1]; - } - - Object? operator [](String key) { - switch (key) { - case 'length': - return length; - case 'index0': - return index0; - case 'depth0': - return depth0; - case 'index': - return index; - case 'depth': - return depth; - case 'revindex0': - return revindex0; - case 'revindex': - return revindex; - case 'first': - return first; - case 'last': - return last; - case 'prev': - case 'previtem': - return prev; - case 'next': - case 'nextitem': - return next; - case 'call': - return call; - case 'cycle': - return cycle; - case 'changed': - return changed; - default: - var invocation = Invocation.getter(Symbol(key)); - throw NoSuchMethodError.withInvocation(this, invocation); - } - } - - String call(Object? data) { - return recurse(data, depth); - } - - Object? cycle(Iterable values) { - var list = values.toList(); - - if (list.isEmpty) { - // TODO(loop): update error - throw TypeError(); - } - - return list[index0 % list.length]; - } - - bool changed(Object? item) { - if (index0 == 0) { - return true; - } - - if (item == prev) { - return false; - } - - return true; - } -} - -final class LoopIterator implements Iterator { - LoopIterator(this.context); - - final LoopContext context; - - @override - Object? get current { - return context.values[context.index0]; - } - - @override - bool moveNext() { - if (context.index < context.length) { - context.index0 += 1; - return true; - } - - return false; - } -} - -final class Cycler extends Iterable { - Cycler(Iterable values) - : values = List.of(values), - length = values.length, - index = 0; - - final List values; - - @override - final int length; - - int index; - - Object? get current { - return values[index]; - } - - @override - Iterator get iterator { - return CyclerIterator(this); - } - - Object? next() { - var result = current; - index = (index + 1) % length; - return result; - } - - void reset() { - index = 0; - } -} - -final class CyclerIterator implements Iterator { - CyclerIterator(this.cycler); - - final Cycler cycler; - - @override - Object? current; - - @override - bool moveNext() { - current = cycler.next(); - return true; - } -} +export 'package:jinja/src/runtime.dart' + show LoopContext, LoopIterator, Cycler, CyclerIterator; diff --git a/lib/src/nodes.dart b/lib/src/nodes.dart index a05b4b3..c333562 100644 --- a/lib/src/nodes.dart +++ b/lib/src/nodes.dart @@ -137,7 +137,7 @@ final class Output extends Node { } final class TemplateNode extends Node { - const TemplateNode({ + TemplateNode({ this.blocks = const [], this.macros = const [], required this.body, @@ -169,14 +169,6 @@ final class TemplateNode extends Node { @override Iterable findAll() sync* { - for (var block in blocks) { - if (block case T block) { - yield block; - } - - yield* block.findAll(); - } - if (body case T body) { yield body; } diff --git a/lib/src/nodes/statements.dart b/lib/src/nodes/statements.dart index edc5f77..8948ffb 100644 --- a/lib/src/nodes/statements.dart +++ b/lib/src/nodes/statements.dart @@ -1,6 +1,6 @@ part of '../nodes.dart'; -abstract class ImportContext { +abstract interface class ImportContext { bool get withContext; } diff --git a/lib/src/optimizer.dart b/lib/src/optimizer.dart index 30b9d9d..7137e08 100644 --- a/lib/src/optimizer.dart +++ b/lib/src/optimizer.dart @@ -413,10 +413,7 @@ class Optimizer implements Visitor { @override TemplateNode visitTemplateNode(TemplateNode node, Context context) { - return node.copyWith( - blocks: visitNodes(node.blocks, context), - body: visitNode(node.body, context), - ); + return node.copyWith(body: visitNode(node.body, context)); } @override diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 96734b9..6490080 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -9,7 +9,7 @@ final class Parser { Parser(this.environment, {this.path}) : endTokensStack = >[], tagStack = [], - blocks = []; + blocks = {}; final Environment environment; @@ -19,7 +19,7 @@ final class Parser { final List tagStack; - final List blocks; + final Set blocks; Extends? extendsNode; @@ -322,7 +322,7 @@ final class Parser { var token = reader.next(); var name = reader.expect('name'); - if (blocks.any((block) => block.name == name.value)) { + if (!blocks.add(name.value)) { fail("Block '${name.value}' defined twice.", reader.current.line); } @@ -350,15 +350,12 @@ final class Parser { reader.next(); } - var block = Block( + return Block( name: name.value, scoped: scoped, required: required, body: body, ); - - blocks.add(block); - return block; } Extends parseExtends(TokenReader reader) { @@ -1319,20 +1316,7 @@ final class Parser { Node scan(Iterable tokens) { var reader = TokenReader(tokens); var nodes = subParse(reader); - - if (extendsNode case var extendsNode?) { - nodes = [extendsNode]; - } - - if (blocks.isEmpty) { - if (nodes.length == 1) { - return nodes.first; - } - - return Output(nodes: nodes); - } - - return TemplateNode(blocks: blocks.toList(), body: Output(nodes: nodes)); + return Output(nodes: nodes); } List subParse( @@ -1397,6 +1381,6 @@ final class Parser { Node parse(String template) { var tokens = environment.lex(template, path: path); - return scan(tokens); + return TemplateNode(body: scan(tokens)); } } diff --git a/lib/src/renderer.dart b/lib/src/renderer.dart index ca0a9e3..24a0fba 100644 --- a/lib/src/renderer.dart +++ b/lib/src/renderer.dart @@ -2,7 +2,6 @@ import 'dart:math' as math; import 'package:jinja/src/environment.dart'; import 'package:jinja/src/exceptions.dart'; -import 'package:jinja/src/loop.dart'; import 'package:jinja/src/nodes.dart'; import 'package:jinja/src/runtime.dart'; import 'package:jinja/src/tests.dart'; @@ -19,13 +18,6 @@ abstract base class RenderContext extends Context { super.data, }); - @override - RenderContext derived({String? template, Map? data}); - - Object? finalize(Object? object) { - return environment.finalize(this, object); - } - void assignTargets(Object? target, Object? current) { if (target is String) { set(target, current); @@ -46,15 +38,22 @@ abstract base class RenderContext extends Context { } else if (target is NamespaceValue) { var value = resolve(target.name); - if (value is Namespace) { - value[target.item] = current; - } else { - throw TemplateRuntimeError('Non-namespace object'); + if (value is! Namespace) { + throw TemplateRuntimeError('Non-namespace object.'); } + + value[target.item] = current; } else { throw TypeError(); } } + + @override + RenderContext derived({String? template, Map? data}); + + Object? finalize(Object? object) { + return environment.finalize(this, object); + } } base class StringSinkRenderContext extends RenderContext { @@ -87,7 +86,7 @@ base class StringSinkRenderContext extends RenderContext { return StringSinkRenderContext( environment, sink ?? this.sink, - template: template, + template: template ?? this.template, blocks: blocks, parent: parent, data: data, @@ -105,11 +104,11 @@ base class StringSinkRenderer const StringSinkRenderer(); Map getDataForTargets(Object? targets, Object? current) { - if (targets case String string) { - return {string: current}; + if (targets is String) { + return {targets: current}; } - if (targets case List targets) { + if (targets is List) { var names = targets.cast(); var values = list(current); @@ -140,8 +139,8 @@ base class StringSinkRenderer String macro(List positional, Map named) { var buffer = StringBuffer(); var derived = context.derived(sink: buffer); - var index = 0; + var index = 0; var length = node.positional.length; for (; index < length; index += 1) { @@ -258,7 +257,7 @@ base class StringSinkRenderer buffer.write(value.accept(this, context)); } - return '$buffer'; + return buffer.toString(); } @override @@ -387,7 +386,7 @@ base class StringSinkRenderer var derived = context.derived(sink: buffer); node.body.accept(this, derived); - Object? value = '$buffer'; + Object? value = buffer.toString(); var filters = node.filters; @@ -448,7 +447,7 @@ base class StringSinkRenderer var derived = context.derived(sink: buffer); node.body.accept(this, derived); - Object? value = '$buffer'; + Object? value = buffer.toString(); for (var Filter(name: name, calling: calling) in node.filters) { var (positional, named) = calling.accept(this, context) as Parameters; @@ -482,6 +481,7 @@ base class StringSinkRenderer orElse.accept(this, context); } + // Empty string prevents calling `finalize` on `null`. return ''; } @@ -510,6 +510,7 @@ base class StringSinkRenderer node.body.accept(this, forContext); } + // Empty string prevents calling `finalize` on `null`. return ''; } @@ -643,35 +644,57 @@ base class StringSinkRenderer @override void visitTemplateNode(TemplateNode node, StringSinkRenderContext context) { + // TODO(renderer): add `TemplateReference` var self = Namespace(); for (var block in node.blocks) { + var blockName = block.name; + + // TODO(compiler): switch to `ContextCallback` String render() { - var blocks = context.blocks[block.name]; + var blocks = context.blocks[blockName]; - if (blocks == null || blocks.isEmpty) { - throw UndefinedError("Block '${block.name}' is not defined."); + if (blocks == null) { + throw UndefinedError("Block '$blockName' is not defined."); } - blocks.last(context); + // TODO(renderer): check if empty + blocks[0](context); return ''; } - self[block.name] = render; + self[blockName] = render; - var blocks = context.blocks[block.name] ??= []; + var blocks = context.blocks[blockName] ??= []; if (block.required) { - String callback(Context context) { + Never callback(Context context) { throw TemplateRuntimeError( "Required block '${block.name}' not found."); } blocks.add(callback); } else { - String callback(Context context) { + var parentIndex = blocks.length + 1; + + void callback(Context context) { + var current = context.get('super'); + + // TODO(renderer): add `BlockReference` + String parent() { + var blocks = context.blocks[blockName]!; + + if (parentIndex >= blocks.length) { + throw TemplateRuntimeError("Super block '$blockName' not found."); + } + + blocks[parentIndex](context); + return ''; + } + + context.set('super', parent); block.body.accept(this, context); - return ''; + context.set('super', current); } blocks.add(callback); diff --git a/lib/src/runtime.dart b/lib/src/runtime.dart index 07ac9a9..88979d0 100644 --- a/lib/src/runtime.dart +++ b/lib/src/runtime.dart @@ -70,7 +70,11 @@ base class Context { return parent[name]; } - return environment.undefined(name, template); + return undefined(name, template); + } + + Object? get(String key) { + return context[key]; } void set(String key, Object? value) { @@ -115,6 +119,204 @@ base class Context { } } +final class LoopContext extends Iterable { + LoopContext(this.values, this.depth0, this.recurse) + : length = values.length, + index0 = -1; + + final List values; + + @override + final int length; + + final int depth0; + + final String Function(Object? data, [int depth]) recurse; + + int index0; + + @override + LoopIterator get iterator { + return LoopIterator(this); + } + + int get index { + return index0 + 1; + } + + int get depth { + return depth0 + 1; + } + + int get revindex0 { + return length - index; + } + + int get revindex { + return length - index0; + } + + @override + bool get first { + return index0 == 0; + } + + @override + bool get last { + return index == length; + } + + Object? get next { + if (last) { + return null; + } + + return values[index0 + 1]; + } + + Object? get prev { + if (first) { + return null; + } + + return values[index0 - 1]; + } + + Object? operator [](String key) { + switch (key) { + case 'length': + return length; + case 'index0': + return index0; + case 'depth0': + return depth0; + case 'index': + return index; + case 'depth': + return depth; + case 'revindex0': + return revindex0; + case 'revindex': + return revindex; + case 'first': + return first; + case 'last': + return last; + case 'prev': + case 'previtem': + return prev; + case 'next': + case 'nextitem': + return next; + case 'call': + return call; + case 'cycle': + return cycle; + case 'changed': + return changed; + default: + var invocation = Invocation.getter(Symbol(key)); + throw NoSuchMethodError.withInvocation(this, invocation); + } + } + + String call(Object? data) { + return recurse(data, depth); + } + + Object? cycle(Iterable values) { + var list = values.toList(); + + if (list.isEmpty) { + // TODO(loop): update error + throw TypeError(); + } + + return list[index0 % list.length]; + } + + bool changed(Object? item) { + if (index0 == 0) { + return true; + } + + if (item == prev) { + return false; + } + + return true; + } +} + +final class LoopIterator implements Iterator { + LoopIterator(this.context); + + final LoopContext context; + + @override + Object? get current { + return context.values[context.index0]; + } + + @override + bool moveNext() { + if (context.index < context.length) { + context.index0 += 1; + return true; + } + + return false; + } +} + +final class Cycler extends Iterable { + Cycler(Iterable values) + : values = List.of(values), + length = values.length, + index = 0; + + final List values; + + @override + final int length; + + int index; + + Object? get current { + return values[index]; + } + + @override + Iterator get iterator { + return CyclerIterator(this); + } + + Object? next() { + var result = current; + index = (index + 1) % length; + return result; + } + + void reset() { + index = 0; + } +} + +final class CyclerIterator implements Iterator { + CyclerIterator(this.cycler); + + final Cycler cycler; + + @override + Object? current; + + @override + bool moveNext() { + current = cycler.next(); + return true; + } +} + base class Namespace { Namespace([Map? data]) : context = {...?data}; diff --git a/lib/src/visitor.dart b/lib/src/visitor.dart index b28918d..11ca044 100644 --- a/lib/src/visitor.dart +++ b/lib/src/visitor.dart @@ -87,183 +87,183 @@ class ThrowingVisitor implements Visitor { @override R visitArray(Array node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitAttribute(Attribute node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitCall(Call node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitCalling(Calling node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitCompare(Compare node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitConcat(Concat node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitCondition(Condition node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitConstant(Constant node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitDict(Dict node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitFilter(Filter node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitItem(Item node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitLogical(Logical node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitName(Name node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitNamespaceRef(NamespaceRef node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitScalar(Scalar node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitTest(Test node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitTuple(Tuple node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitUnary(Unary node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } // Statements @override R visitAssign(Assign node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitAssignBlock(AssignBlock node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitBlock(Block node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitCallBlock(CallBlock node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitData(Data node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitDo(Do node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitExtends(Extends node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitFilterBlock(FilterBlock node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitFor(For node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitFromImport(FromImport node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitIf(If node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitImport(Import node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitInclude(Include node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitInterpolation(Interpolation node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitMacro(Macro node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitOutput(Output node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitTemplateNode(TemplateNode node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } @override R visitWith(With node, C context) { - throw UnimplementedError('$node'); + throw UnimplementedError(); } } diff --git a/pubspec.yaml b/pubspec.yaml index db1ede8..a4dac83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: jinja -version: 0.6.1-dev.4 +version: 0.6.1-dev.5 description: >- Jinja2 template engine for Dart. Variables, expressions, control structures and template inheritance. diff --git a/test/api_test.dart b/test/api_test.dart index f1b4f34..a6cec19 100644 --- a/test/api_test.dart +++ b/test/api_test.dart @@ -2,7 +2,6 @@ library; import 'package:jinja/jinja.dart'; -import 'package:jinja/src/loop.dart'; import 'package:jinja/src/runtime.dart'; import 'package:test/test.dart'; diff --git a/test/statements/set_test.dart b/test/statements/set_test.dart index 7a95936..bb3d66c 100644 --- a/test/statements/set_test.dart +++ b/test/statements/set_test.dart @@ -30,7 +30,7 @@ void main() { expect( () => tmpl.render({'foo': emptyMap}), throwsA(predicate( - (error) => error.message == 'Non-namespace object'))); + (error) => error.message == 'Non-namespace object.'))); }); test('namespace redefined', () { @@ -39,7 +39,7 @@ void main() { expect( () => tmpl.render({'namespace': () => emptyMap}), throwsA(predicate( - (error) => error.message == 'Non-namespace object'))); + (error) => error.message == 'Non-namespace object.'))); }); test('namespace', () { diff --git a/test/test.dart b/test/test.dart deleted file mode 100644 index 8102a69..0000000 --- a/test/test.dart +++ /dev/null @@ -1,19 +0,0 @@ -// ignore_for_file: avoid_print - -import 'dart:convert'; - -import 'package:jinja/jinja.dart'; - -const Map sources = { - 'include': '{% macro test(foo) %}[{{ foo }}]{% endmacro %}', -}; - -void main() { - var loader = MapLoader(sources); - var env = Environment(loader: loader); - var tmpl = env.fromString(''' -{% from "include" import test -%} -{{ test("foo") }}'''); - print(JsonEncoder.withIndent(' ').convert(env.getTemplate('include').body.toJson())); - print(tmpl.render()); -} From 3fa4a9e46dc28c2c5fe34b291b8ddaca6736a7d4 Mon Sep 17 00:00:00 2001 From: Olzhas Suleimen Date: Fri, 27 Sep 2024 12:00:17 +0500 Subject: [PATCH 4/7] Update CI configuration. --- .github/workflows/test.yaml | 1 + .github/workflows/test_web.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 03db21a..da07aeb 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,6 +4,7 @@ on: push: branches: - main + - dev pull_request: defaults: diff --git a/.github/workflows/test_web.yaml b/.github/workflows/test_web.yaml index 8b44009..8b5092e 100644 --- a/.github/workflows/test_web.yaml +++ b/.github/workflows/test_web.yaml @@ -4,6 +4,7 @@ on: push: branches: - main + - dev pull_request: defaults: From a6b7c7fbb11684c0b8ef07e6e6fd5e2b367092e7 Mon Sep 17 00:00:00 2001 From: Olzhas Suleimen Date: Fri, 27 Sep 2024 12:32:47 +0500 Subject: [PATCH 5/7] Update CI configuration *. --- .github/workflows/test_web.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_web.yaml b/.github/workflows/test_web.yaml index 8b5092e..ac8ad1d 100644 --- a/.github/workflows/test_web.yaml +++ b/.github/workflows/test_web.yaml @@ -1,4 +1,4 @@ -name: test +name: test_web on: push: From 45f2ce396fb4fea2c6da2a8ef81af872fbd7523f Mon Sep 17 00:00:00 2001 From: Olzhas Suleimen Date: Fri, 27 Sep 2024 12:40:46 +0500 Subject: [PATCH 6/7] Update CHANGELOG.md. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0e267c..048b227 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 0.6.1-dev.4 ([diff](https://github.com/ykmnkmi/jinja.dart/compare/88996f8..main)) +## 0.6.1-dev.5 ([diff](https://github.com/ykmnkmi/jinja.dart/compare/88996f8..main)) - Added: - `UndefinedError` exception - `UndefinedFactory` typedef From b4a408bf5694fe5bd4e406a947e484e4b97c1877 Mon Sep 17 00:00:00 2001 From: Olzhas Suleimen Date: Fri, 27 Sep 2024 12:43:30 +0500 Subject: [PATCH 7/7] Update CI configuration **. --- .github/workflows/test.yaml | 6 ++++++ .github/workflows/test_web.yaml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index da07aeb..d4e1500 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -5,6 +5,12 @@ on: branches: - main - dev + paths: + - .github/workflows/** + - lib/** + - test/** + - analysis_options.yaml + - pubspec.yaml pull_request: defaults: diff --git a/.github/workflows/test_web.yaml b/.github/workflows/test_web.yaml index ac8ad1d..07f3d12 100644 --- a/.github/workflows/test_web.yaml +++ b/.github/workflows/test_web.yaml @@ -5,6 +5,12 @@ on: branches: - main - dev + paths: + - .github/workflows/** + - lib/** + - test/** + - analysis_options.yaml + - pubspec.yaml pull_request: defaults: