diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart
index 34d4a58507..d956d8eb05 100644
--- a/lib/flutter_html.dart
+++ b/lib/flutter_html.dart
@@ -2,6 +2,7 @@ library flutter_html;
import 'package:flutter/material.dart';
import 'package:flutter_html/html_parser.dart';
+import 'image_properties.dart';
class Html extends StatelessWidget {
Html({
@@ -22,6 +23,9 @@ class Html extends StatelessWidget {
decoration: TextDecoration.underline,
color: Colors.blueAccent,
decorationColor: Colors.blueAccent),
+ this.imageProperties,
+ this.onImageTap,
+ this.showImages = true,
}) : super(key: key);
final String data;
@@ -35,6 +39,11 @@ class Html extends StatelessWidget {
final ImageErrorListener onImageError;
final TextStyle linkStyle;
+ /// Properties for the Image widget that gets rendered by the rich text parser
+ final ImageProperties imageProperties;
+ final OnImageTap onImageTap;
+ final bool showImages;
+
/// Either return a custom widget for specific node types or return null to
/// fallback to the default rendering.
final CustomRender customRender;
@@ -61,6 +70,9 @@ class Html extends StatelessWidget {
html: data,
onImageError: onImageError,
linkStyle: linkStyle,
+ imageProperties: imageProperties,
+ onImageTap: onImageTap,
+ showImages: showImages,
)
: HtmlOldParser(
width: width,
@@ -71,6 +83,7 @@ class Html extends StatelessWidget {
blockSpacing: blockSpacing,
onImageError: onImageError,
linkStyle: linkStyle,
+ showImages: showImages,
),
),
);
diff --git a/lib/html_parser.dart b/lib/html_parser.dart
index b947677b01..70ecedc66a 100644
--- a/lib/html_parser.dart
+++ b/lib/html_parser.dart
@@ -1,5 +1,5 @@
import 'dart:convert';
-
+import 'image_properties.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:html/dom.dart' as dom;
@@ -12,7 +12,7 @@ typedef CustomTextStyle = TextStyle Function(
);
typedef CustomEdgeInsets = EdgeInsets Function(dom.Node node);
typedef OnLinkTap = void Function(String url);
-
+typedef OnImageTap = void Function();
const OFFSET_TAGS_FONT_SIZE_FACTOR =
0.7; //The ratio of the parent font for each of the offset tags: sup or sub
@@ -32,11 +32,7 @@ class LinkTextSpan extends TextSpan {
final String url;
LinkTextSpan(
- {TextStyle style,
- this.url,
- String text,
- OnLinkTap onLinkTap,
- List children})
+ {TextStyle style, this.url, String text, OnLinkTap onLinkTap, List children})
: super(
style: style,
text: text,
@@ -80,11 +76,7 @@ class BlockText extends StatelessWidget {
final Decoration decoration;
BlockText(
- {@required this.child,
- this.padding,
- this.margin,
- this.leadingChar = '',
- this.decoration});
+ {@required this.child, this.padding, this.margin, this.leadingChar = '', this.decoration});
@override
Widget build(BuildContext context) {
@@ -157,6 +149,9 @@ class HtmlRichTextParser extends StatelessWidget {
color: Colors.blueAccent,
decorationColor: Colors.blueAccent,
),
+ this.imageProperties,
+ this.onImageTap,
+ this.showImages = true,
});
final double indentSize = 10.0;
@@ -169,6 +164,9 @@ class HtmlRichTextParser extends StatelessWidget {
final CustomEdgeInsets customEdgeInsets;
final ImageErrorListener onImageError;
final TextStyle linkStyle;
+ final ImageProperties imageProperties;
+ final OnImageTap onImageTap;
+ final bool showImages;
// style elements set a default style
// for all child nodes
@@ -272,8 +270,7 @@ class HtmlRichTextParser extends StatelessWidget {
bool _hasBlockChild(dom.Node node, {bool ignoreSelf = true}) {
bool retval = false;
if (node is dom.Element) {
- if (_supportedBlockElements.contains(node.localName) && !ignoreSelf)
- return true;
+ if (_supportedBlockElements.contains(node.localName) && !ignoreSelf) return true;
node.nodes.forEach((dom.Node node) {
if (_hasBlockChild(node, ignoreSelf: false)) retval = true;
});
@@ -308,8 +305,7 @@ class HtmlRichTextParser extends StatelessWidget {
if (w is BlockText) {
if (w.child.text == null) return;
if ((w.child.text.text == null || w.child.text.text.isEmpty) &&
- (w.child.text.children == null || w.child.text.children.isEmpty))
- return;
+ (w.child.text.children == null || w.child.text.children.isEmpty)) return;
} else if (w is LinkBlock) {
if (w.children.isEmpty) return;
} else if (w is LinkTextSpan) {
@@ -336,8 +332,7 @@ class HtmlRichTextParser extends StatelessWidget {
// function can add child nodes to the parent if it should
//
// each iteration creates a new parseContext as a copy of the previous one if it needs to
- void _parseNode(
- dom.Node node, ParseContext parseContext, BuildContext buildContext) {
+ void _parseNode(dom.Node node, ParseContext parseContext, BuildContext buildContext) {
// TEXT ONLY NODES
// a text only node is a child of a tag with no inner html
if (node is dom.Text) {
@@ -376,18 +371,15 @@ class HtmlRichTextParser extends StatelessWidget {
// debugPrint("Plain Text Node: '$finalText'");
// create a span by default
- TextSpan span = TextSpan(
- text: finalText,
- children: [],
- style: parseContext.childStyle);
+ TextSpan span =
+ TextSpan(text: finalText, children: [], style: parseContext.childStyle);
// in this class, a ParentElement must be a BlockText, LinkTextSpan, Row, Column, TextSpan
// the parseContext might actually be a block level style element, so we
// need to honor the indent and styling specified by that block style.
// e.g. ol, ul, blockquote
- bool treatLikeBlock =
- ['blockquote', 'ul', 'ol'].indexOf(parseContext.blockType) != -1;
+ bool treatLikeBlock = ['blockquote', 'ul', 'ol'].indexOf(parseContext.blockType) != -1;
// if there is no parentElement, contain the span in a BlockText
if (parseContext.parentElement == null) {
@@ -398,18 +390,15 @@ class HtmlRichTextParser extends StatelessWidget {
Decoration decoration;
if (parseContext.blockType == 'blockquote') {
decoration = BoxDecoration(
- border:
- Border(left: BorderSide(color: Colors.black38, width: 2.0)),
+ border: Border(left: BorderSide(color: Colors.black38, width: 2.0)),
);
parseContext.childStyle = parseContext.childStyle.merge(TextStyle(
fontStyle: FontStyle.italic,
));
}
BlockText blockText = BlockText(
- margin: EdgeInsets.only(
- top: 8.0,
- bottom: 8.0,
- left: parseContext.indentLevel * indentSize),
+ margin:
+ EdgeInsets.only(top: 8.0, bottom: 8.0, left: parseContext.indentLevel * indentSize),
padding: EdgeInsets.all(2.0),
decoration: decoration,
child: RichText(
@@ -419,8 +408,7 @@ class HtmlRichTextParser extends StatelessWidget {
);
parseContext.rootWidgetList.add(blockText);
} else {
- parseContext.rootWidgetList
- .add(BlockText(child: RichText(text: span)));
+ parseContext.rootWidgetList.add(BlockText(child: RichText(text: span)));
}
// this allows future items to be added as children of this item
@@ -430,8 +418,7 @@ class HtmlRichTextParser extends StatelessWidget {
} else if (parseContext.parentElement is LinkTextSpan) {
// add this node to the parent as another LinkTextSpan
parseContext.parentElement.children.add(LinkTextSpan(
- style:
- parseContext.parentElement.style.merge(parseContext.childStyle),
+ style: parseContext.parentElement.style.merge(parseContext.childStyle),
url: parseContext.parentElement.url,
text: finalText,
onLinkTap: onLinkTap,
@@ -464,16 +451,14 @@ class HtmlRichTextParser extends StatelessWidget {
//"b","i","em","strong","code","u","small","abbr","acronym"
case "b":
case "strong":
- childStyle =
- childStyle.merge(TextStyle(fontWeight: FontWeight.bold));
+ childStyle = childStyle.merge(TextStyle(fontWeight: FontWeight.bold));
break;
case "i":
case "address":
case "cite":
case "var":
case "em":
- childStyle =
- childStyle.merge(TextStyle(fontStyle: FontStyle.italic));
+ childStyle = childStyle.merge(TextStyle(fontStyle: FontStyle.italic));
break;
case "kbd":
case "samp":
@@ -483,8 +468,7 @@ class HtmlRichTextParser extends StatelessWidget {
break;
case "ins":
case "u":
- childStyle = childStyle
- .merge(TextStyle(decoration: TextDecoration.underline));
+ childStyle = childStyle.merge(TextStyle(decoration: TextDecoration.underline));
break;
case "abbr":
case "acronym":
@@ -560,8 +544,7 @@ class HtmlRichTextParser extends StatelessWidget {
if (_hasBlockChild(node)) {
LinkBlock linkContainer = LinkBlock(
url: url,
- margin: EdgeInsets.only(
- left: parseContext.indentLevel * indentSize),
+ margin: EdgeInsets.only(left: parseContext.indentLevel * indentSize),
onLinkTap: onLinkTap,
children: [],
);
@@ -580,8 +563,7 @@ class HtmlRichTextParser extends StatelessWidget {
} else {
// start a new block element for this link and its text
BlockText blockElement = BlockText(
- margin: EdgeInsets.only(
- left: parseContext.indentLevel * indentSize, top: 10.0),
+ margin: EdgeInsets.only(left: parseContext.indentLevel * indentSize, top: 10.0),
child: RichText(text: span),
);
parseContext.rootWidgetList.add(blockElement);
@@ -593,10 +575,8 @@ class HtmlRichTextParser extends StatelessWidget {
break;
case "br":
- if (parseContext.parentElement != null &&
- parseContext.parentElement is TextSpan) {
- parseContext.parentElement.children
- .add(TextSpan(text: '\n', children: []));
+ if (parseContext.parentElement != null && parseContext.parentElement is TextSpan) {
+ parseContext.parentElement.children.add(TextSpan(text: '\n', children: []));
}
break;
@@ -608,8 +588,7 @@ class HtmlRichTextParser extends StatelessWidget {
children: [],
);
nextContext.rootWidgetList.add(Container(
- margin: EdgeInsets.symmetric(vertical: 12.0),
- child: nextContext.parentElement));
+ margin: EdgeInsets.symmetric(vertical: 12.0), child: nextContext.parentElement));
break;
// we don't handle tbody, thead, or tfoot elements separately for now
@@ -625,11 +604,8 @@ class HtmlRichTextParser extends StatelessWidget {
colspan = int.tryParse(node.attributes['colspan']);
}
nextContext.childStyle = nextContext.childStyle.merge(TextStyle(
- fontWeight: (node.localName == 'th')
- ? FontWeight.bold
- : FontWeight.normal));
- RichText text =
- RichText(text: TextSpan(text: '', children: []));
+ fontWeight: (node.localName == 'th') ? FontWeight.bold : FontWeight.normal));
+ RichText text = RichText(text: TextSpan(text: '', children: []));
Expanded cell = Expanded(
flex: colspan,
child: Container(padding: EdgeInsets.all(1.0), child: text),
@@ -696,42 +672,92 @@ class HtmlRichTextParser extends StatelessWidget {
switch (node.localName) {
case "hr":
- parseContext.rootWidgetList
- .add(Divider(height: 1.0, color: Colors.black38));
+ parseContext.rootWidgetList.add(Divider(height: 1.0, color: Colors.black38));
break;
case "img":
- if (node.attributes['src'] != null) {
- if (node.attributes['src'].startsWith("data:image") &&
- node.attributes['src'].contains("base64,")) {
- precacheImage(
- MemoryImage(
- base64.decode(
- node.attributes['src'].split("base64,")[1].trim(),
+ if (showImages) {
+ if (node.attributes['src'] != null) {
+ if (node.attributes['src'].startsWith("data:image") &&
+ node.attributes['src'].contains("base64,")) {
+ precacheImage(
+ MemoryImage(
+ base64.decode(
+ node.attributes['src'].split("base64,")[1].trim(),
+ ),
),
- ),
- buildContext,
- onError: onImageError,
- );
- parseContext.rootWidgetList.add(Image.memory(base64.decode(
- node.attributes['src'].split("base64,")[1].trim())));
- } else {
- precacheImage(
- NetworkImage(node.attributes['src']),
- buildContext,
- onError: onImageError,
- );
- parseContext.rootWidgetList
- .add(Image.network(node.attributes['src']));
+ buildContext,
+ onError: onImageError,
+ );
+ parseContext.rootWidgetList.add(GestureDetector(
+ child: Image.memory(
+ base64.decode(node.attributes['src'].split("base64,")[1].trim()),
+ width: imageProperties?.width ??
+ ((node.attributes['width'] != null)
+ ? double.parse(node.attributes['width'])
+ : null),
+ height: imageProperties?.height ??
+ ((node.attributes['height'] != null)
+ ? double.parse(node.attributes['height'])
+ : null),
+ scale: imageProperties?.scale ?? 1.0,
+ matchTextDirection: imageProperties?.matchTextDirection ?? false,
+ centerSlice: imageProperties?.centerSlice,
+ filterQuality: imageProperties?.filterQuality ?? FilterQuality.low,
+ alignment: imageProperties?.alignment ?? Alignment.center,
+ colorBlendMode: imageProperties?.colorBlendMode,
+ fit: imageProperties?.fit,
+ color: imageProperties?.color,
+ repeat: imageProperties?.repeat ?? ImageRepeat.noRepeat,
+ semanticLabel: imageProperties?.semanticLabel,
+ excludeFromSemantics: (imageProperties?.semanticLabel == null) ? true : false,
+ ),
+ onTap: onImageTap,
+ ));
+ } else {
+ precacheImage(
+ NetworkImage(node.attributes['src']),
+ buildContext,
+ onError: onImageError,
+ );
+ parseContext.rootWidgetList.add(GestureDetector(
+ child: Image.network(
+ node.attributes['src'],
+ width: imageProperties?.width ??
+ ((node.attributes['width'] != null)
+ ? double.parse(node.attributes['width'])
+ : null),
+ height: imageProperties?.height ??
+ ((node.attributes['height'] != null)
+ ? double.parse(node.attributes['height'])
+ : null),
+ scale: imageProperties?.scale ?? 1.0,
+ matchTextDirection: imageProperties?.matchTextDirection ?? false,
+ centerSlice: imageProperties?.centerSlice,
+ filterQuality: imageProperties?.filterQuality ?? FilterQuality.low,
+ alignment: imageProperties?.alignment ?? Alignment.center,
+ colorBlendMode: imageProperties?.colorBlendMode,
+ fit: imageProperties?.fit,
+ color: imageProperties?.color,
+ repeat: imageProperties?.repeat ?? ImageRepeat.noRepeat,
+ semanticLabel: imageProperties?.semanticLabel,
+ excludeFromSemantics: (imageProperties?.semanticLabel == null) ? true : false,
+ ),
+ onTap: onImageTap,
+ ));
+ }
+ if (node.attributes['alt'] != null) {
+ parseContext.rootWidgetList.add(BlockText(
+ margin: EdgeInsets.symmetric(horizontal: 0.0, vertical: 10.0),
+ padding: EdgeInsets.all(0.0),
+ child: RichText(
+ textAlign: TextAlign.center,
+ text: TextSpan(
+ text: node.attributes['alt'],
+ style: nextContext.childStyle,
+ children: [],
+ ))));
+ }
}
- } else if (node.attributes['alt'] != null) {
- parseContext.rootWidgetList.add(BlockText(
- margin: EdgeInsets.symmetric(horizontal: 0.0, vertical: 10.0),
- padding: EdgeInsets.all(0.0),
- child: RichText(
- text: TextSpan(
- text: node.attributes['alt'],
- children: [],
- ))));
}
break;
case "li":
@@ -743,8 +769,7 @@ class HtmlRichTextParser extends StatelessWidget {
leadingChar = parseContext.listCount.toString() + '.';
}
BlockText blockText = BlockText(
- margin: EdgeInsets.only(
- left: parseContext.indentLevel * indentSize, top: 3.0),
+ margin: EdgeInsets.only(left: parseContext.indentLevel * indentSize, top: 3.0),
child: RichText(
text: TextSpan(
text: '',
@@ -805,14 +830,14 @@ class HtmlRichTextParser extends StatelessWidget {
Decoration decoration;
if (parseContext.blockType == 'blockquote') {
decoration = BoxDecoration(
- border:
- Border(left: BorderSide(color: Colors.black38, width: 2.0)),
+ border: Border(left: BorderSide(color: Colors.black38, width: 2.0)),
);
nextContext.childStyle = nextContext.childStyle.merge(TextStyle(
fontStyle: FontStyle.italic,
));
}
BlockText blockText = BlockText(
+
margin: node.localName != 'body'
? _customEdgeInsets ??
EdgeInsets.only(
@@ -894,6 +919,7 @@ class HtmlOldParser extends StatelessWidget {
decoration: TextDecoration.underline,
color: Colors.blueAccent,
decorationColor: Colors.blueAccent),
+ this.showImages = true,
});
final double width;
@@ -904,6 +930,7 @@ class HtmlOldParser extends StatelessWidget {
final String html;
final ImageErrorListener onImageError;
final TextStyle linkStyle;
+ final bool showImages;
static const _supportedElements = [
"a",
@@ -1005,8 +1032,7 @@ class HtmlOldParser extends StatelessWidget {
Widget _parseNode(dom.Node node) {
if (customRender != null) {
- final Widget customWidget =
- customRender(node, _parseNodeList(node.nodes));
+ final Widget customWidget = customRender(node, _parseNodeList(node.nodes));
if (customWidget != null) {
return customWidget;
}
@@ -1096,9 +1122,8 @@ class HtmlOldParser extends StatelessWidget {
child: Wrap(
children: _parseNodeList(node.nodes),
),
- textDirection: node.attributes["dir"] == "rtl"
- ? TextDirection.rtl
- : TextDirection.ltr,
+ textDirection:
+ node.attributes["dir"] == "rtl" ? TextDirection.rtl : TextDirection.ltr,
);
}
//Direction attribute is required, just render the text normally now.
@@ -1116,8 +1141,7 @@ class HtmlOldParser extends StatelessWidget {
);
case "blockquote":
return Padding(
- padding:
- EdgeInsets.fromLTRB(40.0, blockSpacing, 40.0, blockSpacing),
+ padding: EdgeInsets.fromLTRB(40.0, blockSpacing, 40.0, blockSpacing),
child: Container(
width: width,
child: Wrap(
@@ -1240,8 +1264,7 @@ class HtmlOldParser extends StatelessWidget {
);
case "figure":
return Padding(
- padding:
- EdgeInsets.fromLTRB(40.0, blockSpacing, 40.0, blockSpacing),
+ padding: EdgeInsets.fromLTRB(40.0, blockSpacing, 40.0, blockSpacing),
child: Column(
children: _parseNodeList(node.nodes),
crossAxisAlignment: CrossAxisAlignment.center,
@@ -1367,32 +1390,32 @@ class HtmlOldParser extends StatelessWidget {
case "img":
return Builder(
builder: (BuildContext context) {
- if (node.attributes['src'] != null) {
- if (node.attributes['src'].startsWith("data:image") &&
- node.attributes['src'].contains("base64,")) {
+ if (showImages) {
+ if (node.attributes['src'] != null) {
+ if (node.attributes['src'].startsWith("data:image") &&
+ node.attributes['src'].contains("base64,")) {
+ precacheImage(
+ MemoryImage(base64.decode(node.attributes['src'].split("base64,")[1].trim())),
+ context,
+ onError: onImageError,
+ );
+ return Image.memory(
+ base64.decode(node.attributes['src'].split("base64,")[1].trim()));
+ }
precacheImage(
- MemoryImage(base64.decode(
- node.attributes['src'].split("base64,")[1].trim())),
+ NetworkImage(node.attributes['src']),
context,
onError: onImageError,
);
- return Image.memory(base64.decode(
- node.attributes['src'].split("base64,")[1].trim()));
- }
- precacheImage(
- NetworkImage(node.attributes['src']),
- context,
- onError: onImageError,
- );
- return Image.network(node.attributes['src']);
- } else if (node.attributes['alt'] != null) {
- //Temp fix for https://github.com/flutter/flutter/issues/736
- if (node.attributes['alt'].endsWith(" ")) {
- return Container(
- padding: EdgeInsets.only(right: 2.0),
- child: Text(node.attributes['alt']));
- } else {
- return Text(node.attributes['alt']);
+ return Image.network(node.attributes['src']);
+ } else if (node.attributes['alt'] != null) {
+ //Temp fix for https://github.com/flutter/flutter/issues/736
+ if (node.attributes['alt'].endsWith(" ")) {
+ return Container(
+ padding: EdgeInsets.only(right: 2.0), child: Text(node.attributes['alt']));
+ } else {
+ return Text(node.attributes['alt']);
+ }
}
}
return Container();
@@ -1611,9 +1634,8 @@ class HtmlOldParser extends StatelessWidget {
painter = new TextPainter(
text: new TextSpan(
text: node.text,
- style: parentStyle.merge(TextStyle(
- fontSize:
- parentStyle.fontSize * OFFSET_TAGS_FONT_SIZE_FACTOR)),
+ style: parentStyle.merge(
+ TextStyle(fontSize: parentStyle.fontSize * OFFSET_TAGS_FONT_SIZE_FACTOR)),
),
textDirection: TextDirection.ltr);
painter.layout();
@@ -1644,8 +1666,7 @@ class HtmlOldParser extends StatelessWidget {
top: node.localName == "sub" ? null : 0,
),
style: TextStyle(
- fontSize: parentStyle.fontSize *
- OFFSET_TAGS_FONT_SIZE_FACTOR),
+ fontSize: parentStyle.fontSize * OFFSET_TAGS_FONT_SIZE_FACTOR),
)
],
)
@@ -1760,8 +1781,7 @@ class HtmlOldParser extends StatelessWidget {
String finalText = trimStringHtml(node.text);
//Temp fix for https://github.com/flutter/flutter/issues/736
if (finalText.endsWith(" ")) {
- return Container(
- padding: EdgeInsets.only(right: 2.0), child: Text(finalText));
+ return Container(padding: EdgeInsets.only(right: 2.0), child: Text(finalText));
} else {
return Text(finalText);
}
diff --git a/lib/image_properties.dart b/lib/image_properties.dart
new file mode 100644
index 0000000000..62dfab5b63
--- /dev/null
+++ b/lib/image_properties.dart
@@ -0,0 +1,34 @@
+import 'package:flutter/material.dart';
+
+@immutable
+class ImageProperties {
+ final String semanticLabel;
+ final bool excludeFromSemantics;
+ final double width;
+ final double height;
+ final Color color;
+ final BlendMode colorBlendMode;
+ final BoxFit fit;
+ final AlignmentGeometry alignment;
+ final ImageRepeat repeat;
+ final Rect centerSlice;
+ final bool matchTextDirection;
+ final FilterQuality filterQuality;
+ final double scale;
+
+ const ImageProperties({
+ this.scale = 1,
+ this.semanticLabel,
+ this.excludeFromSemantics = false,
+ this.width,
+ this.height,
+ this.color,
+ this.colorBlendMode,
+ this.fit,
+ this.alignment = Alignment.center,
+ this.repeat = ImageRepeat.noRepeat,
+ this.centerSlice,
+ this.matchTextDirection = false,
+ this.filterQuality = FilterQuality.low,
+ });
+}