diff --git a/README.md b/README.md index 00bcf26d..c934299c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,24 @@ # jael -A simple server-side HTML templating engine for Dart. \ No newline at end of file +[![Pub](https://img.shields.io/pub/v/jael.svg)](https://pub.dartlang.org/packages/jael) +[![build status](https://travis-ci.org/angel-dart/jael.svg)](https://travis-ci.org/angel-dart/jael) + +A simple server-side HTML templating engine for Dart. + +Though its syntax is but a superset of HTML, it supports features such as: +* Loops +* Conditionals +* Template inheritance +* Block scoping +* `switch` syntax +* Interpolation of any Dart expression + +Jael is a good choice for applications of any scale, especially when the development team is small, +or the time invested in building an SPA would be too much. + +## This Repository +Within this repository are three packages: + +* `package:jael` - Contains the Jael parser, AST, and HTML renderer. +* `package:jael_preprocessor` - Handles template inheritance, and facilitates the use of "compile-time" constructs. +* `package:angel_jael` - [Angel](https://angel-dart.github.io) support for Jael. Angel contains other +facilities to speed up application development, so something like Jael is right at home. \ No newline at end of file diff --git a/jael/README.md b/jael/README.md index ee69cfe1..7dbfc6f6 100644 --- a/jael/README.md +++ b/jael/README.md @@ -47,4 +47,4 @@ void myFunction() { ``` Pre-processing (i.e. handling of blocks and includes) is handled -by `package:jael_processor.`. \ No newline at end of file +by `package:jael_preprocessor.`. \ No newline at end of file diff --git a/jael/lib/src/ast/attribute.dart b/jael/lib/src/ast/attribute.dart index 28b02fce..53a0c8c0 100644 --- a/jael/lib/src/ast/attribute.dart +++ b/jael/lib/src/ast/attribute.dart @@ -2,18 +2,28 @@ import 'package:source_span/source_span.dart'; import 'ast_node.dart'; import 'expression.dart'; import 'identifier.dart'; +import 'string.dart'; import 'token.dart'; class Attribute extends AstNode { - final Identifier name; - final Token equals; + final Identifier id; + final StringLiteral string; + final Token equals, nequ; final Expression value; - Attribute(this.name, this.equals, this.value); + Attribute(this.id, this.string, this.equals, this.nequ, this.value); + + bool get isRaw => nequ != null; + + Expression get nameNode => id ?? string; + + String get name => string?.value ?? id.name; @override FileSpan get span { - if (equals == null) return name.span; - return name.span.expand(equals.span).expand(value.span); + if (equals == null) return nameNode.span; + return nameNode.span + .expand(equals?.span ?? nequ.span) + .expand(value.span); } } diff --git a/jael/lib/src/ast/identifier.dart b/jael/lib/src/ast/identifier.dart index c1794827..aa9a0f2b 100644 --- a/jael/lib/src/ast/identifier.dart +++ b/jael/lib/src/ast/identifier.dart @@ -19,9 +19,10 @@ class Identifier extends Expression { return false; default: var symbol = scope.resolve(name); - if (symbol == null) + if (symbol == null) { throw new ArgumentError( 'The name "$name" does not exist in this scope.'); + } return scope .resolve(name) .value; diff --git a/jael/lib/src/renderer.dart b/jael/lib/src/renderer.dart index 73711ba3..81084bdc 100644 --- a/jael/lib/src/renderer.dart +++ b/jael/lib/src/renderer.dart @@ -35,22 +35,27 @@ class Renderer { void renderElement( Element element, CodeBuffer buffer, SymbolTable scope, bool html5) { - if (element.attributes.any((a) => a.name.name == 'for-each')) { - renderForeach(element, buffer, scope, html5); + var childScope = scope.createChild(); + + if (element.attributes.any((a) => a.name == 'for-each')) { + renderForeach(element, buffer, childScope, html5); return; - } else if (element.attributes.any((a) => a.name.name == 'if')) { - renderIf(element, buffer, scope, html5); + } else if (element.attributes.any((a) => a.name == 'if')) { + renderIf(element, buffer, childScope, html5); + return; + } else if (element.tagName.name == 'declare') { + renderDeclare(element, buffer, childScope, html5); return; } buffer..write('<')..write(element.tagName.name); for (var attribute in element.attributes) { - var value = attribute.value?.compute(scope); + var value = attribute.value?.compute(childScope); if (value == false || value == null) continue; - buffer.write(' ${attribute.name.name}'); + buffer.write(' ${attribute.name}'); if (value == true) continue; @@ -71,7 +76,7 @@ class Renderer { msg = value.toString(); } - buffer.write(HTML_ESCAPE.convert(msg)); + buffer.write(attribute.isRaw ? msg : HTML_ESCAPE.convert(msg)); buffer.write('"'); } @@ -87,7 +92,7 @@ class Renderer { for (int i = 0; i < element.children.length; i++) { var child = element.children.elementAt(i); renderElementChild( - child, buffer, scope, html5, i, element.children.length); + child, buffer, childScope, html5, i, element.children.length); } buffer.writeln(); @@ -98,15 +103,14 @@ class Renderer { void renderForeach( Element element, CodeBuffer buffer, SymbolTable scope, bool html5) { - var attribute = - element.attributes.singleWhere((a) => a.name.name == 'for-each'); + var attribute = element.attributes.singleWhere((a) => a.name == 'for-each'); if (attribute.value == null) return; var asAttribute = element.attributes - .firstWhere((a) => a.name.name == 'as', orElse: () => null); + .firstWhere((a) => a.name == 'as', orElse: () => null); var alias = asAttribute?.value?.compute(scope) ?? 'item'; - var otherAttributes = element.attributes - .where((a) => a.name.name != 'for-each' && a.name.name != 'as'); + var otherAttributes = + element.attributes.where((a) => a.name != 'for-each' && a.name != 'as'); Element strippedElement; if (element is SelfClosingElement) @@ -132,11 +136,11 @@ class Renderer { void renderIf( Element element, CodeBuffer buffer, SymbolTable scope, bool html5) { - var attribute = element.attributes.singleWhere((a) => a.name.name == 'if'); + var attribute = element.attributes.singleWhere((a) => a.name == 'if'); if (!attribute.value.compute(scope)) return; - var otherAttributes = element.attributes.where((a) => a.name.name != 'if'); + var otherAttributes = element.attributes.where((a) => a.name != 'if'); Element strippedElement; if (element is SelfClosingElement) @@ -157,6 +161,19 @@ class Renderer { renderElement(strippedElement, buffer, scope, html5); } + void renderDeclare(Element element, CodeBuffer buffer, SymbolTable scope, bool html5) { + for (var attribute in element.attributes) { + scope.add(attribute.name, + value: attribute.value?.compute(scope), constant: true); + } + + for (int i = 0; i < element.children.length; i++) { + var child = element.children.elementAt(i); + renderElementChild( + child, buffer, scope, html5, i, element.children.length); + } + } + void renderElementChild(ElementChild child, CodeBuffer buffer, SymbolTable scope, bool html5, int index, int total) { if (child is Text) { diff --git a/jael/lib/src/text/parser.dart b/jael/lib/src/text/parser.dart index bf7dc4cd..9b32590b 100644 --- a/jael/lib/src/text/parser.dart +++ b/jael/lib/src/text/parser.dart @@ -280,21 +280,36 @@ class Parser { } Attribute parseAttribute() { - var name = parseIdentifier(); - if (name == null) return null; + Identifier id; + StringLiteral string; - if (!next(TokenType.equals)) return new Attribute(name, null, null); + if ((id = parseIdentifier()) != null) { + // Nothing + } else if (next(TokenType.string)) { + string = new StringLiteral(_current, StringLiteral.parseValue(_current)); + } else { + return null; + } + + Token equals, nequ; + + if (next(TokenType.equals)) { + equals = _current; + } else if (next(TokenType.nequ)) { + nequ = _current; + } else { + return new Attribute(id, string, null, null, null); + } - var equals = _current; var value = parseExpression(0); if (value == null) { errors.add(new JaelError(JaelErrorSeverity.error, - 'Missing expression in attribute.', equals.span)); + 'Missing expression in attribute.', equals?.span ?? nequ.span)); return null; } - return new Attribute(name, equals, value); + return new Attribute(id, string, equals, nequ, value); } Expression parseExpression(int precedence) { diff --git a/jael/lib/src/text/scanner.dart b/jael/lib/src/text/scanner.dart index 264bd415..32cea084 100644 --- a/jael/lib/src/text/scanner.dart +++ b/jael/lib/src/text/scanner.dart @@ -28,6 +28,7 @@ final Map _htmlPatterns = { '>': TokenType.gt, '/': TokenType.slash, '=': TokenType.equals, + '!=': TokenType.nequ, _string1: TokenType.string, _string2: TokenType.string, new RegExp(r']*>[^$]*'): TokenType.script_tag, @@ -123,7 +124,7 @@ class _Scanner implements Scanner { var lastToken = _scanFrom(_htmlPatterns, textStart); - if (lastToken?.type == TokenType.equals) { + if (lastToken?.type == TokenType.equals || lastToken?.type == TokenType.nequ) { textStart = null; scanExpressionTokens(); return; diff --git a/jael/pubspec.yaml b/jael/pubspec.yaml index 31c82148..78d43094 100644 --- a/jael/pubspec.yaml +++ b/jael/pubspec.yaml @@ -1,5 +1,5 @@ name: jael -version: 1.0.0-alpha+3 +version: 1.0.0-alpha+4 description: A simple server-side HTML templating engine for Dart. author: Tobe O homepage: https://github.com/angel-dart/jael/tree/master/jael diff --git a/jael/test/render/render_test.dart b/jael/test/render/render_test.dart index 0c00860c..67532822 100644 --- a/jael/test/render/render_test.dart +++ b/jael/test/render/render_test.dart @@ -161,6 +161,111 @@ main() { ''' .trim()); }); + + test('declare', () { + const template = ''' +
+ +
    +
  • {{one}}
  • +
  • {{two}}
  • +
  • {{three}}
  • +
+
    + +
  • {{one}}
  • +
  • {{two}}
  • +
  • {{three}}
  • +
    +
+
+
+'''; + + var buf = new CodeBuffer(); + var document = jael.parseDocument(template, sourceUrl: 'test.jl'); + var scope = new SymbolTable(); + + const jael.Renderer().render(document, buf, scope); + print(buf); + + expect( + buf.toString(), + ''' +
+
    +
  • + 1 +
  • +
  • + 2 +
  • +
  • + 3 +
  • +
+
    +
  • + 1 +
  • +
  • + 2 +
  • +
  • + 4 +
  • +
+
+''' + .trim()); + }); + + test('unescaped attr/interp', () { + const template = ''' +
+ + {{- "" }} +
+'''; + + var buf = new CodeBuffer(); + var document = jael.parseDocument(template, sourceUrl: 'test.jl'); + var scope = new SymbolTable(); + + const jael.Renderer().render(document, buf, scope); + print(buf); + + expect( + buf.toString(), + ''' +
+ + +
+''' + .trim()); + }); + + test('quoted attribute name', () { + const template = ''' + +'''; + + var buf = new CodeBuffer(); + var document = jael.parseDocument(template, sourceUrl: 'test.jl'); + var scope = new SymbolTable(); + + const jael.Renderer().render(document, buf, scope); + print(buf); + + expect( + buf.toString(), + ''' + +''' + .trim()); + }); } const List<_Pokemon> starters = const [ diff --git a/jael_preprocessor/README.md b/jael_preprocessor/README.md new file mode 100644 index 00000000..55f86328 --- /dev/null +++ b/jael_preprocessor/README.md @@ -0,0 +1,49 @@ +# jael_preprocessor +[![Pub](https://img.shields.io/pub/v/jael_preprocessor.svg)](https://pub.dartlang.org/packages/jael_preprocessor) +[![build status](https://travis-ci.org/angel-dart/jael.svg)](https://travis-ci.org/angel-dart/jael) + +A pre-processor for resolving blocks and includes within +[Jael](https://github.com/angel-dart/jael) templates. + +# Installation +In your `pubspec.yaml`: + +```yaml +dependencies: + jael_prepreprocessor: ^1.0.0-alpha +``` + +# Usage +It is unlikely that you will directly use this package, as it is +more of an implementation detail than a requirement. However, it +is responsible for handling `include` and `block` directives +(template inheritance), so you are a package maintainer and want +to support Jael, read on. + +To keep things simple, just use the `resolve` function, which will +take care of inheritance for you. + +```dart +import 'package:jael_preprocessor/jael_preprocessor.dart' as jael; + +myFunction() async { + var doc = await parseTemplateSomehow(); + var resolved = await jael.resolve(doc, dir, onError: (e) => doSomething()); +} +``` + +You may occasionally need to manually patch in functionality that is not +available through the official Jael packages. To achieve this, simply +provide an `Iterable` of `Patcher` functions: + +```dart +myOtherFunction(jael.Document doc) { + return jael.resolve(doc, dir, onError: errorHandler, patch: [ + syntactic(), + sugar(), + etc(), + ]); +} +``` + +**This package uses `package:file`, rather than `dart:io`.** \ No newline at end of file diff --git a/jael_preprocessor/lib/jael_preprocessor.dart b/jael_preprocessor/lib/jael_preprocessor.dart index c0203808..270d61dc 100644 --- a/jael_preprocessor/lib/jael_preprocessor.dart +++ b/jael_preprocessor/lib/jael_preprocessor.dart @@ -3,15 +3,29 @@ import 'dart:collection'; import 'package:file/file.dart'; import 'package:jael/jael.dart'; +/// Modifies a Jael document. +typedef FutureOr Patcher(Document document, Directory currentDirectory, void onError(JaelError)); + /// Expands all `block[name]` tags within the template, replacing them with the correct content. +/// +/// To apply additional patches to resolved documents, provide a set of [patch] +/// functions. Future resolve(Document document, Directory currentDirectory, - {void onError(JaelError error)}) async { + {void onError(JaelError error), Iterable patch}) async { onError ?? (e) => throw e; // Resolve all includes... var includesResolved = await resolveIncludes(document, currentDirectory, onError); - return await applyInheritance(includesResolved, currentDirectory, onError); + var patched = await applyInheritance(includesResolved, currentDirectory, onError); + + if (patch?.isNotEmpty != true) return patched; + + for (var p in patch) { + patched = await p(patched, currentDirectory, onError); + } + + return patched; } /// Folds any `extend` declarations. diff --git a/jael_preprocessor/pubspec.yaml b/jael_preprocessor/pubspec.yaml index 6d63db9e..8325d939 100644 --- a/jael_preprocessor/pubspec.yaml +++ b/jael_preprocessor/pubspec.yaml @@ -1,5 +1,5 @@ name: jael_preprocessor -version: 1.0.0-alpha +version: 1.0.0-alpha+1 description: A pre-processor for resolving blocks and includes within Jael templates. author: Tobe O homepage: https://github.com/angel-dart/jael/tree/master/jael_processor