diff --git a/packages/html/.gitignore b/packages/html/.gitignore
new file mode 100644
index 00000000..fe4c4cfa
--- /dev/null
+++ b/packages/html/.gitignore
@@ -0,0 +1,58 @@
+# See https://www.dartlang.org/tools/private-files.html
+
+# Files and directories created by pub
+.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/
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff:
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/dictionaries
+
+# Sensitive or high-churn files:
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.xml
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+
+# Gradle:
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Mongo Explorer plugin:
+.idea/**/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+.dart_tool
\ No newline at end of file
diff --git a/packages/html/.idea/vcs.xml b/packages/html/.idea/vcs.xml
new file mode 100644
index 00000000..94a25f7f
--- /dev/null
+++ b/packages/html/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/html/.travis.yml b/packages/html/.travis.yml
new file mode 100644
index 00000000..de2210c9
--- /dev/null
+++ b/packages/html/.travis.yml
@@ -0,0 +1 @@
+language: dart
\ No newline at end of file
diff --git a/packages/html/CHANGELOG.md b/packages/html/CHANGELOG.md
new file mode 100644
index 00000000..324effd8
--- /dev/null
+++ b/packages/html/CHANGELOG.md
@@ -0,0 +1,2 @@
+# 2.0.0
+* Angel 2 + Dart 2 updates.
\ No newline at end of file
diff --git a/packages/html/LICENSE b/packages/html/LICENSE
new file mode 100644
index 00000000..89074fd3
--- /dev/null
+++ b/packages/html/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 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/packages/html/README.md b/packages/html/README.md
new file mode 100644
index 00000000..cdf92e78
--- /dev/null
+++ b/packages/html/README.md
@@ -0,0 +1,83 @@
+# html
+[![Pub](https://img.shields.io/pub/v/angel_html.svg)](https://pub.dartlang.org/packages/angel_html)
+[![build status](https://travis-ci.org/angel-dart/html.svg)](https://travis-ci.org/angel-dart/html)
+
+A plug-in that allows you to return html_builder AST's from request handlers, and have them sent as HTML automatically.
+
+[`package:html_builder`](https://github.com/thosakwe/html_builder) is a simple virtual DOM library
+(without diffing, you can find that
+[here](https://github.com/thosakwe/html_builder_vdom)), with a handy Dart DSL that makes it easy to build HTML
+AST's:
+
+```dart
+import 'package:html_builder/elements.dart';
+
+Node myDom = html(lang: 'en', c: [
+ head(c: [
+ meta(name: 'viewport', content: 'width=device-width, initial-scale=1'),
+ title(c: [
+ text('html_builder example page')
+ ]),
+ ]),
+ body(c: [
+ h1(c: [
+ text('Hello world!'),
+ ]),
+ ]),
+]);
+```
+
+This plug-in means that you can now `return` these AST's, and Angel will automatically send them to
+clients. Ultimately, the implication is that you can use `html_builder` as a substitute for a
+templating system within Dart. With [hot reloading](https://github.com/angel-dart/hot), you won't
+even need to reload your server (as it should be).
+
+# Installation
+In your `pubspec.yaml`:
+
+```yaml
+dependencies:
+ angel_html: ^1.0.0
+```
+
+# Usage
+The `renderHtml` function does all the magic for you.
+
+```dart
+configureServer(Angel app) async {
+ // Wire it up!
+ app.fallback(renderHtml());
+
+ // You can pass a custom StringRenderer if you need more control over the output.
+ app.fallback(renderHtml(renderer: new StringRenderer(html5: false)));
+
+ app.get('/greet/:name', (RequestContext req) {
+ return html(lang: 'en', c: [
+ head(c: [
+ meta(name: 'viewport', content: 'width=device-width, initial-scale=1'),
+ title(c: [
+ text('Greetings!')
+ ]),
+ ]),
+ body(c: [
+ h1(c: [
+ text('Hello, ${req.params['id']}!'),
+ ]),
+ ]),
+ ]);
+ });
+}
+```
+
+By default, `renderHtml` will ignore the client's `Accept` header. However, if you pass
+`enforceAcceptHeader` as `true`, then a `406 Not Acceptable` error will be thrown if the
+client doesn't accept `*/*` or `text/html`.
+
+```dart
+configureServer(Angel app) async {
+ // Wire it up!
+ app.fallback(renderHtml(enforceAcceptHeader: true));
+
+ // ...
+}
+```
\ No newline at end of file
diff --git a/packages/html/analysis_options.yaml b/packages/html/analysis_options.yaml
new file mode 100644
index 00000000..c230cee7
--- /dev/null
+++ b/packages/html/analysis_options.yaml
@@ -0,0 +1,4 @@
+include: package:pedantic/analysis_options.yaml
+analyzer:
+ strong-mode:
+ implicit-casts: false
\ No newline at end of file
diff --git a/packages/html/example/main.dart b/packages/html/example/main.dart
new file mode 100644
index 00000000..d1570ec3
--- /dev/null
+++ b/packages/html/example/main.dart
@@ -0,0 +1,46 @@
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_framework/http.dart';
+import 'package:angel_html/angel_html.dart';
+import 'package:html_builder/elements.dart';
+import 'package:logging/logging.dart';
+
+main() async {
+ var app = Angel(), http = AngelHttp(app);
+ app.logger = Logger('angel_html')
+ ..onRecord.listen((rec) {
+ print(rec);
+ if (rec.error != null) print(rec.error);
+ if (rec.stackTrace != null) print(rec.stackTrace);
+ });
+
+ app.fallback(renderHtml());
+
+ app.get('/html', (req, res) {
+ return html(c: [
+ head(c: [
+ title(c: [text('ok')])
+ ])
+ ]);
+ });
+
+ app.get(
+ '/strict',
+ chain([
+ renderHtml(
+ enforceAcceptHeader: true,
+ renderer: StringRenderer(
+ doctype: null,
+ pretty: false,
+ ),
+ ),
+ (req, res) {
+ return div(c: [text('strict')]);
+ },
+ ]),
+ );
+
+ app.fallback((req, res) => throw AngelHttpException.notFound());
+
+ await http.startServer('127.0.0.1', 3000);
+ print('Listening at ${http.uri}');
+}
diff --git a/packages/html/lib/angel_html.dart b/packages/html/lib/angel_html.dart
new file mode 100644
index 00000000..81b7c853
--- /dev/null
+++ b/packages/html/lib/angel_html.dart
@@ -0,0 +1,34 @@
+import 'dart:async';
+import 'package:angel_framework/angel_framework.dart';
+import 'package:html_builder/html_builder.dart';
+
+/// Returns a [RequestMiddleware] that allows you to return `html_builder` [Node]s as responses.
+///
+/// You can provide a custom [renderer]. The default renders minified HTML5 pages.
+///
+/// Set [enforceAcceptHeader] to `true` to throw a `406 Not Acceptable` if the client doesn't accept HTML responses.
+RequestHandler renderHtml({StringRenderer renderer, bool enforceAcceptHeader}) {
+ renderer ??= new StringRenderer(pretty: false, html5: true);
+
+ return (RequestContext req, ResponseContext res) {
+ var oldSerializer = res.serializer;
+
+ res.serializer = (data) {
+ if (data is! Node)
+ return oldSerializer(data);
+ else {
+ if (enforceAcceptHeader == true && !req.accepts('text/html'))
+ throw new AngelHttpException.notAcceptable();
+
+ var content = renderer.render(data as Node);
+ res
+ ..headers['content-type'] = 'text/html'
+ ..write(content);
+ res.close();
+ return '';
+ }
+ };
+
+ return new Future.value(true);
+ };
+}
diff --git a/packages/html/pubspec.yaml b/packages/html/pubspec.yaml
new file mode 100644
index 00000000..4851fd05
--- /dev/null
+++ b/packages/html/pubspec.yaml
@@ -0,0 +1,16 @@
+name: angel_html
+version: 2.0.0
+description: Support for rendering html_builder AST's as responses in Angel.
+author: Tobe O
+homepage: https://github.com/angel-dart/html_builder
+environment:
+ sdk: ">=2.0.0-dev <3.0.0"
+dependencies:
+ angel_framework: ^2.0.0-alpha
+ html_builder: ^1.0.0
+dev_dependencies:
+ angel_test: ^2.0.0-alpha
+ html: ^0.13.2
+ logging: ^0.11.0
+ test: ^1.0.0
+ pedantic: ^1.0.0
\ No newline at end of file
diff --git a/packages/html/test/all_test.dart b/packages/html/test/all_test.dart
new file mode 100644
index 00000000..e6777418
--- /dev/null
+++ b/packages/html/test/all_test.dart
@@ -0,0 +1,83 @@
+import 'package:angel_framework/angel_framework.dart';
+import 'package:angel_html/angel_html.dart';
+import 'package:angel_test/angel_test.dart';
+import 'package:html_builder/elements.dart';
+import 'package:html_builder/html_builder.dart';
+import 'package:test/test.dart';
+
+main() {
+ Angel app;
+ TestClient client;
+
+ setUp(() async {
+ app = new Angel();
+
+ app.fallback(renderHtml());
+
+ app.get('/html', (req, res) {
+ return html(c: [
+ head(c: [
+ title(c: [text('ok')])
+ ])
+ ]);
+ });
+
+ app.get(
+ '/strict',
+ chain([
+ renderHtml(
+ enforceAcceptHeader: true,
+ renderer: new StringRenderer(
+ doctype: null,
+ pretty: false,
+ ),
+ ),
+ (req, res) {
+ return div(c: [text('strict')]);
+ },
+ ]),
+ );
+ client = await connectTo(app);
+ });
+
+ tearDown(() => client.close());
+
+ test('sets content type and body', () async {
+ var response = await client.get('/html');
+ print('Response: ${response.body}');
+
+ expect(
+ response,
+ allOf(
+ hasContentType('text/html'),
+ hasBody(
+ 'ok')));
+ });
+
+ group('enforce accept header', () {
+ test('sends if correct accept or wildcard', () async {
+ var response = await client.get('/strict', headers: {'accept': '*/*'});
+ print('Response: ${response.body}');
+ expect(response,
+ allOf(hasContentType('text/html'), hasBody('strict
')));
+
+ response = await client.get('/strict',
+ headers: {'accept': 'text/html,application/json,text/xml'});
+ print('Response: ${response.body}');
+ expect(response,
+ allOf(hasContentType('text/html'), hasBody('strict
')));
+ });
+
+ test('throws if incorrect or no accept', () async {
+ var response = await client.get('/strict');
+ print('Response: ${response.body}');
+ expect(response, hasStatus(406));
+
+ response = await client
+ .get('/strict', headers: {'accept': 'application/json,text/xml'});
+ print('Response: ${response.body}');
+ expect(response,
+ isAngelHttpException(statusCode: 406, message: '406 Not Acceptable'));
+ });
+ });
+}