platform/packages/seo/lib/src/inline_assets.dart

121 lines
4.1 KiB
Dart
Raw Normal View History

2018-07-09 06:14:25 +00:00
import 'dart:async';
2018-11-08 16:17:44 +00:00
import 'dart:convert';
2018-07-09 06:14:25 +00:00
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_static/angel_static.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;
2018-11-08 16:17:44 +00:00
/// Inlines assets into buffered responses, resolving paths from an [assetDirectory].
2018-07-09 06:19:22 +00:00
///
/// 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`.
2018-11-08 16:17:44 +00:00
RequestHandler inlineAssets(Directory assetDirectory) {
return (req, res) {
2018-11-08 16:17:44 +00:00
if (!res.isOpen ||
!res.isBuffered ||
res.contentType.mimeType != 'text/html') {
2021-06-20 12:37:20 +00:00
return Future<bool>.value(true);
} else {
2021-06-20 12:37:20 +00:00
var doc = html.parse(utf8.decode(res.buffer!.takeBytes()));
return inlineAssetsIntoDocument(doc, assetDirectory).then((_) {
2021-06-20 12:37:20 +00:00
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) =>
2021-06-20 12:37:20 +00:00
_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(
2021-06-20 12:37:20 +00:00
html.Document doc, Directory? assetDirectory) async {
var linksWithRel = doc.head
?.getElementsByTagName('link')
2021-06-20 12:37:20 +00:00
.where((link) => link.attributes['rel'] == 'stylesheet') ??
<html.Element>[];
for (var link in linksWithRel) {
if (link.attributes.containsKey('data-no-inline')) {
link.attributes.remove('data-no-inline');
} else {
2021-06-20 12:37:20 +00:00
var uri = Uri.parse(link.attributes['href']!);
if (uri.scheme.isEmpty) {
2021-06-20 12:37:20 +00:00
var styleFile = assetDirectory!.childFile(uri.path);
if (await styleFile.exists()) {
2021-06-20 12:37:20 +00:00
var style = 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 {
2021-06-20 12:37:20 +00:00
var uri = Uri.parse(script.attributes['src']!);
if (uri.scheme.isEmpty) {
2021-06-20 12:37:20 +00:00
var scriptFile = assetDirectory!.childFile(uri.path);
if (await scriptFile.exists()) {
script.attributes.remove('src');
script.innerHtml = await scriptFile.readAsString();
}
}
}
}
}
2018-07-09 06:14:25 +00:00
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<bool> 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());
await inlineAssetsIntoDocument(doc, inner.source);
2018-07-09 06:14:25 +00:00
res
..headers['content-type'] = 'text/html; charset=utf8'
2018-11-08 16:17:44 +00:00
..add(utf8.encode(doc.outerHtml));
2018-07-09 06:14:25 +00:00
return false;
} else {
return await inner.serveFile(file, stat, req, res);
}
}
}