From c2fc88728b26ea6a59065ccd8bd624c7a5a4869f Mon Sep 17 00:00:00 2001 From: thomashii Date: Sat, 11 Sep 2021 10:17:30 +0800 Subject: [PATCH] Added html_builder package --- .gitignore | 8 + LICENSE | 2 +- README.md | 11 +- packages/html_builder/AUTHORS.md | 12 + packages/html_builder/CHANGELOG.md | 36 + packages/html_builder/LICENSE | 29 + packages/html_builder/README.md | 113 + packages/html_builder/analysis_options.yaml | 1 + packages/html_builder/example/main.dart | 15 + .../lib/belatuk_html_builder.dart | 4 + packages/html_builder/lib/elements.dart | 1841 +++++++++++++++++ packages/html_builder/lib/src/mutations.dart | 20 + packages/html_builder/lib/src/node.dart | 63 + .../html_builder/lib/src/node_builder.dart | 108 + packages/html_builder/lib/src/renderer.dart | 136 ++ packages/html_builder/pubspec.yaml | 12 + packages/html_builder/test/render_test.dart | 34 + 17 files changed, 2443 insertions(+), 2 deletions(-) create mode 100644 packages/html_builder/AUTHORS.md create mode 100644 packages/html_builder/CHANGELOG.md create mode 100644 packages/html_builder/LICENSE create mode 100644 packages/html_builder/README.md create mode 100644 packages/html_builder/analysis_options.yaml create mode 100644 packages/html_builder/example/main.dart create mode 100644 packages/html_builder/lib/belatuk_html_builder.dart create mode 100644 packages/html_builder/lib/elements.dart create mode 100644 packages/html_builder/lib/src/mutations.dart create mode 100644 packages/html_builder/lib/src/node.dart create mode 100644 packages/html_builder/lib/src/node_builder.dart create mode 100644 packages/html_builder/lib/src/renderer.dart create mode 100644 packages/html_builder/pubspec.yaml create mode 100644 packages/html_builder/test/render_test.dart diff --git a/.gitignore b/.gitignore index dbef116..a67f375 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/LICENSE b/LICENSE index ce4a4be..df5e063 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/README.md b/README.md index 106eb9a..4daee5f 100644 --- a/README.md +++ b/README.md @@ -1 +1,10 @@ -# server-utilities \ No newline at end of file +# Belatuk Common Utilities + +## About + +This repository contains the common utility packages required for developing backend framework. + +## Available Packages + +* html_builder + \ No newline at end of file diff --git a/packages/html_builder/AUTHORS.md b/packages/html_builder/AUTHORS.md new file mode 100644 index 0000000..ac95ab5 --- /dev/null +++ b/packages/html_builder/AUTHORS.md @@ -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. diff --git a/packages/html_builder/CHANGELOG.md b/packages/html_builder/CHANGELOG.md new file mode 100644 index 0000000..b63fe1f --- /dev/null +++ b/packages/html_builder/CHANGELOG.md @@ -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. diff --git a/packages/html_builder/LICENSE b/packages/html_builder/LICENSE new file mode 100644 index 0000000..df5e063 --- /dev/null +++ b/packages/html_builder/LICENSE @@ -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. diff --git a/packages/html_builder/README.md b/packages/html_builder/README.md new file mode 100644 index 0000000..8d607f9 --- /dev/null +++ b/packages/html_builder/README.md @@ -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)]) + ]) + ]); + }); +} +``` diff --git a/packages/html_builder/analysis_options.yaml b/packages/html_builder/analysis_options.yaml new file mode 100644 index 0000000..ea2c9e9 --- /dev/null +++ b/packages/html_builder/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lints/recommended.yaml \ No newline at end of file diff --git a/packages/html_builder/example/main.dart b/packages/html_builder/example/main.dart new file mode 100644 index 0000000..db5a32c --- /dev/null +++ b/packages/html_builder/example/main.dart @@ -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); +} diff --git a/packages/html_builder/lib/belatuk_html_builder.dart b/packages/html_builder/lib/belatuk_html_builder.dart new file mode 100644 index 0000000..ebf01f4 --- /dev/null +++ b/packages/html_builder/lib/belatuk_html_builder.dart @@ -0,0 +1,4 @@ +export 'src/mutations.dart'; +export 'src/node.dart'; +export 'src/node_builder.dart'; +export 'src/renderer.dart'; diff --git a/packages/html_builder/lib/elements.dart b/packages/html_builder/lib/elements.dart new file mode 100644 index 0000000..2005a9a --- /dev/null +++ b/packages/html_builder/lib/elements.dart @@ -0,0 +1,1841 @@ +/// Helper functions to build common HTML5 elements. +library belatuk_html_builder.elements; + +import 'belatuk_html_builder.dart'; +export 'belatuk_html_builder.dart'; + +Map _apply(Iterable> props, + [Map? attrs]) { + var map = {}; + attrs?.forEach((k, attr) { + if (attr is String && attr.isNotEmpty == true) { + map[k] = attr; + } else if (attr is Iterable && attr.isNotEmpty == true) { + map[k] = attr.toList(); + } else if (attr != null) { + map[k] = attr; + } + }); + + for (var p in props) { + map.addAll(p); + } + + return map.cast(); +} + +Node text(String text) => TextNode(text); + +Node a( + {String? href, + String? rel, + String? target, + String? id, + className, + style, + Map p = const {}, + Iterable c = const []}) => + h( + 'a', + _apply([ + p, + ], { + 'href': href, + 'rel': rel, + 'target': target, + 'id': id, + 'class': className, + 'style': style, + }), + [...c]); + +Node abbr( + {String? title, + String? id, + className, + style, + Map p = const {}, + Iterable c = const []}) => + h( + 'addr', + _apply([p], + {'title': title, 'id': id, 'class': className, 'style': style}), + [...c]); + +Node address({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('address', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node area({ + String? alt, + Iterable? coordinates, + String? download, + String? href, + String? hreflang, + String? media, + String? nohref, + String? rel, + String? shape, + String? target, + String? type, + String? id, + className, + style, + Map p = const {}, +}) => + SelfClosingNode( + 'area', + _apply([ + p + ], { + 'alt': alt, + 'coordinates': coordinates, + 'download': download, + 'href': href, + 'hreflang': hreflang, + 'media': media, + 'nohref': nohref, + 'rel': rel, + 'shape': shape, + 'target': target, + 'type': type, + 'id': id, + 'class': className, + 'style': style + })); + +Node article({ + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('article', _apply([p], {'class': className, 'style': style}), [...c]); + +Node aside({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('aside', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node audio({ + bool? autoplay, + bool? controls, + bool? loop, + bool? muted, + String? preload, + String? src, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'audio', + _apply([ + p + ], { + 'autoplay': autoplay, + 'controls': controls, + 'loop': loop, + 'muted': muted, + 'preload': preload, + 'src': src, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node b({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('b', _apply([p], {'id': id, 'class': className, 'style': style}), [...c]); + +Node base({ + String? href, + String? target, + String? id, + className, + style, + Map p = const {}, +}) => + SelfClosingNode( + 'base', + _apply([ + p + ], { + 'href': href, + 'target': target, + 'id': id, + 'class': className, + 'style': style + })); + +Node bdi({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('bdi', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node bdo({ + String? dir, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'bdo', + _apply([p], {'dir': dir, 'id': id, 'class': className, 'style': style}), + [...c]); + +Node blockquote({ + String? cite, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'blockquote', + _apply( + [p], {'cite': cite, 'id': id, 'class': className, 'style': style}), + [...c]); + +Node body({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('body', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node br() => SelfClosingNode('br'); + +Node button({ + bool? autofocus, + bool? disabled, + form, + String? formaction, + String? formenctype, + String? formmethod, + bool? formnovalidate, + String? formtarget, + String? name, + String? type, + String? value, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'button', + _apply([ + p + ], { + 'autofocus': autofocus, + 'disabled': disabled, + 'form': form, + 'formaction': formaction, + 'formenctype': formenctype, + 'formmethod': formmethod, + 'formnovalidate': formnovalidate, + 'formtarget': formtarget, + 'name': name, + 'type': type, + 'value': value, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node canvas({ + num? height, + num? width, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'canvas', + _apply([ + p + ], { + 'height': height, + 'width': width, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node cite({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('cite', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node caption({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('caption', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node code({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('code', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node col({ + num? span, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'col', + _apply( + [p], {'span': span, 'id': id, 'class': className, 'style': style}), + [...c]); + +Node colgroup({ + num? span, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'colgroup', + _apply( + [p], {'span': span, 'id': id, 'class': className, 'style': style}), + [...c]); + +Node datalist({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('datalist', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node dd({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('dd', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node del({ + String? cite, + String? datetime, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'del', + _apply([ + p + ], { + 'cite': cite, + 'datetime': datetime, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node details({ + bool? open, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'details', + _apply( + [p], {'open': open, 'id': id, 'class': className, 'style': style}), + [...c]); + +Node dfn({ + String? title, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'dfn', + _apply([p], + {'title': title, 'id': id, 'class': className, 'style': style}), + [...c]); + +Node dialog({ + bool? open, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'dialog', + _apply( + [p], {'open': open, 'id': id, 'class': className, 'style': style}), + [...c]); + +Node div({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('div', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node dl({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('dl', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node dt({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('dt', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node em({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('em', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node embed({ + num? height, + String? src, + String? type, + num? width, + String? id, + className, + style, + Map p = const {}, +}) => + SelfClosingNode( + 'embed', + _apply([ + p + ], { + 'height': height, + 'src': src, + 'type': type, + 'width': width, + 'id': id, + 'class': className, + 'style': style + })); + +Node fieldset({ + bool? disabled, + String? form, + String? name, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'fieldset', + _apply([ + p + ], { + 'disabled': disabled, + 'form': form, + 'name': name, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node figcaption({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('figcaption', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node figure({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('figure', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node footer({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('footer', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node form({ + String? accept, + String? acceptCharset, + String? action, + bool? autocomplete, + String? enctype, + String? method, + String? name, + bool? novalidate, + String? target, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'form', + _apply([ + p + ], { + 'accept': accept, + 'accept-charset': acceptCharset, + 'action': action, + 'autocomplete': + autocomplete != null ? (autocomplete ? 'on' : 'off') : null, + 'enctype': enctype, + 'method': method, + 'name': name, + 'novalidate': novalidate, + 'target': target, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node h1({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('h1', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node h2({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('h2', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); +Node h3({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('h3', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node h4({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('h4', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node h5({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('h5', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node h6({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('h6', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node head({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('head', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node header({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('header', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node hr() => SelfClosingNode('hr'); + +Node html({ + String? manifest, + String? xmlns, + String? lang, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'html', + _apply([ + p + ], { + 'manifest': manifest, + 'xmlns': xmlns, + 'lang': lang, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node i({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('i', _apply([p], {'id': id, 'class': className, 'style': style}), [...c]); + +Node iframe({ + num? height, + String? name, + sandbox, + String? src, + String? srcdoc, + num? width, + String? id, + className, + style, + Map p = const {}, +}) => + SelfClosingNode( + 'iframe', + _apply([ + p + ], { + 'height': height, + 'name': name, + 'sandbox': sandbox, + 'src': src, + 'srcdoc': srcdoc, + 'width': width, + 'id': id, + 'class': className, + 'style': style + })); + +Node img({ + String? alt, + String? crossorigin, + num? height, + String? ismap, + String? longdesc, + sizes, + String? src, + String? srcset, + String? usemap, + num? width, + String? id, + className, + style, + Map p = const {}, +}) => + SelfClosingNode( + 'img', + _apply([ + p + ], { + 'alt': alt, + 'crossorigin': crossorigin, + 'height': height, + 'ismap': ismap, + 'longdesc': longdesc, + 'sizes': sizes, + 'src': src, + 'srcset': srcset, + 'usemap': usemap, + 'width': width, + 'id': id, + 'class': className, + 'style': style + })); + +Node input({ + String? accept, + String? alt, + bool? autocomplete, + bool? autofocus, + bool? checked, + String? dirname, + bool? disabled, + String? form, + String? formaction, + String? formenctype, + String? method, + String? formnovalidate, + String? formtarget, + num? height, + String? list, + max, + num? maxlength, + min, + bool? multiple, + String? name, + String? pattern, + String? placeholder, + bool? readonly, + bool? required, + num? size, + String? src, + num? step, + String? type, + String? value, + num? width, + String? id, + className, + style, + Map p = const {}, +}) => + SelfClosingNode( + 'input', + _apply([ + p + ], { + 'accept': accept, + 'alt': alt, + 'autocomplete': + autocomplete == null ? null : (autocomplete ? 'on' : 'off'), + 'autofocus': autofocus, + 'checked': checked, + 'dirname': dirname, + 'disabled': disabled, + 'form': form, + 'formaction': formaction, + 'formenctype': formenctype, + 'method': method, + 'formnovalidate': formnovalidate, + 'formtarget': formtarget, + 'height': height, + 'list': list, + 'max': max, + 'maxlength': maxlength, + 'min': min, + 'multiple': multiple, + 'name': name, + 'pattern': pattern, + 'placeholder': placeholder, + 'readonly': readonly, + 'required': required, + 'size': size, + 'src': src, + 'step': step, + 'type': type, + 'value': value, + 'width': width, + 'id': id, + 'class': className, + 'style': style + })); + +Node ins({ + String? cite, + String? datetime, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'ins', + _apply([ + p + ], { + 'cite': cite, + 'datetime': datetime, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node kbd({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('kbd', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node keygen({ + bool? autofocus, + String? challenge, + bool? disabled, + String? from, + String? keytype, + String? name, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'keygen', + _apply([ + p + ], { + 'autofocus': autofocus, + 'challenge': challenge, + 'disabled': disabled, + 'from': from, + 'keytype': keytype, + 'name': name, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node label({ + String? for_, + String? form, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'label', + _apply([ + p + ], { + 'for': for_, + 'form': form, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node legend({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('legend', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node li({ + num? value, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'li', + _apply([p], + {'value': value, 'id': id, 'class': className, 'style': style}), + [...c]); + +Node link({ + String? crossorigin, + String? href, + String? hreflang, + String? media, + String? rel, + sizes, + String? target, + String? type, + String? id, + className, + style, + Map p = const {}, +}) => + SelfClosingNode( + 'link', + _apply([ + p + ], { + 'crossorigin': crossorigin, + 'href': href, + 'hreflang': hreflang, + 'media': media, + 'rel': rel, + 'sizes': sizes, + 'target': target, + 'type': type, + 'id': id, + 'class': className, + 'style': style + })); + +Node main({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('main', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node map({ + String? name, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'map', + _apply( + [p], {'name': name, 'id': id, 'class': className, 'style': style}), + [...c]); + +Node mark({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('mark', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node menu({ + String? label, + String? type, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'menu', + _apply([ + p + ], { + 'label': label, + 'type': type, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node menuitem({ + bool? checked, + command, + bool? default_, + bool? disabled, + String? icon, + String? label, + String? radiogroup, + String? type, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'menuitem', + _apply([ + p + ], { + 'checked': checked, + 'command': command, + 'default': default_, + 'disabled': disabled, + 'icon': icon, + 'label': label, + 'radiogroup': radiogroup, + 'type': type, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node meta({ + String? charset, + String? content, + String? httpEquiv, + String? name, + String? id, + className, + style, + Map p = const {}, +}) => + SelfClosingNode( + 'meta', + _apply([ + p + ], { + 'charset': charset, + 'content': content, + 'http-equiv': httpEquiv, + 'name': name, + 'id': id, + 'class': className, + 'style': style + })); + +Node nav({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('nav', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node noscript({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('noscript', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node object({ + String? data, + String? form, + num? height, + String? name, + String? type, + String? usemap, + num? width, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'object', + _apply([ + p + ], { + 'data': data, + 'form': form, + 'height': height, + 'name': name, + 'type': type, + 'usemap': usemap, + 'width': width, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node ol({ + bool? reversed, + num? start, + String? type, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'ol', + _apply([ + p + ], { + 'reversed': reversed, + 'start': start, + 'type': type, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node optgroup({ + bool? disabled, + String? label, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'optgroup', + _apply([ + p + ], { + 'disabled': disabled, + 'label': label, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node option({ + bool? disabled, + String? label, + bool? selected, + String? value, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'option', + _apply([ + p + ], { + 'disabled': disabled, + 'label': label, + 'selected': selected, + 'value': value, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node output({ + String? for_, + String? form, + String? name, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'output', + _apply([ + p + ], { + 'for': for_, + 'form': form, + 'name': name, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node p({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('p', _apply([p], {'id': id, 'class': className, 'style': style}), [...c]); + +Node param({ + String? name, + value, + String? id, + className, + style, + Map p = const {}, +}) => + SelfClosingNode( + 'param', + _apply([ + p + ], { + 'name': name, + 'value': value, + 'id': id, + 'class': className, + 'style': style + })); + +Node picture({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('picture', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node pre({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('pre', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node progress({ + num? max, + num? value, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'progress', + _apply([ + p + ], { + 'max': max, + 'value': value, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node q({ + String? cite, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'q', + _apply( + [p], {'cite': cite, 'id': id, 'class': className, 'style': style}), + [...c]); + +Node rp({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('rp', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node rt({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('rt', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node ruby({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('ruby', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node s({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('s', _apply([p], {'id': id, 'class': className, 'style': style}), [...c]); + +Node samp({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('samp', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node script({ + bool? async, + String? charset, + bool? defer, + String? src, + String? type, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'script', + _apply([ + p + ], { + 'async': async, + 'charset': charset, + 'defer': defer, + 'src': src, + 'type': type, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node section({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('section', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node select({ + bool? autofocus, + bool? disabled, + String? form, + bool? multiple, + bool? required, + num? size, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'select', + _apply([ + p + ], { + 'autofocus': autofocus, + 'disabled': disabled, + 'form': form, + 'multiple': multiple, + 'required': required, + 'size': size, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node small({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('small', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node source({ + String? src, + String? srcset, + String? media, + sizes, + String? type, + String? id, + className, + style, + Map p = const {}, +}) => + SelfClosingNode( + 'source', + _apply([ + p + ], { + 'src': src, + 'srcset': srcset, + 'media': media, + 'sizes': sizes, + 'type': type, + 'id': id, + 'class': className, + 'style': style + })); + +Node span({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('span', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node strong({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('strong', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node style({ + String? media, + bool? scoped, + String? type, + String? id, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'style', + _apply([p], {'media': media, 'scoped': scoped, 'type': type, 'id': id}), + [...c]); + +Node sub({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('sub', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node summary({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('summary', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node sup({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('sup', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node table({ + bool? sortable, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'table', + _apply([ + p + ], { + 'sortable': sortable, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node tbody({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('tbody', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node td({ + num? colspan, + headers, + num? rowspan, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'td', + _apply([ + p + ], { + 'colspan': colspan, + 'headers': headers, + 'rowspan': rowspan, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node textarea({ + bool? autofocus, + num? cols, + String? dirname, + bool? disabled, + String? form, + num? maxlength, + String? name, + String? placeholder, + bool? readonly, + bool? required, + num? rows, + String? wrap, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'textarea', + _apply([ + p + ], { + 'autofocus': autofocus, + 'cols': cols, + 'dirname': dirname, + 'disabled': disabled, + 'form': form, + 'maxlength': maxlength, + 'name': name, + 'placeholder': placeholder, + 'readonly': readonly, + 'required': required, + 'rows': rows, + 'wrap': wrap, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node tfoot({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('tfoot', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node th({ + String? abbr, + num? colspan, + headers, + num? rowspan, + String? scope, + sorted, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'th', + _apply([ + p + ], { + 'abbr': abbr, + 'colspan': colspan, + 'headers': headers, + 'rowspan': rowspan, + 'scope': scope, + 'sorted': sorted, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node thead({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('thead', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node time({ + String? datetime, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'time', + _apply([ + p + ], { + 'datetime': datetime, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node title({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('title', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node tr({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('tr', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node track({ + bool? default_, + String? kind, + String? label, + String? src, + String? srclang, + String? id, + className, + style, + Map p = const {}, +}) => + SelfClosingNode( + 'track', + _apply([ + p + ], { + 'default': default_, + 'kind': kind, + 'label': label, + 'src': src, + 'srclang': srclang, + 'id': id, + 'class': className, + 'style': style + })); + +Node u({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('u', _apply([p], {'id': id, 'class': className, 'style': style}), [...c]); + +Node ul({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('ul', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node var_({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('var', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); + +Node video({ + bool? autoplay, + bool? controls, + num? height, + bool? loop, + bool? muted, + String? poster, + String? preload, + String? src, + num? width, + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h( + 'video', + _apply([ + p + ], { + 'autoplay': autoplay, + 'controls': controls, + 'height': height, + 'loop': loop, + 'muted': muted, + 'poster': poster, + 'preload': preload, + 'src': src, + 'width': width, + 'id': id, + 'class': className, + 'style': style + }), + [...c]); + +Node wbr({ + String? id, + className, + style, + Map p = const {}, + Iterable c = const [], +}) => + h('wbr', _apply([p], {'id': id, 'class': className, 'style': style}), + [...c]); diff --git a/packages/html_builder/lib/src/mutations.dart b/packages/html_builder/lib/src/mutations.dart new file mode 100644 index 0000000..3173163 --- /dev/null +++ b/packages/html_builder/lib/src/mutations.dart @@ -0,0 +1,20 @@ +import 'node.dart'; +import 'node_builder.dart'; + +/// Returns a function that rebuilds an arbitrary [Node] by applying the [transform] to it. +Node Function(Node) rebuild(NodeBuilder Function(NodeBuilder) transform, + {bool selfClosing = false}) { + return (node) => + transform(NodeBuilder.from(node)).build(selfClosing: selfClosing); +} + +/// Applies [f] to all children of this node, recursively. +/// +/// Use this alongside [rebuild]. +Node Function(Node) rebuildRecursive(Node Function(Node) f) { + Node _build(Node node) { + return NodeBuilder.from(f(node)).mapChildren(_build).build(); + } + + return _build; +} diff --git a/packages/html_builder/lib/src/node.dart b/packages/html_builder/lib/src/node.dart new file mode 100644 index 0000000..b547077 --- /dev/null +++ b/packages/html_builder/lib/src/node.dart @@ -0,0 +1,63 @@ +import 'package:collection/collection.dart'; + +/// Shorthand function to generate a new [Node]. +Node h(String tagName, + [Map attributes = const {}, + Iterable children = const []]) => + Node(tagName, attributes, children); + +/// Represents an HTML node. +class Node { + final String tagName; + final Map attributes = {}; + final List children = []; + + Node(this.tagName, + [Map attributes = const {}, + Iterable children = const []]) { + this + ..attributes.addAll(attributes) + ..children.addAll(children); + } + + Node._selfClosing(this.tagName, + [Map attributes = const {}]) { + this.attributes.addAll(attributes); + } + + @override + bool operator ==(other) { + return other is Node && + other.tagName == tagName && + const ListEquality().equals(other.children, children) && + const MapEquality() + .equals(other.attributes, attributes); + } +} + +/// Represents a self-closing tag, i.e. `
`. +class SelfClosingNode extends Node { + /* + @override + final String tagName; + + @override + final Map attributes = {}; + */ + + @override + List get children => List.unmodifiable([]); + + SelfClosingNode(tagName, [Map attributes = const {}]) + : super._selfClosing(tagName, attributes); +} + +/// Represents a text node. +class TextNode extends Node { + final String text; + + TextNode(this.text) : super(':text'); + + @override + bool operator ==(other) => other is TextNode && other.text == text; +} diff --git a/packages/html_builder/lib/src/node_builder.dart b/packages/html_builder/lib/src/node_builder.dart new file mode 100644 index 0000000..ab566c8 --- /dev/null +++ b/packages/html_builder/lib/src/node_builder.dart @@ -0,0 +1,108 @@ +import 'node.dart'; + +/// Helper class to build nodes. +class NodeBuilder { + final String tagName; + final Map attributes; + final Iterable children; + Node? _existing; + + NodeBuilder(this.tagName, + {this.attributes = const {}, this.children = const []}); + + /// Creates a [NodeBuilder] that just spits out an already-existing [Node]. + factory NodeBuilder.existing(Node existingNode) => + NodeBuilder(existingNode.tagName).._existing = existingNode; + + factory NodeBuilder.from(Node node) => NodeBuilder(node.tagName, + attributes: Map.from(node.attributes), + children: List.from(node.children)); + + /// Builds the node. + Node build({bool selfClosing = false}) => + _existing ?? + (selfClosing + ? SelfClosingNode(tagName, attributes) + : Node(tagName, attributes, children)); + + /// Produce a modified copy of this builder. + NodeBuilder change( + {String? tagName, + Map? attributes, + Iterable? children}) { + return NodeBuilder(tagName ?? this.tagName, + attributes: attributes ?? this.attributes, + children: children ?? this.children); + } + + NodeBuilder changeTagName(String tagName) => change(tagName: tagName); + + NodeBuilder changeAttributes(Map attributes) => + change(attributes: attributes); + + NodeBuilder changeChildren(Iterable children) => + change(children: children); + + NodeBuilder changeAttributesMapped( + Map Function(Map) f) { + var map = Map.from(attributes); + return changeAttributes(f(map)); + } + + NodeBuilder changeChildrenMapped(Iterable Function(List) f) { + var list = List.from(children); + return changeChildren(f(list)); + } + + NodeBuilder mapChildren(Node Function(Node) f) => + changeChildrenMapped((list) => list.map(f)); + + NodeBuilder mapAttributes( + MapEntry Function(String, dynamic) f) => + changeAttributesMapped((map) => map.map(f)); + + NodeBuilder setAttribute(String name, dynamic value) => + changeAttributesMapped((map) => map..[name] = value); + + NodeBuilder addChild(Node child) => + changeChildrenMapped((list) => list..add(child)); + + NodeBuilder removeChild(Node child) => + changeChildrenMapped((list) => list..remove(child)); + + NodeBuilder removeAttribute(String name) => + changeAttributesMapped((map) => map..remove(name)); + + NodeBuilder setId(String id) => setAttribute('id', id); + + NodeBuilder setClassName(String className) => + setAttribute('class', className); + + NodeBuilder setClasses(Iterable classes) => + setClassName(classes.join(' ')); + + NodeBuilder setClassesMapped(Iterable Function(List) f) { + var clazz = attributes['class']; + var classes = []; + + if (clazz is String) { + classes.addAll(clazz.split(' ')); + } else if (clazz is Iterable) { + classes.addAll(clazz.map((s) => s.toString())); + } + + return setClasses(f(classes)); + } + + NodeBuilder addClass(String className) => setClassesMapped( + (classes) => classes.contains(className) ? classes : classes + ..add(className)); + + NodeBuilder removeClass(String className) => + setClassesMapped((classes) => classes..remove(className)); + + NodeBuilder toggleClass(String className) => + setClassesMapped((classes) => classes.contains(className) + ? (classes..remove(className)) + : (classes..add(className))); +} diff --git a/packages/html_builder/lib/src/renderer.dart b/packages/html_builder/lib/src/renderer.dart new file mode 100644 index 0000000..31b2434 --- /dev/null +++ b/packages/html_builder/lib/src/renderer.dart @@ -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 { + /// Renders a DOM tree into another representation. + T render(Node rootNode); +} + +/// Renders a DOM tree into a HTML string. +abstract class StringRenderer implements Renderer { + /// Initializes a new [StringRenderer]. + /// + /// If [html5] is not `false` (default: `true`), then self-closing elements will be rendered with a slash before the last angle bracket, ex. `
`. + /// If [pretty] is `true` (default), then [whitespace] (default: `' '`) will be inserted between nodes. + /// You can also provide a [doctype] (default: `html`). + factory StringRenderer( + {bool html5 = true, + bool pretty = true, + String doctype = 'html', + String whitespace = ' '}) => + pretty == true + ? _PrettyStringRendererImpl( + html5: html5 != false, doctype: doctype, whitespace: whitespace) + : _StringRendererImpl(html5: html5 != false, doctype: doctype); +} + +class _StringRendererImpl implements StringRenderer { + final String? doctype; + final bool? html5; + + _StringRendererImpl({this.html5, this.doctype}); + + void _renderInto(Node node, StringBuffer buf) { + if (node is TextNode) { + buf.write(node.text); + } else { + buf.write('<${node.tagName}'); + + node.attributes.forEach((k, v) { + if (v == true) { + buf.write(' $k'); + } else if (v == false || v == null) { + // Ignore + } else if (v is Iterable) { + var val = v.join(' ').replaceAll('"', '\\"'); + buf.write(' $k="$val"'); + } else if (v is Map) { + var val = v.keys + .fold('', (out, k) => out += '$k: ${v[k]};') + .replaceAll('"', '\\"'); + buf.write(' $k="$val"'); + } else { + var val = v.toString().replaceAll('"', '\\"'); + buf.write(' $k="$val"'); + } + }); + + if (node is SelfClosingNode) { + buf.write((html5 != false) ? '>' : '/>'); + } else { + buf.write('>'); + node.children.forEach((child) => _renderInto(child, buf)); + buf.write(''); + } + } + } + + @override + String render(Node rootNode) { + var buf = StringBuffer(); + if (doctype?.isNotEmpty == true) buf.write(''); + _renderInto(rootNode, buf); + return buf.toString(); + } +} + +class _PrettyStringRendererImpl implements StringRenderer { + final bool? html5; + final String? doctype, whitespace; + + _PrettyStringRendererImpl({this.html5, this.whitespace, this.doctype}); + + void _applyTabs(int tabs, StringBuffer buf) { + for (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('', (out, k) => out += '$k: ${v[k]};') + .replaceAll('"', '\\"'); + buf.write(' $k="$val"'); + } else { + var val = v.toString().replaceAll('"', '\\"'); + buf.write(' $k="$val"'); + } + }); + + if (node is SelfClosingNode) { + buf.write((html5 != false) ? '>' : '/>'); + } else { + buf.write('>'); + node.children.forEach((child) => _renderInto(tabs + 1, child, buf)); + buf.writeln(); + _applyTabs(tabs, buf); + buf.write(''); + } + } + } + + @override + String render(Node rootNode) { + var buf = StringBuffer(); + if (doctype?.isNotEmpty == true) buf.writeln(''); + _renderInto(0, rootNode, buf); + return buf.toString(); + } +} diff --git a/packages/html_builder/pubspec.yaml b/packages/html_builder/pubspec.yaml new file mode 100644 index 0000000..4adce2c --- /dev/null +++ b/packages/html_builder/pubspec.yaml @@ -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 diff --git a/packages/html_builder/test/render_test.dart b/packages/html_builder/test/render_test.dart new file mode 100644 index 0000000..17a2867 --- /dev/null +++ b/packages/html_builder/test/render_test.dart @@ -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!'); + }); +}