Added html_builder package
This commit is contained in:
parent
7f365c1e8c
commit
c2fc88728b
17 changed files with 2443 additions and 2 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -19,3 +19,11 @@ doc/api/
|
||||||
*.js_
|
*.js_
|
||||||
*.js.deps
|
*.js.deps
|
||||||
*.js.map
|
*.js.map
|
||||||
|
|
||||||
|
## VsCode
|
||||||
|
.vscode/
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.metals/
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
BSD 3-Clause License
|
BSD 3-Clause License
|
||||||
|
|
||||||
Copyright (c) 2021, dart-backend
|
Copyright (c) 2021, dukefirehawk.com
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|
11
README.md
11
README.md
|
@ -1 +1,10 @@
|
||||||
# server-utilities
|
# Belatuk Common Utilities
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
This repository contains the common utility packages required for developing backend framework.
|
||||||
|
|
||||||
|
## Available Packages
|
||||||
|
|
||||||
|
* html_builder
|
||||||
|
|
12
packages/html_builder/AUTHORS.md
Normal file
12
packages/html_builder/AUTHORS.md
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
Primary Authors
|
||||||
|
===============
|
||||||
|
|
||||||
|
* __[Thomas Hii](dukefirehawk.apps@gmail.com)__
|
||||||
|
|
||||||
|
Thomas is the current maintainer of the code base. He has refactored and migrated the
|
||||||
|
code base to support NNBD.
|
||||||
|
|
||||||
|
* __[Tobe O](thosakwe@gmail.com)__
|
||||||
|
|
||||||
|
Tobe has written much of the original code prior to NNBD migration. He has moved on and
|
||||||
|
is no longer involved with the project.
|
36
packages/html_builder/CHANGELOG.md
Normal file
36
packages/html_builder/CHANGELOG.md
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
## 3.0.0
|
||||||
|
|
||||||
|
* Upgraded from `pendantic` to `lints` linter
|
||||||
|
* Removed deprecated parameters
|
||||||
|
* Published as `belatuk_html_builder` package
|
||||||
|
|
||||||
|
## 2.0.3
|
||||||
|
|
||||||
|
* Added an example
|
||||||
|
* Updated README
|
||||||
|
|
||||||
|
## 2.0.2
|
||||||
|
|
||||||
|
* Run `dartfmt -w .`
|
||||||
|
|
||||||
|
## 2.0.1
|
||||||
|
|
||||||
|
* Added pedantic dart rules
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
* Migrated to work with Dart SDK 2.12.x NNBD
|
||||||
|
|
||||||
|
## 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.
|
29
packages/html_builder/LICENSE
Normal file
29
packages/html_builder/LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2021, dukefirehawk.com
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
113
packages/html_builder/README.md
Normal file
113
packages/html_builder/README.md
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
# Betaluk Html Builder
|
||||||
|
|
||||||
|
[![version](https://img.shields.io/badge/pub-v3.0.0-brightgreen)](https://pub.dartlang.org/packages/belatuk_html_builder)
|
||||||
|
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
|
||||||
|
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dart-backend/belatuk-common-utilities/packages/html_builder/LICENSE)
|
||||||
|
|
||||||
|
**Replacement of `package:html_builder` with breaking changes to support NNBD.**
|
||||||
|
|
||||||
|
This package builds HTML AST's and renders them to HTML. It can be used as an internal DSL, i.e. for a templating engine.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
In your `pubspec.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
belatuk_html_builder: ^2.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:belatuk_html_builder/belatuk_html_builder.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Akin to React.createElement(...);
|
||||||
|
var $el = h('my-element', p: {}, c: []);
|
||||||
|
|
||||||
|
// Attributes can be plain Strings.
|
||||||
|
h('foo', p: {
|
||||||
|
'bar': 'baz'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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:belatuk_html_builder/elements.dart';
|
||||||
|
|
||||||
|
void 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 = StringRenderer().render($dom);
|
||||||
|
```
|
||||||
|
|
||||||
|
Example with the [Angel3](https://pub.dev/packages/angel3_framework) backend framework,
|
||||||
|
which has [dedicated html_builder support](https://github.com/dukefirehawk/angel/tree/html):
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:belatuk_framework/belatuk_framework.dart';
|
||||||
|
import 'package:belatuk_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)])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
1
packages/html_builder/analysis_options.yaml
Normal file
1
packages/html_builder/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
include: package:lints/recommended.yaml
|
15
packages/html_builder/example/main.dart
Normal file
15
packages/html_builder/example/main.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import 'package:belatuk_html_builder/elements.dart';
|
||||||
|
|
||||||
|
void 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')])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
print(dom);
|
||||||
|
}
|
4
packages/html_builder/lib/belatuk_html_builder.dart
Normal file
4
packages/html_builder/lib/belatuk_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';
|
1841
packages/html_builder/lib/elements.dart
Normal file
1841
packages/html_builder/lib/elements.dart
Normal file
File diff suppressed because it is too large
Load diff
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;
|
||||||
|
}
|
63
packages/html_builder/lib/src/node.dart
Normal file
63
packages/html_builder/lib/src/node.dart
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
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 {
|
||||||
|
/*
|
||||||
|
@override
|
||||||
|
final String tagName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Map<String, dynamic> attributes = {};
|
||||||
|
*/
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Node> get children => List<Node>.unmodifiable([]);
|
||||||
|
|
||||||
|
SelfClosingNode(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;
|
||||||
|
}
|
108
packages/html_builder/lib/src/node_builder.dart
Normal file
108
packages/html_builder/lib/src/node_builder.dart
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
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)));
|
||||||
|
}
|
136
packages/html_builder/lib/src/renderer.dart
Normal file
136
packages/html_builder/lib/src/renderer.dart
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
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 (var 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();
|
||||||
|
}
|
||||||
|
}
|
12
packages/html_builder/pubspec.yaml
Normal file
12
packages/html_builder/pubspec.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
name: belatuk_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: 3.0.0
|
||||||
|
homepage: https://github.com/dart-backend/belatuk-common-utilities/packages/html_builder
|
||||||
|
environment:
|
||||||
|
sdk: '>=2.12.0 <3.0.0'
|
||||||
|
dependencies:
|
||||||
|
collection: ^1.15.0
|
||||||
|
dev_dependencies:
|
||||||
|
html: ^0.15.0
|
||||||
|
test: ^1.17.4
|
||||||
|
lints: ^1.0.1
|
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:belatuk_html_builder/elements.dart';
|
||||||
|
import 'package:belatuk_html_builder/belatuk_html_builder.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void 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!');
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue