core nearly done
This commit is contained in:
commit
d2e700edd1
43 changed files with 2157 additions and 0 deletions
51
.gitignore
vendored
Normal file
51
.gitignore
vendored
Normal 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
6
.idea/misc.xml
Normal 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
8
.idea/modules.xml
Normal 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
2
.travis.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
language: dart
|
||||
script: bash ./travis.sh
|
2
README.md
Normal file
2
README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
# jael
|
||||
A simple server-side HTML templating engine for Dart.
|
14
angel_jael/.gitignore
vendored
Normal file
14
angel_jael/.gitignore
vendored
Normal 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/
|
2
angel_jael/analysis_options.yaml
Normal file
2
angel_jael/analysis_options.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
analyzer:
|
||||
strong-mode: true
|
14
jael.iml
Normal file
14
jael.iml
Normal 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
14
jael/.gitignore
vendored
Normal 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
50
jael/README.md
Normal 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.`.
|
2
jael/analysis_options.yaml
Normal file
2
jael/analysis_options.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
analyzer:
|
||||
strong-mode: true
|
4
jael/lib/jael.dart
Normal file
4
jael/lib/jael.dart
Normal file
|
@ -0,0 +1,4 @@
|
|||
export 'src/ast/ast.dart';
|
||||
export 'src/text/parser.dart';
|
||||
export 'src/text/scanner.dart';
|
||||
export 'src/renderer.dart';
|
41
jael/lib/src/ast/array.dart
Normal file
41
jael/lib/src/ast/array.dart
Normal 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
17
jael/lib/src/ast/ast.dart
Normal 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';
|
5
jael/lib/src/ast/ast_node.dart
Normal file
5
jael/lib/src/ast/ast_node.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
import 'package:source_span/source_span.dart';
|
||||
|
||||
abstract class AstNode {
|
||||
FileSpan get span;
|
||||
}
|
19
jael/lib/src/ast/attribute.dart
Normal file
19
jael/lib/src/ast/attribute.dart
Normal 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);
|
||||
}
|
||||
}
|
47
jael/lib/src/ast/binary.dart
Normal file
47
jael/lib/src/ast/binary.dart
Normal 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);
|
||||
}
|
55
jael/lib/src/ast/call.dart
Normal file
55
jael/lib/src/ast/call.dart
Normal 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);
|
||||
}
|
||||
}
|
59
jael/lib/src/ast/document.dart
Normal file
59
jael/lib/src/ast/document.dart
Normal 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);
|
||||
}
|
||||
}
|
90
jael/lib/src/ast/element.dart
Normal file
90
jael/lib/src/ast/element.dart
Normal 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);
|
||||
}
|
||||
}
|
21
jael/lib/src/ast/error.dart
Normal file
21
jael/lib/src/ast/error.dart
Normal 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,
|
||||
}
|
8
jael/lib/src/ast/expression.dart
Normal file
8
jael/lib/src/ast/expression.dart
Normal 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 {}
|
23
jael/lib/src/ast/identifier.dart
Normal file
23
jael/lib/src/ast/identifier.dart
Normal 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;
|
||||
}
|
18
jael/lib/src/ast/interpolation.dart
Normal file
18
jael/lib/src/ast/interpolation.dart
Normal 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
53
jael/lib/src/ast/map.dart
Normal 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);
|
||||
}
|
||||
}
|
23
jael/lib/src/ast/member.dart
Normal file
23
jael/lib/src/ast/member.dart
Normal 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
31
jael/lib/src/ast/new.dart
Normal 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;
|
||||
}
|
||||
}
|
47
jael/lib/src/ast/number.dart
Normal file
47
jael/lib/src/ast/number.dart
Normal 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);
|
||||
}
|
||||
}
|
74
jael/lib/src/ast/string.dart
Normal file
74
jael/lib/src/ast/string.dart
Normal 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;
|
||||
}
|
57
jael/lib/src/ast/token.dart
Normal file
57
jael/lib/src/ast/token.dart
Normal 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
183
jael/lib/src/renderer.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
127
jael/lib/src/text/parselet/infix.dart
Normal file
127
jael/lib/src/text/parselet/infix.dart
Normal 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);
|
||||
}
|
||||
}
|
14
jael/lib/src/text/parselet/parselet.dart
Normal file
14
jael/lib/src/text/parselet/parselet.dart
Normal 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);
|
||||
}
|
142
jael/lib/src/text/parselet/prefix.dart
Normal file
142
jael/lib/src/text/parselet/prefix.dart
Normal 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;
|
||||
}
|
||||
}
|
362
jael/lib/src/text/parser.dart
Normal file
362
jael/lib/src/text/parser.dart
Normal 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);
|
||||
}
|
||||
}
|
177
jael/lib/src/text/scanner.dart
Normal file
177
jael/lib/src/text/scanner.dart
Normal 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
15
jael/pubspec.yaml
Normal 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
|
175
jael/test/render/render_test.dart
Normal file
175
jael/test/render/render_test.dart
Normal 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);
|
||||
}
|
24
jael/test/text/common.dart
Normal file
24
jael/test/text/common.dart
Normal 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);
|
||||
}
|
||||
}
|
62
jael/test/text/scan_test.dart
Normal file
62
jael/test/text/scan_test.dart
Normal 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
14
jael_preprocessor/.gitignore
vendored
Normal 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/
|
2
jael_preprocessor/analysis_options.yaml
Normal file
2
jael_preprocessor/analysis_options.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
analyzer:
|
||||
strong-mode: true
|
3
travis.sh
Normal file
3
travis.sh
Normal file
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
cd jael && pub get && pub run test
|
||||
cd ../angel_jael && pub get && pub run test
|
Loading…
Reference in a new issue