core nearly done

This commit is contained in:
Tobe O 2017-09-29 18:39:37 -04:00
commit d2e700edd1
43 changed files with 2157 additions and 0 deletions

51
.gitignore vendored Normal file
View file

@ -0,0 +1,51 @@
# Created by .ignore support plugin (hsz.mobi)
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# CMake
cmake-build-debug/
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

6
.idea/misc.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/jael.iml" filepath="$PROJECT_DIR$/jael.iml" />
</modules>
</component>
</project>

2
.travis.yml Normal file
View file

@ -0,0 +1,2 @@
language: dart
script: bash ./travis.sh

2
README.md Normal file
View file

@ -0,0 +1,2 @@
# jael
A simple server-side HTML templating engine for Dart.

14
angel_jael/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
# Created by .ignore support plugin (hsz.mobi)
### Dart template
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/

View file

@ -0,0 +1,2 @@
analyzer:
strong-mode: true

14
jael.iml Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$/angel_jael" />
<content url="file://$MODULE_DIR$/jael">
<excludeFolder url="file://$MODULE_DIR$/jael/.pub" />
<excludeFolder url="file://$MODULE_DIR$/jael/build" />
</content>
<content url="file://$MODULE_DIR$/jael_preprocessor" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
</component>
</module>

14
jael/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
# Created by .ignore support plugin (hsz.mobi)
### Dart template
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/

50
jael/README.md Normal file
View file

@ -0,0 +1,50 @@
# 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.
[See documentation.](https://github.com/angel-dart/jael/wiki)
# Installation
In your `pubspec.yaml`:
```yaml
dependencies:
jael: ^1.0.0-alpha
```
# API
The core `jael` package exports classes for parsing Jael templates,
an AST library, and a `Renderer` class that generates HTML on-the-fly.
```dart
import 'package:code_buffer/code_buffer.dart';
import 'package:jael/jael.dart' as jael;
import 'package:symbol_table/symbol_table.dart';
void myFunction() {
const template = '''
<html>
<body>
<h1>Hello</h1>
<img src=profile['avatar']>
</body>
</html>
''';
var buf = new CodeBuffer();
var document = jael.parseDocument(template, sourceUrl: 'test.jl');
var scope = new 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:jael_processor.`.

View file

@ -0,0 +1,2 @@
analyzer:
strong-mode: true

4
jael/lib/jael.dart Normal file
View file

@ -0,0 +1,4 @@
export 'src/ast/ast.dart';
export 'src/text/parser.dart';
export 'src/text/scanner.dart';
export 'src/renderer.dart';

View file

@ -0,0 +1,41 @@
import 'package:source_span/source_span.dart';
import 'expression.dart';
import 'token.dart';
class Array extends Expression {
final Token lBracket, rBracket;
final List<Expression> items;
Array(this.lBracket, this.rBracket, this.items);
@override
compute(scope) => items.map((e) => e.compute(scope)).toList();
@override
FileSpan get span {
return items
.fold<FileSpan>(lBracket.span, (out, i) => out.expand(i.span))
.expand(rBracket.span);
}
}
class IndexerExpression extends Expression {
final Expression target, indexer;
final Token lBracket, rBracket;
IndexerExpression(this.target, this.lBracket, this.indexer, this.rBracket);
@override
FileSpan get span {
return target.span
.expand(lBracket.span)
.expand(indexer.span)
.expand(rBracket.span);
}
@override
compute(scope) {
var a = target.compute(scope), b = indexer.compute(scope);
return a[b];
}
}

17
jael/lib/src/ast/ast.dart Normal file
View file

@ -0,0 +1,17 @@
export 'array.dart';
export 'ast_node.dart';
export 'attribute.dart';
export 'binary.dart';
export 'call.dart';
export 'document.dart';
export 'element.dart';
export 'error.dart';
export 'expression.dart';
export 'identifier.dart';
export 'interpolation.dart';
export 'map.dart';
export 'member.dart';
export 'new.dart';
export 'number.dart';
export 'string.dart';
export 'token.dart';

View file

@ -0,0 +1,5 @@
import 'package:source_span/source_span.dart';
abstract class AstNode {
FileSpan get span;
}

View file

@ -0,0 +1,19 @@
import 'package:source_span/source_span.dart';
import 'ast_node.dart';
import 'expression.dart';
import 'identifier.dart';
import 'token.dart';
class Attribute extends AstNode {
final Identifier name;
final Token equals;
final Expression value;
Attribute(this.name, this.equals, this.value);
@override
FileSpan get span {
if (equals == null) return name.span;
return name.span.expand(equals.span).expand(value.span);
}
}

View file

@ -0,0 +1,47 @@
import 'package:source_span/source_span.dart';
import 'expression.dart';
import 'token.dart';
class BinaryExpression extends Expression {
final Expression left, right;
final Token operator;
BinaryExpression(this.left, this.operator, this.right);
@override
compute(scope) {
var l = left.compute(scope), r = right.compute(scope);
switch (operator?.type) {
case TokenType.asterisk:
return l * r;
case TokenType.slash:
return l / r;
case TokenType.plus:
if (l is String || r is String) return l.toString() + r.toString();
return l + r;
case TokenType.minus:
return l - r;
case TokenType.lt:
return l < r;
case TokenType.gt:
return l > r;
case TokenType.lte:
return l <= r;
case TokenType.gte:
return l >= r;
case TokenType.equ:
return l == r;
case TokenType.nequ:
return l != r;
case TokenType.elvis:
return l ?? r;
default:
throw new UnsupportedError(
'Unsupported binary operator: "${operator?.span ?? "<null>"}".');
}
}
@override
FileSpan get span => left.span.expand(operator.span).expand(right.span);
}

View file

@ -0,0 +1,55 @@
import 'package:source_span/source_span.dart';
import 'package:symbol_table/symbol_table.dart';
import 'ast_node.dart';
import 'expression.dart';
import 'identifier.dart';
import 'token.dart';
class Call extends Expression {
final Expression target;
final Token lParen, rParen;
final List<Expression> arguments;
final List<NamedArgument> namedArguments;
Call(this.target, this.lParen, this.rParen, this.arguments,
this.namedArguments);
@override
FileSpan get span {
return arguments
.fold<FileSpan>(lParen.span, (out, a) => out.expand(a.span))
.expand(namedArguments.fold<FileSpan>(
lParen.span, (out, a) => out.expand(a.span)))
.expand(rParen.span);
}
List computePositional(SymbolTable scope) => arguments.map((e) => e.compute(scope)).toList();
Map<Symbol, dynamic> computeNamed(SymbolTable scope) {
return namedArguments.fold<Map<Symbol, dynamic>>({}, (out, a) {
return out..[new Symbol(a.name.name)] = a.value.compute(scope);
});
}
@override
compute(scope) {
var callee = target.compute(scope);
var args = computePositional(scope);
var named = computeNamed(scope);
return Function.apply(callee, args, named);
}
}
class NamedArgument extends AstNode {
final Identifier name;
final Token colon;
final Expression value;
NamedArgument(this.name, this.colon, this.value);
@override
FileSpan get span {
return name.span.expand(colon.span).expand(value.span);
}
}

View file

@ -0,0 +1,59 @@
import 'package:source_span/source_span.dart';
import 'ast_node.dart';
import 'element.dart';
import 'identifier.dart';
import 'string.dart';
import 'token.dart';
class Document extends AstNode {
final Doctype doctype;
final Element root;
Document(this.doctype, this.root);
@override
FileSpan get span {
if (doctype == null) return root.span;
return doctype.span.expand(root.span);
}
}
class HtmlComment extends ElementChild {
final Token htmlComment;
HtmlComment(this.htmlComment);
@override
FileSpan get span => htmlComment.span;
}
class Text extends ElementChild {
final Token text;
Text(this.text);
@override
FileSpan get span => text.span;
}
class Doctype extends AstNode {
final Token lt, doctype, gt;
final Identifier html, public;
final StringLiteral name, url;
Doctype(this.lt, this.doctype, this.html, this.public, this.name, this.url,
this.gt);
@override
FileSpan get span {
if (public == null)
return lt.span.expand(doctype.span).expand(html.span).expand(gt.span);
return lt.span
.expand(doctype.span)
.expand(html.span)
.expand(public.span)
.expand(name.span)
.expand(url.span)
.expand(gt.span);
}
}

View file

@ -0,0 +1,90 @@
import 'package:source_span/source_span.dart';
import 'ast_node.dart';
import 'attribute.dart';
import 'identifier.dart';
import 'token.dart';
abstract class ElementChild extends AstNode {}
class TextNode extends ElementChild {
final Token text;
TextNode(this.text);
@override
FileSpan get span => text.span;
}
abstract class Element extends ElementChild {
static const List<String> selfClosing = const [
'base',
'basefont',
'frame',
'link',
'meta',
'area',
'br',
'col',
'hr',
'img',
'input',
'param',
];
Identifier get tagName;
Iterable<Attribute> get attributes;
Iterable<ElementChild> get children;
}
class SelfClosingElement extends Element {
final Token lt, slash, gt;
final Identifier tagName;
final Iterable<Attribute> attributes;
@override
Iterable<ElementChild> get children => [];
SelfClosingElement(
this.lt, this.tagName, this.attributes, this.slash, this.gt);
@override
FileSpan get span {
var start = attributes.fold<FileSpan>(
lt.span.expand(tagName.span), (out, a) => out.expand(a.span));
return slash != null
? start.expand(slash.span).expand(gt.span)
: start.expand(gt.span);
}
}
class RegularElement extends Element {
final Token lt, gt, lt2, slash, gt2;
final Identifier tagName, tagName2;
final Iterable<Attribute> attributes;
final Iterable<ElementChild> children;
RegularElement(this.lt, this.tagName, this.attributes, this.gt, this.children,
this.lt2, this.slash, this.tagName2, this.gt2);
@override
FileSpan get span {
var openingTag = attributes
.fold<FileSpan>(
lt.span.expand(tagName.span), (out, a) => out.expand(a.span))
.expand(gt.span);
if (gt2 == null) return openingTag;
return children
.fold<FileSpan>(openingTag, (out, c) => out.expand(c.span))
.expand(lt2.span)
.expand(slash.span)
.expand(tagName2.span)
.expand(gt2.span);
}
}

View file

@ -0,0 +1,21 @@
import 'package:source_span/source_span.dart';
class JaelError {
final JaelErrorSeverity severity;
final String message;
final FileSpan span;
JaelError(this.severity, this.message, this.span);
@override
String toString() {
var label = severity == JaelErrorSeverity.warning ? 'warning' : 'error';
return '$label: ${span.start.toolString}: $message\n' +
span.highlight(color: true);
}
}
enum JaelErrorSeverity {
warning,
error,
}

View file

@ -0,0 +1,8 @@
import 'package:symbol_table/symbol_table.dart';
import 'ast_node.dart';
abstract class Expression extends AstNode {
compute(SymbolTable scope);
}
abstract class Literal extends Expression {}

View file

@ -0,0 +1,23 @@
import 'package:source_span/source_span.dart';
import 'package:symbol_table/symbol_table.dart';
import 'expression.dart';
import 'token.dart';
class Identifier extends Expression {
final Token id;
Identifier(this.id);
@override
compute(SymbolTable scope) {
var symbol = scope.resolve(name);
if (symbol == null)
throw new ArgumentError('The name "$name" does not exist in this scope.');
return scope.resolve(name).value;
}
String get name => id.span.text;
@override
FileSpan get span => id.span;
}

View file

@ -0,0 +1,18 @@
import 'package:source_span/source_span.dart';
import 'element.dart';
import 'expression.dart';
import 'token.dart';
class Interpolation extends ElementChild {
final Token doubleCurlyL, doubleCurlyR;
final Expression expression;
Interpolation(this.doubleCurlyL, this.expression, this.doubleCurlyR);
bool get isRaw => doubleCurlyL.span.text.endsWith('-');
@override
FileSpan get span {
return doubleCurlyL.span.expand(expression.span).expand(doubleCurlyR.span);
}
}

53
jael/lib/src/ast/map.dart Normal file
View file

@ -0,0 +1,53 @@
import 'package:source_span/source_span.dart';
import 'ast_node.dart';
import 'expression.dart';
import 'identifier.dart';
import 'token.dart';
class MapLiteral extends Literal {
final Token lCurly, rCurly;
final List<KeyValuePair> pairs;
MapLiteral(this.lCurly, this.pairs, this.rCurly);
@override
compute(scope) {
return pairs.fold<Map>({}, (out, p) {
var key, value;
if (p.colon == null) {
if (p.key is! Identifier) {
key = value = p.key.compute(scope);
} else {
key = (p.key as Identifier).name;
value = p.key.compute(scope);
}
} else {
key = p.key.compute(scope);
value = p.value.compute(scope);
}
return out..[key] = value;
});
}
@override
FileSpan get span {
return pairs
.fold<FileSpan>(lCurly.span, (out, p) => out.expand(p.span))
.expand(rCurly.span);
}
}
class KeyValuePair extends AstNode {
final Expression key, value;
final Token colon;
KeyValuePair(this.key, this.colon, this.value);
@override
FileSpan get span {
if (colon == null) return key.span;
return colon.span.expand(colon.span).expand(value.span);
}
}

View file

@ -0,0 +1,23 @@
import 'dart:mirrors';
import 'package:source_span/source_span.dart';
import 'package:symbol_table/symbol_table.dart';
import 'expression.dart';
import 'identifier.dart';
import 'token.dart';
class MemberExpression extends Expression {
final Expression expression;
final Token dot;
final Identifier name;
MemberExpression(this.expression, this.dot, this.name);
@override
compute(SymbolTable scope) {
var target = expression.compute(scope);
return reflect(target).getField(new Symbol(name.name)).reflectee;
}
@override
FileSpan get span => expression.span.expand(dot.span).expand(name.span);
}

31
jael/lib/src/ast/new.dart Normal file
View file

@ -0,0 +1,31 @@
import 'dart:mirrors';
import 'package:source_span/source_span.dart';
import 'call.dart';
import 'expression.dart';
import 'member.dart';
import 'token.dart';
class NewExpression extends Expression {
final Token $new;
final Call call;
NewExpression(this.$new, this.call);
@override
FileSpan get span => $new.span.expand(call.span);
@override
compute(scope) {
var targetType = call.target.compute(scope);
var positional = call.computePositional(scope);
var named = call.computeNamed(scope);
var name = '';
if (call.target is MemberExpression)
name = (call.target as MemberExpression).name.name;
return reflectClass(targetType)
.newInstance(new Symbol(name), positional, named)
.reflectee;
}
}

View file

@ -0,0 +1,47 @@
import 'dart:math' as math;
import 'package:source_span/source_span.dart';
import 'expression.dart';
import 'token.dart';
class NumberLiteral extends Literal {
final Token number;
num _value;
NumberLiteral(this.number);
@override
FileSpan get span => number.span;
static num parse(String value) {
var e = value.indexOf('E');
e != -1 ? e : e = value.indexOf('e');
if (e == -1) return num.parse(value);
var plainNumber = num.parse(value.substring(0, e));
var exp = value.substring(e + 1);
return plainNumber * math.pow(10, num.parse(exp));
}
@override
compute(scope) {
return _value ??= parse(number.span.text);
}
}
class HexLiteral extends Literal {
final Token hex;
num _value;
HexLiteral(this.hex);
@override
FileSpan get span => hex.span;
static num parse(String value) => int.parse(value.substring(2), radix: 16);
@override
compute(scope) {
return _value ??= parse(hex.span.text);
}
}

View file

@ -0,0 +1,74 @@
import 'package:charcode/charcode.dart';
import 'package:source_span/source_span.dart';
import 'package:symbol_table/symbol_table.dart';
import '../ast/ast.dart';
import 'expression.dart';
import 'token.dart';
class StringLiteral extends Literal {
final Token string;
final String value;
StringLiteral(this.string, this.value);
static String parseValue(Token string) {
var text = string.span.text.substring(1, string.span.text.length - 1);
var codeUnits = text.codeUnits;
var buf = new StringBuffer();
for (int i = 0; i < codeUnits.length; i++) {
var ch = codeUnits[i];
if (ch == $backslash) {
if (i < codeUnits.length - 5 && codeUnits[i + 1] == $u) {
var c1 = codeUnits[i += 2],
c2 = codeUnits[++i],
c3 = codeUnits[++i],
c4 = codeUnits[++i];
var hexString = new String.fromCharCodes([c1, c2, c3, c4]);
var hexNumber = int.parse(hexString, radix: 16);
buf.write(new String.fromCharCode(hexNumber));
continue;
}
if (i < codeUnits.length - 1) {
var next = codeUnits[++i];
switch (next) {
case $b:
buf.write('\b');
break;
case $f:
buf.write('\f');
break;
case $n:
buf.writeCharCode($lf);
break;
case $r:
buf.writeCharCode($cr);
break;
case $t:
buf.writeCharCode($tab);
break;
default:
buf.writeCharCode(next);
}
} else
throw new JaelError(JaelErrorSeverity.error,
'Unexpected "\\" in string literal.', string.span);
} else {
buf.writeCharCode(ch);
}
}
return buf.toString();
}
@override
compute(SymbolTable scope) {
return value;
}
@override
FileSpan get span => string.span;
}

View file

@ -0,0 +1,57 @@
import 'package:source_span/source_span.dart';
class Token {
final TokenType type;
final FileSpan span;
Token(this.type, this.span);
@override
String toString() {
return '${span.start.toolString}: "${span.text}" => $type';
}
}
enum TokenType {
/*
* HTML
*/
doctype,
htmlComment,
lt,
gt,
slash,
equals,
id,
text,
// Keywords
$new,
/*
* Expression
*/
lBracket,
rBracket,
doubleCurlyL,
doubleCurlyR,
lCurly,
rCurly,
lParen,
rParen,
asterisk,
colon,
comma,
dot,
percent,
plus,
minus,
elvis,
lte,
gte,
equ,
nequ,
number,
hex,
string,
}

183
jael/lib/src/renderer.dart Normal file
View file

@ -0,0 +1,183 @@
import 'dart:convert';
import 'package:code_buffer/code_buffer.dart';
import 'package:symbol_table/symbol_table.dart';
import 'ast/ast.dart';
import 'text/parser.dart';
import 'text/scanner.dart';
/// Parses a Jael document.
Document parseDocument(String text,
{sourceUrl, void onError(JaelError error)}) {
var scanner = scan(text, sourceUrl: sourceUrl);
if (scanner.errors.isNotEmpty && onError != null)
scanner.errors.forEach(onError);
else if (scanner.errors.isNotEmpty) throw scanner.errors.first;
var parser = new Parser(scanner);
var doc = parser.parseDocument();
if (parser.errors.isNotEmpty && onError != null)
parser.errors.forEach(onError);
else if (parser.errors.isNotEmpty) throw parser.errors.first;
return doc;
}
class Renderer {
const Renderer();
void render(Document document, CodeBuffer buffer, SymbolTable scope) {
if (document.doctype != null) buffer.writeln(document.doctype.span.text);
renderElement(
document.root, buffer, scope, document.doctype?.public == null);
}
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);
return;
} else if (element.attributes.any((a) => a.name.name == 'if')) {
renderIf(element, buffer, scope, html5);
return;
}
buffer..write('<')..write(element.tagName.name);
for (var attribute in element.attributes) {
var value = attribute.value?.compute(scope);
if (value == false || value == null) continue;
buffer.write(' ${attribute.name.name}');
if (value == true)
continue;
else
buffer.write('="');
String msg;
if (value is Iterable) {
msg = value.join(' ');
} else if (value is Map) {
msg = value.keys.fold<StringBuffer>(new StringBuffer(), (buf, k) {
var v = value[k];
if (v == null) return buf;
return buf..write('$k=$v;');
}).toString();
} else {
msg = value.toString();
}
buffer.write(HTML_ESCAPE.convert(msg));
buffer.write('"');
}
if (element is SelfClosingElement) {
if (html5)
buffer.writeln('>');
else
buffer.writeln('/>');
} else {
buffer.writeln('>');
buffer.indent();
for (int i = 0; i < element.children.length; i++) {
var child = element.children.elementAt(i);
renderElementChild(
child, buffer, scope, html5, i, element.children.length);
}
buffer.writeln();
buffer.outdent();
buffer.writeln('</${element.tagName.name}>');
}
}
void renderForeach(
Element element, CodeBuffer buffer, SymbolTable scope, bool html5) {
var attribute =
element.attributes.singleWhere((a) => a.name.name == 'for-each');
if (attribute.value == null) return;
var asAttribute = element.attributes
.firstWhere((a) => a.name.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');
Element strippedElement;
if (element is SelfClosingElement)
strippedElement = new SelfClosingElement(element.lt, element.tagName,
otherAttributes, element.slash, element.gt);
else if (element is RegularElement)
strippedElement = new RegularElement(
element.lt,
element.tagName,
otherAttributes,
element.gt,
element.children,
element.lt2,
element.slash,
element.tagName2,
element.gt2);
for (var item in attribute.value.compute(scope)) {
var childScope = scope.createChild(values: {alias: item});
renderElement(strippedElement, buffer, childScope, html5);
}
}
void renderIf(
Element element, CodeBuffer buffer, SymbolTable scope, bool html5) {
var attribute = element.attributes.singleWhere((a) => a.name.name == 'if');
if (!attribute.value.compute(scope)) return;
var otherAttributes = element.attributes.where((a) => a.name.name != 'if');
Element strippedElement;
if (element is SelfClosingElement)
strippedElement = new SelfClosingElement(element.lt, element.tagName,
otherAttributes, element.slash, element.gt);
else if (element is RegularElement)
strippedElement = new RegularElement(
element.lt,
element.tagName,
otherAttributes,
element.gt,
element.children,
element.lt2,
element.slash,
element.tagName2,
element.gt2);
renderElement(strippedElement, buffer, scope, html5);
}
void renderElementChild(ElementChild child, CodeBuffer buffer,
SymbolTable scope, bool html5, int index, int total) {
if (child is Text) {
if (index == 0)
buffer.write(child.span.text.trimLeft());
else if (index == total - 1)
buffer.write(child.span.text.trimRight());
else
buffer.write(child.span.text);
} else if (child is Interpolation) {
var value = child.expression.compute(scope);
if (value != null) {
if (child.isRaw)
buffer.write(value);
else
buffer.write(HTML_ESCAPE.convert(value.toString()));
}
} else if (child is Element) {
buffer.writeln();
renderElement(child, buffer, scope, html5);
}
}
}

View file

@ -0,0 +1,127 @@
part of jael.src.text.parselet;
const Map<TokenType, InfixParselet> infixParselets = const {
TokenType.lParen: const CallParselet(),
TokenType.dot: const MemberParselet(),
TokenType.lBracket: const IndexerParselet(),
TokenType.asterisk: const BinaryParselet(14),
TokenType.slash: const BinaryParselet(14),
TokenType.percent: const BinaryParselet(14),
TokenType.plus: const BinaryParselet(13),
TokenType.minus: const BinaryParselet(13),
TokenType.lt: const BinaryParselet(11),
TokenType.lte: const BinaryParselet(11),
TokenType.gt: const BinaryParselet(11),
TokenType.gte: const BinaryParselet(11),
TokenType.equ: const BinaryParselet(10),
TokenType.nequ: const BinaryParselet(10),
TokenType.equals: const BinaryParselet(3),
};
class BinaryParselet implements InfixParselet {
final int precedence;
const BinaryParselet(this.precedence);
@override
Expression parse(Parser parser, Expression left, Token token) {
var right = parser.parseExpression(precedence);
if (right == null) {
if (token.type != TokenType.gt)
parser.errors.add(new JaelError(
JaelErrorSeverity.error,
'Missing expression after operator "${token.span.text}", following expression ${left.span.text}.',
token.span));
return null;
}
return new BinaryExpression(left, token, right);
}
}
class CallParselet implements InfixParselet {
const CallParselet();
@override
int get precedence => 19;
@override
Expression parse(Parser parser, Expression left, Token token) {
List<Expression> arguments = [];
List<NamedArgument> namedArguments = [];
Expression argument = parser.parseExpression(0);
while (argument != null) {
arguments.add(argument);
if (!parser.next(TokenType.comma)) break;
parser.skipExtraneous(TokenType.comma);
argument = parser.parseExpression(0);
}
NamedArgument namedArgument = parser.parseNamedArgument();
while (namedArgument != null) {
namedArguments.add(namedArgument);
if (!parser.next(TokenType.comma)) break;
parser.skipExtraneous(TokenType.comma);
namedArgument = parser.parseNamedArgument();
}
if (!parser.next(TokenType.rParen)) {
var lastSpan = arguments.isEmpty ? null : arguments.last.span;
lastSpan ??= token.span;
parser.errors.add(new JaelError(JaelErrorSeverity.error,
'Missing ")" after argument list.', lastSpan));
return null;
}
return new Call(left, token, parser.current, arguments, namedArguments);
}
}
class IndexerParselet implements InfixParselet {
const IndexerParselet();
@override
int get precedence => 19;
@override
Expression parse(Parser parser, Expression left, Token token) {
var indexer = parser.parseExpression(0);
if (indexer == null) {
parser.errors.add(new JaelError(
JaelErrorSeverity.error, 'Missing expression after "[".', left.span));
return null;
}
if (!parser.next(TokenType.rBracket)) {
parser.errors.add(
new JaelError(JaelErrorSeverity.error, 'Missing "]".', indexer.span));
return null;
}
return new IndexerExpression(left, token, indexer, parser.current);
}
}
class MemberParselet implements InfixParselet {
const MemberParselet();
@override
int get precedence => 19;
@override
Expression parse(Parser parser, Expression left, Token token) {
var name = parser.parseIdentifier();
if (name == null) {
parser.errors.add(new JaelError(JaelErrorSeverity.error,
'Expected the name of a property following "."', token.span));
return null;
}
return new MemberExpression(left, token, name);
}
}

View file

@ -0,0 +1,14 @@
library jael.src.text.parselet;
import '../../ast/ast.dart';
import '../parser.dart';
part 'infix.dart';
part 'prefix.dart';
abstract class PrefixParselet {
Expression parse(Parser parser, Token token);
}
abstract class InfixParselet {
int get precedence;
Expression parse(Parser parser, Expression left, Token token);
}

View file

@ -0,0 +1,142 @@
part of jael.src.text.parselet;
const Map<TokenType, PrefixParselet> prefixParselets = const {
TokenType.$new: const NewParselet(),
TokenType.number: const NumberParselet(),
TokenType.hex: const HexParselet(),
TokenType.string: const StringParselet(),
TokenType.lCurly: const MapParselet(),
TokenType.lBracket: const ArrayParselet(),
TokenType.id: const IdentifierParselet(),
TokenType.lParen: const ParenthesisParselet(),
};
class NewParselet implements PrefixParselet {
const NewParselet();
@override
Expression parse(Parser parser, Token token) {
var call = parser.parseExpression(0);
if (call == null) {
parser.errors.add(new JaelError(
JaelErrorSeverity.error,
'"new" must precede a call expression. Nothing was found.',
call.span));
return null;
} else if (call is! Call) {
parser.errors.add(new JaelError(
JaelErrorSeverity.error,
'"new" must precede a call expression, not a(n) ${call.runtimeType}.',
call.span));
return null;
} else {
return new NewExpression(token, call);
}
}
}
class NumberParselet implements PrefixParselet {
const NumberParselet();
@override
Expression parse(Parser parser, Token token) => new NumberLiteral(token);
}
class HexParselet implements PrefixParselet {
const HexParselet();
@override
Expression parse(Parser parser, Token token) => new HexLiteral(token);
}
class StringParselet implements PrefixParselet {
const StringParselet();
@override
Expression parse(Parser parser, Token token) =>
new StringLiteral(token, StringLiteral.parseValue(token));
}
class ArrayParselet implements PrefixParselet {
const ArrayParselet();
@override
Expression parse(Parser parser, Token token) {
List<Expression> items = [];
Expression item = parser.parseExpression(0);
while (item != null) {
items.add(item);
if (!parser.next(TokenType.comma)) break;
parser.skipExtraneous(TokenType.comma);
item = parser.parseExpression(0);
}
if (!parser.next(TokenType.rBracket)) {
var lastSpan = items.isEmpty ? null : items.last.span;
lastSpan ??= token.span;
parser.errors.add(new JaelError(JaelErrorSeverity.error,
'Missing "]" to terminate array literal.', lastSpan));
return null;
}
return new Array(token, parser.current, items);
}
}
class MapParselet implements PrefixParselet {
const MapParselet();
@override
Expression parse(Parser parser, Token token) {
var pairs = <KeyValuePair>[];
var pair = parser.parseKeyValuePair();
while (pair != null) {
pairs.add(pair);
if (!parser.next(TokenType.comma)) break;
parser.skipExtraneous(TokenType.comma);
pair = parser.parseKeyValuePair();
}
if (!parser.next(TokenType.rCurly)) {
var lastSpan = pairs.isEmpty ? token.span : pairs.last.span;
parser.errors.add(new JaelError(
JaelErrorSeverity.error, 'Missing "}" in map literal.', lastSpan));
return null;
}
return new MapLiteral(token, pairs, parser.current);
}
}
class IdentifierParselet implements PrefixParselet {
const IdentifierParselet();
@override
Expression parse(Parser parser, Token token) => new Identifier(token);
}
class ParenthesisParselet implements PrefixParselet {
const ParenthesisParselet();
@override
Expression parse(Parser parser, Token token) {
var expression = parser.parseExpression(0);
if (expression == null) {
parser.errors.add(new JaelError(JaelErrorSeverity.error,
'Missing expression after "(".', token.span));
return null;
}
if (!parser.next(TokenType.rParen)) {
parser.errors.add(new JaelError(
JaelErrorSeverity.error, 'Missing ")".', expression.span));
return null;
}
return expression;
}
}

View file

@ -0,0 +1,362 @@
import '../ast/ast.dart';
import 'parselet/parselet.dart';
import 'scanner.dart';
class Parser {
final List<JaelError> errors = [];
final Scanner scanner;
Token _current;
int _index = -1;
Parser(this.scanner);
Token get current => _current;
int _nextPrecedence() {
var tok = peek();
if (tok == null) return 0;
var parser = infixParselets[tok.type];
return parser?.precedence ?? 0;
}
bool next(TokenType type) {
if (_index >= scanner.tokens.length - 1) return false;
var peek = scanner.tokens[_index + 1];
if (peek.type != type) return false;
_current = peek;
_index++;
return true;
}
Token peek() {
if (_index >= scanner.tokens.length - 1) return null;
return scanner.tokens[_index + 1];
}
Token maybe(TokenType type) => next(type) ? _current : null;
void skipExtraneous(TokenType type) {
while (next(type)) {
// Skip...
}
}
Document parseDocument() {
var doctype = parseDoctype();
if (doctype == null) {
var root = parseElement();
if (root == null) return null;
return new Document(null, root);
}
var root = parseElement();
if (root == null) {
errors.add(new JaelError(JaelErrorSeverity.error,
'Missing root element after !DOCTYPE declaration.', doctype.span));
return null;
}
return new Document(doctype, root);
}
StringLiteral implicitString() {
if (next(TokenType.string)) {
return prefixParselets[TokenType.string].parse(this, _current);
} else if (next(TokenType.text)) {
}
return null;
}
Doctype parseDoctype() {
if (!next(TokenType.lt)) return null;
var lt = _current;
if (!next(TokenType.doctype)) {
_index--;
return null;
}
var doctype = _current, html = parseIdentifier();
if (html?.span?.text?.toLowerCase() != 'html') {
errors.add(new JaelError(JaelErrorSeverity.error,
'Expected "html" in doctype declaration.', html?.span ?? doctype.span));
return null;
}
var public = parseIdentifier();
if (public == null) {
if (!next(TokenType.gt)) {
errors.add(new JaelError(JaelErrorSeverity.error,
'Expected ">" in doctype declaration.', html.span));
return null;
}
return new Doctype(lt, doctype, html, null, null, null, _current);
}
if (public?.span?.text?.toLowerCase() != 'public') {
errors.add(new JaelError(JaelErrorSeverity.error,
'Expected "public" in doctype declaration.', public?.span ?? html.span));
return null;
}
var stringParser = prefixParselets[TokenType.string];
if (!next(TokenType.string)) {
errors.add(new JaelError(JaelErrorSeverity.error,
'Expected string in doctype declaration.', public.span));
return null;
}
var name = stringParser.parse(this, _current);
if (!next(TokenType.string)) {
errors.add(new JaelError(JaelErrorSeverity.error,
'Expected string in doctype declaration.', name.span));
return null;
}
var url = stringParser.parse(this, _current);
if (!next(TokenType.gt)) {
errors.add(new JaelError(JaelErrorSeverity.error,
'Expected ">" in doctype declaration.', url.span));
return null;
}
return new Doctype(lt, doctype, html, public, name, url, _current);
}
ElementChild parseElementChild() =>
parseHtmlComment() ??
parseInterpolation() ??
parseText() ??
parseElement();
HtmlComment parseHtmlComment() =>
next(TokenType.htmlComment) ? new HtmlComment(_current) : null;
Text parseText() => next(TokenType.text) ? new Text(_current) : null;
Interpolation parseInterpolation() {
if (!next(TokenType.doubleCurlyL)) return null;
var doubleCurlyL = _current;
var expression = parseExpression(0);
if (expression == null) {
errors.add(new JaelError(JaelErrorSeverity.error,
'Missing expression in interpolation.', doubleCurlyL.span));
return null;
}
if (!next(TokenType.doubleCurlyR)) {
errors.add(new JaelError(JaelErrorSeverity.error,
'Missing closing "}}" in interpolation.', expression.span));
return null;
}
return new Interpolation(doubleCurlyL, expression, _current);
}
Element parseElement() {
if (!next(TokenType.lt)) return null;
var lt = _current;
if (next(TokenType.slash)) {
// We entered a closing tag, don't keep reading...
_index -= 2;
return null;
}
var tagName = parseIdentifier();
if (tagName == null) {
errors.add(
new JaelError(JaelErrorSeverity.error, 'Missing tag name.', lt.span));
return null;
}
List<Attribute> attributes = [];
var attribute = parseAttribute();
while (attribute != null) {
attributes.add(attribute);
attribute = parseAttribute();
}
if (next(TokenType.slash)) {
// Try for self-closing...
var slash = _current;
if (!next(TokenType.gt)) {
errors.add(new JaelError(JaelErrorSeverity.error,
'Missing ">" in self-closing "${tagName.name}" tag.', slash.span));
return null;
}
return new SelfClosingElement(lt, tagName, attributes, slash, _current);
}
if (!next(TokenType.gt)) {
errors.add(new JaelError(
JaelErrorSeverity.error,
'Missing ">" in "${tagName.name}" tag.',
attributes.isEmpty ? tagName.span : attributes.last.span));
return null;
}
var gt = _current;
// Implicit self-closing
if (Element.selfClosing.contains(tagName.name)) {
return new SelfClosingElement(lt, tagName, attributes, null, gt);
}
List<ElementChild> children = [];
var child = parseElementChild();
while (child != null) {
if (child is! HtmlComment) children.add(child);
child = parseElementChild();
}
// Parse closing tag
if (!next(TokenType.lt)) {
errors.add(new JaelError(
JaelErrorSeverity.error,
'Missing closing tag for "${tagName.name}" tag.',
children.isEmpty ? tagName.span : children.last.span));
return null;
}
var lt2 = _current;
if (!next(TokenType.slash)) {
errors.add(new JaelError(JaelErrorSeverity.error,
'Missing "/" in "${tagName.name}" closing tag.', lt2.span));
return null;
}
var slash = _current, tagName2 = parseIdentifier();
if (tagName2 == null) {
errors.add(new JaelError(
JaelErrorSeverity.error,
'Missing "${tagName.name}" in "${tagName.name}" closing tag.',
slash.span));
return null;
}
if (tagName2.name != tagName.name) {
errors.add(new JaelError(
JaelErrorSeverity.error,
'Mismatched closing tags. Expected "${tagName.span}"; got "${tagName2.name}" instead.',
lt2.span));
return null;
}
if (!next(TokenType.gt)) {
errors.add(new JaelError(JaelErrorSeverity.error,
'Missing ">" in "${tagName.name}" closing tag.', tagName2.span));
return null;
}
return new RegularElement(
lt, tagName, attributes, gt, children, lt2, slash, tagName2, _current);
}
Attribute parseAttribute() {
var name = parseIdentifier();
if (name == null) return null;
if (!next(TokenType.equals)) return new Attribute(name, null, null);
var equals = _current;
var value = parseExpression(0);
if (value == null) {
errors.add(new JaelError(JaelErrorSeverity.error,
'Missing expression in attribute.', equals.span));
return null;
}
return new Attribute(name, equals, value);
}
Expression parseExpression(int precedence) {
// Only consume a token if it could potentially be a prefix parselet
for (var type in prefixParselets.keys) {
if (next(type)) {
var left = prefixParselets[type].parse(this, _current);
while (precedence < _nextPrecedence()) {
_current = scanner.tokens[++_index];
var infix = infixParselets[_current.type];
var newLeft = infix.parse(this, left, _current);
if (newLeft == null) {
if (_current.type == TokenType.gt)
_index--;
return left;
}
left = newLeft;
}
return left;
}
}
// Nothing was parsed; return null.
return null;
}
Identifier parseIdentifier() =>
next(TokenType.id) ? new Identifier(_current) : null;
KeyValuePair parseKeyValuePair() {
var key = parseExpression(0);
if (key == null) return null;
if (!next(TokenType.colon)) return new KeyValuePair(key, null, null);
var colon = _current, value = parseExpression(0);
if (value == null) {
errors.add(new JaelError(JaelErrorSeverity.error,
'Missing expression in key-value pair.', colon.span));
return null;
}
return new KeyValuePair(key, colon, value);
}
NamedArgument parseNamedArgument() {
var name = parseIdentifier();
if (name == null) return null;
if (!next(TokenType.colon)) {
errors.add(new JaelError(JaelErrorSeverity.error,
'Missing ":" in named argument.', name.span));
return null;
}
var colon = _current, value = parseExpression(0);
if (value == null) {
errors.add(new JaelError(JaelErrorSeverity.error,
'Missing expression in named argument.', colon.span));
return null;
}
return new NamedArgument(name, colon, value);
}
}

View file

@ -0,0 +1,177 @@
import 'package:string_scanner/string_scanner.dart';
import '../ast/ast.dart';
final RegExp _whitespace = new RegExp(r'[ \n\r\t]+');
final RegExp _string1 = new RegExp(
r"'((\\(['\\/bfnrt]|(u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))|([^'\\]))*'");
final RegExp _string2 = new RegExp(
r'"((\\(["\\/bfnrt]|(u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])))|([^"\\]))*"');
Scanner scan(String text, {sourceUrl}) => new _Scanner(text, sourceUrl)..scan();
abstract class Scanner {
List<JaelError> get errors;
List<Token> get tokens;
}
final Map<Pattern, TokenType> _htmlPatterns = {
'{{': TokenType.doubleCurlyL,
'{{-': TokenType.doubleCurlyL,
//
new RegExp(r'<!--[^$]*-->'): TokenType.htmlComment,
'!DOCTYPE': TokenType.doctype,
'!doctype': TokenType.doctype,
'<': TokenType.lt,
'>': TokenType.gt,
'/': TokenType.slash,
'=': TokenType.equals,
_string1: TokenType.string,
_string2: TokenType.string,
new RegExp(r'([A-Za-z][A-Za-z0-9]*-)*([A-Za-z][A-Za-z0-9]*)'): TokenType.id,
};
final Map<Pattern, TokenType> _expressionPatterns = {
'}}': TokenType.doubleCurlyR,
// Keywords
'new': TokenType.$new,
// Misc.
'*': TokenType.asterisk,
':': TokenType.colon,
',': TokenType.comma,
'.': TokenType.dot,
'=': TokenType.equals,
'-': TokenType.minus,
'%': TokenType.percent,
'+': TokenType.plus,
'[': TokenType.lBracket,
']': TokenType.rBracket,
'{': TokenType.lCurly,
'}': TokenType.rCurly,
'(': TokenType.lParen,
')': TokenType.rParen,
'/': TokenType.slash,
'<': TokenType.lt,
'<=': TokenType.lte,
'>': TokenType.gt,
'>=': TokenType.gte,
'==': TokenType.equ,
'!=': TokenType.nequ,
'=': TokenType.equals,
new RegExp(r'-?[0-9]+(\.[0-9]+)?([Ee][0-9]+)?'): TokenType.number,
new RegExp(r'0x[A-Fa-f0-9]+'): TokenType.hex,
_string1: TokenType.string,
_string2: TokenType.string,
new RegExp('[A-Za-z_\\\$][A-Za-z0-9_\\\$]*'): TokenType.id,
};
class _Scanner implements Scanner {
final List<JaelError> errors = [];
final List<Token> tokens = [];
SpanScanner _scanner;
_Scanner(String text, sourceUrl) {
_scanner = new SpanScanner(text, sourceUrl: sourceUrl);
}
Token _scanFrom(Map<Pattern, TokenType> patterns,
[LineScannerState textStart]) {
var potential = <Token>[];
patterns.forEach((pattern, type) {
if (_scanner.matches(pattern))
potential.add(new Token(type, _scanner.lastSpan));
});
if (potential.isEmpty) return null;
if (textStart != null) {
var span = _scanner.spanFrom(textStart);
tokens.add(new Token(TokenType.text, span));
}
potential.sort((a, b) => b.span.length.compareTo(a.span.length));
var token = potential.first;
tokens.add(token);
_scanner.scan(token.span.text);
return token;
}
void scan() {
while (!_scanner.isDone) scanHtmlTokens();
}
void scanHtmlTokens() {
LineScannerState textStart;
while (!_scanner.isDone) {
var state = _scanner.state;
// Skip whitespace conditionally
if (textStart == null) {
_scanner.scan(_whitespace);
}
var lastToken = _scanFrom(_htmlPatterns, textStart);
if (lastToken?.type == TokenType.equals) {
textStart = null;
scanExpressionTokens();
return;
} else if (lastToken?.type == TokenType.doubleCurlyL) {
textStart = null;
scanExpressionTokens(true);
return;
} else if (lastToken?.type == TokenType.id &&
tokens.length >= 2 &&
tokens[tokens.length - 2].type == TokenType.gt) {
// Fold in the ID into a text node...
tokens.removeLast();
textStart = state;
} else if (lastToken?.type == TokenType.id &&
tokens.length >= 2 &&
tokens[tokens.length - 2].type == TokenType.text) {
// Append the ID into the old text node
tokens.removeLast();
tokens.removeLast();
// Not sure how, but the following logic seems to occur
// automatically:
//
// var textToken = tokens.removeLast();
// var newSpan = textToken.span.expand(lastToken.span);
// tokens.add(new Token(TokenType.text, newSpan));
} else if (lastToken != null) {
textStart = null;
} else if (!_scanner.isDone ?? lastToken == null) {
textStart ??= state;
_scanner.readChar();
}
}
if (textStart != null) {
var span = _scanner.spanFrom(textStart);
tokens.add(new Token(TokenType.text, span));
}
}
void scanExpressionTokens([bool allowGt = false]) {
Token lastToken;
do {
_scanner.scan(_whitespace);
lastToken = _scanFrom(_expressionPatterns);
} while (!_scanner.isDone &&
lastToken != null &&
lastToken.type != TokenType.doubleCurlyR &&
(allowGt || lastToken.type != TokenType.gt));
}
}

15
jael/pubspec.yaml Normal file
View file

@ -0,0 +1,15 @@
name: jael
version: 1.0.0-alpha
description: A simple server-side HTML templating engine for Dart.
author: Tobe O <thosakwe@gmail.com>
homepage:
environment:
sdk: ">=1.19.0"
dependencies:
charcode: ^1.0.0
code_buffer: ^1.0.0
source_span: ^1.0.0
string_scanner: ^1.0.0
symbol_table: ^1.0.0
dev_dependencies:
test: ^0.12.0

View file

@ -0,0 +1,175 @@
import 'package:code_buffer/code_buffer.dart';
import 'package:jael/jael.dart' as jael;
import 'package:symbol_table/symbol_table.dart';
import 'package:test/test.dart';
main() {
test('attribute binding', () {
const template = '''
<html>
<body>
<h1>Hello</h1>
<img src=profile['avatar']>
</body>
</html>
''';
var buf = new CodeBuffer();
var document = jael.parseDocument(template, sourceUrl: 'test.jl');
var scope = new SymbolTable(values: {
'profile': {
'avatar': 'thosakwe.png',
}
});
const jael.Renderer().render(document, buf, scope);
print(buf);
expect(
buf.toString(),
'''
<html>
<body>
<h1>
Hello
</h1>
<img src="thosakwe.png">
</body>
</html>
'''
.trim());
});
test('interpolation', () {
const template = '''
<!doctype HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<body>
<h1>Pokémon</h1>
{{ pokemon.name }} - {{ pokemon.type }}
<img>
</body>
</html>
''';
var buf = new CodeBuffer();
var document = jael.parseDocument(template, sourceUrl: 'test.jl');
var scope = new SymbolTable(values: {
'pokemon': const _Pokemon('Darkrai', 'Dark'),
});
const jael.Renderer().render(document, buf, scope);
print(buf);
expect(
buf.toString(),
'''
<!doctype HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<body>
<h1>
Pokémon
</h1>
Darkrai - Dark
<img/>
</body>
</html>
'''
.trim());
});
test('for loop', () {
const template = '''
<html>
<body>
<h1>Pokémon</h1>
<ul>
<li for-each=starters as="starter">{{ starter.name }} - {{ starter.type }}</li>
</ul>
</body>
</html>
''';
var buf = new CodeBuffer();
var document = jael.parseDocument(template, sourceUrl: 'test.jl');
var scope = new SymbolTable(values: {
'starters': starters,
});
const jael.Renderer().render(document, buf, scope);
print(buf);
expect(
buf.toString(),
'''
<html>
<body>
<h1>
Pokémon
</h1>
<ul>
<li>
Bulbasaur - Grass
</li>
<li>
Charmander - Fire
</li>
<li>
Squirtle - Water
</li>
</ul>
</body>
</html>
'''
.trim());
});
test('conditional', () {
const template = '''
<html>
<body>
<h1>Conditional</h1>
<b if=starters.isEmpty>Empty</b>
<b if=starters.isNotEmpty>Not empty</b>
</body>
</html>
''';
var buf = new CodeBuffer();
var document = jael.parseDocument(template, sourceUrl: 'test.jl');
var scope = new SymbolTable(values: {
'starters': starters,
});
const jael.Renderer().render(document, buf, scope);
print(buf);
expect(
buf.toString(),
'''
<html>
<body>
<h1>
Conditional
</h1>
<b>
Not empty
</b>
</body>
</html>
'''
.trim());
});
}
const List<_Pokemon> starters = const [
const _Pokemon('Bulbasaur', 'Grass'),
const _Pokemon('Charmander', 'Fire'),
const _Pokemon('Squirtle', 'Water'),
];
class _Pokemon {
final String name, type;
const _Pokemon(this.name, this.type);
}

View file

@ -0,0 +1,24 @@
import 'package:matcher/matcher.dart';
import 'package:jael/src/ast/token.dart';
Matcher isToken(TokenType type, [String text]) => new _IsToken(type, text);
class _IsToken extends Matcher {
final TokenType type;
final String text;
_IsToken(this.type, [this.text]);
@override
Description describe(Description description) {
if (text == null) return description.add('has type $type');
return description.add('has type $type and text "$text"');
}
@override
bool matches(item, Map matchState) {
return item is Token &&
item.type == type &&
(text == null || item.span.text == text);
}
}

View file

@ -0,0 +1,62 @@
import 'package:jael/src/ast/token.dart';
import 'package:jael/src/text/scanner.dart';
import 'package:test/test.dart';
import 'common.dart';
main() {
test('plain html', () {
var tokens = scan('<img src="foo.png" />', sourceUrl: 'test.jl').tokens;
tokens.forEach(print);
expect(tokens, hasLength(7));
expect(tokens[0], isToken(TokenType.lt));
expect(tokens[1], isToken(TokenType.id, 'img'));
expect(tokens[2], isToken(TokenType.id, 'src'));
expect(tokens[3], isToken(TokenType.equals));
expect(tokens[4], isToken(TokenType.string, '"foo.png"'));
expect(tokens[5], isToken(TokenType.slash));
expect(tokens[6], isToken(TokenType.gt));
});
test('text node', () {
var tokens = scan('<p>Hello\nworld</p>', sourceUrl: 'test.jl').tokens;
tokens.forEach(print);
expect(tokens, hasLength(8));
expect(tokens[0], isToken(TokenType.lt));
expect(tokens[1], isToken(TokenType.id, 'p'));
expect(tokens[2], isToken(TokenType.gt));
expect(tokens[3], isToken(TokenType.text, 'Hello\nworld'));
expect(tokens[4], isToken(TokenType.lt));
expect(tokens[5], isToken(TokenType.slash));
expect(tokens[6], isToken(TokenType.id, 'p'));
expect(tokens[7], isToken(TokenType.gt));
});
test('mixed', () {
var tokens = scan('<ul number=1 + 2>three{{four > five.six}}</ul>', sourceUrl: 'test.jl').tokens;
tokens.forEach(print);
expect(tokens, hasLength(20));
expect(tokens[0], isToken(TokenType.lt));
expect(tokens[1], isToken(TokenType.id, 'ul'));
expect(tokens[2], isToken(TokenType.id, 'number'));
expect(tokens[3], isToken(TokenType.equals));
expect(tokens[4], isToken(TokenType.number, '1'));
expect(tokens[5], isToken(TokenType.plus));
expect(tokens[6], isToken(TokenType.number, '2'));
expect(tokens[7], isToken(TokenType.gt));
expect(tokens[8], isToken(TokenType.text, 'three'));
expect(tokens[9], isToken(TokenType.doubleCurlyL));
expect(tokens[10], isToken(TokenType.id, 'four'));
expect(tokens[11], isToken(TokenType.gt));
expect(tokens[12], isToken(TokenType.id, 'five'));
expect(tokens[13], isToken(TokenType.dot));
expect(tokens[14], isToken(TokenType.id, 'six'));
expect(tokens[15], isToken(TokenType.doubleCurlyR));
expect(tokens[16], isToken(TokenType.lt));
expect(tokens[17], isToken(TokenType.slash));
expect(tokens[18], isToken(TokenType.id, 'ul'));
expect(tokens[19], isToken(TokenType.gt));
});
}

14
jael_preprocessor/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
# Created by .ignore support plugin (hsz.mobi)
### Dart template
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/

View file

@ -0,0 +1,2 @@
analyzer:
strong-mode: true

3
travis.sh Normal file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
cd jael && pub get && pub run test
cd ../angel_jael && pub get && pub run test