Jael 3

Pub Version (including pre-releases) Null Safety Discord License

A simple server-side HTML templating engine for Dart.

See documentation.

Installation

In your pubspec.yaml:

dependencies:
  jael3: ^10.0.0

Usage

The core jael3 package exports classes for parsing Jael templates, an AST library, and a Renderer class that generates HTML on-the-fly.

import 'package:belatuk_code_buffer/belatuk_code_buffer.dart';
import 'package:belatuk_symbol_table/belatuk_symbol_table.dart';
import 'package:jael3/jael3.dart' as jael;

void myFunction() {
    const template = '''
<html>
  <body>
    <h1>Hello</h1>
    <img src=profile['avatar']>
  </body>
</html>
''';

    var buf = CodeBuffer();
    var document = jael.parseDocument(template, sourceUrl: 'test.jael', asDSX: false);
    var scope = SymbolTable(values: {
      'profile': {
        'avatar': 'thosakwe.png',
      }
    });

    const jael.Renderer().render(document, buf, scope);
    print(buf);
}

Pre-processing (i.e. handling of blocks and includes) is handled by package:jael3_preprocessor..

Members of values

{{ user.name }} reads the member name of user, of the first of these that has it:

  1. the memberResolver given to Renderer.render (angel3_jael gives one that reads the members through the app's reflector);
  2. a key of a Map (a key wins over a member of Map: {{ m.length }} of {'length': 1} is 1);
  3. a common member of a core type: of String, Iterable and List, Map, MapEntry, num and int, DateTime and Duration, such as length, first, join, toUpperCase, toInt, toStringAsFixed or year;
  4. a key of the value's toJson(), as of an @serializable model.

jael3 does not use dart:mirrors, so a getter of another class is read only through a memberResolver, which returns noMember for those it does not know, or toJson():

const jael.Renderer().render(
  document,
  buf,
  scope,
  memberResolver: (target, name) => switch ((target, name)) {
    (User u, 'name') => u.name,
    (User u, 'isAdmin') => u.isAdmin,
    _ => jael.noMember,
  },
);

The toJson() of a value is called once in a render, for all its members. As in Dart, first, last and single of an empty list throw: test isEmpty first, as in <p if=items.isNotEmpty>{{ items.first }}</p>.

With strictResolution: false, an unknown member, or a member of null, is null, as an unknown name is; otherwise it throws a NoSuchMethodError.

Expressions

  • ?. makes the rest of a chain null if its target is: {{ user?.name.length }}, {{ user?.greet() }} and {{ user?.tags[0] }} are null if user is.
  • The operators are *, /, %, +, -, <, <=, >, >=, ==, !=, &&, ||, ??, ! and ? :. &&, || and ?? compute their right side only if needed. With strictResolution: false, !, && and || take only true as true.
  • Map literals, {'a': 1, key: value}, and calls with named arguments, {{ pad('x', width: 3) }}, are supported.

Migrating from 9.x to 10.0.0

The breaking changes of each release are listed in the CHANGELOG. Those of 10.0.0 that may need changes in an app:

  • No dart:mirrors: {{ a.b }} reads b from the memberResolver given to Renderer.render, a key of a Map, a common member of a core type, or a key of the value's toJson(). Getters of other classes need a memberResolver or a toJson(); see Members of values. angel3_jael gives a resolver of the app's reflector.
  • new is removed (NewExpression, TokenType.$new), as it could never be parsed. Put a function that makes the value in the scope instead.
  • strictResolution: false makes an unknown member, or a member of null, null.
  • readMember takes resolver as a named parameter, and the new tryReadMember returns noMember for a member it does not know.

Libraries

jael3