Added html_builder package

This commit is contained in:
thomashii 2021-09-11 10:17:30 +08:00
parent 7f365c1e8c
commit c2fc88728b
17 changed files with 2443 additions and 2 deletions

8
.gitignore vendored
View file

@ -19,3 +19,11 @@ doc/api/
*.js_
*.js.deps
*.js.map
## VsCode
.vscode/
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.metals/

View file

@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2021, dart-backend
Copyright (c) 2021, dukefirehawk.com
All rights reserved.
Redistribution and use in source and binary forms, with or without

View file

@ -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

View 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.

View 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.

View 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.

View 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)])
])
]);
});
}
```

View file

@ -0,0 +1 @@
include: package:lints/recommended.yaml

View 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);
}

View file

@ -0,0 +1,4 @@
export 'src/mutations.dart';
export 'src/node.dart';
export 'src/node_builder.dart';
export 'src/renderer.dart';

File diff suppressed because it is too large Load diff

View 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;
}

View 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;
}

View 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)));
}

View 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();
}
}

View 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

View 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!');
});
}