Root README.md
This commit is contained in:
parent
72844bf6c8
commit
5f18cce1e0
12 changed files with 268 additions and 34 deletions
24
README.md
24
README.md
|
@ -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.
|
|
@ -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.`.
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 [
|
||||
|
|
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: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.
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue