From 60cac8afe01aac5b34fb389b698665ec9700c9ff Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 9 Jul 2018 01:28:31 -0400 Subject: [PATCH 1/8] Initial commit --- .gitignore | 13 +++++++++++++ LICENSE | 21 +++++++++++++++++++++ README.md | 2 ++ 3 files changed, 36 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7bf00e82 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.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/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..f8e6088a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 The Angel Framework + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..09970539 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# seo +Helpers for building SEO-friendly Web pages in Angel. From 83f1f39fbd63b817433002eb852ef529a52aa71f Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 9 Jul 2018 02:14:25 -0400 Subject: [PATCH 2/8] Inline CSS+JS assets --- CHANGELOG.md | 2 + README.md | 45 ++++++++++++++++++++- analysis_options.yaml | 3 ++ example/main.dart | 25 ++++++++++++ example/web/index.html | 18 +++++++++ example/web/not-inlined.css | 3 ++ example/web/not-inlined.js | 3 ++ example/web/site.css | 3 ++ example/web/site.js | 3 ++ lib/angel_seo.dart | 1 + lib/src/inline_assets.dart | 81 +++++++++++++++++++++++++++++++++++++ pubspec.yaml | 9 +++++ 12 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md create mode 100644 analysis_options.yaml create mode 100644 example/main.dart create mode 100644 example/web/index.html create mode 100644 example/web/not-inlined.css create mode 100644 example/web/not-inlined.js create mode 100644 example/web/site.css create mode 100644 example/web/site.js create mode 100644 lib/angel_seo.dart create mode 100644 lib/src/inline_assets.dart create mode 100644 pubspec.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..66bca2d4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2 @@ +# 1.0.0 +* Initial release. \ No newline at end of file diff --git a/README.md b/README.md index 09970539..4c365cc7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,45 @@ # seo -Helpers for building SEO-friendly Web pages in Angel. +Helpers for building SEO-friendly Web pages in Angel. The goal of +`package:angel_seo` is to speed up perceived client page loads, prevent +the infamous +[flash of unstyled content](https://en.wikipedia.org/wiki/Flash_of_unstyled_content), +and other SEO optimizations that can easily become tedious to perform by hand. + +## `inlineAssets` +This function is a simple one; it wraps a `VirtualDirectory` to patch the way it sends +`.html` files. + +In any `.html` file sent down, `link` and `script` elements that point to internal resources +will have the contents of said file read, and inlined into the HTML page itself. + +In this case, "internal resources" refers to a URI *without* a scheme, i.e. `/site.css` or +`foo/bar/baz.js`. + +```dart +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_seo/angel_seo.dart'; +import 'package:angel_static/angel_static.dart'; +import 'package:file/local.dart'; + +main() async { + var app = new Angel()..lazyParseBodies = true; + var fs = const LocalFileSystem(); + var http = new AngelHttp(app); + + var vDir = inlineAssets( + new VirtualDirectory( + app, + fs, + source: fs.directory('web'), + ), + ); + + app.use(vDir.handleRequest); + + app.use(() => throw new AngelHttpException.notFound()); + + var server = await http.startServer('127.0.0.1', 3000); + print('Listening at http://${server.address.address}:${server.port}'); +} + +``` \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..0ac20d6b --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + strong-mode: + implicit-cast: false \ No newline at end of file diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 00000000..8d7d82d1 --- /dev/null +++ b/example/main.dart @@ -0,0 +1,25 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_seo/angel_seo.dart'; +import 'package:angel_static/angel_static.dart'; +import 'package:file/local.dart'; + +main() async { + var app = new Angel()..lazyParseBodies = true; + var fs = const LocalFileSystem(); + var http = new AngelHttp(app); + + var vDir = inlineAssets( + new VirtualDirectory( + app, + fs, + source: fs.directory('web'), + ), + ); + + app.use(vDir.handleRequest); + + app.use(() => throw new AngelHttpException.notFound()); + + var server = await http.startServer('127.0.0.1', 3000); + print('Listening at http://${server.address.address}:${server.port}'); +} diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 00000000..50161197 --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + + Angel SEO + + +

Angel SEO

+

Embrace the power of inlined styles, etc.

+ + \ No newline at end of file diff --git a/example/web/not-inlined.css b/example/web/not-inlined.css new file mode 100644 index 00000000..ab2a12a3 --- /dev/null +++ b/example/web/not-inlined.css @@ -0,0 +1,3 @@ +p { + font-style: italic; +} \ No newline at end of file diff --git a/example/web/not-inlined.js b/example/web/not-inlined.js new file mode 100644 index 00000000..6a8ad492 --- /dev/null +++ b/example/web/not-inlined.js @@ -0,0 +1,3 @@ +window.addEventListener('load', function() { + console.log('THIS message was not from an inlined file.'); +}); \ No newline at end of file diff --git a/example/web/site.css b/example/web/site.css new file mode 100644 index 00000000..acbbda4b --- /dev/null +++ b/example/web/site.css @@ -0,0 +1,3 @@ +h1 { + color: pink; +} \ No newline at end of file diff --git a/example/web/site.js b/example/web/site.js new file mode 100644 index 00000000..283b1eff --- /dev/null +++ b/example/web/site.js @@ -0,0 +1,3 @@ +window.addEventListener('load', function() { + console.log('Hello, inline world!'); +}); \ No newline at end of file diff --git a/lib/angel_seo.dart b/lib/angel_seo.dart new file mode 100644 index 00000000..8288ada9 --- /dev/null +++ b/lib/angel_seo.dart @@ -0,0 +1 @@ +export 'src/inline_assets.dart'; \ No newline at end of file diff --git a/lib/src/inline_assets.dart b/lib/src/inline_assets.dart new file mode 100644 index 00000000..2e33b36f --- /dev/null +++ b/lib/src/inline_assets.dart @@ -0,0 +1,81 @@ +import 'dart:async'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_static/angel_static.dart'; +import 'package:dart2_constant/convert.dart'; +import 'package:file/file.dart'; +import 'package:html/dom.dart' as html; +import 'package:html/parser.dart' as html; +import 'package:path/path.dart' as p; + +VirtualDirectory inlineAssets(VirtualDirectory vDir) => new _InlineAssets(vDir); + +class _InlineAssets extends VirtualDirectory { + final VirtualDirectory inner; + + _InlineAssets(this.inner) + : super(inner.app, inner.fileSystem, + source: inner.source, + indexFileNames: inner.indexFileNames, + publicPath: inner.publicPath, + callback: inner.callback, + allowDirectoryListing: inner.allowDirectoryListing); + + @override + Future serveFile( + File file, FileStat stat, RequestContext req, ResponseContext res) async { + if (p.extension(file.path) == '.html') { + var contents = await file.readAsString(); + var doc = html.parse(contents, sourceUrl: file.uri.toString()); + + var linksWithRel = doc.head + ?.getElementsByTagName('link') + ?.where((link) => link.attributes['rel'] == 'stylesheet') ?? + []; + + for (var link in linksWithRel) { + if (link.attributes.containsKey('data-no-inline')) { + link.attributes.remove('data-no-inline'); + } else { + var uri = Uri.parse(link.attributes['href']); + + if (uri.scheme.isEmpty) { + var styleFile = inner.source.childFile(uri.path); + + if (await styleFile.exists()) { + var style = new html.Element.tag('style') + ..innerHtml = await styleFile.readAsString(); + link.replaceWith(style); + } + } + } + } + + var scripts = doc + .getElementsByTagName('script') + .where((script) => script.attributes.containsKey('src')); + + for (var script in scripts) { + if (script.attributes.containsKey('data-no-inline')) { + script.attributes.remove('data-no-inline'); + } else { + var uri = Uri.parse(script.attributes['src']); + + if (uri.scheme.isEmpty) { + var scriptFile = inner.source.childFile(uri.path); + if (await scriptFile.exists()) { + script.attributes.remove('src'); + script.innerHtml = await scriptFile.readAsString(); + } + } + } + } + + res + ..headers['content-type'] = 'text/html; charset=utf8' + ..buffer.add(utf8.encode(doc.outerHtml)); + return false; + } else { + return await inner.serveFile(file, stat, req, res); + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..781443aa --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,9 @@ +name: angel_seo +dependencies: + angel_framework: ^1.0.0-dev + dart2_constant: ^1.0.0 + file: ^2.0.0 + html: ^0.13.0 + path: ^1.0.0 +dev_dependencies: + angel_static: ^1.3.0 \ No newline at end of file From 3faa5509eac5c5535b637131dd3939332b11a19d Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 9 Jul 2018 02:19:22 -0400 Subject: [PATCH 3/8] Doc comments --- lib/src/inline_assets.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/src/inline_assets.dart b/lib/src/inline_assets.dart index 2e33b36f..e1f4e327 100644 --- a/lib/src/inline_assets.dart +++ b/lib/src/inline_assets.dart @@ -7,6 +7,14 @@ import 'package:html/dom.dart' as html; import 'package:html/parser.dart' as html; import 'package:path/path.dart' as p; +/// Wraps a `VirtualDirectory` to patch the way it sends +/// `.html` files. +/// +/// In any `.html` file sent down, `link` and `script` elements that point to internal resources +/// will have the contents of said file read, and inlined into the HTML page itself. +/// +/// In this case, "internal resources" refers to a URI *without* a scheme, i.e. `/site.css` or +/// `foo/bar/baz.js`. VirtualDirectory inlineAssets(VirtualDirectory vDir) => new _InlineAssets(vDir); class _InlineAssets extends VirtualDirectory { From d997734f0f0cb637617d66419ec73bccd21446e2 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 9 Jul 2018 09:49:25 -0400 Subject: [PATCH 4/8] Added inlineAssets, split out inlineAssetsFromVirtualDirectory --- README.md | 35 +++++++++-- example/main.dart | 18 +++++- lib/src/inline_assets.dart | 120 +++++++++++++++++++++++-------------- 3 files changed, 124 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 4c365cc7..ba53635b 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,12 @@ the infamous and other SEO optimizations that can easily become tedious to perform by hand. ## `inlineAssets` -This function is a simple one; it wraps a `VirtualDirectory` to patch the way it sends -`.html` files. +A +[response finalizer](https://angel-dart.gitbook.io/angel/the-basics/request-lifecycle) +that can be used in any application to patch HTML responses, including those sent with +a templating engine like Jael. -In any `.html` file sent down, `link` and `script` elements that point to internal resources +In any `text/html` response sent down, `link` and `script` elements that point to internal resources will have the contents of said file read, and inlined into the HTML page itself. In this case, "internal resources" refers to a URI *without* a scheme, i.e. `/site.css` or @@ -21,6 +23,32 @@ import 'package:angel_seo/angel_seo.dart'; import 'package:angel_static/angel_static.dart'; import 'package:file/local.dart'; +main() async { + var app = new Angel()..lazyParseBodies = true; + var fs = const LocalFileSystem(); + var http = new AngelHttp(app); + + app.responseFinalizers.add(inlineAssets(fs.directory('web'))); + + app.use(() => throw new AngelHttpException.notFound()); + + var server = await http.startServer('127.0.0.1', 3000); + print('Listening at http://${server.address.address}:${server.port}'); +} +``` + +## `inlineAssetsFromVirtualDirectory` +This function is a simple one; it wraps a `VirtualDirectory` to patch the way it sends +`.html` files. + +Produces the same functionality as `inlineAssets`. + +```dart +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_seo/angel_seo.dart'; +import 'package:angel_static/angel_static.dart'; +import 'package:file/local.dart'; + main() async { var app = new Angel()..lazyParseBodies = true; var fs = const LocalFileSystem(); @@ -41,5 +69,4 @@ main() async { var server = await http.startServer('127.0.0.1', 3000); print('Listening at http://${server.address.address}:${server.port}'); } - ``` \ No newline at end of file diff --git a/example/main.dart b/example/main.dart index 8d7d82d1..762d3e31 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,6 +1,7 @@ import 'package:angel_framework/angel_framework.dart'; import 'package:angel_seo/angel_seo.dart'; import 'package:angel_static/angel_static.dart'; +import 'package:dart2_constant/convert.dart'; import 'package:file/local.dart'; main() async { @@ -8,7 +9,8 @@ main() async { var fs = const LocalFileSystem(); var http = new AngelHttp(app); - var vDir = inlineAssets( + // You can wrap a [VirtualDirectory] + var vDir = inlineAssetsFromVirtualDirectory( new VirtualDirectory( app, fs, @@ -18,6 +20,20 @@ main() async { app.use(vDir.handleRequest); + // OR, just add a finalizer. Note that [VirtualDirectory] *streams* its response, + // so a response finalizer does not touch its contents. + // + // You likely won't need to use both; it just depends on your use case. + app.responseFinalizers.add(inlineAssets(fs.directory('web'))); + + app.get('/using_response_buffer', (ResponseContext res) async { + var indexHtml = fs.directory('web').childFile('index.html'); + var contents = await indexHtml.readAsString(); + res + ..headers['content-type'] = 'text/html; charset=utf-8' + ..buffer.add(utf8.encode(contents)); + }); + app.use(() => throw new AngelHttpException.notFound()); var server = await http.startServer('127.0.0.1', 3000); diff --git a/lib/src/inline_assets.dart b/lib/src/inline_assets.dart index e1f4e327..ed365e83 100644 --- a/lib/src/inline_assets.dart +++ b/lib/src/inline_assets.dart @@ -15,7 +15,81 @@ import 'package:path/path.dart' as p; /// /// In this case, "internal resources" refers to a URI *without* a scheme, i.e. `/site.css` or /// `foo/bar/baz.js`. -VirtualDirectory inlineAssets(VirtualDirectory vDir) => new _InlineAssets(vDir); +RequestMiddleware inlineAssets(Directory assetDirectory) { + return (req, res) { + if (res.willCloseItself || + res.streaming || + res.contentType.mimeType != 'text/html') { + return new Future.value(true); + } else { + var doc = html.parse(utf8.decode(res.buffer.takeBytes())); + return inlineAssetsIntoDocument(doc, assetDirectory).then((_) { + res.buffer.add(utf8.encode(doc.outerHtml)); + return false; + }); + } + }; +} + +/// Wraps a `VirtualDirectory` to patch the way it sends +/// `.html` files. +/// +/// In any `.html` file sent down, `link` and `script` elements that point to internal resources +/// will have the contents of said file read, and inlined into the HTML page itself. +/// +/// In this case, "internal resources" refers to a URI *without* a scheme, i.e. `/site.css` or +/// `foo/bar/baz.js`. +VirtualDirectory inlineAssetsFromVirtualDirectory(VirtualDirectory vDir) => + new _InlineAssets(vDir); + +/// Replaces `link` and `script` tags within a [doc] with the static contents they would otherwise trigger an HTTP request to. +/// +/// Powers both [inlineAssets] and [inlineAssetsFromVirtualDirectory]. +Future inlineAssetsIntoDocument( + html.Document doc, Directory assetDirectory) async { + var linksWithRel = doc.head + ?.getElementsByTagName('link') + ?.where((link) => link.attributes['rel'] == 'stylesheet') ?? + []; + + for (var link in linksWithRel) { + if (link.attributes.containsKey('data-no-inline')) { + link.attributes.remove('data-no-inline'); + } else { + var uri = Uri.parse(link.attributes['href']); + + if (uri.scheme.isEmpty) { + var styleFile = assetDirectory.childFile(uri.path); + + if (await styleFile.exists()) { + var style = new html.Element.tag('style') + ..innerHtml = await styleFile.readAsString(); + link.replaceWith(style); + } + } + } + } + + var scripts = doc + .getElementsByTagName('script') + .where((script) => script.attributes.containsKey('src')); + + for (var script in scripts) { + if (script.attributes.containsKey('data-no-inline')) { + script.attributes.remove('data-no-inline'); + } else { + var uri = Uri.parse(script.attributes['src']); + + if (uri.scheme.isEmpty) { + var scriptFile = assetDirectory.childFile(uri.path); + if (await scriptFile.exists()) { + script.attributes.remove('src'); + script.innerHtml = await scriptFile.readAsString(); + } + } + } + } +} class _InlineAssets extends VirtualDirectory { final VirtualDirectory inner; @@ -34,49 +108,7 @@ class _InlineAssets extends VirtualDirectory { if (p.extension(file.path) == '.html') { var contents = await file.readAsString(); var doc = html.parse(contents, sourceUrl: file.uri.toString()); - - var linksWithRel = doc.head - ?.getElementsByTagName('link') - ?.where((link) => link.attributes['rel'] == 'stylesheet') ?? - []; - - for (var link in linksWithRel) { - if (link.attributes.containsKey('data-no-inline')) { - link.attributes.remove('data-no-inline'); - } else { - var uri = Uri.parse(link.attributes['href']); - - if (uri.scheme.isEmpty) { - var styleFile = inner.source.childFile(uri.path); - - if (await styleFile.exists()) { - var style = new html.Element.tag('style') - ..innerHtml = await styleFile.readAsString(); - link.replaceWith(style); - } - } - } - } - - var scripts = doc - .getElementsByTagName('script') - .where((script) => script.attributes.containsKey('src')); - - for (var script in scripts) { - if (script.attributes.containsKey('data-no-inline')) { - script.attributes.remove('data-no-inline'); - } else { - var uri = Uri.parse(script.attributes['src']); - - if (uri.scheme.isEmpty) { - var scriptFile = inner.source.childFile(uri.path); - if (await scriptFile.exists()) { - script.attributes.remove('src'); - script.innerHtml = await scriptFile.readAsString(); - } - } - } - } + await inlineAssetsIntoDocument(doc, inner.source); res ..headers['content-type'] = 'text/html; charset=utf8' From 67a62e751fb0632540638e767bd7482a2e3a5f03 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Mon, 9 Jul 2018 13:20:14 -0400 Subject: [PATCH 5/8] Add tests + Travis --- .travis.yml | 4 + README.md | 3 + lib/angel_seo.dart | 2 +- pubspec.yaml | 10 ++- test/inline_assets_test.dart | 142 +++++++++++++++++++++++++++++++++++ 5 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 .travis.yml create mode 100644 test/inline_assets_test.dart diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..a9e2c109 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: dart +dart: + - dev + - stable \ No newline at end of file diff --git a/README.md b/README.md index ba53635b..32297322 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # seo +[![Pub](https://img.shields.io/pub/v/angel_seo.svg)](https://pub.dartlang.org/packages/angel_seo) +[![build status](https://travis-ci.org/angel-dart/seo.svg?branch=master)](https://travis-ci.org/angel-dart/seo) + Helpers for building SEO-friendly Web pages in Angel. The goal of `package:angel_seo` is to speed up perceived client page loads, prevent the infamous diff --git a/lib/angel_seo.dart b/lib/angel_seo.dart index 8288ada9..8a291cdc 100644 --- a/lib/angel_seo.dart +++ b/lib/angel_seo.dart @@ -1 +1 @@ -export 'src/inline_assets.dart'; \ No newline at end of file +export 'src/inline_assets.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 781443aa..c919faae 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,9 +1,17 @@ name: angel_seo +description: Helpers for building SEO-friendly Web pages in Angel. +author: Tobe O +homepage: https://github.com/angel-dart/seo +version: 1.0.0 +environment: + sdk: ">=1.8.0 <3.0.0" dependencies: angel_framework: ^1.0.0-dev + angel_static: ^1.3.0 dart2_constant: ^1.0.0 file: ^2.0.0 html: ^0.13.0 path: ^1.0.0 dev_dependencies: - angel_static: ^1.3.0 \ No newline at end of file + angel_test: ^1.0.0 + test: ^0.12.0 \ No newline at end of file diff --git a/test/inline_assets_test.dart b/test/inline_assets_test.dart new file mode 100644 index 00000000..99aa2a21 --- /dev/null +++ b/test/inline_assets_test.dart @@ -0,0 +1,142 @@ +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_seo/angel_seo.dart'; +import 'package:angel_static/angel_static.dart'; +import 'package:angel_test/angel_test.dart'; +import 'package:dart2_constant/convert.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:html/dom.dart' as html; +import 'package:html/parser.dart' as html; +import 'package:test/test.dart'; + +void main() { + group('inlineAssets', () { + group('buffer', inlineAssetsTests((app, dir) { + app.get('/', (ResponseContext res) async { + var indexHtml = dir.childFile('index.html'); + var contents = await indexHtml.readAsString(); + res + ..headers['content-type'] = 'text/html; charset=utf-8' + ..buffer.add(utf8.encode(contents)); + }); + + app.responseFinalizers.add(inlineAssets(dir)); + })); + + group('virtual_directory', inlineAssetsTests((app, dir) { + var vDir = inlineAssetsFromVirtualDirectory( + new VirtualDirectory(app, dir.fileSystem, source: dir)); + app.use(vDir.handleRequest); + })); + }); +} + +void Function() inlineAssetsTests(void Function(Angel, Directory) f) { + return () { + TestClient client; + + setUp(() async { + var app = new Angel(); + var fs = new MemoryFileSystem(); + var dir = fs.currentDirectory; + f(app, dir); + client = await connectTo(app); + + for (var path in contents.keys) { + var file = fs.file(path); + await file.writeAsString(contents[path].trim()); + } + }); + + tearDown(() => client.close()); + + group('sends html', () { + html.Document doc; + + setUp(() async { + var res = await client.get('/', headers: {'accept': 'text/html'}); + doc = html.parse(res.body); + }); + + group('stylesheets', () { + test('replaces with