Skip to content

Commit 1e427df

Browse files
Merge branch 'master' into master
2 parents af77caa + 1169d15 commit 1e427df

File tree

7 files changed

+214
-55
lines changed

7 files changed

+214
-55
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
## [0.9.7] - March 26, 2019:
2+
3+
* Added onImageError callback
4+
5+
## [0.9.6] - March 11, 2019:
6+
7+
* Fix whitespace issue. ([#59](https://github.com/Sub6Resources/flutter_html/issues/59))
8+
9+
## [0.9.5] - March 11, 2019:
10+
11+
* Add support for `span` in `RichText` parser. ([#61](https://github.com/Sub6Resources/flutter_html/issues/61))
12+
* Adds `linkStyle` attribute. ([#70](https://github.com/Sub6Resources/flutter_html/pull/70))
13+
* Adds tests for `header`, `hr`, and `i` ([#62](https://github.com/Sub6Resources/flutter_html/issues/62))
14+
115
## [0.9.4] - February 5, 2019:
216

317
* Fixes `table` error in `RichText` parser. ([#58](https://github.com/Sub6Resources/flutter_html/issues/58))

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render
99
Add the following to your `pubspec.yaml` file:
1010

1111
dependencies:
12-
flutter_html: ^0.9.4
12+
flutter_html: ^0.9.7
1313

1414
## Currently Supported HTML Tags:
1515
`a`, `abbr`, `acronym`, `address`, `article`, `aside`, `b`, `bdi`, `bdo`, `big`, `blockquote`, `body`, `br`, `caption`, `cite`, `code`, `data`, `dd`, `del`, `dfn`, `div`, `dl`, `dt`, `em`, `figcaption`, `figure`, `footer`, `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `header`, `hr`, `i`, `img`, `ins`, `kbd`, `li`, `main`, `mark`, `nav`, `noscript`, `ol`, `p`, `pre`, `q`, `rp`, `rt`, `ruby`, `s`, `samp`, `section`, `small`, `span`, `strike`, `strong`, `sub`, `sup`, `table`, `tbody`, `td`, `template`, `tfoot`, `th`, `thead`, `time`, `tr`, `tt`, `u`, `ul`, `var`
@@ -68,6 +68,9 @@ Check out the official Flutter WebView package here: https://pub.dartlang.org/pa
6868
padding: EdgeInsets.all(8.0),
6969
backgroundColor: Colors.white70,
7070
defaultTextStyle: TextStyle(fontFamily: 'serif'),
71+
linkStyle: const TextStyle(
72+
color: Colors.redAccent,
73+
),
7174
onLinkTap: (url) {
7275
// open url in a webview
7376
},
@@ -85,4 +88,4 @@ Check out the official Flutter WebView package here: https://pub.dartlang.org/pa
8588

8689
This package has a known issue where text does not wrap correctly. Setting `useRichText` to true fixes the issue
8790
by using an alternate parser. The alternate parser, however, does not support the `customRender` callback, and several elements
88-
supported by the default parser are not supported by the alternate parser.
91+
supported by the default parser are not supported by the alternate parser (see [#61](https://github.com/Sub6Resources/flutter_html/issues/61) for a list).

example/main.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ class _MyHomePageState extends State<MyHomePage> {
121121
""",
122122
//Optional parameters:
123123
padding: EdgeInsets.all(8.0),
124+
linkStyle: const TextStyle(
125+
color: Colors.redAccent,
126+
decorationColor: Colors.redAccent,
127+
decoration: TextDecoration.underline,
128+
),
124129
onLinkTap: (url) {
125130
print("Opening $url...");
126131
},

lib/flutter_html.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ class Html extends StatelessWidget {
1717
this.customTextStyle,
1818
this.blockSpacing = 14.0,
1919
this.useRichText = false,
20+
this.onImageError,
21+
this.linkStyle = const TextStyle(
22+
decoration: TextDecoration.underline,
23+
color: Colors.blueAccent,
24+
decorationColor: Colors.blueAccent),
2025
}) : super(key: key);
2126

2227
final String data;
@@ -27,6 +32,8 @@ class Html extends StatelessWidget {
2732
final bool renderNewlines;
2833
final double blockSpacing;
2934
final bool useRichText;
35+
final ImageErrorListener onImageError;
36+
final TextStyle linkStyle;
3037

3138
/// Either return a custom widget for specific node types or return null to
3239
/// fallback to the default rendering.
@@ -52,6 +59,8 @@ class Html extends StatelessWidget {
5259
customEdgeInsets: customEdgeInsets,
5360
customTextStyle: customTextStyle,
5461
html: data,
62+
onImageError: onImageError,
63+
linkStyle: linkStyle,
5564
)
5665
: HtmlOldParser(
5766
width: width,
@@ -60,6 +69,8 @@ class Html extends StatelessWidget {
6069
customRender: customRender,
6170
html: data,
6271
blockSpacing: blockSpacing,
72+
onImageError: onImageError,
73+
linkStyle: linkStyle,
6374
),
6475
),
6576
);

lib/html_parser.dart

Lines changed: 83 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import 'dart:convert';
22

3-
import 'package:flutter/material.dart';
43
import 'package:flutter/gestures.dart';
5-
import 'package:html/parser.dart' as parser;
4+
import 'package:flutter/material.dart';
65
import 'package:html/dom.dart' as dom;
6+
import 'package:html/parser.dart' as parser;
77

88
typedef CustomRender = Widget Function(dom.Node node, List<Widget> children);
99
typedef CustomTextStyle = TextStyle Function(dom.Node node, TextStyle baseStyle);
@@ -148,6 +148,12 @@ class HtmlRichTextParser extends StatelessWidget {
148148
this.html,
149149
this.customTextStyle,
150150
this.customEdgeInsets,
151+
this.onImageError,
152+
this.linkStyle = const TextStyle(
153+
decoration: TextDecoration.underline,
154+
color: Colors.blueAccent,
155+
decorationColor: Colors.blueAccent,
156+
),
151157
});
152158

153159
final double indentSize = 10.0;
@@ -158,6 +164,8 @@ class HtmlRichTextParser extends StatelessWidget {
158164
final String html;
159165
final CustomTextStyle customTextStyle;
160166
final CustomEdgeInsets customEdgeInsets;
167+
final ImageErrorListener onImageError;
168+
final TextStyle linkStyle;
161169

162170
// style elements set a default style
163171
// for all child nodes
@@ -174,7 +182,8 @@ class HtmlRichTextParser extends StatelessWidget {
174182
"acronym",
175183
"ol",
176184
"ul",
177-
"blockquote"
185+
"blockquote",
186+
"span",
178187
];
179188

180189
// specialty elements require unique handling
@@ -266,7 +275,8 @@ class HtmlRichTextParser extends StatelessWidget {
266275
);
267276

268277
// ignore the top level "body"
269-
body.nodes.forEach((dom.Node node) => _parseNode(node, parseContext));
278+
body.nodes
279+
.forEach((dom.Node node) => _parseNode(node, parseContext, context));
270280
// _parseNode(body, parseContext);
271281

272282
// filter out empty widgets
@@ -303,7 +313,8 @@ class HtmlRichTextParser extends StatelessWidget {
303313
// function can add child nodes to the parent if it should
304314
//
305315
// each iteration creates a new parseContext as a copy of the previous one if it needs to
306-
void _parseNode(dom.Node node, ParseContext parseContext) {
316+
void _parseNode(
317+
dom.Node node, ParseContext parseContext, BuildContext buildContext) {
307318
// TEXT ONLY NODES
308319
// a text only node is a child of a tag with no inner html
309320
if (node is dom.Text) {
@@ -329,14 +340,14 @@ class HtmlRichTextParser extends StatelessWidget {
329340
if (!parseContext.parentElement.children.isEmpty) {
330341
lastString = parseContext.parentElement.children.last.text ?? '';
331342
}
332-
if (lastString == '' ||
333-
lastString.endsWith(' ') ||
334-
lastString.endsWith('\n')) finalText = finalText.trimLeft();
343+
if (lastString.endsWith(' ') || lastString.endsWith('\n')) {
344+
finalText = finalText.trimLeft();
345+
}
335346
}
336347
}
337348

338-
// if the finalText is actually empty, just return
339-
if (finalText.trim().isEmpty) return;
349+
// if the finalText is actually empty, just return (unless it's just a space)
350+
if (finalText.trim().isEmpty && finalText != " ") return;
340351

341352
// NOW WE HAVE OUR TRULY FINAL TEXT
342353
// debugPrint("Plain Text Node: '$finalText'");
@@ -477,6 +488,9 @@ class HtmlRichTextParser extends StatelessWidget {
477488
nextContext.indentLevel += 1;
478489
nextContext.blockType = 'blockquote';
479490
break;
491+
case "span":
492+
//No additional styles
493+
break;
480494
}
481495

482496
if (customTextStyle != null) {
@@ -511,13 +525,9 @@ class HtmlRichTextParser extends StatelessWidget {
511525
nextContext.parentElement = linkContainer;
512526
nextContext.rootWidgetList.add(linkContainer);
513527
} else {
514-
TextStyle linkStyle = parseContext.childStyle.merge(TextStyle(
515-
decoration: TextDecoration.underline,
516-
color: Colors.blueAccent,
517-
decorationColor: Colors.blueAccent,
518-
));
528+
TextStyle _linkStyle = parseContext.childStyle.merge(linkStyle);
519529
LinkTextSpan span = LinkTextSpan(
520-
style: linkStyle,
530+
style: _linkStyle,
521531
url: url,
522532
onLinkTap: onLinkTap,
523533
children: <TextSpan>[],
@@ -638,9 +648,23 @@ class HtmlRichTextParser extends StatelessWidget {
638648
if (node.attributes['src'] != null) {
639649
if (node.attributes['src'].startsWith("data:image") &&
640650
node.attributes['src'].contains("base64,")) {
651+
precacheImage(
652+
MemoryImage(
653+
base64.decode(
654+
node.attributes['src'].split("base64,")[1].trim(),
655+
),
656+
),
657+
buildContext,
658+
onError: onImageError,
659+
);
641660
parseContext.rootWidgetList.add(Image.memory(base64.decode(
642661
node.attributes['src'].split("base64,")[1].trim())));
643662
} else {
663+
precacheImage(
664+
NetworkImage(node.attributes['src']),
665+
buildContext,
666+
onError: onImageError,
667+
);
644668
parseContext.rootWidgetList
645669
.add(Image.network(node.attributes['src']));
646670
}
@@ -755,7 +779,7 @@ class HtmlRichTextParser extends StatelessWidget {
755779
}
756780

757781
node.nodes.forEach((dom.Node childNode) {
758-
_parseNode(childNode, nextContext);
782+
_parseNode(childNode, nextContext, buildContext);
759783
});
760784
}
761785
}
@@ -829,6 +853,11 @@ class HtmlOldParser extends StatelessWidget {
829853
this.customRender,
830854
this.blockSpacing,
831855
this.html,
856+
this.onImageError,
857+
this.linkStyle = const TextStyle(
858+
decoration: TextDecoration.underline,
859+
color: Colors.blueAccent,
860+
decorationColor: Colors.blueAccent),
832861
});
833862

834863
final double width;
@@ -837,6 +866,8 @@ class HtmlOldParser extends StatelessWidget {
837866
final CustomRender customRender;
838867
final double blockSpacing;
839868
final String html;
869+
final ImageErrorListener onImageError;
870+
final TextStyle linkStyle;
840871

841872
static const _supportedElements = [
842873
"a",
@@ -957,10 +988,7 @@ class HtmlOldParser extends StatelessWidget {
957988
child: Wrap(
958989
children: _parseNodeList(node.nodes),
959990
),
960-
style: const TextStyle(
961-
decoration: TextDecoration.underline,
962-
color: Colors.blueAccent,
963-
decorationColor: Colors.blueAccent),
991+
style: linkStyle,
964992
),
965993
onTap: () {
966994
if (node.attributes.containsKey('href') && onLinkTap != null) {
@@ -1289,10 +1317,7 @@ class HtmlOldParser extends StatelessWidget {
12891317
case "hr":
12901318
return Padding(
12911319
padding: EdgeInsets.only(top: 7.0, bottom: 7.0),
1292-
child: Container(
1293-
height: 0.0,
1294-
decoration: BoxDecoration(border: Border.all()),
1295-
),
1320+
child: Divider(height: 1.0, color: Colors.black38),
12961321
);
12971322
case "i":
12981323
return DefaultTextStyle.merge(
@@ -1304,24 +1329,39 @@ class HtmlOldParser extends StatelessWidget {
13041329
),
13051330
);
13061331
case "img":
1307-
if (node.attributes['src'] != null) {
1308-
if (node.attributes['src'].startsWith("data:image") &&
1309-
node.attributes['src'].contains("base64,")) {
1310-
return Image.memory(base64
1311-
.decode(node.attributes['src'].split("base64,")[1].trim()));
1312-
}
1313-
return Image.network(node.attributes['src']);
1314-
} else if (node.attributes['alt'] != null) {
1315-
//Temp fix for https://github.com/flutter/flutter/issues/736
1316-
if (node.attributes['alt'].endsWith(" ")) {
1317-
return Container(
1318-
padding: EdgeInsets.only(right: 2.0),
1319-
child: Text(node.attributes['alt']));
1320-
} else {
1321-
return Text(node.attributes['alt']);
1322-
}
1323-
}
1324-
return Container();
1332+
return Builder(
1333+
builder: (BuildContext context) {
1334+
if (node.attributes['src'] != null) {
1335+
if (node.attributes['src'].startsWith("data:image") &&
1336+
node.attributes['src'].contains("base64,")) {
1337+
precacheImage(
1338+
MemoryImage(base64.decode(
1339+
node.attributes['src'].split("base64,")[1].trim())),
1340+
context,
1341+
onError: onImageError,
1342+
);
1343+
return Image.memory(base64.decode(
1344+
node.attributes['src'].split("base64,")[1].trim()));
1345+
}
1346+
precacheImage(
1347+
NetworkImage(node.attributes['src']),
1348+
context,
1349+
onError: onImageError,
1350+
);
1351+
return Image.network(node.attributes['src']);
1352+
} else if (node.attributes['alt'] != null) {
1353+
//Temp fix for https://github.com/flutter/flutter/issues/736
1354+
if (node.attributes['alt'].endsWith(" ")) {
1355+
return Container(
1356+
padding: EdgeInsets.only(right: 2.0),
1357+
child: Text(node.attributes['alt']));
1358+
} else {
1359+
return Text(node.attributes['alt']);
1360+
}
1361+
}
1362+
return Container();
1363+
},
1364+
);
13251365
case "ins":
13261366
return DefaultTextStyle.merge(
13271367
child: Wrap(

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: flutter_html
22
description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!)
3-
version: 0.9.4
3+
version: 0.9.7
44
author: Matthew Whitaker <sub6resources@gmail.com>
55
homepage: https://github.com/Sub6Resources/flutter_html
66

0 commit comments

Comments
 (0)