Root README.md
This commit is contained in:
parent
72844bf6c8
commit
5f18cce1e0
12 changed files with 268 additions and 34 deletions
22
README.md
22
README.md
|
@ -1,2 +1,24 @@
|
||||||
# jael
|
# jael
|
||||||
|
[![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.
|
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.
|
|
@ -47,4 +47,4 @@ void myFunction() {
|
||||||
```
|
```
|
||||||
|
|
||||||
Pre-processing (i.e. handling of blocks and includes) is handled
|
Pre-processing (i.e. handling of blocks and includes) is handled
|
||||||
by `package:jael_processor.`.
|
by `package:jael_preprocessor.`.
|
|
@ -2,18 +2,28 @@ import 'package:source_span/source_span.dart';
|
||||||
import 'ast_node.dart';
|
import 'ast_node.dart';
|
||||||
import 'expression.dart';
|
import 'expression.dart';
|
||||||
import 'identifier.dart';
|
import 'identifier.dart';
|
||||||
|
import 'string.dart';
|
||||||
import 'token.dart';
|
import 'token.dart';
|
||||||
|
|
||||||
class Attribute extends AstNode {
|
class Attribute extends AstNode {
|
||||||
final Identifier name;
|
final Identifier id;
|
||||||
final Token equals;
|
final StringLiteral string;
|
||||||
|
final Token equals, nequ;
|
||||||
final Expression value;
|
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
|
@override
|
||||||
FileSpan get span {
|
FileSpan get span {
|
||||||
if (equals == null) return name.span;
|
if (equals == null) return nameNode.span;
|
||||||
return name.span.expand(equals.span).expand(value.span);
|
return nameNode.span
|
||||||
|
.expand(equals?.span ?? nequ.span)
|
||||||
|
.expand(value.span);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,10 @@ class Identifier extends Expression {
|
||||||
return false;
|
return false;
|
||||||
default:
|
default:
|
||||||
var symbol = scope.resolve(name);
|
var symbol = scope.resolve(name);
|
||||||
if (symbol == null)
|
if (symbol == null) {
|
||||||
throw new ArgumentError(
|
throw new ArgumentError(
|
||||||
'The name "$name" does not exist in this scope.');
|
'The name "$name" does not exist in this scope.');
|
||||||
|
}
|
||||||
return scope
|
return scope
|
||||||
.resolve(name)
|
.resolve(name)
|
||||||
.value;
|
.value;
|
||||||
|
|
|
@ -35,22 +35,27 @@ class Renderer {
|
||||||
|
|
||||||
void renderElement(
|
void renderElement(
|
||||||
Element element, CodeBuffer buffer, SymbolTable scope, bool html5) {
|
Element element, CodeBuffer buffer, SymbolTable scope, bool html5) {
|
||||||
if (element.attributes.any((a) => a.name.name == 'for-each')) {
|
var childScope = scope.createChild();
|
||||||
renderForeach(element, buffer, scope, html5);
|
|
||||||
|
if (element.attributes.any((a) => a.name == 'for-each')) {
|
||||||
|
renderForeach(element, buffer, childScope, html5);
|
||||||
return;
|
return;
|
||||||
} else if (element.attributes.any((a) => a.name.name == 'if')) {
|
} else if (element.attributes.any((a) => a.name == 'if')) {
|
||||||
renderIf(element, buffer, scope, html5);
|
renderIf(element, buffer, childScope, html5);
|
||||||
|
return;
|
||||||
|
} else if (element.tagName.name == 'declare') {
|
||||||
|
renderDeclare(element, buffer, childScope, html5);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer..write('<')..write(element.tagName.name);
|
buffer..write('<')..write(element.tagName.name);
|
||||||
|
|
||||||
for (var attribute in element.attributes) {
|
for (var attribute in element.attributes) {
|
||||||
var value = attribute.value?.compute(scope);
|
var value = attribute.value?.compute(childScope);
|
||||||
|
|
||||||
if (value == false || value == null) continue;
|
if (value == false || value == null) continue;
|
||||||
|
|
||||||
buffer.write(' ${attribute.name.name}');
|
buffer.write(' ${attribute.name}');
|
||||||
|
|
||||||
if (value == true)
|
if (value == true)
|
||||||
continue;
|
continue;
|
||||||
|
@ -71,7 +76,7 @@ class Renderer {
|
||||||
msg = value.toString();
|
msg = value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.write(HTML_ESCAPE.convert(msg));
|
buffer.write(attribute.isRaw ? msg : HTML_ESCAPE.convert(msg));
|
||||||
buffer.write('"');
|
buffer.write('"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +92,7 @@ class Renderer {
|
||||||
for (int i = 0; i < element.children.length; i++) {
|
for (int i = 0; i < element.children.length; i++) {
|
||||||
var child = element.children.elementAt(i);
|
var child = element.children.elementAt(i);
|
||||||
renderElementChild(
|
renderElementChild(
|
||||||
child, buffer, scope, html5, i, element.children.length);
|
child, buffer, childScope, html5, i, element.children.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.writeln();
|
buffer.writeln();
|
||||||
|
@ -98,15 +103,14 @@ class Renderer {
|
||||||
|
|
||||||
void renderForeach(
|
void renderForeach(
|
||||||
Element element, CodeBuffer buffer, SymbolTable scope, bool html5) {
|
Element element, CodeBuffer buffer, SymbolTable scope, bool html5) {
|
||||||
var attribute =
|
var attribute = element.attributes.singleWhere((a) => a.name == 'for-each');
|
||||||
element.attributes.singleWhere((a) => a.name.name == 'for-each');
|
|
||||||
if (attribute.value == null) return;
|
if (attribute.value == null) return;
|
||||||
|
|
||||||
var asAttribute = element.attributes
|
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 alias = asAttribute?.value?.compute(scope) ?? 'item';
|
||||||
var otherAttributes = element.attributes
|
var otherAttributes =
|
||||||
.where((a) => a.name.name != 'for-each' && a.name.name != 'as');
|
element.attributes.where((a) => a.name != 'for-each' && a.name != 'as');
|
||||||
Element strippedElement;
|
Element strippedElement;
|
||||||
|
|
||||||
if (element is SelfClosingElement)
|
if (element is SelfClosingElement)
|
||||||
|
@ -132,11 +136,11 @@ class Renderer {
|
||||||
|
|
||||||
void renderIf(
|
void renderIf(
|
||||||
Element element, CodeBuffer buffer, SymbolTable scope, bool html5) {
|
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;
|
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;
|
Element strippedElement;
|
||||||
|
|
||||||
if (element is SelfClosingElement)
|
if (element is SelfClosingElement)
|
||||||
|
@ -157,6 +161,19 @@ class Renderer {
|
||||||
renderElement(strippedElement, buffer, scope, html5);
|
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,
|
void renderElementChild(ElementChild child, CodeBuffer buffer,
|
||||||
SymbolTable scope, bool html5, int index, int total) {
|
SymbolTable scope, bool html5, int index, int total) {
|
||||||
if (child is Text) {
|
if (child is Text) {
|
||||||
|
|
|
@ -280,21 +280,36 @@ class Parser {
|
||||||
}
|
}
|
||||||
|
|
||||||
Attribute parseAttribute() {
|
Attribute parseAttribute() {
|
||||||
var name = parseIdentifier();
|
Identifier id;
|
||||||
if (name == null) return null;
|
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);
|
var value = parseExpression(0);
|
||||||
|
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
errors.add(new JaelError(JaelErrorSeverity.error,
|
errors.add(new JaelError(JaelErrorSeverity.error,
|
||||||
'Missing expression in attribute.', equals.span));
|
'Missing expression in attribute.', equals?.span ?? nequ.span));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Attribute(name, equals, value);
|
return new Attribute(id, string, equals, nequ, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression parseExpression(int precedence) {
|
Expression parseExpression(int precedence) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ final Map<Pattern, TokenType> _htmlPatterns = {
|
||||||
'>': TokenType.gt,
|
'>': TokenType.gt,
|
||||||
'/': TokenType.slash,
|
'/': TokenType.slash,
|
||||||
'=': TokenType.equals,
|
'=': TokenType.equals,
|
||||||
|
'!=': TokenType.nequ,
|
||||||
_string1: TokenType.string,
|
_string1: TokenType.string,
|
||||||
_string2: TokenType.string,
|
_string2: TokenType.string,
|
||||||
new RegExp(r'<script[^>]*>[^$]*</script>'): TokenType.script_tag,
|
new RegExp(r'<script[^>]*>[^$]*</script>'): TokenType.script_tag,
|
||||||
|
@ -123,7 +124,7 @@ class _Scanner implements Scanner {
|
||||||
|
|
||||||
var lastToken = _scanFrom(_htmlPatterns, textStart);
|
var lastToken = _scanFrom(_htmlPatterns, textStart);
|
||||||
|
|
||||||
if (lastToken?.type == TokenType.equals) {
|
if (lastToken?.type == TokenType.equals || lastToken?.type == TokenType.nequ) {
|
||||||
textStart = null;
|
textStart = null;
|
||||||
scanExpressionTokens();
|
scanExpressionTokens();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: jael
|
name: jael
|
||||||
version: 1.0.0-alpha+3
|
version: 1.0.0-alpha+4
|
||||||
description: A simple server-side HTML templating engine for Dart.
|
description: A simple server-side HTML templating engine for Dart.
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/jael/tree/master/jael
|
homepage: https://github.com/angel-dart/jael/tree/master/jael
|
||||||
|
|
|
@ -161,6 +161,111 @@ main() {
|
||||||
'''
|
'''
|
||||||
.trim());
|
.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 [
|
const List<_Pokemon> starters = const [
|
||||||
|
|
49
jael_preprocessor/README.md
Normal file
49
jael_preprocessor/README.md
Normal 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`.**
|
|
@ -3,15 +3,29 @@ import 'dart:collection';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:jael/jael.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.
|
/// 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,
|
Future<Document> resolve(Document document, Directory currentDirectory,
|
||||||
{void onError(JaelError error)}) async {
|
{void onError(JaelError error), Iterable<Patcher> patch}) async {
|
||||||
onError ?? (e) => throw e;
|
onError ?? (e) => throw e;
|
||||||
|
|
||||||
// Resolve all includes...
|
// Resolve all includes...
|
||||||
var includesResolved =
|
var includesResolved =
|
||||||
await resolveIncludes(document, currentDirectory, onError);
|
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.
|
/// Folds any `extend` declarations.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: jael_preprocessor
|
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.
|
description: A pre-processor for resolving blocks and includes within Jael templates.
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/jael/tree/master/jael_processor
|
homepage: https://github.com/angel-dart/jael/tree/master/jael_processor
|
||||||
|
|
Loading…
Reference in a new issue