Add 'packages/seo/' from commit '43e953044bb7f8eb8fc4c0d11f49e9cd53583e48'
git-subtree-dir: packages/seo git-subtree-mainline:e271a0eafd
git-subtree-split:43e953044b
This commit is contained in:
commit
9458a72c57
16 changed files with 495 additions and 0 deletions
13
packages/seo/.gitignore
vendored
Normal file
13
packages/seo/.gitignore
vendored
Normal file
|
@ -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/
|
4
packages/seo/.travis.yml
Normal file
4
packages/seo/.travis.yml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
language: dart
|
||||||
|
dart:
|
||||||
|
- dev
|
||||||
|
- stable
|
5
packages/seo/CHANGELOG.md
Normal file
5
packages/seo/CHANGELOG.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# 2.0.0
|
||||||
|
* Angel 2 updates.
|
||||||
|
|
||||||
|
# 1.0.0
|
||||||
|
* Initial release.
|
21
packages/seo/LICENSE
Normal file
21
packages/seo/LICENSE
Normal file
|
@ -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.
|
82
packages/seo/README.md
Normal file
82
packages/seo/README.md
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
# 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
|
||||||
|
[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.
|
||||||
|
|
||||||
|
## Disabling inlining per-element
|
||||||
|
Add a `data-no-inline` attribute to a `link` or `script` to prevent inlining it:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="main.dart.js" data-no-inline></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## `inlineAssets`
|
||||||
|
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 `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
|
||||||
|
`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);
|
||||||
|
|
||||||
|
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();
|
||||||
|
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}');
|
||||||
|
}
|
||||||
|
```
|
3
packages/seo/analysis_options.yaml
Normal file
3
packages/seo/analysis_options.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
analyzer:
|
||||||
|
strong-mode:
|
||||||
|
implicit-casts: false
|
43
packages/seo/example/main.dart
Normal file
43
packages/seo/example/main.dart
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:angel_framework/http.dart';
|
||||||
|
import 'package:angel_seo/angel_seo.dart';
|
||||||
|
import 'package:angel_static/angel_static.dart';
|
||||||
|
import 'package:file/local.dart';
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
|
||||||
|
main() async {
|
||||||
|
var app = new Angel();
|
||||||
|
var fs = const LocalFileSystem();
|
||||||
|
var http = new AngelHttp(app);
|
||||||
|
|
||||||
|
// You can wrap a [VirtualDirectory]
|
||||||
|
var vDir = inlineAssetsFromVirtualDirectory(
|
||||||
|
new VirtualDirectory(
|
||||||
|
app,
|
||||||
|
fs,
|
||||||
|
source: fs.directory('web'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.fallback(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', (req, res) async {
|
||||||
|
var indexHtml = fs.directory('web').childFile('index.html');
|
||||||
|
var contents = await indexHtml.readAsString();
|
||||||
|
res
|
||||||
|
..contentType = new MediaType('text', 'html', {'charset': 'utf-8'})
|
||||||
|
..buffer.add(utf8.encode(contents));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.fallback((req, res) => throw new AngelHttpException.notFound());
|
||||||
|
|
||||||
|
var server = await http.startServer('127.0.0.1', 3000);
|
||||||
|
print('Listening at http://${server.address.address}:${server.port}');
|
||||||
|
}
|
18
packages/seo/example/web/index.html
Normal file
18
packages/seo/example/web/index.html
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<link rel="stylesheet" href="site.css">
|
||||||
|
<link rel="stylesheet" href="not-inlined.css" data-no-inline>
|
||||||
|
<script src="site.js"></script>
|
||||||
|
<script src="not-inlined.js" data-no-inline></script>
|
||||||
|
<title>Angel SEO</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Angel SEO</h1>
|
||||||
|
<p>Embrace the power of inlined styles, etc.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
3
packages/seo/example/web/not-inlined.css
Normal file
3
packages/seo/example/web/not-inlined.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
p {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
3
packages/seo/example/web/not-inlined.js
Normal file
3
packages/seo/example/web/not-inlined.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
console.log('THIS message was not from an inlined file.');
|
||||||
|
});
|
3
packages/seo/example/web/site.css
Normal file
3
packages/seo/example/web/site.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
h1 {
|
||||||
|
color: pink;
|
||||||
|
}
|
3
packages/seo/example/web/site.js
Normal file
3
packages/seo/example/web/site.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
console.log('Hello, inline world!');
|
||||||
|
});
|
1
packages/seo/lib/angel_seo.dart
Normal file
1
packages/seo/lib/angel_seo.dart
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export 'src/inline_assets.dart';
|
120
packages/seo/lib/src/inline_assets.dart
Normal file
120
packages/seo/lib/src/inline_assets.dart
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// Inlines assets into buffered responses, resolving paths from an [assetDirectory].
|
||||||
|
///
|
||||||
|
/// 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`.
|
||||||
|
RequestHandler inlineAssets(Directory assetDirectory) {
|
||||||
|
return (req, res) {
|
||||||
|
if (!res.isOpen ||
|
||||||
|
!res.isBuffered ||
|
||||||
|
res.contentType.mimeType != 'text/html') {
|
||||||
|
return new Future<bool>.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') ??
|
||||||
|
<html.Element>[];
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
_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);
|
||||||
|
|
||||||
|
res
|
||||||
|
..headers['content-type'] = 'text/html; charset=utf8'
|
||||||
|
..add(utf8.encode(doc.outerHtml));
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return await inner.serveFile(file, stat, req, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
packages/seo/pubspec.yaml
Normal file
18
packages/seo/pubspec.yaml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
name: angel_seo
|
||||||
|
description: Helper infrastructure for building SEO-friendly Web backends in Angel.
|
||||||
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
|
homepage: https://github.com/angel-dart/seo
|
||||||
|
version: 2.0.0
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.0.0-dev <3.0.0"
|
||||||
|
dependencies:
|
||||||
|
angel_framework: ^2.0.0-alpha
|
||||||
|
angel_static: ^2.0.0-alpha
|
||||||
|
file: ^5.0.0
|
||||||
|
html: ^0.13.0
|
||||||
|
http_parser: ^3.0.0
|
||||||
|
path: ^1.0.0
|
||||||
|
dev_dependencies:
|
||||||
|
angel_test: ^2.0.0-alpha
|
||||||
|
logging:
|
||||||
|
test: ^1.0.0
|
155
packages/seo/test/inline_assets_test.dart
Normal file
155
packages/seo/test/inline_assets_test.dart
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
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:file/file.dart';
|
||||||
|
import 'package:file/memory.dart';
|
||||||
|
import 'package:html/dom.dart' as html;
|
||||||
|
import 'package:html/parser.dart' as html;
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('inlineAssets', () {
|
||||||
|
group('buffer', inlineAssetsTests((app, dir) {
|
||||||
|
app.get('/', (req, res) async {
|
||||||
|
var indexHtml = dir.childFile('index.html');
|
||||||
|
var contents = await indexHtml.readAsBytes();
|
||||||
|
res
|
||||||
|
..useBuffer()
|
||||||
|
..contentType = new MediaType.parse('text/html; charset=utf-8')
|
||||||
|
..buffer.add(contents);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.responseFinalizers.add(inlineAssets(dir));
|
||||||
|
}));
|
||||||
|
|
||||||
|
group('virtual_directory', inlineAssetsTests((app, dir) {
|
||||||
|
var vDir = inlineAssetsFromVirtualDirectory(
|
||||||
|
new VirtualDirectory(app, dir.fileSystem, source: dir));
|
||||||
|
app.fallback(vDir.handleRequest);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Typedef for backwards-compatibility with Dart 1.
|
||||||
|
typedef void InlineAssetTest(Angel app, Directory dir);
|
||||||
|
|
||||||
|
void Function() inlineAssetsTests(InlineAssetTest 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
app.logger = new Logger('angel_seo')
|
||||||
|
..onRecord.listen((rec) {
|
||||||
|
print(rec);
|
||||||
|
if (rec.error != null) print(rec.error);
|
||||||
|
if (rec.stackTrace != null) print(rec.stackTrace);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() => client.close());
|
||||||
|
|
||||||
|
group('sends html', () {
|
||||||
|
html.Document doc;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
var res = await client.get('/', headers: {'accept': 'text/html'});
|
||||||
|
print(res.body);
|
||||||
|
doc = html.parse(res.body);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('stylesheets', () {
|
||||||
|
test('replaces <link> with <style>', () {
|
||||||
|
expect(doc.querySelectorAll('link'), hasLength(1));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('populates a <style>', () {
|
||||||
|
var style = doc.querySelector('style');
|
||||||
|
expect(style?.innerHtml?.trim(), contents['site.css']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('heeds data-no-inline', () {
|
||||||
|
var link = doc.querySelector('link');
|
||||||
|
expect(link.attributes, containsPair('rel', 'stylesheet'));
|
||||||
|
expect(link.attributes, containsPair('href', 'not-inlined.css'));
|
||||||
|
expect(link.attributes.keys, isNot(contains('data-no-inline')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves other attributes', () {
|
||||||
|
var link = doc.querySelector('link');
|
||||||
|
expect(link.attributes, containsPair('data-foo', 'bar'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('scripts', () {
|
||||||
|
test('does not replace <script> with anything', () {
|
||||||
|
expect(doc.querySelectorAll('script'), hasLength(2));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('populates innerHtml', () {
|
||||||
|
var script0 = doc.querySelectorAll('script')[0];
|
||||||
|
expect(script0.innerHtml.trim(), contents['site.js']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('heeds data-no-inline', () {
|
||||||
|
var script1 = doc.querySelectorAll('script')[1];
|
||||||
|
expect(script1.attributes, containsPair('src', 'not-inlined.js'));
|
||||||
|
expect(script1.attributes.keys, isNot(contains('data-no-inline')));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves other attributes', () {
|
||||||
|
var script0 = doc.querySelectorAll('script')[0];
|
||||||
|
var script1 = doc.querySelectorAll('script')[1];
|
||||||
|
expect(script0.attributes, containsPair('data-foo', 'bar'));
|
||||||
|
expect(script1.attributes, containsPair('type', 'text/javascript'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var contents = <String, String>{
|
||||||
|
'index.html': '''<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<link rel="stylesheet" href="site.css">
|
||||||
|
<link data-foo="bar" rel="stylesheet" href="not-inlined.css" data-no-inline>
|
||||||
|
<script data-foo="bar" src="site.js"></script>
|
||||||
|
<script type="text/javascript" src="not-inlined.js" data-no-inline></script>
|
||||||
|
<title>Angel SEO</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Angel SEO</h1>
|
||||||
|
<p>Embrace the power of inlined styles, etc.</p>
|
||||||
|
</body>
|
||||||
|
</html>''',
|
||||||
|
'not-inlined.css': '''p {
|
||||||
|
font-style: italic;
|
||||||
|
}''',
|
||||||
|
'not-inlined.js': '''window.addEventListener('load', function() {
|
||||||
|
console.log('THIS message was not from an inlined file.');
|
||||||
|
});''',
|
||||||
|
'site.css': '''h1 {
|
||||||
|
color: pink;
|
||||||
|
}''',
|
||||||
|
'site.js': '''window.addEventListener('load', function() {
|
||||||
|
console.log('Hello, inline world!');
|
||||||
|
});'''
|
||||||
|
};
|
Loading…
Reference in a new issue