Working, just need switch
This commit is contained in:
parent
cec21c0e7d
commit
576860b06a
7 changed files with 288 additions and 53 deletions
81
angel_jael/README.md
Normal file
81
angel_jael/README.md
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
# jael
|
||||||
|
[![Pub](https://img.shields.io/pub/v/angel_jael.svg)](https://pub.dartlang.org/packages/angel_jael)
|
||||||
|
[![build status](https://travis-ci.org/angel-dart/jael.svg)](https://travis-ci.org/angel-dart/jael)
|
||||||
|
|
||||||
|
|
||||||
|
[Angel](https://angel-dart.github.io)
|
||||||
|
support for
|
||||||
|
[Jael](https://github.com/angel-dart/jael).
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
In your `pubspec.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
angel_jael: ^1.0.0-alpha
|
||||||
|
```
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
Just like `mustache` and other renderers, configuring Angel to use
|
||||||
|
Jael is as simple as calling `app.configure`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_jael/angel_jael.dart';
|
||||||
|
import 'package:file/file.dart';
|
||||||
|
|
||||||
|
AngelConfigurer myPlugin(FileSystem fileSystem) {
|
||||||
|
return (Angel app) async {
|
||||||
|
// Connect Jael to your server...
|
||||||
|
await app.configure(
|
||||||
|
jael(fileSystem.directory('views')),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`package:angel_jael` supports caching views, to improve server performance.
|
||||||
|
You might not want to enable this in development, so consider setting
|
||||||
|
the flag to `app.isProduction`:
|
||||||
|
|
||||||
|
```
|
||||||
|
jael(viewsDirectory, cacheViews: app.isProduction);
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep in mind that this package uses `package:file`, rather than
|
||||||
|
`dart:io`.
|
||||||
|
|
||||||
|
The following is a basic example of a server setup that can render Jael
|
||||||
|
templates from a directory named `views`:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_jael/angel_jael.dart';
|
||||||
|
import 'package:file/local.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
main() async {
|
||||||
|
var app = new Angel();
|
||||||
|
var fileSystem = const LocalFileSystem();
|
||||||
|
|
||||||
|
await app.configure(
|
||||||
|
jael(fileSystem.directory('views')),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Render the contents of views/index.jl
|
||||||
|
app.get('/', (res) => res.render('index', {'title': 'ESKETTIT'}));
|
||||||
|
|
||||||
|
app.use(() => throw new AngelHttpException.notFound());
|
||||||
|
|
||||||
|
app.logger = new Logger('angel')
|
||||||
|
..onRecord.listen((rec) {
|
||||||
|
print(rec);
|
||||||
|
if (rec.error != null) print(rec.error);
|
||||||
|
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||||
|
});
|
||||||
|
|
||||||
|
var server = await app.startServer(null, 3000);
|
||||||
|
print('Listening at http://${server.address.address}:${server.port}');
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
|
@ -6,24 +6,39 @@ import 'package:jael/jael.dart';
|
||||||
import 'package:jael_preprocessor/jael_preprocessor.dart';
|
import 'package:jael_preprocessor/jael_preprocessor.dart';
|
||||||
import 'package:symbol_table/symbol_table.dart';
|
import 'package:symbol_table/symbol_table.dart';
|
||||||
|
|
||||||
|
/// Configures an Angel server to use Jael to render templates.
|
||||||
|
///
|
||||||
|
/// To enable "minified" output, you need to override the [createBuffer] function,
|
||||||
|
/// to instantiate a [CodeBuffer] that emits no spaces or line breaks.
|
||||||
AngelConfigurer jael(Directory viewsDirectory,
|
AngelConfigurer jael(Directory viewsDirectory,
|
||||||
{String fileExtension, CodeBuffer createBuffer()}) {
|
{String fileExtension, bool cacheViews: false, CodeBuffer createBuffer()}) {
|
||||||
|
var cache = <String, Document>{};
|
||||||
fileExtension ??= '.jl';
|
fileExtension ??= '.jl';
|
||||||
createBuffer ??= () => new CodeBuffer();
|
createBuffer ??= () => new CodeBuffer();
|
||||||
|
|
||||||
return (Angel app) async {
|
return (Angel app) async {
|
||||||
app.viewGenerator = (String name, [Map locals]) async {
|
app.viewGenerator = (String name, [Map locals]) async {
|
||||||
var file = viewsDirectory.childFile(name + fileExtension);
|
|
||||||
var contents = await file.readAsString();
|
|
||||||
var errors = <JaelError>[];
|
var errors = <JaelError>[];
|
||||||
var doc =
|
Document processed;
|
||||||
parseDocument(contents, sourceUrl: file.uri, onError: errors.add);
|
|
||||||
var processed = doc;
|
|
||||||
|
|
||||||
try {
|
if (cacheViews == true && cache.containsKey(name)) {
|
||||||
processed = await resolve(doc, viewsDirectory, onError: errors.add);
|
processed = cache[name];
|
||||||
} catch (_) {
|
} else {
|
||||||
// Ignore these errors, so that we can show syntax errors.
|
var file = viewsDirectory.childFile(name + fileExtension);
|
||||||
|
var contents = await file.readAsString();
|
||||||
|
var doc =
|
||||||
|
parseDocument(contents, sourceUrl: file.uri, onError: errors.add);
|
||||||
|
processed = doc;
|
||||||
|
|
||||||
|
try {
|
||||||
|
processed = await resolve(doc, viewsDirectory, onError: errors.add);
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore these errors, so that we can show syntax errors.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheViews == true) {
|
||||||
|
cache[name] = processed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf = createBuffer();
|
var buf = createBuffer();
|
||||||
|
|
83
angel_jael/test/all_test.dart
Normal file
83
angel_jael/test/all_test.dart
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_jael/angel_jael.dart';
|
||||||
|
import 'package:angel_test/angel_test.dart';
|
||||||
|
import 'package:file/memory.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
main() {
|
||||||
|
// These tests need not actually test that the preprocessor or renderer works,
|
||||||
|
// because those packages are already tested.
|
||||||
|
//
|
||||||
|
// Instead, just test that we can render at all.
|
||||||
|
TestClient client;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
var app = new Angel();
|
||||||
|
app.configuration['properties'] = app.configuration;
|
||||||
|
|
||||||
|
var fileSystem = new MemoryFileSystem();
|
||||||
|
var viewsDirectory = fileSystem.directory('views')..createSync();
|
||||||
|
|
||||||
|
viewsDirectory.childFile('layout.jl').writeAsStringSync('''
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Hello</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<block name="content">
|
||||||
|
Fallback content
|
||||||
|
</block>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
''');
|
||||||
|
|
||||||
|
viewsDirectory.childFile('github.jl').writeAsStringSync('''
|
||||||
|
<extend src="layout.jl">
|
||||||
|
<block name="content">{{username}}</block>
|
||||||
|
</extend>
|
||||||
|
''');
|
||||||
|
|
||||||
|
app.get('/github/:username', (String username, ResponseContext res) {
|
||||||
|
return res.render('github', {'username': username});
|
||||||
|
});
|
||||||
|
|
||||||
|
await app.configure(
|
||||||
|
jael(viewsDirectory),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use(() => throw new AngelHttpException.notFound());
|
||||||
|
|
||||||
|
app.logger = new Logger('angel')
|
||||||
|
..onRecord.listen((rec) {
|
||||||
|
print(rec);
|
||||||
|
if (rec.error != null) print(rec.error);
|
||||||
|
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||||
|
});
|
||||||
|
|
||||||
|
client = await connectTo(app);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('can render', () async {
|
||||||
|
var response = await client.get('/github/thosakwe');
|
||||||
|
print('Body:\n${response.body}');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
response,
|
||||||
|
hasBody('''
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>
|
||||||
|
Hello
|
||||||
|
</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
thosakwe
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''
|
||||||
|
.trim()));
|
||||||
|
});
|
||||||
|
}
|
1
jael.iml
1
jael.iml
|
@ -16,6 +16,5 @@
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
import 'package:file/file.dart';
|
import 'package:file/file.dart';
|
||||||
import 'package:jael/jael.dart';
|
import 'package:jael/jael.dart';
|
||||||
|
|
||||||
|
@ -10,10 +11,15 @@ Future<Document> resolve(Document document, Directory currentDirectory,
|
||||||
// Resolve all includes...
|
// Resolve all includes...
|
||||||
var includesResolved =
|
var includesResolved =
|
||||||
await resolveIncludes(document, currentDirectory, onError);
|
await resolveIncludes(document, currentDirectory, onError);
|
||||||
|
return await applyInheritance(includesResolved, currentDirectory, onError);
|
||||||
|
}
|
||||||
|
|
||||||
if (includesResolved.root.tagName.name != 'extend') return includesResolved;
|
/// Folds any `extend` declarations.
|
||||||
|
Future<Document> applyInheritance(Document document, Directory currentDirectory,
|
||||||
|
void onError(JaelError error)) async {
|
||||||
|
if (document.root.tagName.name != 'extend') return document;
|
||||||
|
|
||||||
var element = includesResolved.root;
|
var element = document.root;
|
||||||
var attr = element.attributes
|
var attr = element.attributes
|
||||||
.firstWhere((a) => a.name.name == 'src', orElse: () => null);
|
.firstWhere((a) => a.name.name == 'src', orElse: () => null);
|
||||||
if (attr == null) {
|
if (attr == null) {
|
||||||
|
@ -27,43 +33,92 @@ Future<Document> resolve(Document document, Directory currentDirectory,
|
||||||
element.tagName.span));
|
element.tagName.span));
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
var src = (attr.value as StringLiteral).value;
|
// First, we need to identify the root template.
|
||||||
var file =
|
var chain = new Queue<Document>();
|
||||||
currentDirectory.fileSystem.file(currentDirectory.uri.resolve(src));
|
|
||||||
var contents = await file.readAsString();
|
|
||||||
var doc = parseDocument(contents, sourceUrl: file.uri, onError: onError);
|
|
||||||
var processed = await resolve(
|
|
||||||
doc, currentDirectory.fileSystem.directory(file.dirname),
|
|
||||||
onError: onError);
|
|
||||||
|
|
||||||
Map<String, Element> blocks = {};
|
while (document != null) {
|
||||||
var blockElements = element.children
|
chain.addFirst(document);
|
||||||
.where((e) => e is Element && e.tagName.name == 'block');
|
var parent = getParent(document, onError);
|
||||||
|
if (parent == null) break;
|
||||||
for (Element blockElement in blockElements) {
|
var file = currentDirectory.fileSystem
|
||||||
var nameAttr = blockElement.attributes
|
.file(currentDirectory.uri.resolve(parent));
|
||||||
.firstWhere((a) => a.name.name == 'name', orElse: () => null);
|
var contents = await file.readAsString();
|
||||||
if (nameAttr == null) {
|
document = parseDocument(contents, sourceUrl: file.uri, onError: onError);
|
||||||
onError(new JaelError(JaelErrorSeverity.warning,
|
if (document != null)
|
||||||
'Missing "name" attribute in "block" tag.', blockElement.span));
|
document = await resolveIncludes(document, file.parent, onError);
|
||||||
} else if (nameAttr.value is! StringLiteral) {
|
|
||||||
onError(new JaelError(
|
|
||||||
JaelErrorSeverity.warning,
|
|
||||||
'The "name" attribute in an "block" tag must be a string literal.',
|
|
||||||
nameAttr.span));
|
|
||||||
} else {
|
|
||||||
var name = (nameAttr.value as StringLiteral).value;
|
|
||||||
blocks[name] = blockElement;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Then, for each referenced template, in order, transform the last template
|
||||||
|
// by filling in blocks.
|
||||||
|
document = chain.removeFirst();
|
||||||
|
|
||||||
|
while (chain.isNotEmpty) {
|
||||||
|
var child = chain.removeFirst();
|
||||||
|
var blocks = extractBlockDeclarations(child.root, onError);
|
||||||
|
var blocksExpanded =
|
||||||
|
await expandBlocks(document.root, blocks, currentDirectory, onError);
|
||||||
|
document =
|
||||||
|
new Document(child.doctype ?? document.doctype, blocksExpanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in any remaining blocks
|
||||||
var blocksExpanded =
|
var blocksExpanded =
|
||||||
await _expandBlocks(processed.root, blocks, currentDirectory, onError);
|
await expandBlocks(document.root, {}, currentDirectory, onError);
|
||||||
return new Document(document.doctype ?? processed.doctype, blocksExpanded);
|
return new Document(document.doctype, blocksExpanded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Element> _expandBlocks(Element element, Map<String, Element> blocks,
|
/// Extracts any `block` declarations.
|
||||||
|
Map<String, Element> extractBlockDeclarations(
|
||||||
|
Element element, void onError(JaelError error)) {
|
||||||
|
Map<String, Element> blocks = {};
|
||||||
|
var blockElements =
|
||||||
|
element.children.where((e) => e is Element && e.tagName.name == 'block');
|
||||||
|
|
||||||
|
for (Element blockElement in blockElements) {
|
||||||
|
var nameAttr = blockElement.attributes
|
||||||
|
.firstWhere((a) => a.name.name == 'name', orElse: () => null);
|
||||||
|
if (nameAttr == null) {
|
||||||
|
onError(new JaelError(JaelErrorSeverity.warning,
|
||||||
|
'Missing "name" attribute in "block" tag.', blockElement.span));
|
||||||
|
} else if (nameAttr.value is! StringLiteral) {
|
||||||
|
onError(new JaelError(
|
||||||
|
JaelErrorSeverity.warning,
|
||||||
|
'The "name" attribute in an "block" tag must be a string literal.',
|
||||||
|
nameAttr.span));
|
||||||
|
} else {
|
||||||
|
var name = (nameAttr.value as StringLiteral).value;
|
||||||
|
blocks[name] = blockElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds the name of the parent template.
|
||||||
|
String getParent(Document document, void onError(JaelError error)) {
|
||||||
|
var element = document.root;
|
||||||
|
if (element.tagName.name != 'extend') return null;
|
||||||
|
|
||||||
|
var attr = element.attributes
|
||||||
|
.firstWhere((a) => a.name.name == 'src', orElse: () => null);
|
||||||
|
if (attr == null) {
|
||||||
|
onError(new JaelError(JaelErrorSeverity.warning,
|
||||||
|
'Missing "src" attribute in "extend" tag.', element.tagName.span));
|
||||||
|
return null;
|
||||||
|
} else if (attr.value is! StringLiteral) {
|
||||||
|
onError(new JaelError(
|
||||||
|
JaelErrorSeverity.warning,
|
||||||
|
'The "src" attribute in an "extend" tag must be a string literal.',
|
||||||
|
element.tagName.span));
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return (attr.value as StringLiteral).value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces any `block` tags within the element.
|
||||||
|
Future<Element> expandBlocks(Element element, Map<String, Element> blocks,
|
||||||
Directory currentDirectory, void onError(JaelError error)) async {
|
Directory currentDirectory, void onError(JaelError error)) async {
|
||||||
if (element is SelfClosingElement)
|
if (element is SelfClosingElement)
|
||||||
return element;
|
return element;
|
||||||
|
@ -80,13 +135,11 @@ Future<Element> _expandBlocks(Element element, Map<String, Element> blocks,
|
||||||
if (child.tagName.name != 'block') {
|
if (child.tagName.name != 'block') {
|
||||||
expanded.add(child);
|
expanded.add(child);
|
||||||
} else {
|
} else {
|
||||||
var nameAttr =
|
var nameAttr = child.attributes
|
||||||
child.attributes.firstWhere((a) => a.name.name == 'name', orElse: () => null);
|
.firstWhere((a) => a.name.name == 'name', orElse: () => null);
|
||||||
if (nameAttr == null) {
|
if (nameAttr == null) {
|
||||||
onError(new JaelError(
|
onError(new JaelError(JaelErrorSeverity.warning,
|
||||||
JaelErrorSeverity.warning,
|
'Missing "name" attribute in "block" tag.', child.span));
|
||||||
'Missing "name" attribute in "block" tag.',
|
|
||||||
child.span));
|
|
||||||
} else if (nameAttr.value is! StringLiteral) {
|
} else if (nameAttr.value is! StringLiteral) {
|
||||||
onError(new JaelError(
|
onError(new JaelError(
|
||||||
JaelErrorSeverity.warning,
|
JaelErrorSeverity.warning,
|
||||||
|
@ -117,7 +170,7 @@ Future<Element> _expandBlocks(Element element, Map<String, Element> blocks,
|
||||||
// Resolve all includes...
|
// Resolve all includes...
|
||||||
expanded = await Future.wait(expanded.map((c) {
|
expanded = await Future.wait(expanded.map((c) {
|
||||||
if (c is! Element) return new Future.value(c);
|
if (c is! Element) return new Future.value(c);
|
||||||
return _expandIncludes(c, currentDirectory, onError);
|
return expandBlocks(c, blocks, currentDirectory, onError);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return new RegularElement(
|
return new RegularElement(
|
||||||
|
|
|
@ -21,7 +21,7 @@ main() {
|
||||||
|
|
||||||
// c.jl
|
// c.jl
|
||||||
fileSystem.file('c.jl').writeAsStringSync(
|
fileSystem.file('c.jl').writeAsStringSync(
|
||||||
'<extend src="b.jl"><block name="greeting">Goodbye</block></extend>');
|
'<extend src="b.jl"><block name="greeting"><block name="greeting"></block>Goodbye</block></extend>');
|
||||||
|
|
||||||
// d.jl
|
// d.jl
|
||||||
fileSystem.file('d.jl').writeAsStringSync(
|
fileSystem.file('d.jl').writeAsStringSync(
|
||||||
|
@ -50,7 +50,7 @@ main() {
|
||||||
'''.trim());
|
'''.trim());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('blocks can be overwritten', () async {
|
test('block resolution is recursive', () async {
|
||||||
var file = fileSystem.file('d.jl');
|
var file = fileSystem.file('d.jl');
|
||||||
var original = jael.parseDocument(await file.readAsString(),
|
var original = jael.parseDocument(await file.readAsString(),
|
||||||
sourceUrl: file.uri, onError: (e) => throw e);
|
sourceUrl: file.uri, onError: (e) => throw e);
|
||||||
|
@ -67,7 +67,7 @@ main() {
|
||||||
<b>
|
<b>
|
||||||
a.jl
|
a.jl
|
||||||
</b>
|
</b>
|
||||||
Saluton!
|
Saluton!Goodbye
|
||||||
</i>
|
</i>
|
||||||
'''.trim());
|
'''.trim());
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Fast-fail on errors
|
||||||
|
set -e
|
||||||
|
|
||||||
cd jael && pub get && pub run test
|
cd jael && pub get && pub run test
|
||||||
cd ../jael_preprocessor/ && pub get && pub run test
|
cd ../jael_preprocessor/ && pub get && pub run test
|
||||||
|
cd ../angel_jael/ && pub get && pub run test
|
||||||
|
|
Loading…
Reference in a new issue