From 576860b06ab169e3dc8372a3febac7466743db38 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Sat, 30 Sep 2017 23:14:44 -0400 Subject: [PATCH] Working, just need switch --- angel_jael/README.md | 81 ++++++++++++ angel_jael/lib/angel_jael.dart | 35 +++-- angel_jael/test/all_test.dart | 83 ++++++++++++ jael.iml | 1 - jael_preprocessor/lib/jael_preprocessor.dart | 131 +++++++++++++------ jael_preprocessor/test/block_test.dart | 6 +- travis.sh | 4 + 7 files changed, 288 insertions(+), 53 deletions(-) create mode 100644 angel_jael/README.md create mode 100644 angel_jael/test/all_test.dart diff --git a/angel_jael/README.md b/angel_jael/README.md new file mode 100644 index 00000000..fcb38bf6 --- /dev/null +++ b/angel_jael/README.md @@ -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}'); +} + +``` \ No newline at end of file diff --git a/angel_jael/lib/angel_jael.dart b/angel_jael/lib/angel_jael.dart index 49264581..e5dbf03d 100644 --- a/angel_jael/lib/angel_jael.dart +++ b/angel_jael/lib/angel_jael.dart @@ -6,24 +6,39 @@ import 'package:jael/jael.dart'; import 'package:jael_preprocessor/jael_preprocessor.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, - {String fileExtension, CodeBuffer createBuffer()}) { + {String fileExtension, bool cacheViews: false, CodeBuffer createBuffer()}) { + var cache = {}; fileExtension ??= '.jl'; createBuffer ??= () => new CodeBuffer(); return (Angel app) async { app.viewGenerator = (String name, [Map locals]) async { - var file = viewsDirectory.childFile(name + fileExtension); - var contents = await file.readAsString(); var errors = []; - var doc = - parseDocument(contents, sourceUrl: file.uri, onError: errors.add); - var processed = doc; + Document processed; - try { - processed = await resolve(doc, viewsDirectory, onError: errors.add); - } catch (_) { - // Ignore these errors, so that we can show syntax errors. + if (cacheViews == true && cache.containsKey(name)) { + processed = cache[name]; + } else { + 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(); diff --git a/angel_jael/test/all_test.dart b/angel_jael/test/all_test.dart new file mode 100644 index 00000000..2b239c76 --- /dev/null +++ b/angel_jael/test/all_test.dart @@ -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(''' + + + + Hello + + + + Fallback content + + + + '''); + + viewsDirectory.childFile('github.jl').writeAsStringSync(''' + + {{username}} + + '''); + + 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(''' + + + + + Hello + + + + thosakwe + + + ''' + .trim())); + }); +} diff --git a/jael.iml b/jael.iml index 6f2c9ef7..7a7b1cb3 100644 --- a/jael.iml +++ b/jael.iml @@ -16,6 +16,5 @@ - \ No newline at end of file diff --git a/jael_preprocessor/lib/jael_preprocessor.dart b/jael_preprocessor/lib/jael_preprocessor.dart index 0296b8f0..c0203808 100644 --- a/jael_preprocessor/lib/jael_preprocessor.dart +++ b/jael_preprocessor/lib/jael_preprocessor.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:collection'; import 'package:file/file.dart'; import 'package:jael/jael.dart'; @@ -10,10 +11,15 @@ Future resolve(Document document, Directory currentDirectory, // Resolve all includes... var includesResolved = await resolveIncludes(document, currentDirectory, onError); + return await applyInheritance(includesResolved, currentDirectory, onError); +} - if (includesResolved.root.tagName.name != 'extend') return includesResolved; +/// Folds any `extend` declarations. +Future 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 .firstWhere((a) => a.name.name == 'src', orElse: () => null); if (attr == null) { @@ -27,43 +33,92 @@ Future resolve(Document document, Directory currentDirectory, element.tagName.span)); return null; } else { - var src = (attr.value as StringLiteral).value; - var file = - 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); + // First, we need to identify the root template. + var chain = new Queue(); - Map 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; - } + while (document != null) { + chain.addFirst(document); + var parent = getParent(document, onError); + if (parent == null) break; + var file = currentDirectory.fileSystem + .file(currentDirectory.uri.resolve(parent)); + var contents = await file.readAsString(); + document = parseDocument(contents, sourceUrl: file.uri, onError: onError); + if (document != null) + document = await resolveIncludes(document, file.parent, onError); } + // 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 = - await _expandBlocks(processed.root, blocks, currentDirectory, onError); - return new Document(document.doctype ?? processed.doctype, blocksExpanded); + await expandBlocks(document.root, {}, currentDirectory, onError); + return new Document(document.doctype, blocksExpanded); } } -Future _expandBlocks(Element element, Map blocks, +/// Extracts any `block` declarations. +Map extractBlockDeclarations( + Element element, void onError(JaelError error)) { + Map 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 expandBlocks(Element element, Map blocks, Directory currentDirectory, void onError(JaelError error)) async { if (element is SelfClosingElement) return element; @@ -80,13 +135,11 @@ Future _expandBlocks(Element element, Map blocks, if (child.tagName.name != 'block') { expanded.add(child); } else { - var nameAttr = - child.attributes.firstWhere((a) => a.name.name == 'name', orElse: () => null); + var nameAttr = child.attributes + .firstWhere((a) => a.name.name == 'name', orElse: () => null); if (nameAttr == null) { - onError(new JaelError( - JaelErrorSeverity.warning, - 'Missing "name" attribute in "block" tag.', - child.span)); + onError(new JaelError(JaelErrorSeverity.warning, + 'Missing "name" attribute in "block" tag.', child.span)); } else if (nameAttr.value is! StringLiteral) { onError(new JaelError( JaelErrorSeverity.warning, @@ -117,7 +170,7 @@ Future _expandBlocks(Element element, Map blocks, // Resolve all includes... expanded = await Future.wait(expanded.map((c) { if (c is! Element) return new Future.value(c); - return _expandIncludes(c, currentDirectory, onError); + return expandBlocks(c, blocks, currentDirectory, onError); })); return new RegularElement( diff --git a/jael_preprocessor/test/block_test.dart b/jael_preprocessor/test/block_test.dart index e5d83b97..ba3083c1 100644 --- a/jael_preprocessor/test/block_test.dart +++ b/jael_preprocessor/test/block_test.dart @@ -21,7 +21,7 @@ main() { // c.jl fileSystem.file('c.jl').writeAsStringSync( - 'Goodbye'); + 'Goodbye'); // d.jl fileSystem.file('d.jl').writeAsStringSync( @@ -50,7 +50,7 @@ main() { '''.trim()); }); - test('blocks can be overwritten', () async { + test('block resolution is recursive', () async { var file = fileSystem.file('d.jl'); var original = jael.parseDocument(await file.readAsString(), sourceUrl: file.uri, onError: (e) => throw e); @@ -67,7 +67,7 @@ main() { a.jl - Saluton! + Saluton!Goodbye '''.trim()); }); diff --git a/travis.sh b/travis.sh index f8414112..92084126 100644 --- a/travis.sh +++ b/travis.sh @@ -1,3 +1,7 @@ #!/usr/bin/env bash +# Fast-fail on errors +set -e + cd jael && pub get && pub run test cd ../jael_preprocessor/ && pub get && pub run test +cd ../angel_jael/ && pub get && pub run test