Root README.md

This commit is contained in:
Tobe O 2017-10-02 11:46:00 -04:00
parent 72844bf6c8
commit 5f18cce1e0
12 changed files with 268 additions and 34 deletions

View file

@ -1,2 +1,24 @@
# jael
A simple server-side HTML templating engine for Dart.
[![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.

View file

@ -47,4 +47,4 @@ void myFunction() {
```
Pre-processing (i.e. handling of blocks and includes) is handled
by `package:jael_processor.`.
by `package:jael_preprocessor.`.

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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) {

View file

@ -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) {

View file

@ -28,6 +28,7 @@ final Map<Pattern, TokenType> _htmlPatterns = {
'>': TokenType.gt,
'/': TokenType.slash,
'=': TokenType.equals,
'!=': TokenType.nequ,
_string1: TokenType.string,
_string2: TokenType.string,
new RegExp(r'<script[^>]*>[^$]*</script>'): 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;

View file

@ -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 <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/jael/tree/master/jael

View file

@ -161,6 +161,111 @@ main() {
'''
.trim());
});
test('declare', () {
const template = '''
<div>
<declare one=1 two=2 three=3>
<ul>
<li>{{one}}</li>
<li>{{two}}</li>
<li>{{three}}</li>
</ul>
<ul>
<declare three=4>
<li>{{one}}</li>
<li>{{two}}</li>
<li>{{three}}</li>
</declare>
</ul>
</declare>
</div>
''';
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(),
'''
<div>
<ul>
<li>
1
</li>
<li>
2
</li>
<li>
3
</li>
</ul>
<ul>
<li>
1
</li>
<li>
2
</li>
<li>
4
</li>
</ul>
</div>
'''
.trim());
});
test('unescaped attr/interp', () {
const template = '''
<div>
<img src!="<SCARY XSS>" />
{{- "<MORE SCARY XSS>" }}
</div>
''';
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(),
'''
<div>
<img src="<SCARY XSS>">
<MORE SCARY XSS>
</div>
'''
.trim());
});
test('quoted attribute name', () {
const template = '''
<button '(click)'="myEventHandler(\$event)"></button>
''';
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(),
'''
<button (click)="myEventHandler(\$event)">
</button>
'''
.trim());
});
}
const List<_Pokemon> starters = const [

View file

@ -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`.**

View file

@ -3,15 +3,29 @@ import 'dart:collection';
import 'package:file/file.dart';
import 'package:jael/jael.dart';
/// Modifies a Jael document.
typedef FutureOr<Document> 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<Document> resolve(Document document, Directory currentDirectory,
{void onError(JaelError error)}) async {
{void onError(JaelError error), Iterable<Patcher> 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.

View file

@ -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 <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/jael/tree/master/jael_processor