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'