Added and migrated html_builder
This commit is contained in:
parent
b091b435e4
commit
2a69ecf91b
34 changed files with 3379 additions and 16 deletions
|
@ -24,7 +24,9 @@
|
||||||
* Migrated angel_jael to 4.0.0 (1/1 test passed)
|
* Migrated angel_jael to 4.0.0 (1/1 test passed)
|
||||||
* Migrated pub_sub to 4.0.0 (16/16 tests passed)
|
* Migrated pub_sub to 4.0.0 (16/16 tests passed)
|
||||||
* Migrated production to 3.0.0 (0/0 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)
|
* 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)
|
* Updated static to 3.0.0 (in progress)
|
||||||
* Update basic-sdk-2.12.x boilerplate (in progress)
|
* Update basic-sdk-2.12.x boilerplate (in progress)
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,8 @@ import 'package:html_builder/elements.dart';
|
||||||
import 'package:html_builder/html_builder.dart';
|
import 'package:html_builder/html_builder.dart';
|
||||||
import 'package:io/ansi.dart';
|
import 'package:io/ansi.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:vm_service_lib/vm_service_lib.dart' as vm;
|
import 'package:vm_service/vm_service.dart' as vm;
|
||||||
import 'package:vm_service_lib/vm_service_lib_io.dart' as vm;
|
import 'package:vm_service/vm_service_io.dart' as vm;
|
||||||
import 'package:watcher/watcher.dart';
|
import 'package:watcher/watcher.dart';
|
||||||
|
|
||||||
/// A utility class that watches the filesystem for changes, and starts new instances of an Angel server.
|
/// 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) {
|
if (hot) {
|
||||||
var report = await _client.reloadSources(_mainIsolate.id);
|
var report = await _client.reloadSources(_mainIsolate.id);
|
||||||
|
|
||||||
if (!report.success) {
|
if (report.success != null) {
|
||||||
_logWarning(
|
_logWarning(
|
||||||
'Hot reload failed - perhaps some sources have not been generated yet.');
|
'Hot reload failed - perhaps some sources have not been generated yet.');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: angel_hot
|
name: angel_hot
|
||||||
description: Supports hot reloading/hot code push of Angel servers on file changes.
|
description: Supports hot reloading/hot code push of Angel servers on file changes.
|
||||||
version: 3.0.0
|
version: 4.0.0
|
||||||
author: Tobe O <thosakwe@gmail.com>
|
author: Tobe O <thosakwe@gmail.com>
|
||||||
homepage: https://github.com/angel-dart/hot
|
homepage: https://github.com/angel-dart/hot
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
@ -10,19 +10,19 @@ dependencies:
|
||||||
angel_framework:
|
angel_framework:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/dukefirehawk/angel.git
|
url: https://github.com/dukefirehawk/angel.git
|
||||||
ref: sdk-2.12.x
|
ref: sdk-2.12.x_nnbd
|
||||||
path: packages/framework
|
path: packages/framework
|
||||||
angel_websocket: #^2.0.0-alpha
|
angel_websocket:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/dukefirehawk/angel.git
|
url: https://github.com/dukefirehawk/angel.git
|
||||||
ref: sdk-2.12.x
|
ref: sdk-2.12.x_nnbd
|
||||||
path: packages/websocket
|
path: packages/websocket
|
||||||
charcode: ^1.0.0
|
charcode: ^1.0.0
|
||||||
glob: ^2.0.0
|
glob: ^2.0.0
|
||||||
html_builder: ^1.0.0
|
html_builder: ^1.0.0
|
||||||
io: ^0.3.5
|
io: ^0.3.5
|
||||||
path: ^1.0.0
|
path: ^1.0.0
|
||||||
vm_service_lib: ^3.22.2+1
|
vm_service: ^5.5.0
|
||||||
watcher: ^1.0.0
|
watcher: ^1.0.0
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
http: ^0.13.0
|
http: ^0.13.0
|
||||||
|
|
9
packages/html_builder/CHANGELOG.md
Normal file
9
packages/html_builder/CHANGELOG.md
Normal file
|
@ -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.
|
21
packages/html_builder/LICENSE
Normal file
21
packages/html_builder/LICENSE
Normal file
|
@ -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.
|
108
packages/html_builder/README.md
Normal file
108
packages/html_builder/README.md
Normal file
|
@ -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)])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
3
packages/html_builder/analysis_options.yaml
Normal file
3
packages/html_builder/analysis_options.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
analyzer:
|
||||||
|
strong-mode:
|
||||||
|
implicit-casts: false
|
2041
packages/html_builder/lib/elements.dart
Normal file
2041
packages/html_builder/lib/elements.dart
Normal file
File diff suppressed because it is too large
Load diff
4
packages/html_builder/lib/html_builder.dart
Normal file
4
packages/html_builder/lib/html_builder.dart
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export 'src/mutations.dart';
|
||||||
|
export 'src/node.dart';
|
||||||
|
export 'src/node_builder.dart';
|
||||||
|
export 'src/renderer.dart';
|
20
packages/html_builder/lib/src/mutations.dart
Normal file
20
packages/html_builder/lib/src/mutations.dart
Normal file
|
@ -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;
|
||||||
|
}
|
56
packages/html_builder/lib/src/node.dart
Normal file
56
packages/html_builder/lib/src/node.dart
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
/// Shorthand function to generate a new [Node].
|
||||||
|
Node h(String tagName,
|
||||||
|
[Map<String, dynamic> attributes = const {},
|
||||||
|
Iterable<Node> children = const []]) =>
|
||||||
|
Node(tagName, attributes, children);
|
||||||
|
|
||||||
|
/// Represents an HTML node.
|
||||||
|
class Node {
|
||||||
|
final String tagName;
|
||||||
|
final Map<String, dynamic> attributes = {};
|
||||||
|
final List<Node> children = [];
|
||||||
|
|
||||||
|
Node(this.tagName,
|
||||||
|
[Map<String, dynamic> attributes = const {},
|
||||||
|
Iterable<Node> children = const []]) {
|
||||||
|
this..attributes.addAll(attributes)..children.addAll(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
Node._selfClosing(this.tagName,
|
||||||
|
[Map<String, dynamic> attributes = const {}]) {
|
||||||
|
this..attributes.addAll(attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(other) {
|
||||||
|
return other is Node &&
|
||||||
|
other.tagName == tagName &&
|
||||||
|
const ListEquality<Node>().equals(other.children, children) &&
|
||||||
|
const MapEquality<String, dynamic>()
|
||||||
|
.equals(other.attributes, attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a self-closing tag, i.e. `<br>`.
|
||||||
|
class SelfClosingNode extends Node {
|
||||||
|
final String tagName;
|
||||||
|
final Map<String, dynamic> attributes = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> get children => List<Node>.unmodifiable([]);
|
||||||
|
|
||||||
|
SelfClosingNode(this.tagName, [Map<String, dynamic> 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;
|
||||||
|
}
|
106
packages/html_builder/lib/src/node_builder.dart
Normal file
106
packages/html_builder/lib/src/node_builder.dart
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import 'node.dart';
|
||||||
|
|
||||||
|
/// Helper class to build nodes.
|
||||||
|
class NodeBuilder {
|
||||||
|
final String tagName;
|
||||||
|
final Map<String, dynamic> attributes;
|
||||||
|
final Iterable<Node> 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<String, dynamic>.from(node.attributes),
|
||||||
|
children: List<Node>.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<String, dynamic>? attributes,
|
||||||
|
Iterable<Node>? children}) {
|
||||||
|
return NodeBuilder(tagName ?? this.tagName,
|
||||||
|
attributes: attributes ?? this.attributes,
|
||||||
|
children: children ?? this.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeBuilder changeTagName(String tagName) => change(tagName: tagName);
|
||||||
|
|
||||||
|
NodeBuilder changeAttributes(Map<String, dynamic> attributes) =>
|
||||||
|
change(attributes: attributes);
|
||||||
|
|
||||||
|
NodeBuilder changeChildren(Iterable<Node> children) =>
|
||||||
|
change(children: children);
|
||||||
|
|
||||||
|
NodeBuilder changeAttributesMapped(
|
||||||
|
Map<String, dynamic> Function(Map<String, dynamic>) f) {
|
||||||
|
var map = Map<String, dynamic>.from(attributes);
|
||||||
|
return changeAttributes(f(map));
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeBuilder changeChildrenMapped(Iterable<Node> Function(List<Node>) f) {
|
||||||
|
var list = List<Node>.from(children);
|
||||||
|
return changeChildren(f(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeBuilder mapChildren(Node Function(Node) f) =>
|
||||||
|
changeChildrenMapped((list) => list.map(f));
|
||||||
|
|
||||||
|
NodeBuilder mapAttributes(
|
||||||
|
MapEntry<String, dynamic> 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<String> classes) =>
|
||||||
|
setClassName(classes.join(' '));
|
||||||
|
|
||||||
|
NodeBuilder setClassesMapped(Iterable<String> Function(List<String>) f) {
|
||||||
|
var clazz = attributes['class'];
|
||||||
|
var classes = <String>[];
|
||||||
|
|
||||||
|
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)));
|
||||||
|
}
|
134
packages/html_builder/lib/src/renderer.dart
Normal file
134
packages/html_builder/lib/src/renderer.dart
Normal file
|
@ -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<T> {
|
||||||
|
/// Renders a DOM tree into another representation.
|
||||||
|
T render(Node rootNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a DOM tree into a HTML string.
|
||||||
|
abstract class StringRenderer implements Renderer<String> {
|
||||||
|
/// 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. `<br />`.
|
||||||
|
/// 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<String>('', (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('</${node.tagName}>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String render(Node rootNode) {
|
||||||
|
var buf = StringBuffer();
|
||||||
|
if (doctype?.isNotEmpty == true) buf.write('<!DOCTYPE $doctype>');
|
||||||
|
_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<String>('', (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('</${node.tagName}>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String render(Node rootNode) {
|
||||||
|
var buf = StringBuffer();
|
||||||
|
if (doctype?.isNotEmpty == true) buf.writeln('<!DOCTYPE $doctype>');
|
||||||
|
_renderInto(0, rootNode, buf);
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
}
|
10
packages/html_builder/pubspec.yaml
Normal file
10
packages/html_builder/pubspec.yaml
Normal file
|
@ -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 <thosakwe@gmail.com>
|
||||||
|
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
|
34
packages/html_builder/test/render_test.dart
Normal file
34
packages/html_builder/test/render_test.dart
Normal file
|
@ -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!');
|
||||||
|
});
|
||||||
|
}
|
12
packages/range_header/CHANGELOG.md
Normal file
12
packages/range_header/CHANGELOG.md
Normal file
|
@ -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.
|
21
packages/range_header/LICENSE
Normal file
21
packages/range_header/LICENSE
Normal file
|
@ -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.
|
36
packages/range_header/README.md
Normal file
36
packages/range_header/README.md
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
```
|
3
packages/range_header/analysis_options.yaml
Normal file
3
packages/range_header/analysis_options.yaml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
analyzer:
|
||||||
|
strong-mode:
|
||||||
|
implicit-casts: false
|
28
packages/range_header/example/main.dart
Normal file
28
packages/range_header/example/main.dart
Normal file
|
@ -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<List<int>>()
|
||||||
|
.transform(transformer)
|
||||||
|
.pipe(request.response);
|
||||||
|
}
|
108
packages/range_header/example/server.dart
Normal file
108
packages/range_header/example/server.dart
Normal file
|
@ -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<bool> 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<Uint8List> 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<List<int>>().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<List<int>>()
|
||||||
|
.transform(transformer)
|
||||||
|
.pipe(res);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return await super.serveFile(file, stat, req, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
packages/range_header/lib/range_header.dart
Normal file
4
packages/range_header/lib/range_header.dart
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export 'src/converter.dart';
|
||||||
|
export 'src/exception.dart';
|
||||||
|
export 'src/range_header.dart';
|
||||||
|
export 'src/range_header_item.dart';
|
163
packages/range_header/lib/src/converter.dart
Normal file
163
packages/range_header/lib/src/converter.dart
Normal file
|
@ -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<int>, List<int>> {
|
||||||
|
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<List<int>> bind(Stream<List<int>> stream) {
|
||||||
|
var ctrl = new StreamController<List<int>>();
|
||||||
|
|
||||||
|
new Future(() async {
|
||||||
|
var index = 0;
|
||||||
|
var enqueued = new Queue<List<int>>();
|
||||||
|
var q = new StreamQueue(stream);
|
||||||
|
|
||||||
|
Future<List<int>> 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();
|
||||||
|
}
|
8
packages/range_header/lib/src/exception.dart
Normal file
8
packages/range_header/lib/src/exception.dart
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
class RangeHeaderParseException extends FormatException {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
RangeHeaderParseException(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'Range header parse exception: $message';
|
||||||
|
}
|
143
packages/range_header/lib/src/parser.dart
Normal file
143
packages/range_header/lib/src/parser.dart
Normal file
|
@ -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<Token> scan(String text, List<String> allowedRangeUnits) {
|
||||||
|
List<Token> 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<Token> 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<RangeHeaderItem> 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;
|
||||||
|
}
|
||||||
|
}
|
63
packages/range_header/lib/src/range_header.dart
Normal file
63
packages/range_header/lib/src/range_header.dart
Normal file
|
@ -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<RangeHeaderItem> get items;
|
||||||
|
|
||||||
|
const factory RangeHeader(Iterable<RangeHeaderItem> items,
|
||||||
|
{String rangeUnit}) = _ConstantRangeHeader;
|
||||||
|
|
||||||
|
/// Eliminates any overlapping [items], sorts them, and folds them all into the most efficient representation possible.
|
||||||
|
static UnmodifiableListView<RangeHeaderItem> foldItems(
|
||||||
|
Iterable<RangeHeaderItem> items) {
|
||||||
|
var out = new Set<RangeHeaderItem>();
|
||||||
|
|
||||||
|
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<String> 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<RangeHeaderItem> items_;
|
||||||
|
final String rangeUnit;
|
||||||
|
|
||||||
|
const _ConstantRangeHeader(this.items_, {this.rangeUnit: 'bytes'});
|
||||||
|
|
||||||
|
@override
|
||||||
|
UnmodifiableListView<RangeHeaderItem> get items =>
|
||||||
|
new UnmodifiableListView(items_);
|
||||||
|
}
|
20
packages/range_header/lib/src/range_header_impl.dart
Normal file
20
packages/range_header/lib/src/range_header_impl.dart
Normal file
|
@ -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<RangeHeaderItem> _cached;
|
||||||
|
final List<RangeHeaderItem> _items = [];
|
||||||
|
|
||||||
|
RangeHeaderImpl(this.rangeUnit, [List<RangeHeaderItem> items = const []]) {
|
||||||
|
this._items.addAll(items ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
UnmodifiableListView<RangeHeaderItem> get items =>
|
||||||
|
_cached ??= new UnmodifiableListView<RangeHeaderItem>(_items);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String rangeUnit;
|
||||||
|
}
|
90
packages/range_header/lib/src/range_header_item.dart
Normal file
90
packages/range_header/lib/src/range_header_item.dart
Normal file
|
@ -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<RangeHeaderItem> {
|
||||||
|
/// 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';
|
||||||
|
}
|
||||||
|
}
|
20
packages/range_header/pubspec.yaml
Normal file
20
packages/range_header/pubspec.yaml
Normal file
|
@ -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 <thosakwe@gmail.com>
|
||||||
|
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
|
96
packages/range_header/test/all_test.dart
Normal file
96
packages/range_header/test/all_test.dart
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import 'package:range_header/range_header.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
final Matcher throwsRangeParseException =
|
||||||
|
throwsA(const TypeMatcher<RangeHeaderParseException>());
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import 'package:angel_static/angel_static.dart';
|
||||||
import 'package:file/local.dart';
|
import 'package:file/local.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
main(List<String> args) async {
|
void main(List<String> args) async {
|
||||||
var app = Angel();
|
var app = Angel();
|
||||||
var http = AngelHttp(app);
|
var http = AngelHttp(app);
|
||||||
var fs = const LocalFileSystem();
|
var fs = const LocalFileSystem();
|
||||||
|
|
|
@ -49,7 +49,7 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
||||||
bool allowDirectoryListing,
|
bool allowDirectoryListing,
|
||||||
bool useBuffer = false,
|
bool useBuffer = false,
|
||||||
String publicPath,
|
String publicPath,
|
||||||
callback(File file, RequestContext req, ResponseContext res)})
|
Function(File file, RequestContext req, ResponseContext res) callback})
|
||||||
: super(app, fileSystem,
|
: super(app, fileSystem,
|
||||||
source: source,
|
source: source,
|
||||||
indexFileNames: indexFileNames ?? ['index.html'],
|
indexFileNames: indexFileNames ?? ['index.html'],
|
||||||
|
@ -67,7 +67,7 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
||||||
return super.serveFile(file, stat, req, res);
|
return super.serveFile(file, stat, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool shouldNotCache = noCache == true;
|
var shouldNotCache = noCache == true;
|
||||||
|
|
||||||
if (!shouldNotCache) {
|
if (!shouldNotCache) {
|
||||||
shouldNotCache = req.headers.value('cache-control') == 'no-cache' ||
|
shouldNotCache = req.headers.value('cache-control') == 'no-cache' ||
|
||||||
|
@ -79,7 +79,7 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
||||||
return super.serveFile(file, stat, req, res);
|
return super.serveFile(file, stat, req, res);
|
||||||
} else {
|
} else {
|
||||||
var ifModified = req.headers.ifModifiedSince;
|
var ifModified = req.headers.ifModifiedSince;
|
||||||
bool ifRange = false;
|
var ifRange = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ifModified = HttpDate.parse(req.headers.value('if-range'));
|
ifModified = HttpDate.parse(req.headers.value('if-range'));
|
||||||
|
@ -129,7 +129,7 @@ class CachingVirtualDirectory extends VirtualDirectory {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (etagsToMatchAgainst?.isNotEmpty == true) {
|
if (etagsToMatchAgainst?.isNotEmpty == true) {
|
||||||
bool hasBeenModified = false;
|
var hasBeenModified = false;
|
||||||
|
|
||||||
for (var etag in etagsToMatchAgainst) {
|
for (var etag in etagsToMatchAgainst) {
|
||||||
if (etag == '*') {
|
if (etag == '*') {
|
||||||
|
|
|
@ -225,7 +225,7 @@ class VirtualDirectory {
|
||||||
res.write('<li><a href="$href">$type $stub</a></li>');
|
res.write('<li><a href="$href">$type $stub</a></li>');
|
||||||
}
|
}
|
||||||
|
|
||||||
res..write('</body></html>');
|
res.write('</body></html>');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ dependencies:
|
||||||
angel_framework:
|
angel_framework:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/dukefirehawk/angel.git
|
url: https://github.com/dukefirehawk/angel.git
|
||||||
ref: sdk-2.12.x
|
ref: sdk-2.12.x_nnbd
|
||||||
path: packages/framework
|
path: packages/framework
|
||||||
convert: ^3.0.0
|
convert: ^3.0.0
|
||||||
crypto: ^3.0.0
|
crypto: ^3.0.0
|
||||||
|
@ -22,7 +22,7 @@ dev_dependencies:
|
||||||
angel_test:
|
angel_test:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/dukefirehawk/angel.git
|
url: https://github.com/dukefirehawk/angel.git
|
||||||
ref: sdk-2.12.x
|
ref: sdk-2.12.x_nnbd
|
||||||
path: packages/test
|
path: packages/test
|
||||||
http:
|
http:
|
||||||
logging: ^1.0.0
|
logging: ^1.0.0
|
||||||
|
|
Loading…
Reference in a new issue