From 2a69ecf91bba11958053e51bbdbe1b8f3011db9b Mon Sep 17 00:00:00 2001 From: "thomashii@dukefirehawk.com" Date: Sat, 1 May 2021 10:48:36 +0800 Subject: [PATCH] Added and migrated html_builder --- CHANGELOG.md | 2 + packages/hot/lib/angel_hot.dart | 6 +- packages/hot/pubspec.yaml | 10 +- packages/html_builder/CHANGELOG.md | 9 + packages/html_builder/LICENSE | 21 + packages/html_builder/README.md | 108 + packages/html_builder/analysis_options.yaml | 3 + packages/html_builder/lib/elements.dart | 2041 +++++++++++++++++ packages/html_builder/lib/html_builder.dart | 4 + packages/html_builder/lib/src/mutations.dart | 20 + packages/html_builder/lib/src/node.dart | 56 + .../html_builder/lib/src/node_builder.dart | 106 + packages/html_builder/lib/src/renderer.dart | 134 ++ packages/html_builder/pubspec.yaml | 10 + packages/html_builder/test/render_test.dart | 34 + packages/range_header/CHANGELOG.md | 12 + packages/range_header/LICENSE | 21 + packages/range_header/README.md | 36 + packages/range_header/analysis_options.yaml | 3 + packages/range_header/example/main.dart | 28 + packages/range_header/example/server.dart | 108 + packages/range_header/lib/range_header.dart | 4 + packages/range_header/lib/src/converter.dart | 163 ++ packages/range_header/lib/src/exception.dart | 8 + packages/range_header/lib/src/parser.dart | 143 ++ .../range_header/lib/src/range_header.dart | 63 + .../lib/src/range_header_impl.dart | 20 + .../lib/src/range_header_item.dart | 90 + packages/range_header/pubspec.yaml | 20 + packages/range_header/test/all_test.dart | 96 + packages/static/example/main.dart | 2 +- packages/static/lib/src/cache.dart | 8 +- .../static/lib/src/virtual_directory.dart | 2 +- packages/static/pubspec.yaml | 4 +- 34 files changed, 3379 insertions(+), 16 deletions(-) create mode 100644 packages/html_builder/CHANGELOG.md create mode 100644 packages/html_builder/LICENSE create mode 100644 packages/html_builder/README.md create mode 100644 packages/html_builder/analysis_options.yaml create mode 100644 packages/html_builder/lib/elements.dart create mode 100644 packages/html_builder/lib/html_builder.dart create mode 100644 packages/html_builder/lib/src/mutations.dart create mode 100644 packages/html_builder/lib/src/node.dart create mode 100644 packages/html_builder/lib/src/node_builder.dart create mode 100644 packages/html_builder/lib/src/renderer.dart create mode 100644 packages/html_builder/pubspec.yaml create mode 100644 packages/html_builder/test/render_test.dart create mode 100644 packages/range_header/CHANGELOG.md create mode 100644 packages/range_header/LICENSE create mode 100644 packages/range_header/README.md create mode 100644 packages/range_header/analysis_options.yaml create mode 100644 packages/range_header/example/main.dart create mode 100644 packages/range_header/example/server.dart create mode 100644 packages/range_header/lib/range_header.dart create mode 100644 packages/range_header/lib/src/converter.dart create mode 100644 packages/range_header/lib/src/exception.dart create mode 100644 packages/range_header/lib/src/parser.dart create mode 100644 packages/range_header/lib/src/range_header.dart create mode 100644 packages/range_header/lib/src/range_header_impl.dart create mode 100644 packages/range_header/lib/src/range_header_item.dart create mode 100644 packages/range_header/pubspec.yaml create mode 100644 packages/range_header/test/all_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 68872a60..3748f8de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,9 @@ * Migrated angel_jael to 4.0.0 (1/1 test passed) * Migrated pub_sub to 4.0.0 (16/16 tests passed) * Migrated production to 3.0.0 (0/0 tests passed) +* Added html_builder and migrated to 2.0.0 (16/16 tests passed) * Updated hot to 3.0.0 (in progress) +* Added range_header and migrated to 2.0.0 (16/16 tests passed) * Updated static to 3.0.0 (in progress) * Update basic-sdk-2.12.x boilerplate (in progress) diff --git a/packages/hot/lib/angel_hot.dart b/packages/hot/lib/angel_hot.dart index 1903d656..22b077bf 100644 --- a/packages/hot/lib/angel_hot.dart +++ b/packages/hot/lib/angel_hot.dart @@ -14,8 +14,8 @@ import 'package:html_builder/elements.dart'; import 'package:html_builder/html_builder.dart'; import 'package:io/ansi.dart'; import 'package:path/path.dart' as p; -import 'package:vm_service_lib/vm_service_lib.dart' as vm; -import 'package:vm_service_lib/vm_service_lib_io.dart' as vm; +import 'package:vm_service/vm_service.dart' as vm; +import 'package:vm_service/vm_service_io.dart' as vm; import 'package:watcher/watcher.dart'; /// A utility class that watches the filesystem for changes, and starts new instances of an Angel server. @@ -374,7 +374,7 @@ class HotReloader { if (hot) { var report = await _client.reloadSources(_mainIsolate.id); - if (!report.success) { + if (report.success != null) { _logWarning( 'Hot reload failed - perhaps some sources have not been generated yet.'); } diff --git a/packages/hot/pubspec.yaml b/packages/hot/pubspec.yaml index 9f71785c..4724d788 100644 --- a/packages/hot/pubspec.yaml +++ b/packages/hot/pubspec.yaml @@ -1,6 +1,6 @@ name: angel_hot description: Supports hot reloading/hot code push of Angel servers on file changes. -version: 3.0.0 +version: 4.0.0 author: Tobe O homepage: https://github.com/angel-dart/hot publish_to: none @@ -10,19 +10,19 @@ dependencies: angel_framework: git: url: https://github.com/dukefirehawk/angel.git - ref: sdk-2.12.x + ref: sdk-2.12.x_nnbd path: packages/framework - angel_websocket: #^2.0.0-alpha + angel_websocket: git: url: https://github.com/dukefirehawk/angel.git - ref: sdk-2.12.x + ref: sdk-2.12.x_nnbd path: packages/websocket charcode: ^1.0.0 glob: ^2.0.0 html_builder: ^1.0.0 io: ^0.3.5 path: ^1.0.0 - vm_service_lib: ^3.22.2+1 + vm_service: ^5.5.0 watcher: ^1.0.0 dev_dependencies: http: ^0.13.0 diff --git a/packages/html_builder/CHANGELOG.md b/packages/html_builder/CHANGELOG.md new file mode 100644 index 00000000..1ae83f01 --- /dev/null +++ b/packages/html_builder/CHANGELOG.md @@ -0,0 +1,9 @@ +# 1.0.4 +* Added `rebuild`, `rebuildRecursive`, and `NodeBuilder`. + +# 1.0.3 +* Dart 2 ready! + +# 1.0.2 +Changed `h` and the `Node` constructor to take `Iterable`s of children, +instead of just `List`s. \ No newline at end of file diff --git a/packages/html_builder/LICENSE b/packages/html_builder/LICENSE new file mode 100644 index 00000000..3de28325 --- /dev/null +++ b/packages/html_builder/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Tobe O + +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_builder/README.md b/packages/html_builder/README.md new file mode 100644 index 00000000..56c81331 --- /dev/null +++ b/packages/html_builder/README.md @@ -0,0 +1,108 @@ +# html_builder +[![Pub](https://img.shields.io/pub/v/html_builder.svg)](https://pub.dartlang.org/packages/html_builder) +[![build status](https://travis-ci.org/thosakwe/html_builder.svg)](https://travis-ci.org/thosakwe/html_builder) + +Build HTML AST's and render them to HTML. + +This can be used as an internal DSL, i.e. for a templating engine. + +# Installation +In your `pubspec.yaml`: + +```yaml +dependencies: + html_builder: ^1.0.0 +``` + +# Usage +```dart +import 'package:html_builder/html_builder.dart'; + +main() { + // Akin to React.createElement(...); + var $el = h('my-element', p: {}, c: []); + + + // Attributes can be plain Strings. + h('foo', p: { + 'bar': 'baz' + }); + +i // Null attributes do not appear. + h('foo', p: { + 'does-not-appear': null + }); + + // If an attribute is a bool, then it will only appear if its value is true. + h('foo', p: { + 'appears': true, + 'does-not-appear': false + }); +: + // Or, a String or Map. + h('foo', p: { + 'style': 'background-color: white; color: red;' + }); + + h('foo', p: { + 'style': { + 'background-color': 'white', + 'color': 'red' +/ } + }); + + // Or, a String or Iterable. + h('foo', p: { + 'class': 'a b' + }); + + h('foo', p: { + 'class': ['a', 'b'] + }); +} +``` + +Standard HTML5 elements: +```dart +import 'package:html_builder/elements.dart'; + +main() { + var $dom = html(lang: 'en', c: [ + head(c: [ + title(c: [text('Hello, world!')]) + ]), + body(c: [ + h1(c: [text('Hello, world!')]), + p(c: [text('Ok')]) + ]) + ]); +} +``` + +Rendering to HTML: +```dart +String html = new StringRenderer().render($dom); +``` + +Example with the [Angel](https://github.com/angel-dart/angel) server-side framework, +which has [dedicated html_builder support](https://github.com/angel-dart/html): + +```dart +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:html_builder/elements.dart'; + +configureViews(Angel app) async { + app.get('/foo/:id', (req, res) async { + var foo = await app.service('foo').read(req.params['id']); + return html(c: [ + head(c: [ + title(c: [text(foo.name)]) + ]), + body(c: [ + h1(c: [text(foo.name)]) + ]) + ]); + }); +} +``` diff --git a/packages/html_builder/analysis_options.yaml b/packages/html_builder/analysis_options.yaml new file mode 100644 index 00000000..eae1e42a --- /dev/null +++ b/packages/html_builder/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/packages/html_builder/lib/elements.dart b/packages/html_builder/lib/elements.dart new file mode 100644 index 00000000..924049c0 --- /dev/null +++ b/packages/html_builder/lib/elements.dart @@ -0,0 +1,2041 @@ +/// Helper functions to build common HTML5 elements. +library html_builder.elements; + +import 'html_builder.dart'; +export 'html_builder.dart'; + +Map _apply(Iterable> props, + [Map? attrs]) { + var map = {}; + attrs?.forEach((k, attr) { + if (attr is String && attr.isNotEmpty == true) { + map[k] = attr; + } else if (attr is Iterable && attr.isNotEmpty == true) { + map[k] = attr.toList(); + } else if (attr != null) { + map[k] = attr; + } + }); + + for (var p in props) { + map.addAll(p); + } + + return map.cast(); +} + +Node text(String text) => TextNode(text); + +Node a( + {String? href, + String? rel, + String? target, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'a', + _apply([ + p, + props + ], { + 'href': href, + 'rel': rel, + 'target': target, + 'id': id, + 'class': className, + 'style': style, + }), + []..addAll(c)..addAll(children)); + +Node abbr( + {String? title, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'addr', + _apply([p, props], + {'title': title, 'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node address( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'address', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node area( + {String? alt, + Iterable? coordinates, + String? download, + String? href, + String? hreflang, + String? media, + String? nohref, + String? rel, + String? shape, + String? target, + String? type, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}}) => + SelfClosingNode( + 'area', + _apply([ + p, + props + ], { + 'alt': alt, + 'coordinates': coordinates, + 'download': download, + 'href': href, + 'hreflang': hreflang, + 'media': media, + 'nohref': nohref, + 'rel': rel, + 'shape': shape, + 'target': target, + 'type': type, + 'id': id, + 'class': className, + 'style': style + })); + +Node article( + {className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('article', _apply([p, props], {'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node aside( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'aside', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node audio( + {bool? autoplay, + bool? controls, + bool? loop, + bool? muted, + String? preload, + String? src, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'audio', + _apply([ + p, + props + ], { + 'autoplay': autoplay, + 'controls': controls, + 'loop': loop, + 'muted': muted, + 'preload': preload, + 'src': src, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node b( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('b', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node base( + {String? href, + String? target, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}}) => + SelfClosingNode( + 'base', + _apply([ + p, + props + ], { + 'href': href, + 'target': target, + 'id': id, + 'class': className, + 'style': style + })); + +Node bdi( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('bdi', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node bdo( + {String? dir, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'bdo', + _apply([p, props], + {'dir': dir, 'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node blockquote( + {String? cite, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'blockquote', + _apply([p, props], + {'cite': cite, 'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node body( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'body', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node br() => SelfClosingNode('br'); + +Node button( + {bool? autofocus, + bool? disabled, + form, + String? formaction, + String? formenctype, + String? formmethod, + bool? formnovalidate, + String? formtarget, + String? name, + String? type, + String? value, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'button', + _apply([ + p, + props + ], { + 'autofocus': autofocus, + 'disabled': disabled, + 'form': form, + 'formaction': formaction, + 'formenctype': formenctype, + 'formmethod': formmethod, + 'formnovalidate': formnovalidate, + 'formtarget': formtarget, + 'name': name, + 'type': type, + 'value': value, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node canvas( + {num? height, + num? width, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'canvas', + _apply([ + p, + props + ], { + 'height': height, + 'width': width, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node cite( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'cite', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node caption( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'caption', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node code( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'code', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node col( + {num? span, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'col', + _apply([p, props], + {'span': span, 'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node colgroup( + {num? span, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'colgroup', + _apply([p, props], + {'span': span, 'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node datalist( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'datalist', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node dd( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('dd', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node del( + {String? cite, + String? datetime, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'del', + _apply([ + p, + props + ], { + 'cite': cite, + 'datetime': datetime, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node details( + {bool? open, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'details', + _apply([p, props], + {'open': open, 'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node dfn( + {String? title, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'dfn', + _apply([p, props], + {'title': title, 'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node dialog( + {bool? open, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'dialog', + _apply([p, props], + {'open': open, 'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node div( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('div', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node dl( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('dl', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node dt( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('dt', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node em( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('em', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node embed( + {num? height, + String? src, + String? type, + num? width, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}}) => + SelfClosingNode( + 'embed', + _apply([ + p, + props + ], { + 'height': height, + 'src': src, + 'type': type, + 'width': width, + 'id': id, + 'class': className, + 'style': style + })); + +Node fieldset( + {bool? disabled, + String? form, + String? name, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'fieldset', + _apply([ + p, + props + ], { + 'disabled': disabled, + 'form': form, + 'name': name, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node figcaption( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'figcaption', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node figure( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'figure', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node footer( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'footer', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node form( + {String? accept, + String? acceptCharset, + String? action, + bool? autocomplete, + String? enctype, + String? method, + String? name, + bool? novalidate, + String? target, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'form', + _apply([ + p, + props + ], { + 'accept': accept, + 'accept-charset': acceptCharset, + 'action': action, + 'autocomplete': + autocomplete != null ? (autocomplete ? 'on' : 'off') : null, + 'enctype': enctype, + 'method': method, + 'name': name, + 'novalidate': novalidate, + 'target': target, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node h1( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('h1', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node h2( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('h2', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); +Node h3( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('h3', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node h4( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('h4', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node h5( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('h5', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node h6( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('h6', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node head( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'head', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node header( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'header', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node hr() => SelfClosingNode('hr'); + +Node html( + {String? manifest, + String? xmlns, + String? lang, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'html', + _apply([ + p, + props + ], { + 'manifest': manifest, + 'xmlns': xmlns, + 'lang': lang, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node i( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('i', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node iframe( + {num? height, + String? name, + sandbox, + String? src, + String? srcdoc, + num? width, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}}) => + SelfClosingNode( + 'iframe', + _apply([ + p, + props + ], { + 'height': height, + 'name': name, + 'sandbox': sandbox, + 'src': src, + 'srcdoc': srcdoc, + 'width': width, + 'id': id, + 'class': className, + 'style': style + })); + +Node img( + {String? alt, + String? crossorigin, + num? height, + String? ismap, + String? longdesc, + sizes, + String? src, + String? srcset, + String? usemap, + num? width, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}}) => + SelfClosingNode( + 'img', + _apply([ + p, + props + ], { + 'alt': alt, + 'crossorigin': crossorigin, + 'height': height, + 'ismap': ismap, + 'longdesc': longdesc, + 'sizes': sizes, + 'src': src, + 'srcset': srcset, + 'usemap': usemap, + 'width': width, + 'id': id, + 'class': className, + 'style': style + })); + +Node input( + {String? accept, + String? alt, + bool? autocomplete, + bool? autofocus, + bool? checked, + String? dirname, + bool? disabled, + String? form, + String? formaction, + String? formenctype, + String? method, + String? formnovalidate, + String? formtarget, + num? height, + String? list, + max, + num? maxlength, + min, + bool? multiple, + String? name, + String? pattern, + String? placeholder, + bool? readonly, + bool? required, + num? size, + String? src, + num? step, + String? type, + String? value, + num? width, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}}) => + SelfClosingNode( + 'input', + _apply([ + p, + props + ], { + 'accept': accept, + 'alt': alt, + 'autocomplete': + autocomplete == null ? null : (autocomplete ? 'on' : 'off'), + 'autofocus': autofocus, + 'checked': checked, + 'dirname': dirname, + 'disabled': disabled, + 'form': form, + 'formaction': formaction, + 'formenctype': formenctype, + 'method': method, + 'formnovalidate': formnovalidate, + 'formtarget': formtarget, + 'height': height, + 'list': list, + 'max': max, + 'maxlength': maxlength, + 'min': min, + 'multiple': multiple, + 'name': name, + 'pattern': pattern, + 'placeholder': placeholder, + 'readonly': readonly, + 'required': required, + 'size': size, + 'src': src, + 'step': step, + 'type': type, + 'value': value, + 'width': width, + 'id': id, + 'class': className, + 'style': style + })); + +Node ins( + {String? cite, + String? datetime, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'ins', + _apply([ + p, + props + ], { + 'cite': cite, + 'datetime': datetime, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node kbd( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('kbd', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node keygen( + {bool? autofocus, + String? challenge, + bool? disabled, + String? from, + String? keytype, + String? name, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'keygen', + _apply([ + p, + props + ], { + 'autofocus': autofocus, + 'challenge': challenge, + 'disabled': disabled, + 'from': from, + 'keytype': keytype, + 'name': name, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node label( + {String? for_, + String? form, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'label', + _apply([ + p, + props + ], { + 'for': for_, + 'form': form, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node legend( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'legend', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node li( + {num? value, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'li', + _apply([p, props], + {'value': value, 'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node link( + {String? crossorigin, + String? href, + String? hreflang, + String? media, + String? rel, + sizes, + String? target, + String? type, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}}) => + SelfClosingNode( + 'link', + _apply([ + p, + props + ], { + 'crossorigin': crossorigin, + 'href': href, + 'hreflang': hreflang, + 'media': media, + 'rel': rel, + 'sizes': sizes, + 'target': target, + 'type': type, + 'id': id, + 'class': className, + 'style': style + })); + +Node main( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'main', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node map( + {String? name, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'map', + _apply([p, props], + {'name': name, 'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node mark( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'mark', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node menu( + {String? label, + String? type, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'menu', + _apply([ + p, + props + ], { + 'label': label, + 'type': type, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node menuitem( + {bool? checked, + command, + bool? default_, + bool? disabled, + String? icon, + String? label, + String? radiogroup, + String? type, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'menuitem', + _apply([ + p, + props + ], { + 'checked': checked, + 'command': command, + 'default': default_, + 'disabled': disabled, + 'icon': icon, + 'label': label, + 'radiogroup': radiogroup, + 'type': type, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node meta( + {String? charset, + String? content, + String? httpEquiv, + String? name, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}}) => + SelfClosingNode( + 'meta', + _apply([ + p, + props + ], { + 'charset': charset, + 'content': content, + 'http-equiv': httpEquiv, + 'name': name, + 'id': id, + 'class': className, + 'style': style + })); + +Node nav( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('nav', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node noscript( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'noscript', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node object( + {String? data, + String? form, + num? height, + String? name, + String? type, + String? usemap, + num? width, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'object', + _apply([ + p, + props + ], { + 'data': data, + 'form': form, + 'height': height, + 'name': name, + 'type': type, + 'usemap': usemap, + 'width': width, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node ol( + {bool? reversed, + num? start, + String? type, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'ol', + _apply([ + p, + props + ], { + 'reversed': reversed, + 'start': start, + 'type': type, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node optgroup( + {bool? disabled, + String? label, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'optgroup', + _apply([ + p, + props + ], { + 'disabled': disabled, + 'label': label, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node option( + {bool? disabled, + String? label, + bool? selected, + String? value, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'option', + _apply([ + p, + props + ], { + 'disabled': disabled, + 'label': label, + 'selected': selected, + 'value': value, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node output( + {String? for_, + String? form, + String? name, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'output', + _apply([ + p, + props + ], { + 'for': for_, + 'form': form, + 'name': name, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node p( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('p', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node param( + {String? name, + value, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}}) => + SelfClosingNode( + 'param', + _apply([ + p, + props + ], { + 'name': name, + 'value': value, + 'id': id, + 'class': className, + 'style': style + })); + +Node picture( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'picture', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node pre( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('pre', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node progress( + {num? max, + num? value, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'progress', + _apply([ + p, + props + ], { + 'max': max, + 'value': value, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node q( + {String? cite, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'q', + _apply([p, props], + {'cite': cite, 'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node rp( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('rp', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node rt( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('rt', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node ruby( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'ruby', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node s( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('s', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node samp( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'samp', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node script( + {bool? async, + String? charset, + bool? defer, + String? src, + String? type, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'script', + _apply([ + p, + props + ], { + 'async': async, + 'charset': charset, + 'defer': defer, + 'src': src, + 'type': type, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node section( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'section', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node select( + {bool? autofocus, + bool? disabled, + String? form, + bool? multiple, + bool? required, + num? size, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'select', + _apply([ + p, + props + ], { + 'autofocus': autofocus, + 'disabled': disabled, + 'form': form, + 'multiple': multiple, + 'required': required, + 'size': size, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node small( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'small', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node source( + {String? src, + String? srcset, + String? media, + sizes, + String? type, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}}) => + SelfClosingNode( + 'source', + _apply([ + p, + props + ], { + 'src': src, + 'srcset': srcset, + 'media': media, + 'sizes': sizes, + 'type': type, + 'id': id, + 'class': className, + 'style': style + })); + +Node span( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'span', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node strong( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'strong', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node style( + {String? media, + bool? scoped, + String? type, + String? id, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'style', + _apply([p, props], + {'media': media, 'scoped': scoped, 'type': type, 'id': id}), + []..addAll(c)..addAll(children)); + +Node sub( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('sub', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node summary( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'summary', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node sup( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('sup', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node table( + {bool? sortable, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'table', + _apply([ + p, + props + ], { + 'sortable': sortable, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node tbody( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'tbody', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node td( + {num? colspan, + headers, + num? rowspan, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'td', + _apply([ + p, + props + ], { + 'colspan': colspan, + 'headers': headers, + 'rowspan': rowspan, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node textarea( + {bool? autofocus, + num? cols, + String? dirname, + bool? disabled, + String? form, + num? maxlength, + String? name, + String? placeholder, + bool? readonly, + bool? required, + num? rows, + String? wrap, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'textarea', + _apply([ + p, + props + ], { + 'autofocus': autofocus, + 'cols': cols, + 'dirname': dirname, + 'disabled': disabled, + 'form': form, + 'maxlength': maxlength, + 'name': name, + 'placeholder': placeholder, + 'readonly': readonly, + 'required': required, + 'rows': rows, + 'wrap': wrap, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node tfoot( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'tfoot', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node th( + {String? abbr, + num? colspan, + headers, + num? rowspan, + String? scope, + sorted, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'th', + _apply([ + p, + props + ], { + 'abbr': abbr, + 'colspan': colspan, + 'headers': headers, + 'rowspan': rowspan, + 'scope': scope, + 'sorted': sorted, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node thead( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'thead', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node time( + {String? datetime, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'time', + _apply([ + p, + props + ], { + 'datetime': datetime, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node title( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'title', + _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node tr( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('tr', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node track( + {bool? default_, + String? kind, + String? label, + String? src, + String? srclang, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}}) => + SelfClosingNode( + 'track', + _apply([ + p, + props + ], { + 'default': default_, + 'kind': kind, + 'label': label, + 'src': src, + 'srclang': srclang, + 'id': id, + 'class': className, + 'style': style + })); + +Node u( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('u', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node ul( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('ul', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node var_( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('var', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); + +Node video( + {bool? autoplay, + bool? controls, + num? height, + bool? loop, + bool? muted, + String? poster, + String? preload, + String? src, + num? width, + String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h( + 'video', + _apply([ + p, + props + ], { + 'autoplay': autoplay, + 'controls': controls, + 'height': height, + 'loop': loop, + 'muted': muted, + 'poster': poster, + 'preload': preload, + 'src': src, + 'width': width, + 'id': id, + 'class': className, + 'style': style + }), + []..addAll(c)..addAll(children)); + +Node wbr( + {String? id, + className, + style, + Map p: const {}, + @deprecated Map props: const {}, + Iterable c: const [], + @deprecated Iterable children: const []}) => + h('wbr', _apply([p, props], {'id': id, 'class': className, 'style': style}), + []..addAll(c)..addAll(children)); diff --git a/packages/html_builder/lib/html_builder.dart b/packages/html_builder/lib/html_builder.dart new file mode 100644 index 00000000..a1786ca7 --- /dev/null +++ b/packages/html_builder/lib/html_builder.dart @@ -0,0 +1,4 @@ +export 'src/mutations.dart'; +export 'src/node.dart'; +export 'src/node_builder.dart'; +export 'src/renderer.dart'; \ No newline at end of file diff --git a/packages/html_builder/lib/src/mutations.dart b/packages/html_builder/lib/src/mutations.dart new file mode 100644 index 00000000..04818063 --- /dev/null +++ b/packages/html_builder/lib/src/mutations.dart @@ -0,0 +1,20 @@ +import 'node.dart'; +import 'node_builder.dart'; + +/// Returns a function that rebuilds an arbitrary [Node] by applying the [transform] to it. +Node Function(Node) rebuild(NodeBuilder Function(NodeBuilder) transform, + {bool selfClosing: false}) { + return (node) => + transform(NodeBuilder.from(node)).build(selfClosing: selfClosing); +} + +/// Applies [f] to all children of this node, recursively. +/// +/// Use this alongside [rebuild]. +Node Function(Node) rebuildRecursive(Node Function(Node) f) { + Node _build(Node node) { + return NodeBuilder.from(f(node)).mapChildren(_build).build(); + } + + return _build; +} diff --git a/packages/html_builder/lib/src/node.dart b/packages/html_builder/lib/src/node.dart new file mode 100644 index 00000000..a81b50ed --- /dev/null +++ b/packages/html_builder/lib/src/node.dart @@ -0,0 +1,56 @@ +import 'package:collection/collection.dart'; + +/// Shorthand function to generate a new [Node]. +Node h(String tagName, + [Map attributes = const {}, + Iterable children = const []]) => + Node(tagName, attributes, children); + +/// Represents an HTML node. +class Node { + final String tagName; + final Map attributes = {}; + final List children = []; + + Node(this.tagName, + [Map attributes = const {}, + Iterable children = const []]) { + this..attributes.addAll(attributes)..children.addAll(children); + } + + Node._selfClosing(this.tagName, + [Map attributes = const {}]) { + this..attributes.addAll(attributes); + } + + @override + bool operator ==(other) { + return other is Node && + other.tagName == tagName && + const ListEquality().equals(other.children, children) && + const MapEquality() + .equals(other.attributes, attributes); + } +} + +/// Represents a self-closing tag, i.e. `
`. +class SelfClosingNode extends Node { + final String tagName; + final Map attributes = {}; + + @override + List get children => List.unmodifiable([]); + + SelfClosingNode(this.tagName, [Map attributes = const {}]) + : super._selfClosing(tagName, attributes); +} + +/// Represents a text node. +class TextNode extends Node { + final String text; + + TextNode(this.text) : super(':text'); + + @override + bool operator ==(other) => other is TextNode && other.text == text; +} diff --git a/packages/html_builder/lib/src/node_builder.dart b/packages/html_builder/lib/src/node_builder.dart new file mode 100644 index 00000000..daea4fa2 --- /dev/null +++ b/packages/html_builder/lib/src/node_builder.dart @@ -0,0 +1,106 @@ +import 'node.dart'; + +/// Helper class to build nodes. +class NodeBuilder { + final String tagName; + final Map attributes; + final Iterable children; + Node? _existing; + + NodeBuilder(this.tagName, + {this.attributes: const {}, this.children: const []}); + + /// Creates a [NodeBuilder] that just spits out an already-existing [Node]. + factory NodeBuilder.existing(Node existingNode) => + NodeBuilder(existingNode.tagName).._existing = existingNode; + + factory NodeBuilder.from(Node node) => NodeBuilder(node.tagName, + attributes: Map.from(node.attributes), + children: List.from(node.children)); + + /// Builds the node. + Node build({bool selfClosing: false}) => + _existing ?? + (selfClosing + ? SelfClosingNode(tagName, attributes) + : Node(tagName, attributes, children)); + + /// Produce a modified copy of this builder. + NodeBuilder change( + {String? tagName, + Map? attributes, + Iterable? children}) { + return NodeBuilder(tagName ?? this.tagName, + attributes: attributes ?? this.attributes, + children: children ?? this.children); + } + + NodeBuilder changeTagName(String tagName) => change(tagName: tagName); + + NodeBuilder changeAttributes(Map attributes) => + change(attributes: attributes); + + NodeBuilder changeChildren(Iterable children) => + change(children: children); + + NodeBuilder changeAttributesMapped( + Map Function(Map) f) { + var map = Map.from(attributes); + return changeAttributes(f(map)); + } + + NodeBuilder changeChildrenMapped(Iterable Function(List) f) { + var list = List.from(children); + return changeChildren(f(list)); + } + + NodeBuilder mapChildren(Node Function(Node) f) => + changeChildrenMapped((list) => list.map(f)); + + NodeBuilder mapAttributes( + MapEntry Function(String, dynamic) f) => + changeAttributesMapped((map) => map.map(f)); + + NodeBuilder setAttribute(String name, dynamic value) => + changeAttributesMapped((map) => map..[name] = value); + + NodeBuilder addChild(Node child) => + changeChildrenMapped((list) => list..add(child)); + + NodeBuilder removeChild(Node child) => + changeChildrenMapped((list) => list..remove(child)); + + NodeBuilder removeAttribute(String name) => + changeAttributesMapped((map) => map..remove(name)); + + NodeBuilder setId(String id) => setAttribute('id', id); + + NodeBuilder setClassName(String className) => + setAttribute('class', className); + + NodeBuilder setClasses(Iterable classes) => + setClassName(classes.join(' ')); + + NodeBuilder setClassesMapped(Iterable Function(List) f) { + var clazz = attributes['class']; + var classes = []; + + if (clazz is String) + classes.addAll(clazz.split(' ')); + else if (clazz is Iterable) classes.addAll(clazz.map((s) => s.toString())); + + return setClasses(f(classes)); + } + + NodeBuilder addClass(String className) => setClassesMapped( + (classes) => classes.contains(className) ? classes : classes + ..add(className)); + + NodeBuilder removeClass(String className) => + setClassesMapped((classes) => classes..remove(className)); + + NodeBuilder toggleClass(String className) => + setClassesMapped((classes) => classes.contains(className) + ? (classes..remove(className)) + : (classes..add(className))); +} diff --git a/packages/html_builder/lib/src/renderer.dart b/packages/html_builder/lib/src/renderer.dart new file mode 100644 index 00000000..d77b5461 --- /dev/null +++ b/packages/html_builder/lib/src/renderer.dart @@ -0,0 +1,134 @@ +import 'node.dart'; + +/// An object that can render a DOM tree into another representation, i.e. a `String`. +abstract class Renderer { + /// Renders a DOM tree into another representation. + T render(Node rootNode); +} + +/// Renders a DOM tree into a HTML string. +abstract class StringRenderer implements Renderer { + /// Initializes a new [StringRenderer]. + /// + /// If [html5] is not `false` (default: `true`), then self-closing elements will be rendered with a slash before the last angle bracket, ex. `
`. + /// If [pretty] is `true` (default), then [whitespace] (default: `' '`) will be inserted between nodes. + /// You can also provide a [doctype] (default: `html`). + factory StringRenderer( + {bool html5: true, + bool pretty: true, + String doctype: 'html', + String whitespace: ' '}) => + pretty == true + ? _PrettyStringRendererImpl( + html5: html5 != false, doctype: doctype, whitespace: whitespace) + : _StringRendererImpl(html5: html5 != false, doctype: doctype); +} + +class _StringRendererImpl implements StringRenderer { + final String? doctype; + final bool? html5; + + _StringRendererImpl({this.html5, this.doctype}); + + void _renderInto(Node node, StringBuffer buf) { + if (node is TextNode) { + buf.write(node.text); + } else { + buf.write('<${node.tagName}'); + + node.attributes.forEach((k, v) { + if (v == true) { + buf.write(' $k'); + } else if (v == false || v == null) { + // Ignore + } else if (v is Iterable) { + var val = v.join(' ').replaceAll('"', '\\"'); + buf.write(' $k="$val"'); + } else if (v is Map) { + var val = v.keys + .fold('', (out, k) => out += '$k: ${v[k]};') + .replaceAll('"', '\\"'); + buf.write(' $k="$val"'); + } else { + var val = v.toString().replaceAll('"', '\\"'); + buf.write(' $k="$val"'); + } + }); + + if (node is SelfClosingNode) { + buf.write((html5 != false) ? '>' : '/>'); + } else { + buf.write('>'); + node.children.forEach((child) => _renderInto(child, buf)); + buf.write(''); + } + } + } + + @override + String render(Node rootNode) { + var buf = StringBuffer(); + if (doctype?.isNotEmpty == true) buf.write(''); + _renderInto(rootNode, buf); + return buf.toString(); + } +} + +class _PrettyStringRendererImpl implements StringRenderer { + final bool? html5; + final String? doctype, whitespace; + + _PrettyStringRendererImpl({this.html5, this.whitespace, this.doctype}); + + void _applyTabs(int tabs, StringBuffer buf) { + for (int i = 0; i < tabs; i++) buf.write(whitespace ?? ' '); + } + + void _renderInto(int tabs, Node node, StringBuffer buf) { + if (tabs > 0) buf.writeln(); + _applyTabs(tabs, buf); + + if (node is TextNode) { + buf.write(node.text); + } else { + buf.write('<${node.tagName}'); + + node.attributes.forEach((k, v) { + if (v == true) { + buf.write(' $k'); + } else if (v == false || v == null) { + // Ignore + } else if (v is Iterable) { + var val = v.join(' ').replaceAll('"', '\\"'); + buf.write(' $k="$val"'); + } else if (v is Map) { + var val = v.keys + .fold('', (out, k) => out += '$k: ${v[k]};') + .replaceAll('"', '\\"'); + buf.write(' $k="$val"'); + } else { + var val = v.toString().replaceAll('"', '\\"'); + buf.write(' $k="$val"'); + } + }); + + if (node is SelfClosingNode) { + buf.write((html5 != false) ? '>' : '/>'); + } else { + buf.write('>'); + node.children.forEach((child) => _renderInto(tabs + 1, child, buf)); + buf.writeln(); + _applyTabs(tabs, buf); + buf.write(''); + } + } + } + + @override + String render(Node rootNode) { + var buf = StringBuffer(); + if (doctype?.isNotEmpty == true) buf.writeln(''); + _renderInto(0, rootNode, buf); + return buf.toString(); + } +} diff --git a/packages/html_builder/pubspec.yaml b/packages/html_builder/pubspec.yaml new file mode 100644 index 00000000..29ac379c --- /dev/null +++ b/packages/html_builder/pubspec.yaml @@ -0,0 +1,10 @@ +name: html_builder +description: Build HTML AST's and render them to HTML. This can be used as an internal DSL, i.e. for a templating engine. +version: 2.0.0 +author: Tobe O +homepage: https://github.com/thosakwe/html_builder +environment: + sdk: '>=2.12.0 <3.0.0' +dev_dependencies: + html: ^0.15.0 + test: ^1.17.3 \ No newline at end of file diff --git a/packages/html_builder/test/render_test.dart b/packages/html_builder/test/render_test.dart new file mode 100644 index 00000000..d0c3a4a8 --- /dev/null +++ b/packages/html_builder/test/render_test.dart @@ -0,0 +1,34 @@ +import 'package:html/parser.dart' as html5; +import 'package:html_builder/elements.dart'; +import 'package:html_builder/html_builder.dart'; +import 'package:test/test.dart'; + +main() { + test('pretty', () { + var $dom = html( + lang: 'en', + c: [ + head(c: [ + title(c: [text('Hello, world!')]) + ]), + body( + p: {'unresolved': true}, + c: [ + h1(c: [text('Hello, world!')]), + br(), + hr(), + ], + ) + ], + ); + + var rendered = StringRenderer().render($dom); + print(rendered); + + var $parsed = html5.parse(rendered); + var $title = $parsed.querySelector('title')!; + expect($title.text.trim(), 'Hello, world!'); + var $h1 = $parsed.querySelector('h1')!; + expect($h1.text.trim(), 'Hello, world!'); + }); +} diff --git a/packages/range_header/CHANGELOG.md b/packages/range_header/CHANGELOG.md new file mode 100644 index 00000000..71cde747 --- /dev/null +++ b/packages/range_header/CHANGELOG.md @@ -0,0 +1,12 @@ +# 2.0.2 +* Fix bug in `toContentRange` that printed invalid indices. +* Fold header items by default. + +# 2.0.1 +* Adjust `RangeHeaderTransformer` to properly print the content range of each item, +when multiple are present. + +# 2.0.0 +* Dart 2 update. +* Add `RangeHeaderTransformer`. +* Overall restructuring/refactoring. \ No newline at end of file diff --git a/packages/range_header/LICENSE b/packages/range_header/LICENSE new file mode 100644 index 00000000..3de28325 --- /dev/null +++ b/packages/range_header/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Tobe O + +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/range_header/README.md b/packages/range_header/README.md new file mode 100644 index 00000000..b2dbee4a --- /dev/null +++ b/packages/range_header/README.md @@ -0,0 +1,36 @@ +# range_header + +[![Pub](https://img.shields.io/pub/v/range_header.svg)](https://pub.dartlang.org/packages/range_header) +[![build status](https://travis-ci.org/thosakwe/range_header.svg)](https://travis-ci.org/thosakwe/range_header) + +Range header parser for Dart. + +# Installation +In your `pubspec.yaml`: + +```yaml +dependencies: + range_header: ^2.0.0 +``` + +# Usage + +```dart +handleRequest(HttpRequest request) async { + // Parse the header + var header = new RangeHeader.parse(request.headers.value(HttpHeaders.rangeHeader)); + + // Optimize/canonicalize it + var items = RangeHeader.foldItems(header.items); + header = new RangeHeader(items); + + // Get info + header.items; + header.rangeUnit; + print(header.items[0].toContentRange(fileSize)); + + // Serve the file + var transformer = new RangeHeaderTransformer(header); + await file.openRead().transform(transformer).pipe(request.response); +} +``` diff --git a/packages/range_header/analysis_options.yaml b/packages/range_header/analysis_options.yaml new file mode 100644 index 00000000..eae1e42a --- /dev/null +++ b/packages/range_header/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/packages/range_header/example/main.dart b/packages/range_header/example/main.dart new file mode 100644 index 00000000..2ed95533 --- /dev/null +++ b/packages/range_header/example/main.dart @@ -0,0 +1,28 @@ +import 'dart:io'; +import 'package:range_header/range_header.dart'; + +var file = new File('some_video.mp4'); + +handleRequest(HttpRequest request) async { + // Parse the header + var header = + new RangeHeader.parse(request.headers.value(HttpHeaders.rangeHeader)); + + // Optimize/canonicalize it + var items = RangeHeader.foldItems(header.items); + header = new RangeHeader(items); + + // Get info + header.items; + header.rangeUnit; + header.items.forEach((item) => item.toContentRange(400)); + + // Serve the file + var transformer = + new RangeHeaderTransformer(header, 'video/mp4', await file.length()); + await file + .openRead() + .cast>() + .transform(transformer) + .pipe(request.response); +} diff --git a/packages/range_header/example/server.dart b/packages/range_header/example/server.dart new file mode 100644 index 00000000..38346e92 --- /dev/null +++ b/packages/range_header/example/server.dart @@ -0,0 +1,108 @@ +import 'dart:async'; +import 'dart:io' show HttpHeaders, HttpStatus; +import 'dart:typed_data'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_framework/http.dart'; +import 'package:angel_static/angel_static.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:http_parser/http_parser.dart'; +import 'package:logging/logging.dart'; +import 'package:range_header/range_header.dart'; + +main() async { + var app = new Angel(); + var http = new AngelHttp(app); + var fs = const LocalFileSystem(); + var vDir = new _RangingVirtualDirectory(app, fs.currentDirectory); + app.logger = new Logger('range_header') + ..onRecord.listen((rec) { + print(rec); + if (rec.error != null) print(rec.error); + if (rec.stackTrace != null) print(rec.stackTrace); + }); + app.mimeTypeResolver + ..addExtension('dart', 'text/dart') + ..addExtension('lock', 'text/plain') + ..addExtension('md', 'text/plain') + ..addExtension('packages', 'text/plain') + ..addExtension('yaml', 'text/plain') + ..addExtension('yml', 'text/plain'); + app.fallback(vDir.handleRequest); + app.fallback((req, res) => throw new AngelHttpException.notFound()); + await http.startServer('127.0.0.1', 3000); + print('Listening at ${http.uri}'); +} + +class _RangingVirtualDirectory extends VirtualDirectory { + _RangingVirtualDirectory(Angel app, Directory source) + : super(app, source.fileSystem, + source: source, allowDirectoryListing: true); + + @override + Future serveFile( + File file, FileStat stat, RequestContext req, ResponseContext res) async { + res.headers[HttpHeaders.acceptRangesHeader] = 'bytes'; + + if (req.headers.value(HttpHeaders.rangeHeader)?.startsWith('bytes') == + true) { + var header = + new RangeHeader.parse(req.headers.value(HttpHeaders.rangeHeader)); + header = new RangeHeader(RangeHeader.foldItems(header.items)); + + if (header.items.length == 1) { + var item = header.items[0]; + Stream stream; + int len = 0, total = await file.length(); + + if (item.start == -1) { + if (item.end == -1) { + len = total; + stream = file.openRead(); + } else { + len = item.end + 1; + stream = file.openRead(0, item.end + 1); + } + } else { + if (item.end == -1) { + len = total - item.start; + stream = file.openRead(item.start); + } else { + len = item.end - item.start + 1; + stream = file.openRead(item.start, item.end + 1); + } + } + + res.contentType = new MediaType.parse( + app.mimeTypeResolver.lookup(file.path) ?? + 'application/octet-stream'); + res.statusCode = HttpStatus.partialContent; + res.headers[HttpHeaders.contentLengthHeader] = len.toString(); + res.headers[HttpHeaders.contentRangeHeader] = + 'bytes ' + item.toContentRange(total); + await stream.cast>().pipe(res); + return false; + } else { + var totalFileSize = await file.length(); + var transformer = new RangeHeaderTransformer( + header, + app.mimeTypeResolver.lookup(file.path) ?? + 'application/octet-stream', + await file.length()); + res.statusCode = HttpStatus.partialContent; + res.headers[HttpHeaders.contentLengthHeader] = + transformer.computeContentLength(totalFileSize).toString(); + res.contentType = new MediaType( + 'multipart', 'byteranges', {'boundary': transformer.boundary}); + await file + .openRead() + .cast>() + .transform(transformer) + .pipe(res); + return false; + } + } else { + return await super.serveFile(file, stat, req, res); + } + } +} diff --git a/packages/range_header/lib/range_header.dart b/packages/range_header/lib/range_header.dart new file mode 100644 index 00000000..ea17d35d --- /dev/null +++ b/packages/range_header/lib/range_header.dart @@ -0,0 +1,4 @@ +export 'src/converter.dart'; +export 'src/exception.dart'; +export 'src/range_header.dart'; +export 'src/range_header_item.dart'; diff --git a/packages/range_header/lib/src/converter.dart b/packages/range_header/lib/src/converter.dart new file mode 100644 index 00000000..824067e9 --- /dev/null +++ b/packages/range_header/lib/src/converter.dart @@ -0,0 +1,163 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io' show BytesBuilder; +import 'dart:math'; +import 'package:async/async.dart'; +import 'package:charcode/ascii.dart'; +import 'range_header.dart'; + +/// A [StreamTransformer] that uses a parsed [RangeHeader] and transforms an input stream +/// into one compatible with the `multipart/byte-ranges` specification. +class RangeHeaderTransformer + extends StreamTransformerBase, List> { + final RangeHeader header; + final String boundary, mimeType; + final int totalLength; + + RangeHeaderTransformer(this.header, this.mimeType, this.totalLength, + {String boundary}) + : this.boundary = boundary ?? _randomString() { + if (header == null || header.items.isEmpty) { + throw new ArgumentError('`header` cannot be null or empty.'); + } + } + + /// Computes the content length that will be written to a response, given a stream of the given [totalFileSize]. + int computeContentLength(int totalFileSize) { + int len = 0; + + for (var item in header.items) { + if (item.start == -1) { + if (item.end == -1) { + len += totalFileSize; + } else { + //len += item.end + 1; + len += item.end + 1; + } + } else if (item.end == -1) { + len += totalFileSize - item.start; + //len += totalFileSize - item.start - 1; + } else { + len += item.end - item.start; + } + + // Take into consideration the fact that delimiters are written. + len += utf8.encode('--$boundary\r\n').length; + len += utf8.encode('Content-Type: $mimeType\r\n').length; + len += utf8 + .encode( + 'Content-Range: ${header.rangeUnit} ${item.toContentRange(totalLength)}/$totalLength\r\n\r\n') + .length; + len += 2; // CRLF + } + + len += utf8.encode('--$boundary--\r\n').length; + + return len; + } + + @override + Stream> bind(Stream> stream) { + var ctrl = new StreamController>(); + + new Future(() async { + var index = 0; + var enqueued = new Queue>(); + var q = new StreamQueue(stream); + + Future> absorb(int length) async { + var out = new BytesBuilder(); + + while (out.length < length) { + var remaining = length - out.length; + + while (out.length < length && enqueued.isNotEmpty) { + remaining = length - out.length; + var blob = enqueued.removeFirst(); + + if (blob.length > remaining) { + enqueued.addFirst(blob.skip(remaining).toList()); + blob = blob.take(remaining).toList(); + } + + out.add(blob); + index += blob.length; + } + + if (out.length < length && await q.hasNext) { + var blob = await q.next; + remaining = length - out.length; + + if (blob.length > remaining) { + enqueued.addFirst(blob.skip(remaining).toList()); + blob = blob.take(remaining).toList(); + } + + out.add(blob); + index += blob.length; + } + + // If we get this far, and the stream is EMPTY, the user requested + // too many bytes. + if (out.length < length && enqueued.isEmpty && !(await q.hasNext)) { + throw new StateError( + 'The range denoted is bigger than the size of the input stream.'); + } + } + + return out.takeBytes(); + } + + for (var item in header.items) { + var chunk = new BytesBuilder(); + + // Skip until we reach the start index. + while (index < item.start) { + var remaining = item.start - index; + await absorb(remaining); + } + + // Next, absorb until we reach the end. + if (item.end == -1) { + while (enqueued.isNotEmpty) chunk.add(enqueued.removeFirst()); + while (await q.hasNext) chunk.add(await q.next); + } else { + var remaining = item.end - index; + chunk.add(await absorb(remaining)); + } + + // Next, write the boundary and data. + ctrl.add(utf8.encode('--$boundary\r\n')); + ctrl.add(utf8.encode('Content-Type: $mimeType\r\n')); + ctrl.add(utf8.encode( + 'Content-Range: ${header.rangeUnit} ${item.toContentRange(totalLength)}/$totalLength\r\n\r\n')); + ctrl.add(chunk.takeBytes()); + ctrl.add(const [$cr, $lf]); + + // If this range was unbounded, don't bother looping any further. + if (item.end == -1) break; + } + + ctrl.add(utf8.encode('--$boundary--\r\n')); + + ctrl.close(); + }).catchError(ctrl.addError); + + return ctrl.stream; + } +} + +var _rnd = new Random(); +String _randomString( + {int length: 32, + String validChars: + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'}) { + var len = _rnd.nextInt((length - 10)) + 10; + var buf = new StringBuffer(); + + while (buf.length < len) + buf.writeCharCode(validChars.codeUnitAt(_rnd.nextInt(validChars.length))); + + return buf.toString(); +} diff --git a/packages/range_header/lib/src/exception.dart b/packages/range_header/lib/src/exception.dart new file mode 100644 index 00000000..0c8f49a9 --- /dev/null +++ b/packages/range_header/lib/src/exception.dart @@ -0,0 +1,8 @@ +class RangeHeaderParseException extends FormatException { + final String message; + + RangeHeaderParseException(this.message); + + @override + String toString() => 'Range header parse exception: $message'; +} diff --git a/packages/range_header/lib/src/parser.dart b/packages/range_header/lib/src/parser.dart new file mode 100644 index 00000000..f7473c4d --- /dev/null +++ b/packages/range_header/lib/src/parser.dart @@ -0,0 +1,143 @@ +import 'package:charcode/charcode.dart'; +import 'package:source_span/source_span.dart'; +import 'package:string_scanner/string_scanner.dart'; +import 'exception.dart'; +import 'range_header.dart'; +import 'range_header_impl.dart'; +import 'range_header_item.dart'; + +final RegExp _rgxInt = new RegExp(r'[0-9]+'); +final RegExp _rgxWs = new RegExp(r'[ \n\r\t]'); + +enum TokenType { RANGE_UNIT, COMMA, INT, DASH, EQUALS } + +class Token { + final TokenType type; + final SourceSpan span; + + Token(this.type, this.span); +} + +List scan(String text, List allowedRangeUnits) { + List tokens = []; + var scanner = new SpanScanner(text); + + while (!scanner.isDone) { + // Skip whitespace + scanner.scan(_rgxWs); + + if (scanner.scanChar($comma)) + tokens.add(new Token(TokenType.COMMA, scanner.lastSpan)); + else if (scanner.scanChar($dash)) + tokens.add(new Token(TokenType.DASH, scanner.lastSpan)); + else if (scanner.scan(_rgxInt)) + tokens.add(new Token(TokenType.INT, scanner.lastSpan)); + else if (scanner.scanChar($equal)) + tokens.add(new Token(TokenType.EQUALS, scanner.lastSpan)); + else { + bool matched = false; + + for (var unit in allowedRangeUnits) { + if (scanner.scan(unit)) { + tokens.add(new Token(TokenType.RANGE_UNIT, scanner.lastSpan)); + matched = true; + break; + } + } + + if (!matched) { + var ch = scanner.readChar(); + throw new RangeHeaderParseException( + 'Unexpected character: "${new String.fromCharCode(ch)}"'); + } + } + } + + return tokens; +} + +class Parser { + Token _current; + int _index = -1; + final List tokens; + + Parser(this.tokens); + + Token get current => _current; + + bool get done => _index >= tokens.length - 1; + + RangeHeaderParseException _expected(String type) { + int offset = current?.span?.start?.offset; + + if (offset == null) return new RangeHeaderParseException('Expected $type.'); + + Token peek; + + if (_index < tokens.length - 1) peek = tokens[_index + 1]; + + if (peek != null && peek.span != null) { + return new RangeHeaderParseException( + 'Expected $type at offset $offset, found "${peek.span.text}" instead. \nSource:\n${peek.span?.highlight() ?? peek.type}'); + } else + return new RangeHeaderParseException( + 'Expected $type at offset $offset, but the header string ended without one.\nSource:\n${current.span?.highlight() ?? current.type}'); + } + + bool next(TokenType type) { + if (done) return false; + var tok = tokens[_index + 1]; + if (tok.type == type) { + _index++; + _current = tok; + return true; + } else + return false; + } + + RangeHeader parseRangeHeader() { + if (next(TokenType.RANGE_UNIT)) { + var unit = current.span.text; + next(TokenType.EQUALS); // Consume =, if any. + + List items = []; + RangeHeaderItem item = parseHeaderItem(); + + while (item != null) { + items.add(item); + // Parse comma + if (next(TokenType.COMMA)) { + item = parseHeaderItem(); + } else + item = null; + } + + if (items.isEmpty) + throw _expected('range'); + else + return new RangeHeaderImpl(unit, items); + } else + return null; + } + + RangeHeaderItem parseHeaderItem() { + if (next(TokenType.INT)) { + // i.e 500-544, or 600- + var start = int.parse(current.span.text); + if (next(TokenType.DASH)) { + if (next(TokenType.INT)) { + return new RangeHeaderItem(start, int.parse(current.span.text)); + } else + return new RangeHeaderItem(start); + } else + throw _expected('"-"'); + } else if (next(TokenType.DASH)) { + // i.e. -599 + if (next(TokenType.INT)) { + return new RangeHeaderItem(-1, int.parse(current.span.text)); + } else + throw _expected('integer'); + } else + return null; + } +} diff --git a/packages/range_header/lib/src/range_header.dart b/packages/range_header/lib/src/range_header.dart new file mode 100644 index 00000000..88943de1 --- /dev/null +++ b/packages/range_header/lib/src/range_header.dart @@ -0,0 +1,63 @@ +import 'dart:collection'; +import 'parser.dart'; +import 'range_header_item.dart'; +import 'range_header_impl.dart'; + +/// Represents the contents of a parsed `Range` header. +abstract class RangeHeader { + /// Returns an immutable list of the ranges that were parsed. + UnmodifiableListView get items; + + const factory RangeHeader(Iterable items, + {String rangeUnit}) = _ConstantRangeHeader; + + /// Eliminates any overlapping [items], sorts them, and folds them all into the most efficient representation possible. + static UnmodifiableListView foldItems( + Iterable items) { + var out = new Set(); + + for (var item in items) { + // Remove any overlapping items, consolidate them. + while (out.any((x) => x.overlaps(item))) { + var f = out.firstWhere((x) => x.overlaps(item)); + out.remove(f); + item = item.consolidate(f); + } + + out.add(item); + } + + return new UnmodifiableListView(out.toList()..sort()); + } + + /// Attempts to parse a [RangeHeader] from its [text] representation. + /// + /// You can optionally pass a custom list of [allowedRangeUnits]. + /// The default is `['bytes']`. + /// + /// If [fold] is `true`, the items will be folded into the most compact + /// possible representation. + factory RangeHeader.parse(String text, + {Iterable allowedRangeUnits, bool fold: true}) { + var tokens = scan(text, allowedRangeUnits?.toList() ?? ['bytes']); + var parser = new Parser(tokens); + var header = parser.parseRangeHeader(); + if (header == null) return null; + var items = foldItems(header.items); + return RangeHeaderImpl(header.rangeUnit, items); + } + + /// Returns this header's range unit. Most commonly, this is `bytes`. + String get rangeUnit; +} + +class _ConstantRangeHeader implements RangeHeader { + final Iterable items_; + final String rangeUnit; + + const _ConstantRangeHeader(this.items_, {this.rangeUnit: 'bytes'}); + + @override + UnmodifiableListView get items => + new UnmodifiableListView(items_); +} diff --git a/packages/range_header/lib/src/range_header_impl.dart b/packages/range_header/lib/src/range_header_impl.dart new file mode 100644 index 00000000..e1016502 --- /dev/null +++ b/packages/range_header/lib/src/range_header_impl.dart @@ -0,0 +1,20 @@ +import 'dart:collection'; +import 'range_header.dart'; +import 'range_header_item.dart'; + +/// Represents the contents of a parsed `Range` header. +class RangeHeaderImpl implements RangeHeader { + UnmodifiableListView _cached; + final List _items = []; + + RangeHeaderImpl(this.rangeUnit, [List items = const []]) { + this._items.addAll(items ?? []); + } + + @override + UnmodifiableListView get items => + _cached ??= new UnmodifiableListView(_items); + + @override + final String rangeUnit; +} diff --git a/packages/range_header/lib/src/range_header_item.dart b/packages/range_header/lib/src/range_header_item.dart new file mode 100644 index 00000000..418031be --- /dev/null +++ b/packages/range_header/lib/src/range_header_item.dart @@ -0,0 +1,90 @@ +import 'dart:math'; +import 'package:quiver_hashcode/hashcode.dart'; + +/// Represents an individual range, with an optional start index and optional end index. +class RangeHeaderItem implements Comparable { + /// The index at which this chunk begins. May be `-1`. + final int start; + + /// The index at which this chunk ends. May be `-1`. + final int end; + + const RangeHeaderItem([this.start = -1, this.end = -1]); + + /// Joins two items together into the largest possible range. + RangeHeaderItem consolidate(RangeHeaderItem other) { + if (!(other.overlaps(this))) + throw new ArgumentError('The two ranges do not overlap.'); + return new RangeHeaderItem(min(start, other.start), max(end, other.end)); + } + + @override + int get hashCode => hash2(start, end); + + @override + bool operator ==(other) => + other is RangeHeaderItem && other.start == start && other.end == end; + + bool overlaps(RangeHeaderItem other) { + if (other.start <= start) { + return other.end < start; + } else if (other.start > start) { + return other.start <= end; + } + return false; + } + + @override + int compareTo(RangeHeaderItem other) { + if (other.start > start) { + return -1; + } else if (other.start == start) { + if (other.end == end) { + return 0; + } else if (other.end < end) { + return 1; + } else { + return -1; + } + } else if (other.start < start) { + return 1; + } else { + return -1; + } + } + + @override + String toString() { + if (start > -1 && end > -1) + return '$start-$end'; + else if (start > -1) + return '$start-'; + else + return '-$end'; + } + + /// Creates a representation of this instance suitable for a `Content-Range` header. + /// + /// This can only be used if the user request only one range. If not, send a + /// `multipart/byteranges` response. + /// + /// Please adhere to the standard!!! + /// http://httpwg.org/specs/rfc7233.html + + String toContentRange([int totalSize]) { + // var maxIndex = totalSize != null ? (totalSize - 1).toString() : '*'; + var s = start > -1 ? start : 0; + + if (end == -1) { + if (totalSize == null) { + throw new UnsupportedError( + 'If the end of this range is unknown, `totalSize` must not be null.'); + } else { + // if (end == totalSize - 1) { + return '$s-${totalSize - 1}/$totalSize'; + } + } + + return '$s-$end/$totalSize'; + } +} diff --git a/packages/range_header/pubspec.yaml b/packages/range_header/pubspec.yaml new file mode 100644 index 00000000..1d580106 --- /dev/null +++ b/packages/range_header/pubspec.yaml @@ -0,0 +1,20 @@ +name: range_header +version: 2.0.2+2 +description: Range header parser for Dart. Beyond parsing, a stream transformer is included. +author: Tobe O +homepage: https://github.com/thosakwe/range_header +environment: + sdk: ">=2.0.0-dev <3.0.0" +dependencies: + async: ^2.0.0 + charcode: ^1.0.0 + quiver_hashcode: ^2.0.0 + source_span: ^1.0.0 + string_scanner: ^1.0.0 +dev_dependencies: + angel_framework: + angel_static: ^2.0.0 + file: + http_parser: ^3.0.0 + logging: ^0.11.0 + test: ^1.0.0 \ No newline at end of file diff --git a/packages/range_header/test/all_test.dart b/packages/range_header/test/all_test.dart new file mode 100644 index 00000000..67e90b8d --- /dev/null +++ b/packages/range_header/test/all_test.dart @@ -0,0 +1,96 @@ +import 'package:range_header/range_header.dart'; +import 'package:test/test.dart'; + +final Matcher throwsRangeParseException = + throwsA(const TypeMatcher()); + +main() { + group('one item', () { + test('start and end', () { + var r = new RangeHeader.parse('bytes 1-200'); + expect(r.items, hasLength(1)); + expect(r.items.first.start, 1); + expect(r.items.first.end, 200); + }); + + test('start only', () { + var r = new RangeHeader.parse('bytes 1-'); + expect(r.items, hasLength(1)); + expect(r.items.first.start, 1); + expect(r.items.first.end, -1); + }); + + test('end only', () { + var r = new RangeHeader.parse('bytes -200'); + print(r.items); + expect(r.items, hasLength(1)); + expect(r.items.first.start, -1); + expect(r.items.first.end, 200); + }); + }); + + group('multiple items', () { + test('three items', () { + var r = new RangeHeader.parse('bytes 1-20, 21-40, 41-60'); + print(r.items); + expect(r.items, hasLength(3)); + expect(r.items[0].start, 1); + expect(r.items[0].end, 20); + expect(r.items[1].start, 21); + expect(r.items[1].end, 40); + expect(r.items[2].start, 41); + expect(r.items[2].end, 60); + }); + + test('one item without end', () { + var r = new RangeHeader.parse('bytes 1-20, 21-'); + print(r.items); + expect(r.items, hasLength(2)); + expect(r.items[0].start, 1); + expect(r.items[0].end, 20); + expect(r.items[1].start, 21); + expect(r.items[1].end, -1); + }); + }); + + group('failures', () { + test('no start with no end', () { + expect(new RangeHeader.parse('-'), isNull); + }); + }); + + group('exceptions', () { + test('invalid character', () { + expect(() => new RangeHeader.parse('!!!'), throwsRangeParseException); + }); + + test('no ranges', () { + expect(() => new RangeHeader.parse('bytes'), throwsRangeParseException); + }); + + test('no dash after int', () { + expect(() => new RangeHeader.parse('bytes 3'), throwsRangeParseException); + expect( + () => new RangeHeader.parse('bytes 3,'), throwsRangeParseException); + expect( + () => new RangeHeader.parse('bytes 3 24'), throwsRangeParseException); + }); + + test('no int after dash', () { + expect( + () => new RangeHeader.parse('bytes -,'), throwsRangeParseException); + }); + }); + + group('complete coverage', () { + test('exception toString()', () { + var m = new RangeHeaderParseException('hey'); + expect(m.toString(), contains('hey')); + }); + }); + + test('content-range', () { + expect( + new RangeHeader.parse('bytes 1-2').items[0].toContentRange(3), '1-2/3'); + }); +} diff --git a/packages/static/example/main.dart b/packages/static/example/main.dart index 61791fda..16e4ad08 100644 --- a/packages/static/example/main.dart +++ b/packages/static/example/main.dart @@ -4,7 +4,7 @@ import 'package:angel_static/angel_static.dart'; import 'package:file/local.dart'; import 'package:logging/logging.dart'; -main(List args) async { +void main(List args) async { var app = Angel(); var http = AngelHttp(app); var fs = const LocalFileSystem(); diff --git a/packages/static/lib/src/cache.dart b/packages/static/lib/src/cache.dart index 0f859a39..13042cdd 100644 --- a/packages/static/lib/src/cache.dart +++ b/packages/static/lib/src/cache.dart @@ -49,7 +49,7 @@ class CachingVirtualDirectory extends VirtualDirectory { bool allowDirectoryListing, bool useBuffer = false, String publicPath, - callback(File file, RequestContext req, ResponseContext res)}) + Function(File file, RequestContext req, ResponseContext res) callback}) : super(app, fileSystem, source: source, indexFileNames: indexFileNames ?? ['index.html'], @@ -67,7 +67,7 @@ class CachingVirtualDirectory extends VirtualDirectory { return super.serveFile(file, stat, req, res); } - bool shouldNotCache = noCache == true; + var shouldNotCache = noCache == true; if (!shouldNotCache) { shouldNotCache = req.headers.value('cache-control') == 'no-cache' || @@ -79,7 +79,7 @@ class CachingVirtualDirectory extends VirtualDirectory { return super.serveFile(file, stat, req, res); } else { var ifModified = req.headers.ifModifiedSince; - bool ifRange = false; + var ifRange = false; try { ifModified = HttpDate.parse(req.headers.value('if-range')); @@ -129,7 +129,7 @@ class CachingVirtualDirectory extends VirtualDirectory { } if (etagsToMatchAgainst?.isNotEmpty == true) { - bool hasBeenModified = false; + var hasBeenModified = false; for (var etag in etagsToMatchAgainst) { if (etag == '*') { diff --git a/packages/static/lib/src/virtual_directory.dart b/packages/static/lib/src/virtual_directory.dart index 7daa243e..6642c27a 100644 --- a/packages/static/lib/src/virtual_directory.dart +++ b/packages/static/lib/src/virtual_directory.dart @@ -225,7 +225,7 @@ class VirtualDirectory { res.write('
  • $type $stub
  • '); } - res..write(''); + res.write(''); return false; } diff --git a/packages/static/pubspec.yaml b/packages/static/pubspec.yaml index f426f088..fd1e714a 100644 --- a/packages/static/pubspec.yaml +++ b/packages/static/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: angel_framework: git: url: https://github.com/dukefirehawk/angel.git - ref: sdk-2.12.x + ref: sdk-2.12.x_nnbd path: packages/framework convert: ^3.0.0 crypto: ^3.0.0 @@ -22,7 +22,7 @@ dev_dependencies: angel_test: git: url: https://github.com/dukefirehawk/angel.git - ref: sdk-2.12.x + ref: sdk-2.12.x_nnbd path: packages/test http: logging: ^1.0.0