flutter_widget_from_html_core 0.5.0-rc.2020081901
flutter_widget_from_html_core: ^0.5.0-rc.2020081901 copied to clipboard
Flutter package for widget tree building from html that focuses on correctness and extensibility.
Flutter Widget from HTML (core) #
A Flutter package for building Flutter widget tree from HTML (supports most popular tags and stylings).
This core package implements html parsing and widget building logic so it's easy to extend and fit your app's use case. It tries to render an optimal tree: use RichText with specific TextStyle, merge text spans together, show images in sized box, etc.
If this is your first time here, consider using the flutter_widget_from_html package as a quick starting point.
Usage #
To use this package, add flutter_widget_from_html_core as a dependency in your pubspec.yaml file.
See the Demo app for inspiration.
Example #
const kHtml = '''
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<p>A paragraph with <strong>strong</strong> <em>emphasized</em> text.</p>
<p>And of course, cat image:</p>
<figure>
<img src="https://media.giphy.com/media/6VoDJzfRjJNbG/giphy-downsized.gif" width="250" height="171" />
<figcaption>Source: <a href="https://gph.is/QFgPA0">https://gph.is/QFgPA0</a></figcaption>
</figure>
''';
class HelloWorldCoreScreen extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text('HelloWorldCoreScreen')),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: HtmlWidget(
kHtml,
onTapUrl: (url) => showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('onTapUrl'),
content: Text(url),
),
),
),
),
),
);
}
void main() => runApp(WidgetsApp(home: HelloWorldCoreScreen()));
[../../demo_app/screenshots/HelloWorldCoreScreen.gif?raw=true]
Features #
HTML tags #
Below tags are the ones that have special meaning / styling, all other tags will be parsed as text.
- A: underline, blue color, no default onTap action (use
flutter_widget_from_htmlfor that) - H1/H2/H3/H4/H5/H6
- IMG with support for asset (
asset://), data uri and network image without caching (useflutter_widget_from_htmlfor that) - LI/OL/UL with support for:
- Attributes:
type,start,reversed - Inline style
list-style-typevalues:lower-alpha,upper-alpha,lower-latin,upper-latin,circle,decimal,disc,lower-roman,upper-roman,square
- Attributes:
- TABLE/CAPTION/THEAD/TBODY/TFOOT/TR/TD/TH with support for:
- TABLE attributes (
border,cellpadding) and inline style (border) - TD/TH attributes
colspan,rowspanare parsed but ignored during rendering, useflutter_widget_from_htmlif you need them
- TABLE attributes (
- ABBR, ACRONYM, ADDRESS, ARTICLE, ASIDE, B, BIG, BLOCKQUOTE, BR, CENTER, CITE, CODE, DD, DEL, DFN, DIV, DL, DT, EM, FIGCAPTION, FIGURE, FONT, FOOTER, HEADER, HR, I, IMG, INS, KBD, MAIN, NAV, P, PRE, Q, RP, RT, RUBY, S, SAMP, SECTION, STRIKE, STRONG, SUB, SUP, TT, U, VAR
However, these tags and their contents will be ignored:
- IFRAME (use
flutter_widget_from_htmlfor web view support) - SCRIPT
- STYLE
- SVG (use
flutter_widget_from_htmlfor SVG support)
Attributes #
- dir:
auto,ltrandrtl
Inline stylings #
- background (color only), background-color: hex values,
rgb(),hsl()or named colors - border-top, border-bottom: overline/underline with support for dashed/dotted/double/solid style
- color: hex values,
rgb(),hsl()or named colors - direction (similar to
dirattribute) - font-family
- font-size: absolute (e.g.
xx-large), relative (larger,smaller) or values inem,%,ptandpx - font-style: italic/normal
- font-weight: bold/normal/100..900
- line-height:
normalnumber or values inem,%,ptandpx - margin and margin-xxx: values in
em,ptandpx - padding and padding-xxx: values in
em,ptandpx - vertical-align: baseline/top/bottom/middle/sub/super
- text-align: center/justify/left/right
- text-decoration: line-through/none/overline/underline
- text-overflow: clip/ellipsis. Note:
text-overflow: ellipsisshould be used in conjuntion withmax-linesor-webkit-line-clampfor better result. - Sizing (width & height, max-xxx, min-xxx) with values in
em,ptandpx
Extensibility #
There are two ways to change the built widget tree.
- Use callbacks like
customStylesBuilderorcustomWidgetBuilderfor small changes - Use a custom
WidgetFactoryfor complete control of the rendering process
Callbacks #
For cosmetic changes like color, italic, etc., use customStylesBuilder to specify inline styles (see supported list above) for each DOM element. Some common conditionals:
- If HTML tag is H1
e.localName == 'h1' - If the element has
fooCSS classe.classes.contains('foo') - If an attribute has a specific value
e.attributes['x'] == 'y'
This example changes the color for a CSS class:
|
[../../demo_app/screenshots/CustomStylesBuilderScreen.png?raw=true] |
For fairly simple widget, use customWidgetBuilder. You will need to handle the DOM element and its children manually. The next example renders a carousel:
const kHtml = '''
<p>...</p>
<div class="carousel">
<div class="image">
<img src="https://images.unsplash.com/photo-1514888286974-6c03e2ca1dba" />
</div>
...
</div>
<p>...</p>
''';
class CustomWidgetBuilderScreen extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text('CustomStylesBuilderScreen'),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: HtmlWidget(
kHtml,
customWidgetBuilder: (e) {
if (!e.classes.contains('carousel')) return null;
final srcs = <String>[];
for (final child in e.children) {
for (final grandChild in child.children) {
srcs.add(grandChild.attributes['src']);
}
}
return CarouselSlider(
options: CarouselOptions(
autoPlay: true,
autoPlayAnimationDuration:
const Duration(milliseconds: 250),
autoPlayInterval: const Duration(milliseconds: 1000),
enlargeCenterPage: true,
enlargeStrategy: CenterPageEnlargeStrategy.scale,
),
items: srcs.map(_toItem).toList(growable: false),
);
},
),
),
),
);
static Widget _toItem(String src) => Container(
child: Center(
child: Image.network(src, fit: BoxFit.cover, width: 1000),
),
);
}
[../../demo_app/screenshots/CustomWidgetBuilderScreen.gif?raw=true]
Custom WidgetFactory #
The HTML string is parsed into DOM elements and each element is visited once to populate a BuildMetadata and collect BuiltPieces. See step by step how it works:
| Step | Integration point | |
|---|---|---|
| 1 | Parse the tag and attributes map | WidgetFactory.parseTag(BuildMetadata) |
| 2 | Inform parents if any | BuildOp.onChild(BuildMetadata) |
| 3 | Populate default inline styles | BuildOp.defaultStyles(BuildMetadata) |
| 4 | customStyleBuilder / customWidgetBuilder will be called if configured |
|
| 5 | Parse inline style key+value pairs, parseStyle may be called multiple times |
WidgetFactory.parseStyle(BuildMetadata, String, String) |
| 6 | Repeat with children elements to collect BuiltPieces |
|
| 7 | Inform build ops | BuildOp.onPieces(BuildMetadata, Iterable<BuiltPiece>) |
| 8 | a. If not a block element, go to 10 | |
| b. Build widgets from pieces | ||
| 9 | Inform build ops | BuildOp.onWidgets(BuildMetadata, Iterable<Widget>) |
| 10 | The end |
Notes:
- Text related styling can be changed with
TextStyleBuilder, just register your callback and it will be called when the build context is ready.- The first parameter is a
TextStyleHtmlwhich is immutable and is calculated from the root down to your element, your callback must return aTextStyleHtmlby callingcopyWithor simply return the parent itself. - Optionally, pass any object on registration and your callback will receive it as the second parameter.
- The first parameter is a
// simple callback: set text color to accent color
meta.tsb((parent, _) =>
parent.copyWith(
style: parent.style.copyWith(
color: parent.getDependency<ThemeData>().accentColor,
),
));
// callback using second param: set height to input value
TextStyleHtml callback(TextStyleHtml parent, double value) =>
parent.copyWith(height: value)
// register with some value
meta.tsb<int>(callback, 2);
- Other complicated styling are supported via
BuildOp
meta.register(BuildOp(
onPieces: (meta, pieces) => pieces,
onWidgets: (meta, widgets) => widgets,
...,
priority: 9999,
));
- Each metadata can has multiple text style builder callbacks and build ops.
- There are two types of
BuiltPiece:BuiltPiece.text()contains aTextBitsBuiltPiece.widgets()contains widgets
The example below replaces smilie inline image with an emoji:
const kHtml = """
<p>Hello <img class="smilie smilie-1" alt=":)" src="http://domain.com/sprites.png" />!</p>
<p>How are you <img class="smilie smilie-2" alt=":P" src="http://domain.com/sprites.png" />?
""";
const kSmilies = {':)': '🙂'};
class SmilieScreen extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text('SmilieScreen'),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: HtmlWidget(
kHtml,
factoryBuilder: () => _SmiliesWidgetFactory(),
),
),
);
}
class _SmiliesWidgetFactory extends WidgetFactory {
final smilieOp = BuildOp(
onPieces: (meta, pieces) {
final alt = meta.element.attributes['alt'];
final text = kSmilies.containsKey(alt) ? kSmilies[alt] : alt;
return pieces..first?.text?.addText(text);
},
);
@override
void parse(BuildMetadata meta) {
final e = meta.element;
if (e.localName == 'img' &&
e.classes.contains('smilie') &&
e.attributes.containsKey('alt')) {
meta.register(smilieOp);
return;
}
return super.parse(meta);
}
}
[../../demo_app/screenshots/SmilieScreen.png?raw=true]