From 316ca120877a9c6ae1032ac669ab1c7c8c9e115a Mon Sep 17 00:00:00 2001 From: Loi Tran Date: Thu, 16 Aug 2018 18:41:22 -0700 Subject: [PATCH 001/638] add ability to pass in callback function for link clicks --- lib/flutter_html.dart | 6 ++++-- lib/html_parser.dart | 49 ++++++++++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 85b02ead92..e8abe70f91 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -9,13 +9,15 @@ class Html extends StatelessWidget { @required this.data, this.padding, this.backgroundColor, - this.defaultTextStyle = const TextStyle(color: Colors.black)}) + this.defaultTextStyle = const TextStyle(color: Colors.black), + this.onLinkTap}) : super(key: key); final String data; final EdgeInsetsGeometry padding; final Color backgroundColor; final TextStyle defaultTextStyle; + final Function onLinkTap; @override Widget build(BuildContext context) { @@ -24,7 +26,7 @@ class Html extends StatelessWidget { color: backgroundColor, child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: HtmlParser(defaultTextStyle: defaultTextStyle).parse(data), + children: HtmlParser(defaultTextStyle: defaultTextStyle, onLinkTap: onLinkTap).parse(data), ), ); } diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 11811ca921..704b26abe3 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -3,9 +3,13 @@ import 'package:html/parser.dart' as parser; import 'package:html/dom.dart' as dom; class HtmlParser { - HtmlParser({this.defaultTextStyle = const TextStyle(color: Colors.black)}); + HtmlParser({ + this.defaultTextStyle = const TextStyle(color: Colors.black), + this.onLinkTap + }); final TextStyle defaultTextStyle; + final Function onLinkTap; static const _supportedElements = [ "a", @@ -92,18 +96,27 @@ class HtmlParser { } switch (node.localName) { case "a": - return RichText( + return GestureDetector( + child: RichText( text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - decoration: TextDecoration.underline, - ), - ) - ], - style: defaultTextStyle, - )); + children: [ + TextSpan( + children: _parseInlineElement(node), + style: TextStyle( + decoration: TextDecoration.underline, + ), + ) + ], + style: defaultTextStyle, + ), + ), + onTap: () { + if (node.attributes.containsKey('href')) { + String url = node.attributes['href']; + onLinkTap(url); + } + } + ); case "abbr": return RichText( text: TextSpan( @@ -440,11 +453,13 @@ class HtmlParser { crossAxisAlignment: CrossAxisAlignment.start, ); case "p": - return RichText( - text: TextSpan( - children: _parseInlineElement(node), - style: defaultTextStyle, - )); + return Padding( + padding: EdgeInsets.only(top: 14.0, bottom: 14.0), + child: Column( + children: _parseNodeList(node.nodes), + crossAxisAlignment: CrossAxisAlignment.start, + ), + ); case "pre": return Padding( padding: const EdgeInsets.only(top: 14.0, bottom: 14.0), From 12bcf28c9579be64f1ac1be2b3f03390cbc24128 Mon Sep 17 00:00:00 2001 From: Loi Tran Date: Thu, 16 Aug 2018 18:45:13 -0700 Subject: [PATCH 002/638] updated readme example --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index cfe891b2bd..dc4fd6d603 100644 --- a/README.md +++ b/README.md @@ -175,4 +175,7 @@ until official support is added. padding: EdgeInsets.all(8.0), backgroundColor: Colors.white70, defaultTextStyle: TextStyle(color: Colors.black), + onLinkTap: (url) { + // open url in a webview + } ) \ No newline at end of file From 1a26c864caf366f8f2e38b82f3a2285d403f9116 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Thu, 23 Aug 2018 22:39:59 -0600 Subject: [PATCH 003/638] Version 0.5.0 --- CHANGELOG.md | 5 + README.md | 4 +- example/main.dart | 2 +- lib/flutter_html.dart | 12 +- lib/html_parser.dart | 1063 +++++++++++++----------------------- pubspec.lock | 108 ++-- pubspec.yaml | 4 +- test/html_parser_test.dart | 35 +- 8 files changed, 449 insertions(+), 784 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 346fcc6695..89409dfda4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [0.5.0] - August 23, 2018: + +* Major refactor that makes entire tree a Widget and eliminates the need to distinguish between inline and block elements. +* Fixed #7, #9, #10, and #11. + ## [0.4.1] - August 15, 2018: * Fixed issue with images not loading when inside of `p` tag (#6) diff --git a/README.md b/README.md index 31635649ac..ee8e0936a8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.4.1 + flutter_html: ^0.5.0 ## Currently Supported HTML Tags: @@ -174,5 +174,5 @@ until official support is added. //Optional parameters: padding: EdgeInsets.all(8.0), backgroundColor: Colors.white70, - defaultTextStyle: TextStyle(color: Colors.black), + defaultTextStyle: TextStyle(fontFamily: 'serif'), ) \ No newline at end of file diff --git a/example/main.dart b/example/main.dart index 79d0fad6b4..d9b4d3f055 100644 --- a/example/main.dart +++ b/example/main.dart @@ -21,7 +21,7 @@ void main() { //Optional parameters: padding: EdgeInsets.all(8.0), backgroundColor: Colors.white70, - defaultTextStyle: TextStyle(color: Colors.black), + defaultTextStyle: TextStyle(fontFamily: 'serif'), ), ), ), diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 85b02ead92..8c92acdc8e 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -19,12 +19,18 @@ class Html extends StatelessWidget { @override Widget build(BuildContext context) { + final double width = MediaQuery.of(context).size.width; + return Container( padding: padding, color: backgroundColor, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: HtmlParser(defaultTextStyle: defaultTextStyle).parse(data), + width: width, + child: DefaultTextStyle.merge( + style: defaultTextStyle, + child: Wrap( + alignment: WrapAlignment.start, + children: HtmlParser(width: width).parse(data), + ), ), ); } diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 2ccb24e6eb..d87b89957e 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -3,9 +3,9 @@ import 'package:html/parser.dart' as parser; import 'package:html/dom.dart' as dom; class HtmlParser { - HtmlParser({this.defaultTextStyle = const TextStyle(color: Colors.black)}); + HtmlParser({@required this.width}); - final TextStyle defaultTextStyle; + final double width; static const _supportedElements = [ "a", @@ -92,268 +92,262 @@ class HtmlParser { } switch (node.localName) { case "a": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - decoration: TextDecoration.underline, - ), - ) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + decoration: TextDecoration.underline, + color: Colors.blueAccent, + decorationColor: Colors.blueAccent), + ); case "abbr": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - decoration: TextDecoration.underline, - decorationStyle: TextDecorationStyle.dotted, - ), - ) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + decoration: TextDecoration.underline, + decorationStyle: TextDecorationStyle.dotted, + ), + ); case "address": - return RichText( - text: TextSpan(children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle(fontStyle: FontStyle.italic), - ) - ], style: defaultTextStyle)); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontStyle: FontStyle.italic, + ), + ); case "article": - return Column( - children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.start, + return Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), ); case "aside": - return Column( - children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.start, + return Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), ); case "b": - return RichText( - text: TextSpan(children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle(fontWeight: FontWeight.bold), - ) - ], style: defaultTextStyle)); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ); case "blockquote": return Padding( - padding: EdgeInsets.fromLTRB(40.0, 14.0, 40.0, 14.0), - child: Column( + padding: EdgeInsets.fromLTRB(40.0, 14.0, 40.0, 14.0), + child: Container( + width: width, + child: Wrap( children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.start, - )); + ), + ), + ); case "body": - return Column( - children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.start, + return Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), ); case "br": - return Container(height: 14.0); + if (node.previousElementSibling != null && + node.previousElementSibling.localName == "br") { + return Container(width: width, height: 14.0); + } + return Container(width: width); case "caption": - return Column( + return Wrap( children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.center, ); case "cite": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontStyle: FontStyle.italic, - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontStyle: FontStyle.italic, + ), + ); case "code": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontFamily: 'monospace', - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontFamily: 'monospace', + ), + ); case "data": - return RichText( - text: TextSpan( - children: _parseInlineElement(node), - style: defaultTextStyle, - )); + return Wrap( + children: _parseNodeList(node.nodes), + ); case "dd": return Padding( padding: EdgeInsets.only(left: 40.0), - child: Column( - children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.start, + child: Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), )); case "del": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - decoration: TextDecoration.lineThrough, - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + decoration: TextDecoration.lineThrough, + ), + ); case "dfn": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontStyle: FontStyle.italic, - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontStyle: FontStyle.italic, + ), + ); case "div": - return Column( - children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.start, + return Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), ); case "dl": return Padding( - padding: EdgeInsets.only(top: 16.0, bottom: 16.0), + padding: EdgeInsets.only(top: 14.0, bottom: 14.0), child: Column( children: _parseNodeList(node.nodes), crossAxisAlignment: CrossAxisAlignment.start, )); case "dt": - return RichText( - text: TextSpan( - children: _parseInlineElement(node), - style: defaultTextStyle, - )); + return Wrap( + children: _parseNodeList(node.nodes), + ); case "em": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontStyle: FontStyle.italic, - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontStyle: FontStyle.italic, + ), + ); case "figcaption": - return RichText( - text: TextSpan( - children: _parseInlineElement(node), - style: defaultTextStyle, - )); + return Wrap( + children: _parseNodeList(node.nodes), + ); case "figure": return Padding( padding: EdgeInsets.fromLTRB(40.0, 14.0, 40.0, 14.0), child: Column( children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, )); case "footer": - return Column( - children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.start, + return Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), ); case "h1": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontSize: 28.0, - fontWeight: FontWeight.bold, - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), + ), + style: const TextStyle( + fontSize: 28.0, + fontWeight: FontWeight.bold, + ), + ); case "h2": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontSize: 21.0, - fontWeight: FontWeight.bold, - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), + ), + style: const TextStyle( + fontSize: 21.0, + fontWeight: FontWeight.bold, + ), + ); case "h3": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), + ), + style: const TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), + ); case "h4": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), + ), + style: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.bold, + ), + ); case "h5": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontSize: 12.0, - fontWeight: FontWeight.bold, - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), + ), + style: const TextStyle( + fontSize: 12.0, + fontWeight: FontWeight.bold, + ), + ); case "h6": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontSize: 10.0, - fontWeight: FontWeight.bold, - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), + ), + style: const TextStyle( + fontSize: 10.0, + fontWeight: FontWeight.bold, + ), + ); case "header": - return Column( - children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.start, + return Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), ); case "hr": return Padding( @@ -364,75 +358,71 @@ class HtmlParser { ), ); case "i": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle(fontStyle: FontStyle.italic), - ) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontStyle: FontStyle.italic, + ), + ); case "img": return Image.network(node.attributes['src']); case "ins": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - decoration: TextDecoration.underline, - ), - ) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + decoration: TextDecoration.underline, + ), + ); case "kbd": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontFamily: 'monospace', - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontFamily: 'monospace', + ), + ); case "li": - return RichText( - text: TextSpan( - children: _parseInlineElement(node), - style: defaultTextStyle, - )); + return Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), + ); case "main": - return Column( - children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.start, + return Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), ); case "mark": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - color: Colors.black, background: _getPaint(Colors.yellow)), - ) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: TextStyle( + color: Colors.black, + background: _getPaint(Colors.yellow), + ), + ); case "nav": - return Column( - children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.start, + return Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), ); case "noscript": - return Column( - children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.start, + return Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), ); case "ol": return Column( @@ -442,115 +432,95 @@ class HtmlParser { case "p": return Padding( padding: EdgeInsets.only(top: 14.0, bottom: 14.0), - child: Column( - children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.start, + child: Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), ), ); case "pre": return Padding( - padding: const EdgeInsets.only(top: 14.0, bottom: 14.0), - child: RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontFamily: 'monospace', - )) - ], - style: defaultTextStyle, - )), + padding: const EdgeInsets.all(14.0), + child: DefaultTextStyle.merge( + child: Text(node.innerHtml), + style: const TextStyle( + fontFamily: 'monospace', + ), + ), ); case "q": - return RichText( - text: TextSpan( - children: [ - TextSpan(text: "\""), - TextSpan( - children: _parseInlineElement(node), - ), - TextSpan(text: "\"") - ], - style: defaultTextStyle, - )); + List children = List(); + children.add(Text("\"")); + children.addAll(_parseNodeList(node.nodes)); + children.add(Text("\"")); + return DefaultTextStyle.merge( + child: Wrap( + children: children, + ), + style: const TextStyle( + fontStyle: FontStyle.italic, + ), + ); case "rp": - return RichText( - text: TextSpan( - children: _parseInlineElement(node), - style: defaultTextStyle, - )); + return Wrap( + children: _parseNodeList(node.nodes), + ); case "rt": - return RichText( - text: TextSpan( - children: _parseInlineElement(node), - style: defaultTextStyle, - )); + return Wrap( + children: _parseNodeList(node.nodes), + ); case "ruby": - return RichText( - text: TextSpan( - children: _parseInlineElement(node), - style: defaultTextStyle, - )); + return Wrap( + children: _parseNodeList(node.nodes), + ); case "s": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - decoration: TextDecoration.lineThrough, - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + decoration: TextDecoration.lineThrough, + ), + ); case "samp": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontFamily: 'monospace', - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontFamily: 'monospace', + ), + ); case "section": - return Column( - children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.start, + return Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + ), ); case "small": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontSize: 10.0, - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontSize: 10.0, + ), + ); case "span": - return RichText( - text: TextSpan( - children: _parseInlineElement(node), - style: defaultTextStyle, - )); + return Wrap( + children: _parseNodeList(node.nodes), + ); case "strong": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - fontWeight: FontWeight.bold, - )) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ); case "table": return Column( children: _parseNodeList(node.nodes), @@ -562,11 +532,11 @@ class HtmlParser { crossAxisAlignment: CrossAxisAlignment.start, ); case "td": - return Column( + return Row( children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.center, ); case "template": + //Not usually displayed in HTML return Container(); case "tfoot": return Column( @@ -574,9 +544,13 @@ class HtmlParser { crossAxisAlignment: CrossAxisAlignment.start, ); case "th": - return Column( - children: _parseNodeList(node.nodes), - crossAxisAlignment: CrossAxisAlignment.center, + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), ); case "thead": return Column( @@ -584,52 +558,53 @@ class HtmlParser { crossAxisAlignment: CrossAxisAlignment.start, ); case "time": - return RichText( - text: TextSpan( - children: _parseInlineElement(node), - style: defaultTextStyle, - )); + return Wrap( + children: _parseNodeList(node.nodes), + ); case "tr": return Row( children: _parseNodeList(node.nodes), crossAxisAlignment: CrossAxisAlignment.center, ); case "u": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle( - decoration: TextDecoration.underline, - ), - ) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + decoration: TextDecoration.underline, + ), + ); case "ul": return Column( children: _parseNodeList(node.nodes), crossAxisAlignment: CrossAxisAlignment.start, ); case "var": - return RichText( - text: TextSpan( - children: [ - TextSpan( - children: _parseInlineElement(node), - style: TextStyle(fontStyle: FontStyle.italic), - ) - ], - style: defaultTextStyle, - )); + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontStyle: FontStyle.italic, + ), + ); } } else if (node is dom.Text) { + //We don't need to worry about rendering extra whitespace if (node.text.trim() == "") { return Container(); } - print("Plain Text Node: '${node.text}'"); - return Text(node.text, style: defaultTextStyle); + + print("Plain Text Node: '${trimStringHtml(node.text)}'"); + 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)); + } else { + return Text(finalText); + } } return Container(); } @@ -640,293 +615,17 @@ class HtmlParser { }).toList(); } - static const _supportedInlineElements = [ - "a", - "abbr", - "address", - "b", - "br", - "cite", - "code", - "data", - "dfn", - "dt", - "em", - "figcaption", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "i", - "ins", - "kbd", - "mark", - "p", - "pre", - "q", - "rp", - "rt", - "ruby", - "s", - "samp", - "small", - "span", - "strong", - "time", - "u", - "var", - ]; - - List _parseInlineElement(dom.Element element) { - List textSpanList = new List(); - - element.nodes.forEach((node) { - if (node is dom.Element) { - print("Found inline ${node.localName}"); - if (!_supportedInlineElements.contains(node.localName)) { - textSpanList.add(TextSpan(text: node.text)); - } else { - switch (node.localName) { - case "a": - textSpanList.add(TextSpan( - style: TextStyle(decoration: TextDecoration.underline), - children: _parseInlineElement(node), - )); - break; - case "abbr": - textSpanList.add(TextSpan( - style: TextStyle( - decoration: TextDecoration.underline, - decorationStyle: TextDecorationStyle.dotted, - ), - children: _parseInlineElement(node), - )); - break; - break; - case "address": - textSpanList.add(TextSpan( - style: TextStyle(fontWeight: FontWeight.bold), - children: _parseInlineElement(node), - )); - break; - case "b": - textSpanList.add(TextSpan( - style: TextStyle(fontWeight: FontWeight.bold), - children: _parseInlineElement(node), - )); - break; - case "br": - textSpanList.add(TextSpan( - text: "\n", - )); - break; - case "cite": - textSpanList.add(TextSpan( - style: TextStyle(fontStyle: FontStyle.italic), - children: _parseInlineElement(node), - )); - break; - case "code": - textSpanList.add(TextSpan( - style: TextStyle(fontFamily: 'monospace'), - children: _parseInlineElement(node), - )); - break; - case "data": - textSpanList.add(TextSpan( - children: _parseInlineElement(node), - )); - break; - case "del": - textSpanList.add(TextSpan( - style: TextStyle(decoration: TextDecoration.lineThrough), - children: _parseInlineElement(node), - )); - break; - case "dfn": - textSpanList.add(TextSpan( - style: TextStyle(fontStyle: FontStyle.italic), - children: _parseInlineElement(node), - )); - break; - case "dt": - textSpanList.add(TextSpan( - children: _parseInlineElement(node), - )); - break; - case "em": - textSpanList.add(TextSpan( - style: TextStyle(fontStyle: FontStyle.italic), - children: _parseInlineElement(node), - )); - break; - case "figcaption": - textSpanList.add(TextSpan( - children: _parseInlineElement(node), - )); - break; - case "h1": - textSpanList.add(TextSpan( - style: TextStyle( - fontSize: 28.0, - fontWeight: FontWeight.bold, - ), - )); - break; - case "h2": - textSpanList.add(TextSpan( - style: TextStyle( - fontSize: 21.0, - fontWeight: FontWeight.bold, - ), - )); - break; - case "h3": - textSpanList.add(TextSpan( - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - )); - break; - case "h4": - textSpanList.add(TextSpan( - style: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.bold, - ), - )); - break; - case "h5": - textSpanList.add(TextSpan( - style: TextStyle( - fontSize: 12.0, - fontWeight: FontWeight.bold, - ), - )); - break; - case "h6": - textSpanList.add(TextSpan( - style: TextStyle( - fontSize: 10.0, - fontWeight: FontWeight.bold, - ), - )); - break; - case "i": - textSpanList.add(TextSpan( - style: TextStyle(fontStyle: FontStyle.italic), - children: _parseInlineElement(node))); - break; - case "ins": - textSpanList.add(TextSpan( - style: TextStyle(decoration: TextDecoration.underline), - children: _parseInlineElement(node))); - break; - case "kbd": - textSpanList.add(TextSpan( - style: TextStyle(fontFamily: 'monospace'), - children: _parseInlineElement(node), - )); - break; - case "mark": - textSpanList.add(TextSpan( - style: TextStyle( - color: Colors.black, background: _getPaint(Colors.yellow)), - children: _parseInlineElement(node), - )); - break; - case "p": - textSpanList.add(TextSpan(children: _parseInlineElement(node))); - break; - case "pre": - textSpanList.add(TextSpan( - style: TextStyle(fontFamily: 'monospace'), - )); - break; - case "q": - textSpanList.add(TextSpan( - children: [ - TextSpan(text: "\""), - TextSpan(children: _parseInlineElement(node)), - TextSpan(text: "\""), - ], - )); - break; - case "rp": - textSpanList.add(TextSpan( - children: _parseInlineElement(node), - )); - break; - case "rt": - textSpanList.add(TextSpan( - children: _parseInlineElement(node), - )); - break; - case "ruby": - textSpanList.add(TextSpan( - children: _parseInlineElement(node), - )); - break; - case "s": - textSpanList.add(TextSpan( - style: TextStyle(decoration: TextDecoration.lineThrough), - children: _parseInlineElement(node), - )); - break; - case "samp": - textSpanList.add(TextSpan( - style: TextStyle(fontFamily: 'monospace'), - children: _parseInlineElement(node), - )); - break; - case "small": - textSpanList.add(TextSpan( - style: TextStyle(fontSize: 10.0), - children: _parseInlineElement(node), - )); - break; - case "span": - textSpanList.add(TextSpan( - children: _parseInlineElement(node), - )); - break; - case "strong": - textSpanList.add(TextSpan( - style: TextStyle(fontWeight: FontWeight.bold), - children: _parseInlineElement(node))); - break; - case "time": - textSpanList.add(TextSpan( - children: _parseInlineElement(node), - )); - break; - case "u": - textSpanList.add(TextSpan( - style: TextStyle(decoration: TextDecoration.underline), - children: _parseInlineElement(node))); - break; - case "var": - textSpanList.add(TextSpan( - style: TextStyle(fontStyle: FontStyle.italic), - children: _parseInlineElement(node))); - break; - } - } - } else { - print("Text Node: '${node.text}'"); - textSpanList.add(TextSpan(text: node.text)); - } - }); - - return textSpanList; - } - Paint _getPaint(Color color) { Paint paint = new Paint(); paint.color = color; return paint; } + + String trimStringHtml(String stringToTrim) { + stringToTrim = stringToTrim.replaceAll("\n", ""); + while (stringToTrim.indexOf(" ") != -1) { + stringToTrim = stringToTrim.replaceAll(" ", " "); + } + return stringToTrim; + } } diff --git a/pubspec.lock b/pubspec.lock index 7618e5e1db..c55ae3391b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,35 +7,35 @@ packages: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.32.4" + version: "0.31.2-alpha.2" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.5.0" + version: "1.4.3" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.0.7" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.3" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.1.1" collection: dependency: transitive description: @@ -49,21 +49,21 @@ packages: name: convert url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.1" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.3" csslib: dependency: transitive description: name: csslib url: "https://pub.dartlang.org" source: hosted - version: "0.14.4+1" + version: "0.14.4" flutter: dependency: "direct main" description: flutter @@ -80,77 +80,70 @@ packages: name: front_end url: "https://pub.dartlang.org" source: hosted - version: "0.1.4" + version: "0.1.0-alpha.12" glob: dependency: transitive description: name: glob url: "https://pub.dartlang.org" source: hosted - version: "1.1.7" + version: "1.1.5" html: dependency: "direct main" description: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.13.3+2" + version: "0.13.3" http: dependency: transitive description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.11.3+17" + version: "0.11.3+16" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.4" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.3" + version: "3.1.2" io: dependency: transitive description: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.3" + version: "0.3.2+1" js: dependency: transitive description: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.1+1" - json_rpc_2: - dependency: transitive - description: - name: json_rpc_2 - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.9" + version: "0.6.1" kernel: dependency: transitive description: name: kernel url: "https://pub.dartlang.org" source: hosted - version: "0.3.4" + version: "0.3.0-alpha.12" logging: dependency: transitive description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.3+2" + version: "0.11.3+1" matcher: dependency: transitive description: @@ -171,98 +164,98 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "0.9.6+2" + version: "0.9.6" multi_server_socket: dependency: transitive description: name: multi_server_socket url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.1" node_preamble: dependency: transitive description: name: node_preamble url: "https://pub.dartlang.org" source: hosted - version: "1.4.4" + version: "1.4.1" package_config: dependency: transitive description: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.0.3" package_resolver: dependency: transitive description: name: package_resolver url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.2" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.6.2" + version: "1.5.1" plugin: dependency: transitive description: name: plugin url: "https://pub.dartlang.org" source: hosted - version: "0.2.0+3" + version: "0.2.0+2" pool: dependency: transitive description: name: pool url: "https://pub.dartlang.org" source: hosted - version: "1.3.6" + version: "1.3.4" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "1.4.2" + version: "1.4.1" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.0+1" + version: "0.29.0+1" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.3+3" + version: "0.7.3" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.3" shelf_static: dependency: transitive description: name: shelf_static url: "https://pub.dartlang.org" source: hosted - version: "0.2.8" + version: "0.2.7" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.2+4" + version: "0.2.2" sky_engine: dependency: transitive description: flutter @@ -274,56 +267,56 @@ packages: name: source_map_stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.1.5" + version: "1.1.4" source_maps: dependency: transitive description: name: source_maps url: "https://pub.dartlang.org" source: hosted - version: "0.10.7" + version: "0.10.5" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.4.1" + version: "1.4.0" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.3" + version: "1.9.2" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "1.6.8" + version: "1.6.6" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.2" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.0" test: dependency: transitive description: name: test url: "https://pub.dartlang.org" source: hosted - version: "0.12.41" + version: "0.12.37" typed_data: dependency: transitive description: @@ -337,42 +330,35 @@ packages: name: utf url: "https://pub.dartlang.org" source: hosted - version: "0.9.0+5" + version: "0.9.0+4" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" - vm_service_client: - dependency: transitive - description: - name: vm_service_client - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.6" + version: "2.0.6" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+10" + version: "0.9.7+7" web_socket_channel: dependency: transitive description: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.0.9" + version: "1.0.7" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "2.1.15" + version: "2.1.13" sdks: - dart: ">=2.0.0-dev.62.0 <=2.0.0-dev.69.5.flutter-eab492385c" - flutter: ">=0.5.0 <0.6.0" + dart: ">=2.0.0-dev.52.0 <=2.0.0-dev.58.0.flutter-f981f09760" + flutter: ">=0.5.0 <0.8.0" diff --git a/pubspec.yaml b/pubspec.yaml index 9957d3d075..1e942f2d1f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,12 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 60 different html tags!) -version: 0.4.1 +version: 0.5.0 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html environment: sdk: '>=1.19.0 <3.0.0' - flutter: ^0.5.0 + flutter: '>=0.5.0 <0.8.0' dependencies: html: ^0.13.3 diff --git a/test/html_parser_test.dart b/test/html_parser_test.dart index 856c1f53e0..bf09259e41 100644 --- a/test/html_parser_test.dart +++ b/test/html_parser_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_html/flutter_html.dart'; void main() { test('Checks that `parse` does not throw an exception', () { - final elementList = HtmlParser().parse("Bold Text"); + final elementList = HtmlParser(width: 42.0).parse("Bold Text"); expect(elementList, isNotNull); }); @@ -72,7 +72,7 @@ void main() { ), )); - expect(find.byType(RichText), findsOneWidget); + expect(find.byType(RichText), findsNWidgets(3)); }); testWidgets('Tests that the header elements (h1-h6) get rendered correctly', @@ -88,35 +88,4 @@ void main() { expect(find.byType(RichText), findsNWidgets(6)); }); - - testWidgets('Tests the provided example', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( - home: Scaffold( - body: Html( - data: """ -
-

Demo Page

-

This is a fantastic nonexistent product that you should buy!

-

Pricing

-

Lorem ipsum dolor sit amet.

-

The Team

-

There isn't really a team...

-

Installation

-

You cannot install a nonexistent product!

-
- """, - //Optional parameters: - padding: EdgeInsets.all(8.0), - backgroundColor: Colors.white70, - defaultTextStyle: TextStyle(color: Colors.black), - ), - ), - )); - - //Expect one RichText for each of the children of
- expect(find.byType(RichText), findsNWidgets(8)); - - //Expect 3. One created by Html widget as part of the container, one for the , and one for the
- expect(find.byType(Column), findsNWidgets(3)); - }); } From cea2c4ac6e23b3e95ecaf1ce7ba24092c352b36d Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Thu, 23 Aug 2018 23:08:55 -0600 Subject: [PATCH 004/638] Add pub shield --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ee8e0936a8..a0574ea4af 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # flutter_html +[![pub package](https://img.shields.io/pub/v/flutter_html.svg)](https://pub.dartlang.org/packages/flutter_html) A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 60 different html tags!) From 8e0b497bf1d117186a1bc5148f53401604ff49b2 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Sat, 25 Aug 2018 16:02:28 -0600 Subject: [PATCH 005/638] Version 0.5.1 --- CHANGELOG.md | 8 ++- README.md | 2 +- lib/html_parser.dart | 31 +++++++++--- pubspec.lock | 114 ++++++++++++++++++++++++------------------- pubspec.yaml | 2 +- 5 files changed, 97 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89409dfda4..5d2e0a60cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,15 @@ +## [0.5.1] - August 25, 2018: + +* Fixed issue with table rows not lining up correctly ([#4](https://github.com/Sub6Resources/flutter_html/issues/4)) + ## [0.5.0] - August 23, 2018: * Major refactor that makes entire tree a Widget and eliminates the need to distinguish between inline and block elements. -* Fixed #7, #9, #10, and #11. +* Fixed [#7](https://github.com/Sub6Resources/flutter_html/issues/7), [#9](https://github.com/Sub6Resources/flutter_html/issues/9), [#10](https://github.com/Sub6Resources/flutter_html/issues/10), and [#11](https://github.com/Sub6Resources/flutter_html/issues/11). ## [0.4.1] - August 15, 2018: -* Fixed issue with images not loading when inside of `p` tag (#6) +* Fixed issue with images not loading when inside of `p` tag ([#6](https://github.com/Sub6Resources/flutter_html/issues/6)) ## [0.4.0] - August 15, 2018: diff --git a/README.md b/README.md index a0574ea4af..2b31f3e26f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.5.0 + flutter_html: ^0.5.1 ## Currently Supported HTML Tags: diff --git a/lib/html_parser.dart b/lib/html_parser.dart index d87b89957e..7f41544b6a 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -167,8 +167,12 @@ class HtmlParser { } return Container(width: width); case "caption": - return Wrap( - children: _parseNodeList(node.nodes), + return Container( + width: width, + child: Wrap( + alignment: WrapAlignment.center, + children: _parseNodeList(node.nodes), + ), ); case "cite": return DefaultTextStyle.merge( @@ -532,8 +536,15 @@ class HtmlParser { crossAxisAlignment: CrossAxisAlignment.start, ); case "td": - return Row( - children: _parseNodeList(node.nodes), + int colspan = 1; + if (node.attributes['colspan'] != null) { + colspan = int.tryParse(node.attributes['colspan']); + } + return Expanded( + flex: colspan, + child: Wrap( + children: _parseNodeList(node.nodes), + ), ); case "template": //Not usually displayed in HTML @@ -544,9 +555,17 @@ class HtmlParser { crossAxisAlignment: CrossAxisAlignment.start, ); case "th": + int colspan = 1; + if (node.attributes['colspan'] != null) { + colspan = int.tryParse(node.attributes['colspan']); + } return DefaultTextStyle.merge( - child: Wrap( - children: _parseNodeList(node.nodes), + child: Expanded( + flex: colspan, + child: Wrap( + alignment: WrapAlignment.center, + children: _parseNodeList(node.nodes), + ), ), style: const TextStyle( fontWeight: FontWeight.bold, diff --git a/pubspec.lock b/pubspec.lock index c55ae3391b..7b1bb67945 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,63 +7,63 @@ packages: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "0.31.2-alpha.2" + version: "0.32.4" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.4.3" + version: "1.5.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.0.7" + version: "2.0.8" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.4" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.6" + version: "1.14.11" convert: dependency: transitive description: name: convert url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" crypto: dependency: transitive description: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.6" csslib: dependency: transitive description: name: csslib url: "https://pub.dartlang.org" source: hosted - version: "0.14.4" + version: "0.14.5" flutter: dependency: "direct main" description: flutter @@ -80,182 +80,189 @@ packages: name: front_end url: "https://pub.dartlang.org" source: hosted - version: "0.1.0-alpha.12" + version: "0.1.4" glob: dependency: transitive description: name: glob url: "https://pub.dartlang.org" source: hosted - version: "1.1.5" + version: "1.1.7" html: dependency: "direct main" description: name: html url: "https://pub.dartlang.org" source: hosted - version: "0.13.3" + version: "0.13.3+3" http: dependency: transitive description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.11.3+16" + version: "0.11.3+17" http_multi_server: dependency: transitive description: name: http_multi_server url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.2" + version: "3.1.3" io: dependency: transitive description: name: io url: "https://pub.dartlang.org" source: hosted - version: "0.3.2+1" + version: "0.3.3" js: dependency: transitive description: name: js url: "https://pub.dartlang.org" source: hosted - version: "0.6.1" + version: "0.6.1+1" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" kernel: dependency: transitive description: name: kernel url: "https://pub.dartlang.org" source: hosted - version: "0.3.0-alpha.12" + version: "0.3.4" logging: dependency: transitive description: name: logging url: "https://pub.dartlang.org" source: hosted - version: "0.11.3+1" + version: "0.11.3+2" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.2+1" + version: "0.12.3+1" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.5" + version: "1.1.6" mime: dependency: transitive description: name: mime url: "https://pub.dartlang.org" source: hosted - version: "0.9.6" + version: "0.9.6+2" multi_server_socket: dependency: transitive description: name: multi_server_socket url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" node_preamble: dependency: transitive description: name: node_preamble url: "https://pub.dartlang.org" source: hosted - version: "1.4.1" + version: "1.4.4" package_config: dependency: transitive description: name: package_config url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.5" package_resolver: dependency: transitive description: name: package_resolver url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.4" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.5.1" + version: "1.6.2" plugin: dependency: transitive description: name: plugin url: "https://pub.dartlang.org" source: hosted - version: "0.2.0+2" + version: "0.2.0+3" pool: dependency: transitive description: name: pool url: "https://pub.dartlang.org" source: hosted - version: "1.3.4" + version: "1.3.6" pub_semver: dependency: transitive description: name: pub_semver url: "https://pub.dartlang.org" source: hosted - version: "1.4.1" + version: "1.4.2" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "0.29.0+1" + version: "2.0.0+1" shelf: dependency: transitive description: name: shelf url: "https://pub.dartlang.org" source: hosted - version: "0.7.3" + version: "0.7.3+3" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.4" shelf_static: dependency: transitive description: name: shelf_static url: "https://pub.dartlang.org" source: hosted - version: "0.2.7" + version: "0.2.8" shelf_web_socket: dependency: transitive description: name: shelf_web_socket url: "https://pub.dartlang.org" source: hosted - version: "0.2.2" + version: "0.2.2+4" sky_engine: dependency: transitive description: flutter @@ -267,98 +274,105 @@ packages: name: source_map_stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.1.4" + version: "1.1.5" source_maps: dependency: transitive description: name: source_maps url: "https://pub.dartlang.org" source: hosted - version: "0.10.5" + version: "0.10.7" source_span: dependency: transitive description: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.4.0" + version: "1.4.1" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.2" + version: "1.9.3" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "1.6.6" + version: "1.6.8" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.4" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" test: dependency: transitive description: name: test url: "https://pub.dartlang.org" source: hosted - version: "0.12.37" + version: "1.3.0" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.1.5" + version: "1.1.6" utf: dependency: transitive description: name: utf url: "https://pub.dartlang.org" source: hosted - version: "0.9.0+4" + version: "0.9.0+5" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.8" + vm_service_client: + dependency: transitive + description: + name: vm_service_client + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.6" watcher: dependency: transitive description: name: watcher url: "https://pub.dartlang.org" source: hosted - version: "0.9.7+7" + version: "0.9.7+10" web_socket_channel: dependency: transitive description: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "1.0.7" + version: "1.0.9" yaml: dependency: transitive description: name: yaml url: "https://pub.dartlang.org" source: hosted - version: "2.1.13" + version: "2.1.15" sdks: - dart: ">=2.0.0-dev.52.0 <=2.0.0-dev.58.0.flutter-f981f09760" + dart: ">=2.0.0-dev.68.0 <3.0.0" flutter: ">=0.5.0 <0.8.0" diff --git a/pubspec.yaml b/pubspec.yaml index 1e942f2d1f..faca9da9d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 60 different html tags!) -version: 0.5.0 +version: 0.5.1 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From f5081f2b65f139c6af4a395e8f465119855f32ec Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Sat, 25 Aug 2018 20:33:20 -0600 Subject: [PATCH 006/638] Version 0.5.2 --- CHANGELOG.md | 4 ++++ README.md | 6 +++--- lib/html_parser.dart | 21 +++++++++++++++++++++ pubspec.yaml | 2 +- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d2e0a60cc..b8481b324b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.5.2] - August 25, 2018: + +* Adds support for `bdi` and `bdo` + ## [0.5.1] - August 25, 2018: * Fixed issue with table rows not lining up correctly ([#4](https://github.com/Sub6Resources/flutter_html/issues/4)) diff --git a/README.md b/README.md index 2b31f3e26f..a9ce332f1f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.5.1 + flutter_html: ^0.5.2 ## Currently Supported HTML Tags: @@ -17,6 +17,8 @@ Add the following to your `pubspec.yaml` file: * `article` * `aside` * `b` + * `bdi` + * `bdo` * `blockquote` * `body` * `br` @@ -86,8 +88,6 @@ Add the following to your `pubspec.yaml` file: > These are elements that are planned, but present a specific challenge that makes them somewhat difficult to implement. * `audio` - * `bdi` - * `bdo` * `details` * `source` * `sub` diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 7f41544b6a..3776f11c4b 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -14,6 +14,8 @@ class HtmlParser { "article", "aside", "b", + "bdi", + "bdo", "blockquote", "body", "br", @@ -143,6 +145,25 @@ class HtmlParser { fontWeight: FontWeight.bold, ), ); + case "bdi": + return Wrap( + children: _parseNodeList(node.nodes), + ); + case "bdo": + if (node.attributes["dir"] != null) { + return Directionality( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + textDirection: node.attributes["dir"] == "rtl" + ? TextDirection.rtl + : TextDirection.ltr, + ); + } + //Direction attribute is required, just render the text normally now. + return Wrap( + children: _parseNodeList(node.nodes), + ); case "blockquote": return Padding( padding: EdgeInsets.fromLTRB(40.0, 14.0, 40.0, 14.0), diff --git a/pubspec.yaml b/pubspec.yaml index faca9da9d8..536a33a981 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 60 different html tags!) -version: 0.5.1 +version: 0.5.2 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From 88c3568124ee12c76142eb76bde0a2c818ea5d6a Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Sat, 25 Aug 2018 21:50:21 -0600 Subject: [PATCH 007/638] Version 0.5.3 --- CHANGELOG.md | 4 ++++ README.md | 7 ++++--- lib/html_parser.dart | 20 ++++++++++++++++++++ pubspec.yaml | 2 +- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8481b324b..acf70daf39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.5.3] - August 25, 2018: + +* Adds support for `strike`, and `tt`. + ## [0.5.2] - August 25, 2018: * Adds support for `bdi` and `bdo` diff --git a/README.md b/README.md index a9ce332f1f..8893e10106 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.5.2 + flutter_html: ^0.5.3 ## Currently Supported HTML Tags: @@ -64,6 +64,7 @@ Add the following to your `pubspec.yaml` file: * `section` * `small` * `span` + * `strike` (legacy html tag) * `strong` * `table` * `tbody` @@ -74,6 +75,7 @@ Add the following to your `pubspec.yaml` file: * `thead` * `time` * `tr` + * `tt` (legacy html tag) * `u` * `var` @@ -114,6 +116,7 @@ Add the following to your `pubspec.yaml` file: * `big` (deprecated) * `button` * `canvas` + * `center` (deprecated) * `col` * `colgroup` * `datalist` @@ -144,11 +147,9 @@ Add the following to your `pubspec.yaml` file: * `progress` * `script` * `select` (`form` elements are outside the scope of this package) - * `strike` (deprecated) * `style` * `textarea` (`form` elements are outside the scope of this package) * `title` (`head` elements are not rendered) - * `tt` (deprecated) ## Why this package? diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 3776f11c4b..24d6f79a58 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -62,6 +62,7 @@ class HtmlParser { "section", "small", "span", + "strike", "strong", "table", "tbody", @@ -72,6 +73,7 @@ class HtmlParser { "thead", "time", "tr", + "tt", "u", "ul", //partial "var", @@ -537,6 +539,15 @@ class HtmlParser { return Wrap( children: _parseNodeList(node.nodes), ); + case "strike": + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + decoration: TextDecoration.lineThrough, + ), + ); case "strong": return DefaultTextStyle.merge( child: Wrap( @@ -606,6 +617,15 @@ class HtmlParser { children: _parseNodeList(node.nodes), crossAxisAlignment: CrossAxisAlignment.center, ); + case "tt": + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontFamily: 'monospace', + ), + ); case "u": return DefaultTextStyle.merge( child: Wrap( diff --git a/pubspec.yaml b/pubspec.yaml index 536a33a981..365da8d811 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 60 different html tags!) -version: 0.5.2 +version: 0.5.3 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From 4f9185c6149c2bbc71f7ec344e01e18952fa2a4f Mon Sep 17 00:00:00 2001 From: Loi Tran Date: Fri, 31 Aug 2018 20:06:23 -0700 Subject: [PATCH 008/638] Make sure onLinkTap is not null --- lib/html_parser.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index a3df9f064a..7878b7a0ad 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -111,7 +111,7 @@ class HtmlParser { decorationColor: Colors.blueAccent), ), onTap: () { - if (node.attributes.containsKey('href')) { + if (node.attributes.containsKey('href') && onLinkTap != null) { String url = node.attributes['href']; onLinkTap(url); } From bb097552e816db7b52bb051fa1df3f64c169d927 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Fri, 31 Aug 2018 21:53:16 -0600 Subject: [PATCH 009/638] Version 0.5.4 --- CHANGELOG.md | 4 ++++ README.md | 4 ++-- lib/html_parser.dart | 29 ++++++++++++++--------------- pubspec.yaml | 2 +- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acf70daf39..3e3db1a326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.5.4] - August 31, 2018: + +* Adds `onLinkTap` callback. + ## [0.5.3] - August 25, 2018: * Adds support for `strike`, and `tt`. diff --git a/README.md b/README.md index f4416761c6..9c07557341 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,11 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.5.3 + flutter_html: ^0.5.4 ## Currently Supported HTML Tags: + * `a` * `abbr` * `address` * `article` @@ -82,7 +83,6 @@ Add the following to your `pubspec.yaml` file: ### Partially supported elements: > These are common elements that aren't yet fully supported, but won't be ignored and will still render. - * `a` * `ol` * `ul` diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 7878b7a0ad..f968d16817 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -101,22 +101,21 @@ class HtmlParser { switch (node.localName) { case "a": return GestureDetector( - child: DefaultTextStyle.merge( - child: Wrap( - children: _parseNodeList(node.nodes), + child: DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + decoration: TextDecoration.underline, + color: Colors.blueAccent, + decorationColor: Colors.blueAccent), ), - style: const TextStyle( - decoration: TextDecoration.underline, - color: Colors.blueAccent, - decorationColor: Colors.blueAccent), - ), - onTap: () { - if (node.attributes.containsKey('href') && onLinkTap != null) { - String url = node.attributes['href']; - onLinkTap(url); - } - } - ); + onTap: () { + if (node.attributes.containsKey('href') && onLinkTap != null) { + String url = node.attributes['href']; + onLinkTap(url); + } + }); case "abbr": return DefaultTextStyle.merge( child: Wrap( diff --git a/pubspec.yaml b/pubspec.yaml index 365da8d811..2b2545b08a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 60 different html tags!) -version: 0.5.3 +version: 0.5.4 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From 6f684ac2955a653a837e494a2aff8baca1cb409f Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Tue, 4 Sep 2018 13:57:33 -0600 Subject: [PATCH 010/638] Version 0.5.5 --- CHANGELOG.md | 4 ++++ README.md | 8 +++++--- lib/flutter_html.dart | 2 +- lib/html_parser.dart | 21 +++++++++++++++++++++ pubspec.yaml | 2 +- 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e3db1a326..d5deaf3d83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.5.5] - September 4, 2018: + +* Adds support for `acronym`, and `big`. + ## [0.5.4] - August 31, 2018: * Adds `onLinkTap` callback. diff --git a/README.md b/README.md index 9c07557341..3d64121a02 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,20 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.5.4 + flutter_html: ^0.5.5 ## Currently Supported HTML Tags: * `a` * `abbr` + * `acronym` * `address` * `article` * `aside` * `b` * `bdi` * `bdo` + * `big` * `blockquote` * `body` * `br` @@ -65,7 +67,7 @@ Add the following to your `pubspec.yaml` file: * `section` * `small` * `span` - * `strike` (legacy html tag) + * `strike` * `strong` * `table` * `tbody` @@ -76,7 +78,7 @@ Add the following to your `pubspec.yaml` file: * `thead` * `time` * `tr` - * `tt` (legacy html tag) + * `tt` * `u` * `var` diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 5922663975..5aa845e1bf 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -10,7 +10,7 @@ class Html extends StatelessWidget { this.padding, this.backgroundColor, this.defaultTextStyle = const TextStyle(color: Colors.black), - this.onLinkTap + this.onLinkTap, }) : super(key: key); final String data; diff --git a/lib/html_parser.dart b/lib/html_parser.dart index f968d16817..f2e1704597 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -14,12 +14,14 @@ class HtmlParser { static const _supportedElements = [ "a", "abbr", + "acronym", "address", "article", "aside", "b", "bdi", "bdo", + "big", "blockquote", "body", "br", @@ -126,6 +128,16 @@ class HtmlParser { decorationStyle: TextDecorationStyle.dotted, ), ); + case "acronym": + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + decoration: TextDecoration.underline, + decorationStyle: TextDecorationStyle.dotted, + ), + ); case "address": return DefaultTextStyle.merge( child: Wrap( @@ -177,6 +189,15 @@ class HtmlParser { return Wrap( children: _parseNodeList(node.nodes), ); + case "big": + return DefaultTextStyle.merge( + child: Wrap( + children: _parseNodeList(node.nodes), + ), + style: const TextStyle( + fontSize: 20.0, + ), + ); case "blockquote": return Padding( padding: EdgeInsets.fromLTRB(40.0, 14.0, 40.0, 14.0), diff --git a/pubspec.yaml b/pubspec.yaml index 2b2545b08a..c274532963 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 60 different html tags!) -version: 0.5.4 +version: 0.5.5 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From f1e56078211ea2fbd54068dd48835c382cc79622 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Tue, 4 Sep 2018 18:43:40 -0600 Subject: [PATCH 011/638] Version 0.5.6 --- CHANGELOG.md | 4 + README.md | 5 +- example/main.dart | 144 ++++++++++++++++++---- lib/flutter_html.dart | 8 +- lib/html_parser.dart | 40 +++++- pubspec.lock | 278 ------------------------------------------ pubspec.yaml | 4 +- 7 files changed, 176 insertions(+), 307 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5deaf3d83..e0e8d76969 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.5.6] - September 4, 2018: + +* Adds partial support for `center` and a `renderNewlines` property on the `Html` widget. + ## [0.5.5] - September 4, 2018: * Adds support for `acronym`, and `big`. diff --git a/README.md b/README.md index 3d64121a02..3edfdf5c76 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # flutter_html [![pub package](https://img.shields.io/pub/v/flutter_html.svg)](https://pub.dartlang.org/packages/flutter_html) -A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 60 different html tags!) +A Flutter widget for rendering static html tags as Flutter widgets. (Will render 70 different html tags!) ## Installing: Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.5.5 + flutter_html: ^0.5.6 ## Currently Supported HTML Tags: @@ -85,6 +85,7 @@ Add the following to your `pubspec.yaml` file: ### Partially supported elements: > These are common elements that aren't yet fully supported, but won't be ignored and will still render. + * `center` * `ol` * `ul` diff --git a/example/main.dart b/example/main.dart index d9b4d3f055..f5f0d0f551 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,29 +1,129 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; -void main() { - runApp( - MaterialApp( - home: Scaffold( - body: Html( - data: """ -
-

Demo Page

-

This is a fantastic nonexistent product that you should buy!

-

Pricing

-

Lorem ipsum dolor sit amet.

-

The Team

-

There isn't really a team...

-

Installation

-

You cannot install a nonexistent product!

+void main() => runApp(new MyApp()); + +class MyApp extends StatelessWidget { + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return new MaterialApp( + title: 'Flutter Demo', + theme: new ThemeData( + primarySwatch: Colors.blue, + ), + home: new MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({Key key, this.title}) : super(key: key); + + final String title; + + @override + _MyHomePageState createState() => new _MyHomePageState(); +} + +class _MyHomePageState extends State { + @override + Widget build(BuildContext context) { + return new Scaffold( + appBar: new AppBar( + title: new Text(widget.title), + ), + body: new Center( + child: SingleChildScrollView( + child: Html( + data: """ +
+
+

Header 1

+

Header 2

+

Header 3

+

Header 4

+
Header 5
+
Header 6
+
+ Below hr + Bold +
+

Demo Page

+

This is a fantastic nonexistent product that you should really really really consider buying!

+ https://github.com
+
+

Pricing

+

Lorem ipsum dolor sit amet.

+
+ This is some center text... ABBR and ACRONYM +
+

The Team

+

There isn't really a team...

+

Installation

+

You cannot install a nonexistent product!

+
+

bdi and bdo Test:

+

+ In the example below, usernames are shown along with the number of points in a contest. + If the bdi element is not supported in the browser, the username of the Arabic user would confuse the text (the bidirectional algorithm would put the colon and the number "90" next to the word "User" rather than next to the word "points"). +

+ +
    +
  • User hrefs: 60 points
  • +
  • User jdoe: 80 points
  • +
  • User إيان: 90 points
  • + Swapped! + This text will go left to right! + With bdi: User إيان: 90 points + Without bdi: User إيان: 90 points + ltr w/ bdi: User إيان: 90 points + ltr w/o bdi: User إيان: 90 points +
+
+
+ + + + + + + + + + + +
This is the table's caption
Head 1Head 2Head 3
Data 1Long Data 2Really, realllllly, long data 3
Data 1Long Data 2Really, realllllly, long data 3
Data 1Long Data 2Really, realllllly, long data 3
Different 1Different reallllllly long 2Diff 3
This spans 2 columnsNormal td
In foot 1In foot 2In foot long 2
+
+
Nested div
+
+
+            jQuery("#monkey");
+            
+
+

This is a fancy quote

+
+
+ Second nested div
+
+ +
Available on GitHub
+
+
+
Third nested div
- """, - //Optional parameters: - padding: EdgeInsets.all(8.0), - backgroundColor: Colors.white70, - defaultTextStyle: TextStyle(fontFamily: 'serif'), +

Second header

+

Third header

+
Fourth div
+ """, + //Optional parameters: + padding: EdgeInsets.all(8.0), + onLinkTap: (url) { + print("Opening $url..."); + }, + ), ), ), - ), - ); + ); + } } diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 5aa845e1bf..ea479aafeb 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -11,6 +11,7 @@ class Html extends StatelessWidget { this.backgroundColor, this.defaultTextStyle = const TextStyle(color: Colors.black), this.onLinkTap, + this.renderNewlines = false, }) : super(key: key); final String data; @@ -18,6 +19,7 @@ class Html extends StatelessWidget { final Color backgroundColor; final TextStyle defaultTextStyle; final Function onLinkTap; + final bool renderNewlines; @override Widget build(BuildContext context) { @@ -31,7 +33,11 @@ class Html extends StatelessWidget { style: defaultTextStyle, child: Wrap( alignment: WrapAlignment.start, - children: HtmlParser(width: width, onLinkTap: onLinkTap).parse(data), + children: HtmlParser( + width: width, + onLinkTap: onLinkTap, + renderNewlines: renderNewlines, + ).parse(data), ), ), ); diff --git a/lib/html_parser.dart b/lib/html_parser.dart index f2e1704597..6c9ff6ce9f 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -6,10 +6,12 @@ class HtmlParser { HtmlParser({ @required this.width, this.onLinkTap, + this.renderNewlines, }); final double width; final Function onLinkTap; + final bool renderNewlines; static const _supportedElements = [ "a", @@ -27,6 +29,7 @@ class HtmlParser { "br", "caption", "cite", + "center", "code", "data", "dd", @@ -89,6 +92,11 @@ class HtmlParser { List parse(String data) { List widgetList = new List(); + if (renderNewlines) { + print("Before: $data"); + data = data.replaceAll("\n", "
"); + print("After: $data"); + } dom.Document document = parser.parse(data); widgetList.add(_parseNode(document.body)); return widgetList; @@ -216,8 +224,7 @@ class HtmlParser { ), ); case "br": - if (node.previousElementSibling != null && - node.previousElementSibling.localName == "br") { + if (_isNotFirstBreakTag(node)) { return Container(width: width, height: 14.0); } return Container(width: width); @@ -229,6 +236,13 @@ class HtmlParser { children: _parseNodeList(node.nodes), ), ); + case "center": + return Container( + width: width, + child: Wrap( + children: _parseNodeList(node.nodes), + alignment: WrapAlignment.center, + )); case "cite": return DefaultTextStyle.merge( child: Wrap( @@ -720,4 +734,26 @@ class HtmlParser { } return stringToTrim; } + + bool _isNotFirstBreakTag(dom.Node node) { + int index = node.parentNode.nodes.indexOf(node); + if (index == 0) { + if (node.parentNode == null) { + return false; + } + return _isNotFirstBreakTag(node.parentNode); + } else if (node.parentNode.nodes[index - 1] is dom.Element) { + if ((node.parentNode.nodes[index - 1] as dom.Element).localName == "br") { + return true; + } + return false; + } else if (node.parentNode.nodes[index - 1] is dom.Text) { + if ((node.parentNode.nodes[index - 1] as dom.Text).text.trim() == "") { + return _isNotFirstBreakTag(node.parentNode.nodes[index - 1]); + } else { + return false; + } + } + return false; + } } diff --git a/pubspec.lock b/pubspec.lock index 7b1bb67945..98a0c0236c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,13 +1,6 @@ # Generated by pub # See https://www.dartlang.org/tools/pub/glossary#lockfile packages: - analyzer: - dependency: transitive - description: - name: analyzer - url: "https://pub.dartlang.org" - source: hosted - version: "0.32.4" args: dependency: transitive description: @@ -15,20 +8,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.0" - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.8" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" charcode: dependency: transitive description: @@ -43,20 +22,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.11" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.6" csslib: dependency: transitive description: @@ -69,25 +34,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - front_end: - dependency: transitive - description: - name: front_end - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.4" - glob: - dependency: transitive - description: - name: glob - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.7" html: dependency: "direct main" description: @@ -95,55 +41,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.13.3+3" - http: - dependency: transitive - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.11.3+17" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.5" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.3" - io: - dependency: transitive - description: - name: io - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.3" - js: - dependency: transitive - description: - name: js - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.1+1" - json_rpc_2: - dependency: transitive - description: - name: json_rpc_2 - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.9" - kernel: - dependency: transitive - description: - name: kernel - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.4" logging: dependency: transitive description: @@ -151,13 +48,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.11.3+2" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.3+1" meta: dependency: transitive description: @@ -165,41 +55,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.6" - mime: - dependency: transitive - description: - name: mime - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.6+2" - multi_server_socket: - dependency: transitive - description: - name: multi_server_socket - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - node_preamble: - dependency: transitive - description: - name: node_preamble - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.4" - package_config: - dependency: transitive - description: - name: package_config - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" - package_resolver: - dependency: transitive - description: - name: package_resolver - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" path: dependency: transitive description: @@ -207,81 +62,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.2" - plugin: - dependency: transitive - description: - name: plugin - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0+3" - pool: - dependency: transitive - description: - name: pool - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.6" - pub_semver: - dependency: transitive - description: - name: pub_semver - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.2" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0+1" - shelf: - dependency: transitive - description: - name: shelf - url: "https://pub.dartlang.org" - source: hosted - version: "0.7.3+3" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - shelf_static: - dependency: transitive - description: - name: shelf_static - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.8" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.2+4" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.5" - source_maps: - dependency: transitive - description: - name: source_maps - url: "https://pub.dartlang.org" - source: hosted - version: "0.10.7" source_span: dependency: transitive description: @@ -289,41 +74,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.9.3" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "1.6.8" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - test: - dependency: transitive - description: - name: test - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" typed_data: dependency: transitive description: @@ -345,34 +95,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" - vm_service_client: - dependency: transitive - description: - name: vm_service_client - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.6" - watcher: - dependency: transitive - description: - name: watcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.7+10" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.9" - yaml: - dependency: transitive - description: - name: yaml - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.15" sdks: dart: ">=2.0.0-dev.68.0 <3.0.0" flutter: ">=0.5.0 <0.8.0" diff --git a/pubspec.yaml b/pubspec.yaml index c274532963..589c370570 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html -description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 60 different html tags!) -version: 0.5.5 +description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render 70 different html tags!) +version: 0.5.6 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From 74dfee4edace3c41cb56db78232faaac64e99cbf Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Tue, 4 Sep 2018 22:52:11 -0600 Subject: [PATCH 012/638] Version 0.6.0 --- CHANGELOG.md | 5 + README.md | 139 ++------------------------ pubspec.lock | 278 +++++++++++++++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 4 +- 4 files changed, 295 insertions(+), 131 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0e8d76969..5a151b35a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [0.6.0] - September 4, 2018: + +* Update README.md and example +* GitHub version 0.6.0 milestone reached + ## [0.5.6] - September 4, 2018: * Adds partial support for `center` and a `renderNewlines` property on the `Html` widget. diff --git a/README.md b/README.md index 3edfdf5c76..c1bda9ecf9 100644 --- a/README.md +++ b/README.md @@ -1,109 +1,29 @@ # flutter_html [![pub package](https://img.shields.io/pub/v/flutter_html.svg)](https://pub.dartlang.org/packages/flutter_html) -A Flutter widget for rendering static html tags as Flutter widgets. (Will render 70 different html tags!) +A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) ## Installing: Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.5.6 + flutter_html: ^0.6.0 ## Currently Supported HTML Tags: - - * `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` - * `p` - * `pre` - * `q` - * `rp` - * `rt` - * `ruby` - * `s` - * `samp` - * `section` - * `small` - * `span` - * `strike` - * `strong` - * `table` - * `tbody` - * `td` - * `template` - * `tfoot` - * `th` - * `thead` - * `time` - * `tr` - * `tt` - * `u` - * `var` +`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`, `p`, `pre`, `q`, `rp`, `rt`, `ruby`, `s`, `samp`, `section`, `small`, `span`, `strike`, `strong`, `table`, `tbody`, `td`, `template`, `tfoot`, `th`, `thead`, `time`, `tr`, `tt`, `u`, `var` ### Partially supported elements: -> These are common elements that aren't yet fully supported, but won't be ignored and will still render. +> These are common elements that aren't yet fully supported, but won't be ignored and will still render somewhat correctly. - * `center` - * `ol` - * `ul` +`center`, `ol` , `ul` ### List of _planned_ supported elements: > These are elements that are planned, but present a specific challenge that makes them somewhat difficult to implement. - * `audio` - * `details` - * `source` - * `sub` - * `summary` - * `sup` - * `svg` - * `track` - * `video` - * `wbr` +`audio`, `details`, `source`, `sub`, `summary`, `sup`, `svg`, `track`, `video`, `wbr` -### Here are a list of elements that I don't plan on implementing: +### List of elements that I don't plan on implementing: > Feel free to open an issue if you have a good reason and feel like you can convince me to implement them. A _well written_ and _complete_ pull request implementing one of these is always welcome, @@ -111,48 +31,7 @@ Add the following to your `pubspec.yaml` file: > Note: These unsupported tags will just be ignored. - * `acronym` (deprecated, use `abbr` instead) - * `applet` (deprecated) - * `area` - * `base` (`head` elements are not rendered) - * `basefont` (deprecated, use defaultTextStyle on `Html` widget instead) - * `big` (deprecated) - * `button` - * `canvas` - * `center` (deprecated) - * `col` - * `colgroup` - * `datalist` - * `dialog` - * `dir` (deprecated) - * `embed` - * `font` (deprecated) - * `fieldset` (`form` elements are outside the scope of this package) - * `form` (`form`s are outside the scope of this package) - * `frame` (deprecated) - * `frameset` (deprecated) - * `head` (`head` elements are not rendered) - * `iframe` - * `input` (`form` elements are outside the scope of this package) - * `label` (`form` elements are outside the scope of this package) - * `legend` (`form` elements are outside the scope of this package) - * `link` (`head` elements are not rendered) - * `map` - * `meta` (`head` elements are not rendered) - * `meter` (outside the scope for now; maybe later) - * `noframe` (deprecated) - * `object` - * `optgroup` (`form` elements are outside the scope of this package) - * `option` (`form` elements are outside the scope of this package) - * `output` - * `param` - * `picture` - * `progress` - * `script` - * `select` (`form` elements are outside the scope of this package) - * `style` - * `textarea` (`form` elements are outside the scope of this package) - * `title` (`head` elements are not rendered) +`applet`, `area`, `base`, `basefont`, `button`, `canvas`, `col`, `colgroup`, `datalist`, `dialog`, `dir`, `embed`, `font`, `fieldset`, `form`, `frame`, `frameset`, `head`, `iframe`, `input`, `label`, `legend`, `link`, `map`, `meta`, `meter`, `noframe`, `object`, `optgroup`, `option`, `output`, `param`, `picture`, `progress`, `script`, `select`, `style`, `textarea`, `title` ## Why this package? @@ -165,6 +44,7 @@ until official support is added. Html( data: """ +

Demo Page

This is a fantastic nonexistent product that you should buy!

@@ -174,6 +54,7 @@ until official support is added.

There isn't really a team...

Installation

You cannot install a nonexistent product!

+
""", //Optional parameters: diff --git a/pubspec.lock b/pubspec.lock index 98a0c0236c..7b1bb67945 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,13 @@ # Generated by pub # See https://www.dartlang.org/tools/pub/glossary#lockfile packages: + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "0.32.4" args: dependency: transitive description: @@ -8,6 +15,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.0" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" charcode: dependency: transitive description: @@ -22,6 +43,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" csslib: dependency: transitive description: @@ -34,6 +69,25 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + front_end: + dependency: transitive + description: + name: front_end + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.7" html: dependency: "direct main" description: @@ -41,6 +95,55 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.13.3+3" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.3+17" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1+1" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + kernel: + dependency: transitive + description: + name: kernel + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.4" logging: dependency: transitive description: @@ -48,6 +151,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.11.3+2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.3+1" meta: dependency: transitive description: @@ -55,6 +165,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.6" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.6+2" + multi_server_socket: + dependency: transitive + description: + name: multi_server_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + node_preamble: + dependency: transitive + description: + name: node_preamble + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_resolver: + dependency: transitive + description: + name: package_resolver + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" path: dependency: transitive description: @@ -62,11 +207,81 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.2" + plugin: + dependency: transitive + description: + name: plugin + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+3" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.6" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.2" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0+1" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.3+3" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.8" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.2+4" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.5" + source_maps: + dependency: transitive + description: + name: source_maps + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.7" source_span: dependency: transitive description: @@ -74,6 +289,41 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.8" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + test: + dependency: transitive + description: + name: test + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" typed_data: dependency: transitive description: @@ -95,6 +345,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + vm_service_client: + dependency: transitive + description: + name: vm_service_client + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.6" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.7+10" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.9" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.15" sdks: dart: ">=2.0.0-dev.68.0 <3.0.0" flutter: ">=0.5.0 <0.8.0" diff --git a/pubspec.yaml b/pubspec.yaml index 589c370570..ce4ba357e4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html -description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render 70 different html tags!) -version: 0.5.6 +description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render overr 70 different html tags!) +version: 0.6.0 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From b5d9d4aba75156c3ca4809018498c6b0c918d4ba Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Tue, 4 Sep 2018 22:55:12 -0600 Subject: [PATCH 013/638] Version 0.6.1 --- CHANGELOG.md | 4 ++++ README.md | 2 +- pubspec.yaml | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a151b35a6..7d54fb1f1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.6.1] - September 4, 2018: + +* Fixed minor typo + ## [0.6.0] - September 4, 2018: * Update README.md and example diff --git a/README.md b/README.md index c1bda9ecf9..79b5d12cae 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.6.0 + flutter_html: ^0.6.1 ## Currently Supported HTML Tags: `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`, `p`, `pre`, `q`, `rp`, `rt`, `ruby`, `s`, `samp`, `section`, `small`, `span`, `strike`, `strong`, `table`, `tbody`, `td`, `template`, `tfoot`, `th`, `thead`, `time`, `tr`, `tt`, `u`, `var` diff --git a/pubspec.yaml b/pubspec.yaml index ce4ba357e4..8cc234c663 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html -description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render overr 70 different html tags!) -version: 0.6.0 +description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) +version: 0.6.1 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From 20a3ff2c54d9512be0e3c2637abfcb8291355a99 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Wed, 5 Sep 2018 21:14:50 -0600 Subject: [PATCH 014/638] Version 0.6.2 --- CHANGELOG.md | 5 +++++ README.md | 2 +- lib/html_parser.dart | 16 ++++++++++++++-- pubspec.yaml | 2 +- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d54fb1f1d..a359be19fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [0.6.2] - September 5, 2018: + +* Adds check for `img src` before trying to load it. +* Adds support for `img alt` attribute. + ## [0.6.1] - September 4, 2018: * Fixed minor typo diff --git a/README.md b/README.md index 79b5d12cae..c54d2a45ad 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.6.1 + flutter_html: ^0.6.2 ## Currently Supported HTML Tags: `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`, `p`, `pre`, `q`, `rp`, `rt`, `ruby`, `s`, `samp`, `section`, `small`, `span`, `strike`, `strong`, `table`, `tbody`, `td`, `template`, `tfoot`, `th`, `thead`, `time`, `tr`, `tt`, `u`, `var` diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 6c9ff6ce9f..971c8232ed 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -6,7 +6,7 @@ class HtmlParser { HtmlParser({ @required this.width, this.onLinkTap, - this.renderNewlines, + this.renderNewlines = false, }); final double width; @@ -440,7 +440,19 @@ class HtmlParser { ), ); case "img": - return Image.network(node.attributes['src']); + if (node.attributes['src'] != null) { + 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(); case "ins": return DefaultTextStyle.merge( child: Wrap( diff --git a/pubspec.yaml b/pubspec.yaml index 8cc234c663..039df4f513 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) -version: 0.6.1 +version: 0.6.2 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From 9b3f7db4cd16673f59729d77fc3dfe95568497c4 Mon Sep 17 00:00:00 2001 From: Kiefer Lam Date: Sat, 8 Sep 2018 17:59:47 +0100 Subject: [PATCH 015/638] Remove <0.8.0 flutter version constraint --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 039df4f513..22f738b0ff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ homepage: https://github.com/Sub6Resources/flutter_html environment: sdk: '>=1.19.0 <3.0.0' - flutter: '>=0.5.0 <0.8.0' + flutter: '>=0.5.0' dependencies: html: ^0.13.3 From 282ba052782c3d111cc61df2db5dee228935b625 Mon Sep 17 00:00:00 2001 From: Kiefer Lam Date: Sun, 9 Sep 2018 00:55:24 +0100 Subject: [PATCH 016/638] Add middle dot before li contents --- lib/html_parser.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 971c8232ed..edcb07a70e 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -474,8 +474,11 @@ class HtmlParser { case "li": return Container( width: width, - child: Wrap( - children: _parseNodeList(node.nodes), + child: Row( + children: [ + Text('•'), + Wrap(children: _parseNodeList(node.nodes)), + ], ), ); case "main": From 6dea9ce7cdb170596ecfcb523e43c9bd6544f793 Mon Sep 17 00:00:00 2001 From: Kiefer Lam Date: Sun, 9 Sep 2018 01:01:30 +0100 Subject: [PATCH 017/638] Add padding to middle dot --- lib/html_parser.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index edcb07a70e..e59f63ee5f 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -476,8 +476,10 @@ class HtmlParser { width: width, child: Row( children: [ - Text('•'), - Wrap(children: _parseNodeList(node.nodes)), + Container( + child: Text('•'), + padding: EdgeInsets.symmetric(horizontal: 4.0)), + Wrap(children: _parseNodeList(node.nodes)) ], ), ); From 1349a6336352855ea42728cffaad680a8a6320a3 Mon Sep 17 00:00:00 2001 From: Kiefer Lam Date: Sun, 9 Sep 2018 02:01:23 +0100 Subject: [PATCH 018/638] Add switch for parent type of li node to determine which mark to use --- lib/html_parser.dart | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index e59f63ee5f..98cba65f51 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -472,13 +472,25 @@ class HtmlParser { ), ); case "li": + String type = node.parent.localName; // Parent type; usually ol or ul + EdgeInsets markPadding = EdgeInsets.symmetric(horizontal: 4.0); + Widget mark; + switch (type) { + case "ul": + mark = Container(child: Text('•'), padding: markPadding); + break; + case "ol": //TODO Use index as mark + mark = Container(child: Text('•'), padding: markPadding); + break; + default: //Fallback to middle dot + mark = Container(width: 0.0, height: 0.0); + break; + } return Container( width: width, child: Row( children: [ - Container( - child: Text('•'), - padding: EdgeInsets.symmetric(horizontal: 4.0)), + mark, Wrap(children: _parseNodeList(node.nodes)) ], ), From afae3df9c1a2329c0f8dd41972acb51c10a92bf9 Mon Sep 17 00:00:00 2001 From: Kiefer Lam Date: Sun, 9 Sep 2018 11:51:13 +0100 Subject: [PATCH 019/638] Change Row to Wrap to prevent overflow offscreen --- lib/html_parser.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 98cba65f51..6af8f3d12c 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -488,7 +488,7 @@ class HtmlParser { } return Container( width: width, - child: Row( + child: Wrap( children: [ mark, Wrap(children: _parseNodeList(node.nodes)) From bd19c5711a045f922c66065edb22dcb96876e251 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Mon, 10 Sep 2018 18:46:28 -0600 Subject: [PATCH 020/638] Version 0.7.0 --- CHANGELOG.md | 4 ++++ README.md | 6 +++--- lib/html_parser.dart | 7 ++++--- pubspec.lock | 2 +- pubspec.yaml | 2 +- 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a359be19fe..25be4f0e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.7.0] - September 10, 2018: + +* Adds full support for `ul` + ## [0.6.2] - September 5, 2018: * Adds check for `img src` before trying to load it. diff --git a/README.md b/README.md index c54d2a45ad..8b5c308a54 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,15 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.6.2 + flutter_html: ^0.7.0 ## Currently Supported HTML Tags: -`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`, `p`, `pre`, `q`, `rp`, `rt`, `ruby`, `s`, `samp`, `section`, `small`, `span`, `strike`, `strong`, `table`, `tbody`, `td`, `template`, `tfoot`, `th`, `thead`, `time`, `tr`, `tt`, `u`, `var` +`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`, `p`, `pre`, `q`, `rp`, `rt`, `ruby`, `s`, `samp`, `section`, `small`, `span`, `strike`, `strong`, `table`, `tbody`, `td`, `template`, `tfoot`, `th`, `thead`, `time`, `tr`, `tt`, `u`, `ul`, `var` ### Partially supported elements: > These are common elements that aren't yet fully supported, but won't be ignored and will still render somewhat correctly. -`center`, `ol` , `ul` +`center`, `ol` ### List of _planned_ supported elements: > These are elements that are planned, but present a specific challenge that makes them somewhat difficult to implement. diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 6af8f3d12c..23198fe8bd 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -473,14 +473,15 @@ class HtmlParser { ); case "li": String type = node.parent.localName; // Parent type; usually ol or ul - EdgeInsets markPadding = EdgeInsets.symmetric(horizontal: 4.0); + const EdgeInsets markPadding = EdgeInsets.symmetric(horizontal: 4.0); Widget mark; switch (type) { case "ul": mark = Container(child: Text('•'), padding: markPadding); break; - case "ol": //TODO Use index as mark - mark = Container(child: Text('•'), padding: markPadding); + case "ol": + int index = node.parent.children.indexOf(node) + 1; + mark = Container(child: Text("$index."), padding: markPadding); break; default: //Fallback to middle dot mark = Container(width: 0.0, height: 0.0); diff --git a/pubspec.lock b/pubspec.lock index 7b1bb67945..5bb8a63f39 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -375,4 +375,4 @@ packages: version: "2.1.15" sdks: dart: ">=2.0.0-dev.68.0 <3.0.0" - flutter: ">=0.5.0 <0.8.0" + flutter: ">=0.5.0" diff --git a/pubspec.yaml b/pubspec.yaml index 22f738b0ff..39f1ebb2b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) -version: 0.6.2 +version: 0.7.0 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From 574c4d327ef89219e744f2f8807d6e55870b5c67 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Tue, 11 Sep 2018 20:50:40 -0600 Subject: [PATCH 021/638] Version 0.7.1 --- CHANGELOG.md | 7 ++++++- README.md | 6 +++--- lib/html_parser.dart | 9 ++++++--- pubspec.yaml | 2 +- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25be4f0e9d..7ef7fd037d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ +## [0.7.1] - September 11, 2018: + +* Fixes issue with text nodes that contain only a space. ([#24](https://github.com/Sub6Resources/flutter_html/issues/24)) +* Fixes typo in README.md from 0.7.0. + ## [0.7.0] - September 10, 2018: -* Adds full support for `ul` +* Adds full support for `ul` and `ol` ## [0.6.2] - September 5, 2018: diff --git a/README.md b/README.md index 8b5c308a54..3679c758bd 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,15 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.7.0 + flutter_html: ^0.7.1 ## Currently Supported HTML Tags: -`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`, `p`, `pre`, `q`, `rp`, `rt`, `ruby`, `s`, `samp`, `section`, `small`, `span`, `strike`, `strong`, `table`, `tbody`, `td`, `template`, `tfoot`, `th`, `thead`, `time`, `tr`, `tt`, `u`, `ul`, `var` +`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`, `table`, `tbody`, `td`, `template`, `tfoot`, `th`, `thead`, `time`, `tr`, `tt`, `u`, `ul`, `var` ### Partially supported elements: > These are common elements that aren't yet fully supported, but won't be ignored and will still render somewhat correctly. -`center`, `ol` +`center` ### List of _planned_ supported elements: > These are elements that are planned, but present a specific challenge that makes them somewhat difficult to implement. diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 23198fe8bd..cadaed159d 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -728,8 +728,11 @@ class HtmlParser { } } else if (node is dom.Text) { //We don't need to worry about rendering extra whitespace - if (node.text.trim() == "") { - return Container(); + if (node.text.trim() == "" && node.text.indexOf(" ") == -1) { + return Wrap(); + } + if (node.text.trim() == "" && node.text.indexOf(" ") != -1) { + node.text = " "; } print("Plain Text Node: '${trimStringHtml(node.text)}'"); @@ -742,7 +745,7 @@ class HtmlParser { return Text(finalText); } } - return Container(); + return Wrap(); } List _parseNodeList(List nodeList) { diff --git a/pubspec.yaml b/pubspec.yaml index 39f1ebb2b8..5f9b0695c9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) -version: 0.7.0 +version: 0.7.1 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From 5d3137c8634566019208bb8f1509d8e5aeeea5ec Mon Sep 17 00:00:00 2001 From: Ben Bieker Date: Tue, 11 Sep 2018 16:18:21 +0200 Subject: [PATCH 022/638] Add custom node rendering - It is now possible to render any dom.Node with a custom renderer if necessary. - The fallback is the default rendering like before. --- lib/flutter_html.dart | 6 ++++++ lib/html_parser.dart | 28 +++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index ea479aafeb..97f0f081ba 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -12,6 +12,7 @@ class Html extends StatelessWidget { this.defaultTextStyle = const TextStyle(color: Colors.black), this.onLinkTap, this.renderNewlines = false, + this.customRender, }) : super(key: key); final String data; @@ -21,6 +22,10 @@ class Html extends StatelessWidget { final Function onLinkTap; final bool renderNewlines; + /// Either return a custom widget for specific node types or return null to + /// fallback to the default rendering. + final CustomRender customRender; + @override Widget build(BuildContext context) { final double width = MediaQuery.of(context).size.width; @@ -37,6 +42,7 @@ class Html extends StatelessWidget { width: width, onLinkTap: onLinkTap, renderNewlines: renderNewlines, + customRender: customRender, ).parse(data), ), ), diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 23198fe8bd..bb3e81a086 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -2,16 +2,20 @@ import 'package:flutter/material.dart'; import 'package:html/parser.dart' as parser; import 'package:html/dom.dart' as dom; +typedef CustomRender = Widget Function(dom.Node node, List children); + class HtmlParser { HtmlParser({ @required this.width, this.onLinkTap, this.renderNewlines = false, + this.customRender, }); final double width; final Function onLinkTap; final bool renderNewlines; + final CustomRender customRender; static const _supportedElements = [ "a", @@ -93,9 +97,14 @@ class HtmlParser { List widgetList = new List(); if (renderNewlines) { - print("Before: $data"); + assert(() { + print("Before: $data"); + return true; + }()); data = data.replaceAll("\n", "
"); - print("After: $data"); + assert(() { + print("After: $data"); + }()); } dom.Document document = parser.parse(data); widgetList.add(_parseNode(document.body)); @@ -103,11 +112,24 @@ class HtmlParser { } Widget _parseNode(dom.Node node) { + if (customRender != null) { + final Widget customWidget = + customRender(node, _parseNodeList(node.nodes)); + if (customWidget != null) { + return customWidget; + } + } + if (node is dom.Element) { - print("Found ${node.localName}"); + assert(() { + print("Found ${node.localName}"); + return true; + }()); + if (!_supportedElements.contains(node.localName)) { return Container(); } + switch (node.localName) { case "a": return GestureDetector( From fe799f3897b8a2449281374e7fc4ac1136ef6748 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Thu, 18 Oct 2018 21:42:27 -0600 Subject: [PATCH 023/638] Version 0.8.0 --- CHANGELOG.md | 5 +++++ README.md | 12 ++++++++++-- example/main.dart | 9 +++++++++ pubspec.yaml | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef7fd037d..16888f4e34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [0.8.0] - October 18, 2018: + +* Adds custom tag callback +* Logging no longer shows up in production. + ## [0.7.1] - September 11, 2018: * Fixes issue with text nodes that contain only a space. ([#24](https://github.com/Sub6Resources/flutter_html/issues/24)) diff --git a/README.md b/README.md index 3679c758bd..46a8151eca 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.7.1 + flutter_html: ^0.8.0 ## Currently Supported HTML Tags: `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`, `table`, `tbody`, `td`, `template`, `tfoot`, `th`, `thead`, `time`, `tr`, `tt`, `u`, `ul`, `var` @@ -63,5 +63,13 @@ until official support is added. defaultTextStyle: TextStyle(fontFamily: 'serif'), onLinkTap: (url) { // open url in a webview - } + }, + customRender: (node, children) { + if(node is dom.Element) { + switch(node.localName) { + case "video": return Chewie(...); + case "custom_tag": return CustomWidget(...); + } + } + }, ) \ No newline at end of file diff --git a/example/main.dart b/example/main.dart index f5f0d0f551..cbe4b3fd71 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:html/dom.dart' as dom; void main() => runApp(new MyApp()); @@ -121,6 +122,14 @@ class _MyHomePageState extends State { onLinkTap: (url) { print("Opening $url..."); }, + customRender: (node, children) { + if (node is dom.Element) { + switch (node.localName) { + case "custom_tag": + return Column(children: children); + } + } + }, ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 5f9b0695c9..bf3bdbfa11 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) -version: 0.7.1 +version: 0.8.0 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From 3e9fbfd725e1ef6df971da2ed400320dde90b2e5 Mon Sep 17 00:00:00 2001 From: EdwardWong Date: Fri, 19 Oct 2018 16:11:39 +0800 Subject: [PATCH 024/638] Add OnLinkTap typedef for better indication of exact function type --- lib/flutter_html.dart | 2 +- lib/html_parser.dart | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 97f0f081ba..05f344c437 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -19,7 +19,7 @@ class Html extends StatelessWidget { final EdgeInsetsGeometry padding; final Color backgroundColor; final TextStyle defaultTextStyle; - final Function onLinkTap; + final OnLinkTap onLinkTap; final bool renderNewlines; /// Either return a custom widget for specific node types or return null to diff --git a/lib/html_parser.dart b/lib/html_parser.dart index b8e2900e46..00b3008e22 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -3,6 +3,7 @@ import 'package:html/parser.dart' as parser; import 'package:html/dom.dart' as dom; typedef CustomRender = Widget Function(dom.Node node, List children); +typedef OnLinkTap = void Function(String url); class HtmlParser { HtmlParser({ @@ -13,7 +14,7 @@ class HtmlParser { }); final double width; - final Function onLinkTap; + final OnLinkTap onLinkTap; final bool renderNewlines; final CustomRender customRender; From 06caf69dc01478e812fe221280f616c1a4ff0c7b Mon Sep 17 00:00:00 2001 From: EdwardWong Date: Fri, 19 Oct 2018 16:12:15 +0800 Subject: [PATCH 025/638] Change "print" to "debugPrint" to avoid release mode logging --- lib/html_parser.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 00b3008e22..984a63e450 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -99,12 +99,12 @@ class HtmlParser { if (renderNewlines) { assert(() { - print("Before: $data"); + debugPrint("Before: $data"); return true; }()); data = data.replaceAll("\n", "
"); assert(() { - print("After: $data"); + debugPrint("After: $data"); }()); } dom.Document document = parser.parse(data); @@ -123,7 +123,7 @@ class HtmlParser { if (node is dom.Element) { assert(() { - print("Found ${node.localName}"); + debugPrint("Found ${node.localName}"); return true; }()); @@ -758,7 +758,7 @@ class HtmlParser { node.text = " "; } - print("Plain Text Node: '${trimStringHtml(node.text)}'"); + debugPrint("Plain Text Node: '${trimStringHtml(node.text)}'"); String finalText = trimStringHtml(node.text); //Temp fix for https://github.com/flutter/flutter/issues/736 if (finalText.endsWith(" ")) { From 6b6ea88ceb186e89713bff68eb360fdaa9068f60 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Fri, 19 Oct 2018 12:04:47 -0600 Subject: [PATCH 026/638] Version 0.8.1 --- CHANGELOG.md | 4 ++++ README.md | 2 +- lib/html_parser.dart | 7 ------- pubspec.yaml | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16888f4e34..c7fdece19c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.8.1] - October 19, 2018: + +* Adds `typedef` for `onLinkTap` function. + ## [0.8.0] - October 18, 2018: * Adds custom tag callback diff --git a/README.md b/README.md index 46a8151eca..66934577e6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.8.0 + flutter_html: ^0.8.1 ## Currently Supported HTML Tags: `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`, `table`, `tbody`, `td`, `template`, `tfoot`, `th`, `thead`, `time`, `tr`, `tt`, `u`, `ul`, `var` diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 984a63e450..72a802c881 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -98,14 +98,7 @@ class HtmlParser { List widgetList = new List(); if (renderNewlines) { - assert(() { - debugPrint("Before: $data"); - return true; - }()); data = data.replaceAll("\n", "
"); - assert(() { - debugPrint("After: $data"); - }()); } dom.Document document = parser.parse(data); widgetList.add(_parseNode(document.body)); diff --git a/pubspec.yaml b/pubspec.yaml index bf3bdbfa11..7a85b25c43 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) -version: 0.8.0 +version: 0.8.1 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From 4b8b108568b8e9c173f383b3428e74e6024a7552 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Thu, 1 Nov 2018 22:30:17 -0600 Subject: [PATCH 027/638] Version 0.8.2 --- CHANGELOG.md | 4 ++++ README.md | 2 +- lib/html_parser.dart | 6 ------ pubspec.yaml | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7fdece19c..a483ba0522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.8.2] - November 1, 2018: + +* Removes debug prints. + ## [0.8.1] - October 19, 2018: * Adds `typedef` for `onLinkTap` function. diff --git a/README.md b/README.md index 66934577e6..ce2f551c07 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.8.1 + flutter_html: ^0.8.2 ## Currently Supported HTML Tags: `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`, `table`, `tbody`, `td`, `template`, `tfoot`, `th`, `thead`, `time`, `tr`, `tt`, `u`, `ul`, `var` diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 72a802c881..b9fa8f4b19 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -115,11 +115,6 @@ class HtmlParser { } if (node is dom.Element) { - assert(() { - debugPrint("Found ${node.localName}"); - return true; - }()); - if (!_supportedElements.contains(node.localName)) { return Container(); } @@ -751,7 +746,6 @@ class HtmlParser { node.text = " "; } - debugPrint("Plain Text Node: '${trimStringHtml(node.text)}'"); String finalText = trimStringHtml(node.text); //Temp fix for https://github.com/flutter/flutter/issues/736 if (finalText.endsWith(" ")) { diff --git a/pubspec.yaml b/pubspec.yaml index 7a85b25c43..3ea28be2ed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) -version: 0.8.1 +version: 0.8.2 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From a8f09fd73212ce90cccfe9a0289341271805ba93 Mon Sep 17 00:00:00 2001 From: Jeff Mikels Date: Tue, 6 Nov 2018 11:43:12 -0500 Subject: [PATCH 028/638] Added support for rendering as RichText Widgets --- lib/flutter_html.dart | 11 +- lib/html_parser.dart | 650 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 658 insertions(+), 3 deletions(-) diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 05f344c437..a04b329d83 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -13,6 +13,7 @@ class Html extends StatelessWidget { this.onLinkTap, this.renderNewlines = false, this.customRender, + this.useRichText = false, }) : super(key: key); final String data; @@ -21,6 +22,7 @@ class Html extends StatelessWidget { final TextStyle defaultTextStyle; final OnLinkTap onLinkTap; final bool renderNewlines; + final bool useRichText; /// Either return a custom widget for specific node types or return null to /// fallback to the default rendering. @@ -38,7 +40,14 @@ class Html extends StatelessWidget { style: defaultTextStyle, child: Wrap( alignment: WrapAlignment.start, - children: HtmlParser( + children: (useRichText) + ? HtmlRichTextParser( + width: width, + onLinkTap: onLinkTap, + renderNewlines: renderNewlines, + customRender: customRender, + ).parse(data) + : HtmlOldParser( width: width, onLinkTap: onLinkTap, renderNewlines: renderNewlines, diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 72a802c881..bfb66258f9 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -1,12 +1,658 @@ import 'package:flutter/material.dart'; +import 'package:flutter/gestures.dart'; import 'package:html/parser.dart' as parser; import 'package:html/dom.dart' as dom; typedef CustomRender = Widget Function(dom.Node node, List children); typedef OnLinkTap = void Function(String url); -class HtmlParser { - HtmlParser({ +class LinkTextSpan extends TextSpan { + // Beware! + // + // This class is only safe because the TapGestureRecognizer is not + // given a deadline and therefore never allocates any resources. + // + // In any other situation -- setting a deadline, using any of the less trivial + // recognizers, etc -- you would have to manage the gesture recognizer's + // lifetime and call dispose() when the TextSpan was no longer being rendered. + // + // Since TextSpan itself is @immutable, this means that you would have to + // manage the recognizer from outside the TextSpan, e.g. in the State of a + // stateful widget that then hands the recognizer to the TextSpan. + final String url; + + LinkTextSpan({ TextStyle style, this.url, String text, OnLinkTap onLinkTap, List children }) : super( + style: style, + text: text, + children: children ?? [], + recognizer: TapGestureRecognizer()..onTap = () { + onLinkTap(url); + } + ); +} + +class LinkBlock extends Container { + // final String url; + // final EdgeInsets padding; + // final EdgeInsets margin; + // final OnLinkTap onLinkTap; + final List children; + + LinkBlock({ String url, EdgeInsets padding, EdgeInsets margin, OnLinkTap onLinkTap, List this.children }):super( + padding: padding, + margin:margin, + child: GestureDetector( + onTap: (){ + onLinkTap(url); + }, + child:Column( + children: children, + ) + ) + ); +} + +class BlockText extends StatelessWidget { + + final RichText child; + final EdgeInsets padding; + final EdgeInsets margin; + final String leadingChar; + final Decoration decoration; + + BlockText({@required this.child, this.padding, this.margin, this.leadingChar = '',this.decoration}); + + @override + Widget build(BuildContext context) { + return Container( + width:double.infinity, + padding: this.padding, + margin: this.margin, + decoration:this.decoration, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + leadingChar.isNotEmpty ? Text(leadingChar) : Container(), + Expanded(child:child), + ], + ) + ); + } +} + +class ParseContext { + List rootWidgetList; // the widgetList accumulator + dynamic parentElement; // the parent spans accumulator + int indentLevel = 0; + int listCount = 0; + String listChar = '•'; + String blockType; // blockType can be 'p', 'div', 'ul', 'ol', 'blockquote' + bool condenseWhitespace = true; + bool spansOnly = false; + TextStyle childStyle; + + ParseContext({ + this.rootWidgetList, + this.parentElement, + this.indentLevel = 0, + this.listCount = 0, + this.listChar = '•', + this.blockType, + this.condenseWhitespace = true, + this.spansOnly = false, + this.childStyle}) { + childStyle = childStyle ?? TextStyle(); + } + + ParseContext.fromContext(ParseContext parseContext){ + rootWidgetList = parseContext.rootWidgetList; + parentElement = parseContext.parentElement; + indentLevel = parseContext.indentLevel; + listCount = parseContext.listCount; + listChar = parseContext.listChar; + blockType = parseContext.blockType; + condenseWhitespace = parseContext.condenseWhitespace; + spansOnly = parseContext.spansOnly; + childStyle = parseContext.childStyle ?? TextStyle(); + } +} + +class HtmlRichTextParser { + HtmlRichTextParser({ + @required this.width, + this.onLinkTap, + this.renderNewlines = false, + this.customRender, + this.context, + }); + + final double indentSize = 10.0; + + final double width; + final onLinkTap; + final bool renderNewlines; + final CustomRender customRender; + final BuildContext context; + + // style elements set a default style + // for all child nodes + // treat ol, ul, and blockquote like style elements also + static const _supportedStyleElements = [ + "b","i","em","strong","code","u","small","abbr","acronym", "ol", "ul", "blockquote" + ]; + + // specialty elements require unique handling + // eg. the "a" tag can contain a block of text or an image + // sometimes "a" will be rendered with a textspan and recognizer + // sometimes "a" will be rendered with a clickable Block + static const _supportedSpecialtyElements = [ + "a", + "br", + "table", + "tbody", + "td", + "tfoot", + "th", + "thead", + "tr", + ]; + + // block elements are always rendered with a new + // block-level widget, if a block level element + // is found inside another block level element, + // we simply treat it as a new block level element + static const _supportedBlockElements = [ + "article" + "body", + "center", + "dd", + "dfn", + "div", + "dl", + "dt", + "figcaption", + "figure", + "footer", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "header", + "hr", + "img", + "li", + "main", + "p", + "pre", + "section", + ]; + + static get _supportedElements => List() + ..addAll(_supportedStyleElements) + ..addAll(_supportedSpecialtyElements) + ..addAll(_supportedBlockElements); + + // this function is called recursively for each child + // however, the first time it is called, we make sure + // to ignore the node itself, so we only pay attention + // to the children + bool _hasBlockChild(dom.Node node, {bool ignoreSelf = true}) { + bool retval = false; + if (node is dom.Element) { + if (_supportedBlockElements.contains(node.localName) && !ignoreSelf) return true; + node.nodes.forEach((dom.Node node) { + if (_hasBlockChild(node, ignoreSelf: false)) retval = true; + }); + } + return retval; + } + + // Parses an html string and returns a list of RichText widgets that represent the body of your html document. + List parse(String data) { + + if (renderNewlines) { + data = data.replaceAll("\n", "
"); + } + dom.Document document = parser.parse(data); + dom.Node body = document.body; + + List widgetList = new List(); + ParseContext parseContext = ParseContext( + rootWidgetList: widgetList, + childStyle: DefaultTextStyle.of(context).style, + ); + + // ignore the top level "body" + body.nodes.forEach((dom.Node node)=>_parseNode(node, parseContext)); + // _parseNode(body, parseContext); + + // eliminate empty widgets + List retval = []; + widgetList.forEach((dynamic w){ + if (w is BlockText) { + if (w.child.text == null) return; + if (w.child.text.text.isEmpty && w.child.text.children.isEmpty) return; + } else if (w is LinkBlock) { + if (w.children.isEmpty) return; + } else if (w is LinkTextSpan) { + if (w.text.isEmpty && w.children.isEmpty) return; + } + retval.add(w); + }); + return retval; + } + + // THE WORKHORSE FUNCTION!! + // call the function with the current node and a ParseContext + // the ParseContext is used to do a number of things + // first, since we call this function recursively, the parseContext holds references to + // all the data that is relevant to a particular iteration and its child iterations + // it holds information about whether to indent the text, whether we are in a list, etc. + // + // secondly, it holds the 'global' widgetList that accumulates all the block-level widgets + // + // thirdly, it holds a reference to the most recent "parent" so that this iteration of the + // 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) { + + // TEXT ONLY NODES + // a text only node is a child of a tag with no inner html + if (node is dom.Text) { + + // WHITESPACE CONSIDERATIONS --- + // truly empty nodes, should just be ignored + if (node.text.trim() == "" && node.text.indexOf(" ") == -1) { + return; + } + + // empty strings of whitespace might be significant or not, condense it by default + if (node.text.trim() == "" && node.text.indexOf(" ") != -1 && parseContext.condenseWhitespace) { + node.text = " "; + } + + // we might want to preserve internal whitespace + String finalText = parseContext.condenseWhitespace ? condenseHtmlWhitespace(node.text) : node.text; + + // if this is part of a string of spans, we will preserve leading and trailing whitespace + if (!(parseContext.parentElement is TextSpan || parseContext.parentElement is LinkTextSpan)) + finalText = finalText.trim(); + + + // NOW WE HAVE OUR TRULY FINAL TEXT + debugPrint("Plain Text Node: '$finalText'"); + + // create a span by default + TextSpan span = TextSpan(text:finalText, children:[], style:parseContext.childStyle); + + // in this class, a ParentElement must be a BlockText, LinkTextSpan, Row, Column, TextSpan + + // if there is no parentElement, contain the span in a BlockText + if (parseContext.parentElement == null){ + parseContext.parentElement = span; + parseContext.rootWidgetList.add(BlockText(child:RichText(text: span))); + + // if the parent is a LinkTextSpan, keep the main attributes of that span going. + } 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), + url: parseContext.parentElement.url, + text: finalText, + onLinkTap: onLinkTap, + ) + ); + + // if the parent is a normal span, just add this to that list + } else { + parseContext.parentElement.children.add(span); + } + return; + } + // OTHER ELEMENT NODES + else if (node is dom.Element) { + assert(() { + debugPrint("Found ${node.localName}"); + debugPrint(node.outerHtml); + return true; + }()); + + if (! _supportedElements.contains(node.localName)) { + return; + } + + // make a copy of the current context so that we can modify + // pieces of it for the next iteration of this function + ParseContext nextContext = new ParseContext.fromContext(parseContext); + + // handle style elements + if ( _supportedStyleElements.contains(node.localName)) { + TextStyle childStyle = parseContext.childStyle ?? TextStyle(); + + switch (node.localName) { + //"b","i","em","strong","code","u","small","abbr","acronym" + case "b": + case "strong": + childStyle = childStyle.merge(TextStyle(fontWeight: FontWeight.bold)); + break; + case "i": + case "em": + childStyle = childStyle.merge(TextStyle(fontStyle: FontStyle.italic)); + break; + case "code": + childStyle = childStyle.merge(TextStyle(fontFamily: 'monospace')); + break; + case "u": + childStyle = childStyle.merge(TextStyle(decoration: TextDecoration.underline)); + break; + case "abbr": + case "acronym": + childStyle = childStyle.merge(TextStyle( + decoration: TextDecoration.underline, + decorationStyle: TextDecorationStyle.dotted, + )); + break; + case "small": + childStyle = childStyle.merge(TextStyle(fontSize: 12.0)); + break; + case "ol": + nextContext.indentLevel += 1; + nextContext.listChar = '#'; + nextContext.listCount = 0; + nextContext.blockType = 'ol'; + break; + case "ul": + nextContext.indentLevel += 1; + nextContext.listChar = '•'; + nextContext.listCount = 0; + nextContext.blockType = 'ul'; + break; + case "blockquote": + nextContext.indentLevel += 1; + nextContext.blockType = 'blockquote'; + break; + } + nextContext.childStyle = childStyle; + } + + // handle specialty elements + else if ( _supportedSpecialtyElements.contains(node.localName)) { + // should support "a","br","table","tbody","thead","tfoot","th","tr","td" + + switch (node.localName) { + case "a": + // if this item has block children, we create + // a container and gesture recognizer for the entire + // element, otherwise, we create a LinkTextSpan + String url = node.attributes['href'] ?? null; + + if (_hasBlockChild(node)) { + LinkBlock linkContainer = LinkBlock( + url: url, + margin: EdgeInsets.only(left:parseContext.indentLevel * indentSize), + onLinkTap: onLinkTap, + children: [], + ); + nextContext.parentElement = linkContainer; + nextContext.rootWidgetList.add(linkContainer); + } + else { + TextStyle linkStyle = parseContext.childStyle.merge( + TextStyle( + decoration: TextDecoration.underline, + color: Colors.blueAccent, + decorationColor: Colors.blueAccent, + ) + ); + LinkTextSpan span = LinkTextSpan( + style: linkStyle, + url: url, + onLinkTap: onLinkTap, + ); + if (parseContext.parentElement is TextSpan){ + nextContext.parentElement.children.add(span); + } + 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), + child: RichText(text: span), + ); + parseContext.rootWidgetList.add(blockElement); + } + nextContext.childStyle = linkStyle; + nextContext.parentElement = span; + } + break; + + case "br": + if (parseContext.parentElement != null && parseContext.parentElement is TextSpan) { + parseContext.parentElement.children.add(TextSpan(text:'\n', children: [])); + } + break; + + case "table": + case "tbody": + case "thead": + // new block, so clear out the parent element + parseContext.parentElement = null; + nextContext.parentElement = Column(crossAxisAlignment: CrossAxisAlignment.start,); + nextContext.rootWidgetList.add(nextContext.parentElement); + break; + + case "td": + case "th": + int colspan = 1; + if (node.attributes['colspan'] != null) { + colspan = int.tryParse(node.attributes['colspan']); + } + Expanded cell = Expanded( + flex: colspan, + child: Wrap(), + ); + nextContext.parentElement.children.add(cell); + nextContext.parentElement = cell.child; + break; + + case "tr": + Row row = Row(crossAxisAlignment: CrossAxisAlignment.center,); + nextContext.parentElement.children.add(row); + nextContext.parentElement = row; + break; + } + } + + // handle block elements + else if ( _supportedBlockElements.contains(node.localName)) { + + // block elements only show up at the "root" widget level + // so if we have a block element, reset the parentElement to null + parseContext.parentElement = null; + TextAlign textAlign = TextAlign.left; + + switch (node.localName) { + case "hr": + parseContext.rootWidgetList.add(Divider(height:1.0, color:Colors.black38)); + break; + case "img": + if (node.attributes['src'] != null) { + parseContext.rootWidgetList.add(Image.network(node.attributes['src'])); + } 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": + String leadingChar = parseContext.listChar; + if (parseContext.blockType == 'ol') { + nextContext.listCount += 1; + leadingChar = nextContext.listCount.toString() + '.'; + } + BlockText blockText = BlockText( + margin:EdgeInsets.only(left: parseContext.indentLevel * indentSize, top:3.0), + child: RichText( + text:TextSpan( + text:'', + style:nextContext.childStyle, + children: [], + ), + ), + leadingChar: '$leadingChar ', + ); + parseContext.rootWidgetList.add(blockText); + nextContext.parentElement = blockText.child.text; + nextContext.spansOnly = true; + break; + + case "h1": + nextContext.childStyle = nextContext.childStyle.merge( + TextStyle(fontSize: 26.0, fontWeight: FontWeight.bold), + ); + continue myDefault; + case "h2": + nextContext.childStyle = nextContext.childStyle.merge( + TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold), + ); + continue myDefault; + case "h3": + nextContext.childStyle = nextContext.childStyle.merge( + TextStyle(fontSize: 22.0, fontWeight: FontWeight.bold), + ); + continue myDefault; + case "h4": + nextContext.childStyle = nextContext.childStyle.merge( + TextStyle(fontSize: 20.0, fontWeight: FontWeight.w100), + ); + continue myDefault; + case "h5": + nextContext.childStyle = nextContext.childStyle.merge( + TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold), + ); + continue myDefault; + case "h6": + nextContext.childStyle = nextContext.childStyle.merge( + TextStyle(fontSize: 18.0, fontWeight: FontWeight.w100), + ); + continue myDefault; + + case "pre": + nextContext.condenseWhitespace = false; + continue myDefault; + + case "center": + textAlign = TextAlign.center; + // no break here + continue myDefault; + + myDefault: + default: + Decoration decoration; + if (parseContext.blockType == 'blockquote') { + decoration = BoxDecoration( + border: Border(left: BorderSide(color:Colors.black38, width:2.0)), + ); + nextContext.childStyle = nextContext.childStyle.merge(TextStyle( + fontStyle: FontStyle.italic, + )); + } + BlockText blockText = BlockText( + margin:EdgeInsets.symmetric(vertical:8.0), + padding:EdgeInsets.only(left: parseContext.indentLevel * indentSize,), + decoration:decoration, + child: RichText( + textAlign: textAlign, + text:TextSpan( + text:'', + style: nextContext.childStyle, + children: [], + ), + ), + ); + parseContext.rootWidgetList.add(blockText); + nextContext.parentElement = blockText.child.text; + nextContext.spansOnly = true; + } + } + + node.nodes.forEach((dom.Node childNode){ + _parseNode(childNode, nextContext); + }); + } + } + + // List _parseNodeList({ + // @required List nodeList, + // @required List rootWidgetList, // the widgetList accumulator + // int parentIndex, // the parent spans list accumulator + // int indentLevel = 0, + // int listCount = 0, + // String listChar = '•', + // String blockType, // blockType can be 'p', 'div', 'ul', 'ol', 'blockquote' + // bool condenseWhitespace = true, + // }) { + // return nodeList.map((node) { + // return _parseNode( + // node: node, + // rootWidgetList: rootWidgetList, + // parentIndex: parentIndex, + // indentLevel: indentLevel, + // listCount: listCount, + // listChar: listChar, + // blockType: blockType, + // condenseWhitespace: condenseWhitespace, + // ); + // }).toList(); + // } + + Paint _getPaint(Color color) { + Paint paint = new Paint(); + paint.color = color; + return paint; + } + + String condenseHtmlWhitespace(String stringToTrim) { + stringToTrim = stringToTrim.replaceAll("\n", " "); + while (stringToTrim.indexOf(" ") != -1) { + stringToTrim = stringToTrim.replaceAll(" ", " "); + } + return stringToTrim; + } + + bool _isNotFirstBreakTag(dom.Node node) { + int index = node.parentNode.nodes.indexOf(node); + if (index == 0) { + if (node.parentNode == null) { + return false; + } + return _isNotFirstBreakTag(node.parentNode); + } else if (node.parentNode.nodes[index - 1] is dom.Element) { + if ((node.parentNode.nodes[index - 1] as dom.Element).localName == "br") { + return true; + } + return false; + } else if (node.parentNode.nodes[index - 1] is dom.Text) { + if ((node.parentNode.nodes[index - 1] as dom.Text).text.trim() == "") { + return _isNotFirstBreakTag(node.parentNode.nodes[index - 1]); + } else { + return false; + } + } + return false; + } +} + + + + +class HtmlOldParser { + HtmlOldParser({ @required this.width, this.onLinkTap, this.renderNewlines = false, From 8fcdd431348ee03eec31a916cfcd2ffd9d189daf Mon Sep 17 00:00:00 2001 From: Jeff Mikels Date: Tue, 6 Nov 2018 11:45:20 -0500 Subject: [PATCH 029/638] fixing the test to keep using the old parser --- test/html_parser_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/html_parser_test.dart b/test/html_parser_test.dart index bf09259e41..af06eeecc1 100644 --- a/test/html_parser_test.dart +++ b/test/html_parser_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_html/flutter_html.dart'; void main() { test('Checks that `parse` does not throw an exception', () { - final elementList = HtmlParser(width: 42.0).parse("Bold Text"); + final elementList = HtmlOldParser(width: 42.0).parse("Bold Text"); expect(elementList, isNotNull); }); From 98b713e82ce37282fe9602d8cdf422de4dfc1dc2 Mon Sep 17 00:00:00 2001 From: Jeff Mikels Date: Tue, 6 Nov 2018 12:31:55 -0500 Subject: [PATCH 030/638] fixed ordered list numbering --- lib/flutter_html.dart | 32 +++++++++++++++----------------- lib/html_parser.dart | 40 ++++++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index a04b329d83..e7fdef9c15 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -9,7 +9,7 @@ class Html extends StatelessWidget { @required this.data, this.padding, this.backgroundColor, - this.defaultTextStyle = const TextStyle(color: Colors.black), + this.defaultTextStyle, this.onLinkTap, this.renderNewlines = false, this.customRender, @@ -37,22 +37,20 @@ class Html extends StatelessWidget { color: backgroundColor, width: width, child: DefaultTextStyle.merge( - style: defaultTextStyle, - child: Wrap( - alignment: WrapAlignment.start, - children: (useRichText) - ? HtmlRichTextParser( - width: width, - onLinkTap: onLinkTap, - renderNewlines: renderNewlines, - customRender: customRender, - ).parse(data) - : HtmlOldParser( - width: width, - onLinkTap: onLinkTap, - renderNewlines: renderNewlines, - customRender: customRender, - ).parse(data), + style: defaultTextStyle ?? DefaultTextStyle.of(context).style, + child: (useRichText) + ? HtmlRichTextParser( + width: width, + onLinkTap: onLinkTap, + renderNewlines: renderNewlines, + html: data, + ) + : HtmlOldParser( + width: width, + onLinkTap: onLinkTap, + renderNewlines: renderNewlines, + customRender: customRender, + html: data, ), ), ); diff --git a/lib/html_parser.dart b/lib/html_parser.dart index bfb66258f9..6a3bd6f052 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -117,13 +117,12 @@ class ParseContext { } } -class HtmlRichTextParser { +class HtmlRichTextParser extends StatelessWidget{ HtmlRichTextParser({ @required this.width, this.onLinkTap, this.renderNewlines = false, - this.customRender, - this.context, + this.html, }); final double indentSize = 10.0; @@ -131,8 +130,7 @@ class HtmlRichTextParser { final double width; final onLinkTap; final bool renderNewlines; - final CustomRender customRender; - final BuildContext context; + final String html; // style elements set a default style // for all child nodes @@ -210,7 +208,10 @@ class HtmlRichTextParser { } // Parses an html string and returns a list of RichText widgets that represent the body of your html document. - List parse(String data) { + + @override + Widget build(BuildContext context) { + String data = html; if (renderNewlines) { data = data.replaceAll("\n", "
"); @@ -229,7 +230,7 @@ class HtmlRichTextParser { // _parseNode(body, parseContext); // eliminate empty widgets - List retval = []; + List children = []; widgetList.forEach((dynamic w){ if (w is BlockText) { if (w.child.text == null) return; @@ -239,9 +240,12 @@ class HtmlRichTextParser { } else if (w is LinkTextSpan) { if (w.text.isEmpty && w.children.isEmpty) return; } - retval.add(w); + children.add(w); }); - return retval; + + return Column( + children: children, + ); } // THE WORKHORSE FUNCTION!! @@ -492,8 +496,10 @@ class HtmlRichTextParser { case "li": String leadingChar = parseContext.listChar; if (parseContext.blockType == 'ol') { - nextContext.listCount += 1; - leadingChar = nextContext.listCount.toString() + '.'; + // nextContext will handle nodes under this 'li' + // but we want to increment the count at this level + parseContext.listCount += 1; + leadingChar = parseContext.listCount.toString() + '.'; } BlockText blockText = BlockText( margin:EdgeInsets.only(left: parseContext.indentLevel * indentSize, top:3.0), @@ -651,18 +657,20 @@ class HtmlRichTextParser { -class HtmlOldParser { +class HtmlOldParser extends StatelessWidget { HtmlOldParser({ @required this.width, this.onLinkTap, this.renderNewlines = false, this.customRender, + this.html, }); final double width; final OnLinkTap onLinkTap; final bool renderNewlines; final CustomRender customRender; + final String html; static const _supportedElements = [ "a", @@ -739,6 +747,14 @@ class HtmlOldParser { "var", ]; + @override + Widget build(BuildContext context) { + return Wrap( + alignment: WrapAlignment.start, + children: parse(html), + ); + } + ///Parses an html string and returns a list of widgets that represent the body of your html document. List parse(String data) { List widgetList = new List(); From 9bc5cc6aee99d6da55ef87d04a591d14a793c7bf Mon Sep 17 00:00:00 2001 From: Richboy Date: Mon, 24 Dec 2018 07:22:03 -0500 Subject: [PATCH 031/638] Added support for sup and sub tags in addition to a new blockSpacing property --- example/main.dart | 38 +++++++-------- lib/flutter_html.dart | 11 +++-- lib/html_parser.dart | 104 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 120 insertions(+), 33 deletions(-) diff --git a/example/main.dart b/example/main.dart index cbe4b3fd71..55035f0847 100644 --- a/example/main.dart +++ b/example/main.dart @@ -63,29 +63,31 @@ class _MyHomePageState extends State {

There isn't really a team...

Installation

You cannot install a nonexistent product!

+

Don't ask me to find x in

+

log2(x2 - 6x) = 3 + log2(1 - x)

-

bdi and bdo Test:

-

- In the example below, usernames are shown along with the number of points in a contest. - If the bdi element is not supported in the browser, the username of the Arabic user would confuse the text (the bidirectional algorithm would put the colon and the number "90" next to the word "User" rather than next to the word "points"). -

- -
    -
  • User hrefs: 60 points
  • -
  • User jdoe: 80 points
  • -
  • User إيان: 90 points
  • - Swapped! - This text will go left to right! - With bdi: User إيان: 90 points - Without bdi: User إيان: 90 points - ltr w/ bdi: User إيان: 90 points - ltr w/o bdi: User إيان: 90 points -
+

bdi and bdo Test:

+

+ In the example below, usernames are shown along with the number of points in a contest. + If the bdi element is not supported in the browser, the username of the Arabic user would confuse the text (the bidirectional algorithm would put the colon and the number "90" next to the word "User" rather than next to the word "points"). +

+ +
    +
  • User hrefs: 60 points
  • +
  • User jdoe: 80 points
  • +
  • User إيان: 90 points
  • + Swapped! + This text will go left to right! + With bdi: User إيان: 90 points + Without bdi: User إيان: 90 points + ltr w/ bdi: User إيان: 90 points + ltr w/o bdi: User إيان: 90 points +
- + diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 05f344c437..97168a8b65 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -13,6 +13,7 @@ class Html extends StatelessWidget { this.onLinkTap, this.renderNewlines = false, this.customRender, + this.blockSpacing, }) : super(key: key); final String data; @@ -21,6 +22,7 @@ class Html extends StatelessWidget { final TextStyle defaultTextStyle; final OnLinkTap onLinkTap; final bool renderNewlines; + final double blockSpacing; /// Either return a custom widget for specific node types or return null to /// fallback to the default rendering. @@ -39,10 +41,11 @@ class Html extends StatelessWidget { child: Wrap( alignment: WrapAlignment.start, children: HtmlParser( - width: width, - onLinkTap: onLinkTap, - renderNewlines: renderNewlines, - customRender: customRender, + width: width, + onLinkTap: onLinkTap, + renderNewlines: renderNewlines, + customRender: customRender, + blockSpacing: blockSpacing ).parse(data), ), ), diff --git a/lib/html_parser.dart b/lib/html_parser.dart index b9fa8f4b19..b687b6c84b 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -4,6 +4,8 @@ import 'package:html/dom.dart' as dom; typedef CustomRender = Widget Function(dom.Node node, List children); typedef OnLinkTap = void Function(String url); +const OFFSET_TAGS_FONT_SIZE_FACTOR = 0.7; //The ratio of the parent font for each of the offset tags: sup or sub +const BLOCK_SPACING = 14.0; //The default spacing between block elements. Can be customized by the new verticalSpacing property class HtmlParser { HtmlParser({ @@ -11,12 +13,14 @@ class HtmlParser { this.onLinkTap, this.renderNewlines = false, this.customRender, - }); + blockSpacing + }) : this.blockSpacing = blockSpacing ?? BLOCK_SPACING; final double width; final OnLinkTap onLinkTap; final bool renderNewlines; final CustomRender customRender; + final double blockSpacing; static const _supportedElements = [ "a", @@ -78,6 +82,8 @@ class HtmlParser { "span", "strike", "strong", + "sub", + "sup", "table", "tbody", "td", @@ -108,7 +114,7 @@ class HtmlParser { Widget _parseNode(dom.Node node) { if (customRender != null) { final Widget customWidget = - customRender(node, _parseNodeList(node.nodes)); + customRender(node, _parseNodeList(node.nodes)); if (customWidget != null) { return customWidget; } @@ -170,6 +176,7 @@ class HtmlParser { return Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ); @@ -177,6 +184,7 @@ class HtmlParser { return Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ); @@ -219,10 +227,11 @@ class HtmlParser { ); case "blockquote": return Padding( - padding: EdgeInsets.fromLTRB(40.0, 14.0, 40.0, 14.0), + padding: EdgeInsets.fromLTRB(40.0, BLOCK_SPACING, 40.0, BLOCK_SPACING), child: Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ), @@ -231,18 +240,20 @@ class HtmlParser { return Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ); case "br": if (_isNotFirstBreakTag(node)) { - return Container(width: width, height: 14.0); + return Container(width: width, height: BLOCK_SPACING); } return Container(width: width); case "caption": return Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, alignment: WrapAlignment.center, children: _parseNodeList(node.nodes), ), @@ -251,6 +262,7 @@ class HtmlParser { return Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), alignment: WrapAlignment.center, )); @@ -282,6 +294,7 @@ class HtmlParser { child: Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), )); @@ -307,12 +320,13 @@ class HtmlParser { return Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ); case "dl": return Padding( - padding: EdgeInsets.only(top: 14.0, bottom: 14.0), + padding: EdgeInsets.only(top: BLOCK_SPACING, bottom: BLOCK_SPACING), child: Column( children: _parseNodeList(node.nodes), crossAxisAlignment: CrossAxisAlignment.start, @@ -336,7 +350,7 @@ class HtmlParser { ); case "figure": return Padding( - padding: EdgeInsets.fromLTRB(40.0, 14.0, 40.0, 14.0), + padding: EdgeInsets.fromLTRB(40.0, BLOCK_SPACING, 40.0, BLOCK_SPACING), child: Column( children: _parseNodeList(node.nodes), crossAxisAlignment: CrossAxisAlignment.center, @@ -345,6 +359,7 @@ class HtmlParser { return Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ); @@ -353,6 +368,7 @@ class HtmlParser { child: Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ), @@ -366,6 +382,7 @@ class HtmlParser { child: Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ), @@ -379,6 +396,7 @@ class HtmlParser { child: Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ), @@ -392,6 +410,7 @@ class HtmlParser { child: Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ), @@ -405,6 +424,7 @@ class HtmlParser { child: Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ), @@ -418,6 +438,7 @@ class HtmlParser { child: Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ), @@ -430,6 +451,7 @@ class HtmlParser { return Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ); @@ -501,9 +523,13 @@ class HtmlParser { return Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: [ mark, - Wrap(children: _parseNodeList(node.nodes)) + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: _parseNodeList(node.nodes) + ) ], ), ); @@ -511,6 +537,7 @@ class HtmlParser { return Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ); @@ -528,6 +555,7 @@ class HtmlParser { return Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ); @@ -535,6 +563,8 @@ class HtmlParser { return Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.start, children: _parseNodeList(node.nodes), ), ); @@ -545,17 +575,19 @@ class HtmlParser { ); case "p": return Padding( - padding: EdgeInsets.only(top: 14.0, bottom: 14.0), + padding: EdgeInsets.only(top: BLOCK_SPACING, bottom: BLOCK_SPACING), child: Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + alignment: WrapAlignment.start, //@ominibyte Added this for when the line breaks. I think it looks better children: _parseNodeList(node.nodes), ), ), ); case "pre": return Padding( - padding: const EdgeInsets.all(14.0), + padding: const EdgeInsets.all(BLOCK_SPACING), child: DefaultTextStyle.merge( child: Text(node.innerHtml), style: const TextStyle( @@ -610,6 +642,7 @@ class HtmlParser { return Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ); @@ -644,6 +677,53 @@ class HtmlParser { fontWeight: FontWeight.bold, ), ); + case "sub": + case "sup": + //Use builder to capture the parent font to inherit the font styles + return Builder(builder: (BuildContext context){ + final DefaultTextStyle parent = DefaultTextStyle.of(context); + TextStyle parentStyle = parent.style; + + var painter = new TextPainter(text: new TextSpan(text: node.text, style: parentStyle,), textDirection: TextDirection.ltr); + painter.layout(); + //print(painter.size); + + //Get the height from the default text + var height = painter.size.height * 1.35; //compute a higher height for the text to increase the offset of the Positioned text + + painter = new TextPainter(text: new TextSpan(text: node.text, style: parentStyle.merge(TextStyle(fontSize: parentStyle.fontSize * OFFSET_TAGS_FONT_SIZE_FACTOR)),), textDirection: TextDirection.ltr); + painter.layout(); + //print(painter.size); + + //Get the width from the reduced/positioned text + var width = painter.size.width; + + //print("Width: $width, Height: $height"); + + return DefaultTextStyle.merge( + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Stack( + fit: StackFit.loose, + children: [ + //The Stack needs a non-positioned object for the next widget to respect the space so we create + //a sized box to fill the required space + SizedBox(width: width, height: height,), + DefaultTextStyle.merge( + child: Positioned( + child: Wrap(children: _parseNodeList(node.nodes)), + bottom: node.localName == "sub" ? 0 : null, + top: node.localName == "sub" ? null : 0, + ), + style: TextStyle(fontSize: parentStyle.fontSize * OFFSET_TAGS_FONT_SIZE_FACTOR), + ) + ], + ) + ], + ), + ); + }); case "table": return Column( children: _parseNodeList(node.nodes), @@ -662,11 +742,12 @@ class HtmlParser { return Expanded( flex: colspan, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), ), ); case "template": - //Not usually displayed in HTML + //Not usually displayed in HTML return Container(); case "tfoot": return Column( @@ -682,6 +763,7 @@ class HtmlParser { child: Expanded( flex: colspan, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, alignment: WrapAlignment.center, children: _parseNodeList(node.nodes), ), @@ -743,7 +825,7 @@ class HtmlParser { return Wrap(); } if (node.text.trim() == "" && node.text.indexOf(" ") != -1) { - node.text = " "; + node.text = "";//@ominibyte Looks better without the space } String finalText = trimStringHtml(node.text); From ec962c6965e559f62d5fb3718f59176b3dba7ceb Mon Sep 17 00:00:00 2001 From: Richboy Date: Mon, 24 Dec 2018 07:52:31 -0500 Subject: [PATCH 032/638] Fixed a bug in blockSpacing --- lib/html_parser.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index b687b6c84b..6340f775df 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -227,7 +227,7 @@ class HtmlParser { ); case "blockquote": return Padding( - padding: EdgeInsets.fromLTRB(40.0, BLOCK_SPACING, 40.0, BLOCK_SPACING), + padding: EdgeInsets.fromLTRB(40.0, blockSpacing, 40.0, blockSpacing), child: Container( width: width, child: Wrap( @@ -246,7 +246,7 @@ class HtmlParser { ); case "br": if (_isNotFirstBreakTag(node)) { - return Container(width: width, height: BLOCK_SPACING); + return Container(width: width, height: blockSpacing); } return Container(width: width); case "caption": @@ -326,7 +326,7 @@ class HtmlParser { ); case "dl": return Padding( - padding: EdgeInsets.only(top: BLOCK_SPACING, bottom: BLOCK_SPACING), + padding: EdgeInsets.only(top: blockSpacing, bottom: blockSpacing), child: Column( children: _parseNodeList(node.nodes), crossAxisAlignment: CrossAxisAlignment.start, @@ -350,7 +350,7 @@ class HtmlParser { ); case "figure": return Padding( - padding: EdgeInsets.fromLTRB(40.0, BLOCK_SPACING, 40.0, BLOCK_SPACING), + padding: EdgeInsets.fromLTRB(40.0, blockSpacing, 40.0, blockSpacing), child: Column( children: _parseNodeList(node.nodes), crossAxisAlignment: CrossAxisAlignment.center, @@ -575,7 +575,7 @@ class HtmlParser { ); case "p": return Padding( - padding: EdgeInsets.only(top: BLOCK_SPACING, bottom: BLOCK_SPACING), + padding: EdgeInsets.only(top: blockSpacing, bottom: blockSpacing), child: Container( width: width, child: Wrap( @@ -587,7 +587,7 @@ class HtmlParser { ); case "pre": return Padding( - padding: const EdgeInsets.all(BLOCK_SPACING), + padding: EdgeInsets.all(blockSpacing), child: DefaultTextStyle.merge( child: Text(node.innerHtml), style: const TextStyle( From 7136a59f35ca3b3372a969ccb60f22370b003d9a Mon Sep 17 00:00:00 2001 From: Jeff Mikels Date: Sat, 19 Jan 2019 14:47:37 -0500 Subject: [PATCH 033/638] check for null before calling isEmpty --- lib/html_parser.dart | 312 +++++++++++++++++++++---------------- test/html_parser_test.dart | 8 +- 2 files changed, 182 insertions(+), 138 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 6a3bd6f052..706e434a77 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -21,14 +21,20 @@ class LinkTextSpan extends TextSpan { // stateful widget that then hands the recognizer to the TextSpan. final String url; - LinkTextSpan({ TextStyle style, this.url, String text, OnLinkTap onLinkTap, List children }) : super( - style: style, - text: text, - children: children ?? [], - recognizer: TapGestureRecognizer()..onTap = () { - onLinkTap(url); - } - ); + LinkTextSpan( + {TextStyle style, + this.url, + String text, + OnLinkTap onLinkTap, + List children}) + : super( + style: style, + text: text, + children: children ?? [], + recognizer: TapGestureRecognizer() + ..onTap = () { + onLinkTap(url); + }); } class LinkBlock extends Container { @@ -38,73 +44,80 @@ class LinkBlock extends Container { // final OnLinkTap onLinkTap; final List children; - LinkBlock({ String url, EdgeInsets padding, EdgeInsets margin, OnLinkTap onLinkTap, List this.children }):super( - padding: padding, - margin:margin, - child: GestureDetector( - onTap: (){ - onLinkTap(url); - }, - child:Column( - children: children, - ) - ) - ); + LinkBlock( + {String url, + EdgeInsets padding, + EdgeInsets margin, + OnLinkTap onLinkTap, + List this.children}) + : super( + padding: padding, + margin: margin, + child: GestureDetector( + onTap: () { + onLinkTap(url); + }, + child: Column( + children: children, + ))); } class BlockText extends StatelessWidget { - final RichText child; final EdgeInsets padding; final EdgeInsets margin; final String leadingChar; final Decoration decoration; - BlockText({@required this.child, this.padding, this.margin, this.leadingChar = '',this.decoration}); - + BlockText( + {@required this.child, + this.padding, + this.margin, + this.leadingChar = '', + this.decoration}); + @override Widget build(BuildContext context) { return Container( - width:double.infinity, - padding: this.padding, - margin: this.margin, - decoration:this.decoration, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - leadingChar.isNotEmpty ? Text(leadingChar) : Container(), - Expanded(child:child), - ], - ) - ); + width: double.infinity, + padding: this.padding, + margin: this.margin, + decoration: this.decoration, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + leadingChar.isNotEmpty ? Text(leadingChar) : Container(), + Expanded(child: child), + ], + )); } } class ParseContext { - List rootWidgetList; // the widgetList accumulator - dynamic parentElement; // the parent spans accumulator + List rootWidgetList; // the widgetList accumulator + dynamic parentElement; // the parent spans accumulator int indentLevel = 0; int listCount = 0; String listChar = '•'; - String blockType; // blockType can be 'p', 'div', 'ul', 'ol', 'blockquote' + String blockType; // blockType can be 'p', 'div', 'ul', 'ol', 'blockquote' bool condenseWhitespace = true; bool spansOnly = false; TextStyle childStyle; - ParseContext({ - this.rootWidgetList, - this.parentElement, - this.indentLevel = 0, - this.listCount = 0, - this.listChar = '•', - this.blockType, - this.condenseWhitespace = true, - this.spansOnly = false, - this.childStyle}) { - childStyle = childStyle ?? TextStyle(); - } + ParseContext( + {this.rootWidgetList, + this.parentElement, + this.indentLevel = 0, + this.listCount = 0, + this.listChar = '•', + this.blockType, + this.condenseWhitespace = true, + this.spansOnly = false, + this.childStyle}) { + childStyle = childStyle ?? TextStyle(); + } - ParseContext.fromContext(ParseContext parseContext){ + ParseContext.fromContext(ParseContext parseContext) { rootWidgetList = parseContext.rootWidgetList; parentElement = parseContext.parentElement; indentLevel = parseContext.indentLevel; @@ -117,7 +130,7 @@ class ParseContext { } } -class HtmlRichTextParser extends StatelessWidget{ +class HtmlRichTextParser extends StatelessWidget { HtmlRichTextParser({ @required this.width, this.onLinkTap, @@ -136,7 +149,18 @@ class HtmlRichTextParser extends StatelessWidget{ // for all child nodes // treat ol, ul, and blockquote like style elements also static const _supportedStyleElements = [ - "b","i","em","strong","code","u","small","abbr","acronym", "ol", "ul", "blockquote" + "b", + "i", + "em", + "strong", + "code", + "u", + "small", + "abbr", + "acronym", + "ol", + "ul", + "blockquote" ]; // specialty elements require unique handling @@ -161,7 +185,7 @@ class HtmlRichTextParser extends StatelessWidget{ // we simply treat it as a new block level element static const _supportedBlockElements = [ "article" - "body", + "body", "center", "dd", "dfn", @@ -199,7 +223,8 @@ 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; }); @@ -218,7 +243,7 @@ class HtmlRichTextParser extends StatelessWidget{ } dom.Document document = parser.parse(data); dom.Node body = document.body; - + List widgetList = new List(); ParseContext parseContext = ParseContext( rootWidgetList: widgetList, @@ -226,15 +251,17 @@ class HtmlRichTextParser extends StatelessWidget{ ); // ignore the top level "body" - body.nodes.forEach((dom.Node node)=>_parseNode(node, parseContext)); + body.nodes.forEach((dom.Node node) => _parseNode(node, parseContext)); // _parseNode(body, parseContext); - // eliminate empty widgets + // filter out empty widgets List children = []; - widgetList.forEach((dynamic w){ + widgetList.forEach((dynamic w) { if (w is BlockText) { if (w.child.text == null) return; - if (w.child.text.text.isEmpty && w.child.text.children.isEmpty) return; + if ((w.child.text.text == null || w.child.text.text.isEmpty) && + (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) { @@ -262,11 +289,9 @@ class HtmlRichTextParser extends StatelessWidget{ // // each iteration creates a new parseContext as a copy of the previous one if it needs to void _parseNode(dom.Node node, ParseContext parseContext) { - // TEXT ONLY NODES // a text only node is a child of a tag with no inner html if (node is dom.Text) { - // WHITESPACE CONSIDERATIONS --- // truly empty nodes, should just be ignored if (node.text.trim() == "" && node.text.indexOf(" ") == -1) { @@ -274,44 +299,50 @@ class HtmlRichTextParser extends StatelessWidget{ } // empty strings of whitespace might be significant or not, condense it by default - if (node.text.trim() == "" && node.text.indexOf(" ") != -1 && parseContext.condenseWhitespace) { + if (node.text.trim() == "" && + node.text.indexOf(" ") != -1 && + parseContext.condenseWhitespace) { node.text = " "; } // we might want to preserve internal whitespace - String finalText = parseContext.condenseWhitespace ? condenseHtmlWhitespace(node.text) : node.text; + String finalText = parseContext.condenseWhitespace + ? condenseHtmlWhitespace(node.text) + : node.text; // if this is part of a string of spans, we will preserve leading and trailing whitespace - if (!(parseContext.parentElement is TextSpan || parseContext.parentElement is LinkTextSpan)) + if (!(parseContext.parentElement is TextSpan || + parseContext.parentElement is LinkTextSpan)) finalText = finalText.trim(); - // NOW WE HAVE OUR TRULY FINAL TEXT - debugPrint("Plain Text Node: '$finalText'"); + // 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 // if there is no parentElement, contain the span in a BlockText - if (parseContext.parentElement == null){ + if (parseContext.parentElement == null) { parseContext.parentElement = span; - parseContext.rootWidgetList.add(BlockText(child:RichText(text: span))); + parseContext.rootWidgetList.add(BlockText(child: RichText(text: span))); - // if the parent is a LinkTextSpan, keep the main attributes of that span going. - } else if (parseContext.parentElement is LinkTextSpan){ + // if the parent is a LinkTextSpan, keep the main attributes of that span going. + } 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), - url: parseContext.parentElement.url, - text: finalText, - onLinkTap: onLinkTap, - ) - ); - - // if the parent is a normal span, just add this to that list + parseContext.parentElement.children.add(LinkTextSpan( + style: + parseContext.parentElement.style.merge(parseContext.childStyle), + url: parseContext.parentElement.url, + text: finalText, + onLinkTap: onLinkTap, + )); + + // if the parent is a normal span, just add this to that list } else { parseContext.parentElement.children.add(span); } @@ -320,12 +351,12 @@ class HtmlRichTextParser extends StatelessWidget{ // OTHER ELEMENT NODES else if (node is dom.Element) { assert(() { - debugPrint("Found ${node.localName}"); - debugPrint(node.outerHtml); + // debugPrint("Found ${node.localName}"); + // debugPrint(node.outerHtml); return true; }()); - if (! _supportedElements.contains(node.localName)) { + if (!_supportedElements.contains(node.localName)) { return; } @@ -334,24 +365,27 @@ class HtmlRichTextParser extends StatelessWidget{ ParseContext nextContext = new ParseContext.fromContext(parseContext); // handle style elements - if ( _supportedStyleElements.contains(node.localName)) { + if (_supportedStyleElements.contains(node.localName)) { TextStyle childStyle = parseContext.childStyle ?? TextStyle(); switch (node.localName) { //"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 "em": - childStyle = childStyle.merge(TextStyle(fontStyle: FontStyle.italic)); + childStyle = + childStyle.merge(TextStyle(fontStyle: FontStyle.italic)); break; case "code": childStyle = childStyle.merge(TextStyle(fontFamily: 'monospace')); break; case "u": - childStyle = childStyle.merge(TextStyle(decoration: TextDecoration.underline)); + childStyle = childStyle + .merge(TextStyle(decoration: TextDecoration.underline)); break; case "abbr": case "acronym": @@ -384,7 +418,7 @@ class HtmlRichTextParser extends StatelessWidget{ } // handle specialty elements - else if ( _supportedSpecialtyElements.contains(node.localName)) { + else if (_supportedSpecialtyElements.contains(node.localName)) { // should support "a","br","table","tbody","thead","tfoot","th","tr","td" switch (node.localName) { @@ -397,33 +431,32 @@ 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: [], ); nextContext.parentElement = linkContainer; nextContext.rootWidgetList.add(linkContainer); - } - else { - TextStyle linkStyle = parseContext.childStyle.merge( - TextStyle( - decoration: TextDecoration.underline, - color: Colors.blueAccent, - decorationColor: Colors.blueAccent, - ) - ); + } else { + TextStyle linkStyle = parseContext.childStyle.merge(TextStyle( + decoration: TextDecoration.underline, + color: Colors.blueAccent, + decorationColor: Colors.blueAccent, + )); LinkTextSpan span = LinkTextSpan( style: linkStyle, url: url, onLinkTap: onLinkTap, + children: [], ); - if (parseContext.parentElement is TextSpan){ + if (parseContext.parentElement is TextSpan) { nextContext.parentElement.children.add(span); - } - else { + } 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); @@ -434,17 +467,21 @@ 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; - + case "table": case "tbody": case "thead": // new block, so clear out the parent element parseContext.parentElement = null; - nextContext.parentElement = Column(crossAxisAlignment: CrossAxisAlignment.start,); + nextContext.parentElement = Column( + crossAxisAlignment: CrossAxisAlignment.start, + ); nextContext.rootWidgetList.add(nextContext.parentElement); break; @@ -463,7 +500,9 @@ class HtmlRichTextParser extends StatelessWidget{ break; case "tr": - Row row = Row(crossAxisAlignment: CrossAxisAlignment.center,); + Row row = Row( + crossAxisAlignment: CrossAxisAlignment.center, + ); nextContext.parentElement.children.add(row); nextContext.parentElement = row; break; @@ -471,8 +510,7 @@ class HtmlRichTextParser extends StatelessWidget{ } // handle block elements - else if ( _supportedBlockElements.contains(node.localName)) { - + else if (_supportedBlockElements.contains(node.localName)) { // block elements only show up at the "root" widget level // so if we have a block element, reset the parentElement to null parseContext.parentElement = null; @@ -480,17 +518,22 @@ 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) { - parseContext.rootWidgetList.add(Image.network(node.attributes['src'])); + parseContext.rootWidgetList + .add(Image.network(node.attributes['src'])); } 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:[],)) - )); + 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": @@ -502,11 +545,12 @@ 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:'', - style:nextContext.childStyle, + text: TextSpan( + text: '', + style: nextContext.childStyle, children: [], ), ), @@ -516,7 +560,7 @@ class HtmlRichTextParser extends StatelessWidget{ nextContext.parentElement = blockText.child.text; nextContext.spansOnly = true; break; - + case "h1": nextContext.childStyle = nextContext.childStyle.merge( TextStyle(fontSize: 26.0, fontWeight: FontWeight.bold), @@ -562,20 +606,23 @@ 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:EdgeInsets.symmetric(vertical:8.0), - padding:EdgeInsets.only(left: parseContext.indentLevel * indentSize,), - decoration:decoration, + margin: EdgeInsets.symmetric(vertical: 8.0), + padding: EdgeInsets.only( + left: parseContext.indentLevel * indentSize, + ), + decoration: decoration, child: RichText( textAlign: textAlign, - text:TextSpan( - text:'', + text: TextSpan( + text: '', style: nextContext.childStyle, children: [], ), @@ -587,7 +634,7 @@ class HtmlRichTextParser extends StatelessWidget{ } } - node.nodes.forEach((dom.Node childNode){ + node.nodes.forEach((dom.Node childNode) { _parseNode(childNode, nextContext); }); } @@ -654,9 +701,6 @@ class HtmlRichTextParser extends StatelessWidget{ } } - - - class HtmlOldParser extends StatelessWidget { HtmlOldParser({ @required this.width, @@ -778,7 +822,7 @@ class HtmlOldParser extends StatelessWidget { if (node is dom.Element) { assert(() { - debugPrint("Found ${node.localName}"); + // debugPrint("Found ${node.localName}"); return true; }()); @@ -1413,7 +1457,7 @@ class HtmlOldParser extends StatelessWidget { node.text = " "; } - debugPrint("Plain Text Node: '${trimStringHtml(node.text)}'"); + // debugPrint("Plain Text Node: '${trimStringHtml(node.text)}'"); String finalText = trimStringHtml(node.text); //Temp fix for https://github.com/flutter/flutter/issues/736 if (finalText.endsWith(" ")) { diff --git a/test/html_parser_test.dart b/test/html_parser_test.dart index af06eeecc1..c86c4662f1 100644 --- a/test/html_parser_test.dart +++ b/test/html_parser_test.dart @@ -4,10 +4,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_html/flutter_html.dart'; void main() { - test('Checks that `parse` does not throw an exception', () { - final elementList = HtmlOldParser(width: 42.0).parse("Bold Text"); - expect(elementList, isNotNull); - }); + // test('Checks that `parse` does not throw an exception', () { + // final elementList = HtmlOldParser(width: 42.0).parse("Bold Text"); + // expect(elementList, isNotNull); + // }); testWidgets('Tests some plain old text', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( From 48bc6d329ecfbd75f9c5225d6cc1e64d7ceeb59d Mon Sep 17 00:00:00 2001 From: jiangliang Date: Tue, 29 Jan 2019 13:52:03 +0800 Subject: [PATCH 034/638] Center label vertically centered --- lib/html_parser.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index b9fa8f4b19..02477f5daf 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -251,6 +251,7 @@ class HtmlParser { return Container( width: width, child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: _parseNodeList(node.nodes), alignment: WrapAlignment.center, )); From dc9207710760578e070e6939c235e2283ab7816b Mon Sep 17 00:00:00 2001 From: Jeff Mikels Date: Thu, 31 Jan 2019 14:04:32 -0500 Subject: [PATCH 035/638] plain text nodes inside blockquote, ol, and ul, are now properly indented --- lib/html_parser.dart | 71 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 706e434a77..8b36b8dac6 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -102,6 +102,7 @@ class ParseContext { String blockType; // blockType can be 'p', 'div', 'ul', 'ol', 'blockquote' bool condenseWhitespace = true; bool spansOnly = false; + bool inBlock = false; TextStyle childStyle; ParseContext( @@ -113,6 +114,7 @@ class ParseContext { this.blockType, this.condenseWhitespace = true, this.spansOnly = false, + this.inBlock = false, this.childStyle}) { childStyle = childStyle ?? TextStyle(); } @@ -126,6 +128,7 @@ class ParseContext { blockType = parseContext.blockType; condenseWhitespace = parseContext.condenseWhitespace; spansOnly = parseContext.spansOnly; + inBlock = parseContext.inBlock; childStyle = parseContext.childStyle ?? TextStyle(); } } @@ -298,14 +301,14 @@ class HtmlRichTextParser extends StatelessWidget { return; } - // empty strings of whitespace might be significant or not, condense it by default - if (node.text.trim() == "" && - node.text.indexOf(" ") != -1 && - parseContext.condenseWhitespace) { - node.text = " "; - } + // if (node.text.trim() == "" && + // node.text.indexOf(" ") != -1 && + // parseContext.condenseWhitespace) { + // node.text = " "; + // } // we might want to preserve internal whitespace + // empty strings of whitespace might be significant or not, condense it by default String finalText = parseContext.condenseWhitespace ? condenseHtmlWhitespace(node.text) : node.text; @@ -315,6 +318,9 @@ class HtmlRichTextParser extends StatelessWidget { parseContext.parentElement is LinkTextSpan)) finalText = finalText.trim(); + // if the finalText is actually empty, just return + if (finalText.isEmpty) return; + // NOW WE HAVE OUR TRULY FINAL TEXT // debugPrint("Plain Text Node: '$finalText'"); @@ -326,10 +332,48 @@ class HtmlRichTextParser extends StatelessWidget { // 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; + // if there is no parentElement, contain the span in a BlockText if (parseContext.parentElement == null) { + // if this is inside a context that should be treated like a block + // but the context is not actually a block, create a block + // and append it to the root widget tree + if (treatLikeBlock) { + Decoration decoration; + if (parseContext.blockType == 'blockquote') { + decoration = BoxDecoration( + 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), + padding: EdgeInsets.all(2.0), + decoration: decoration, + child: RichText( + textAlign: TextAlign.left, + text: span, + ), + ); + parseContext.rootWidgetList.add(blockText); + } else { + parseContext.rootWidgetList + .add(BlockText(child: RichText(text: span))); + } + + // this allows future items to be added as children parseContext.parentElement = span; - parseContext.rootWidgetList.add(BlockText(child: RichText(text: span))); // if the parent is a LinkTextSpan, keep the main attributes of that span going. } else if (parseContext.parentElement is LinkTextSpan) { @@ -348,6 +392,7 @@ class HtmlRichTextParser extends StatelessWidget { } return; } + // OTHER ELEMENT NODES else if (node is dom.Element) { assert(() { @@ -460,6 +505,7 @@ class HtmlRichTextParser extends StatelessWidget { child: RichText(text: span), ); parseContext.rootWidgetList.add(blockElement); + nextContext.inBlock = true; } nextContext.childStyle = linkStyle; nextContext.parentElement = span; @@ -559,6 +605,7 @@ class HtmlRichTextParser extends StatelessWidget { parseContext.rootWidgetList.add(blockText); nextContext.parentElement = blockText.child.text; nextContext.spansOnly = true; + nextContext.inBlock = true; break; case "h1": @@ -614,10 +661,11 @@ class HtmlRichTextParser extends StatelessWidget { )); } BlockText blockText = BlockText( - margin: EdgeInsets.symmetric(vertical: 8.0), - padding: EdgeInsets.only( - 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( textAlign: textAlign, @@ -631,6 +679,7 @@ class HtmlRichTextParser extends StatelessWidget { parseContext.rootWidgetList.add(blockText); nextContext.parentElement = blockText.child.text; nextContext.spansOnly = true; + nextContext.inBlock = true; } } From ce387e01aec2a40d54353ef054c66cab233940cf Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Thu, 31 Jan 2019 18:16:44 -0700 Subject: [PATCH 036/638] Version 0.9.0 --- .gitignore | 2 + CHANGELOG.md | 4 + LICENSE | 2 +- README.md | 10 +- lib/flutter_html.dart | 26 ++--- pubspec.lock | 220 +----------------------------------------- pubspec.yaml | 36 +------ 7 files changed, 34 insertions(+), 266 deletions(-) diff --git a/.gitignore b/.gitignore index 886e88e840..68c69d3ef2 100644 --- a/.gitignore +++ b/.gitignore @@ -99,6 +99,8 @@ obj/ .idea/sqlDataSources.xml .idea/dynamic.xml .idea/uiDesigner.xml +.idea/markdown-navigator.xml +.idea/markdown-navigator/ # OS-specific files .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index a483ba0522..dd0e9f1062 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.9.0] - January 31, 2019: + +* Adds an alternate `RichText` parser and `useRichText` parameter. ([#37](https://github.com/Sub6Resources/flutter_html/pull/37)) + ## [0.8.2] - November 1, 2018: * Removes debug prints. diff --git a/LICENSE b/LICENSE index 0fd1500466..b0f300d959 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Matthew Whitaker +Copyright (c) 2019 Matthew Whitaker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ce2f551c07..70a7b278b3 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.8.2 + flutter_html: ^0.9.0 ## Currently Supported HTML Tags: `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`, `table`, `tbody`, `td`, `template`, `tfoot`, `th`, `thead`, `time`, `tr`, `tt`, `u`, `ul`, `var` @@ -72,4 +72,10 @@ until official support is added. } } }, - ) \ No newline at end of file + ) + +## `useRichText` parameter + +This package has a known issue where text does not wrap correctly. Setting `useRichText` to true fixes the issue +by using an alternate parser. The alternate parser, however, does not support the `customRender` callback, and several elements +supported by the default parser are not supported by the alternate parser. \ No newline at end of file diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index e7fdef9c15..027ea021e8 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -39,19 +39,19 @@ class Html extends StatelessWidget { child: DefaultTextStyle.merge( style: defaultTextStyle ?? DefaultTextStyle.of(context).style, child: (useRichText) - ? HtmlRichTextParser( - width: width, - onLinkTap: onLinkTap, - renderNewlines: renderNewlines, - html: data, - ) - : HtmlOldParser( - width: width, - onLinkTap: onLinkTap, - renderNewlines: renderNewlines, - customRender: customRender, - html: data, - ), + ? HtmlRichTextParser( + width: width, + onLinkTap: onLinkTap, + renderNewlines: renderNewlines, + html: data, + ) + : HtmlOldParser( + width: width, + onLinkTap: onLinkTap, + renderNewlines: renderNewlines, + customRender: customRender, + html: data, + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index 5bb8a63f39..4d7c167048 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,13 +1,6 @@ # Generated by pub # See https://www.dartlang.org/tools/pub/glossary#lockfile packages: - analyzer: - dependency: transitive - description: - name: analyzer - url: "https://pub.dartlang.org" - source: hosted - version: "0.32.4" args: dependency: transitive description: @@ -43,20 +36,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.11" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.6" csslib: dependency: transitive description: @@ -74,20 +53,6 @@ packages: description: flutter source: sdk version: "0.0.0" - front_end: - dependency: transitive - description: - name: front_end - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.4" - glob: - dependency: transitive - description: - name: glob - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.7" html: dependency: "direct main" description: @@ -95,55 +60,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.13.3+3" - http: - dependency: transitive - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.11.3+17" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.5" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.3" - io: - dependency: transitive - description: - name: io - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.3" - js: - dependency: transitive - description: - name: js - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.1+1" - json_rpc_2: - dependency: transitive - description: - name: json_rpc_2 - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.9" - kernel: - dependency: transitive - description: - name: kernel - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.4" logging: dependency: transitive description: @@ -165,41 +81,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.6" - mime: - dependency: transitive - description: - name: mime - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.6+2" - multi_server_socket: - dependency: transitive - description: - name: multi_server_socket - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - node_preamble: - dependency: transitive - description: - name: node_preamble - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.4" - package_config: - dependency: transitive - description: - name: package_config - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" - package_resolver: - dependency: transitive - description: - name: package_resolver - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" path: dependency: transitive description: @@ -207,81 +88,18 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.2" - plugin: - dependency: transitive - description: - name: plugin - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0+3" - pool: - dependency: transitive - description: - name: pool - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.6" - pub_semver: - dependency: transitive - description: - name: pub_semver - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.2" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.0+1" - shelf: - dependency: transitive - description: - name: shelf - url: "https://pub.dartlang.org" - source: hosted - version: "0.7.3+3" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - shelf_static: - dependency: transitive - description: - name: shelf_static - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.8" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.2+4" + version: "2.0.1" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.5" - source_maps: - dependency: transitive - description: - name: source_maps - url: "https://pub.dartlang.org" - source: hosted - version: "0.10.7" source_span: dependency: transitive description: @@ -317,13 +135,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" - test: + test_api: dependency: transitive description: - name: test + name: test_api url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "0.2.1" typed_data: dependency: transitive description: @@ -345,34 +163,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" - vm_service_client: - dependency: transitive - description: - name: vm_service_client - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.6" - watcher: - dependency: transitive - description: - name: watcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.7+10" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.9" - yaml: - dependency: transitive - description: - name: yaml - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.15" sdks: - dart: ">=2.0.0-dev.68.0 <3.0.0" + dart: ">=2.0.0 <3.0.0" flutter: ">=0.5.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3ea28be2ed..5450c8afe1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) -version: 0.8.2 +version: 0.9.0 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html @@ -17,39 +17,5 @@ dev_dependencies: flutter_test: sdk: flutter -# For information on the generic Dart part of this file, see the -# following page: https://www.dartlang.org/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.io/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.io/assets-and-images/#resolution-aware. - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.io/custom-fonts/#from-packages From 82a1e6be788dc4f5f1baf8f3f139add72fd98ba7 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Thu, 31 Jan 2019 19:50:50 -0700 Subject: [PATCH 037/638] Version 0.9.1 --- CHANGELOG.md | 5 ++++ README.md | 6 ++--- lib/flutter_html.dart | 2 +- lib/html_parser.dart | 56 +++++++++++++++++++++++++++++-------------- pubspec.yaml | 2 +- 5 files changed, 48 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd0e9f1062..4f66a560c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [0.9.1] - January 31, 2019: + +* Adds full support for `sub` and `sup`. ([#46](https://github.com/Sub6Resources/flutter_html/pull/46)) +* Fixes weak warning caught by Pub analysis ([#54](https://github.com/Sub6Resources/flutter_html/issues/54)) + ## [0.9.0] - January 31, 2019: * Adds an alternate `RichText` parser and `useRichText` parameter. ([#37](https://github.com/Sub6Resources/flutter_html/pull/37)) diff --git a/README.md b/README.md index 70a7b278b3..52279d2e3a 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.9.0 + flutter_html: ^0.9.1 ## Currently Supported HTML Tags: -`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`, `table`, `tbody`, `td`, `template`, `tfoot`, `th`, `thead`, `time`, `tr`, `tt`, `u`, `ul`, `var` +`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` ### Partially supported elements: > These are common elements that aren't yet fully supported, but won't be ignored and will still render somewhat correctly. @@ -21,7 +21,7 @@ Add the following to your `pubspec.yaml` file: ### List of _planned_ supported elements: > These are elements that are planned, but present a specific challenge that makes them somewhat difficult to implement. -`audio`, `details`, `source`, `sub`, `summary`, `sup`, `svg`, `track`, `video`, `wbr` +`audio`, `details`, `source`, `summary`, `svg`, `track`, `video`, `wbr` ### List of elements that I don't plan on implementing: diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index dd383fbf56..49399c2fd4 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -13,7 +13,7 @@ class Html extends StatelessWidget { this.onLinkTap, this.renderNewlines = false, this.customRender, - this.blockSpacing, + this.blockSpacing = 14.0, this.useRichText = false, }) : super(key: key); diff --git a/lib/html_parser.dart b/lib/html_parser.dart index adc996ef41..cc54c1fc39 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -5,7 +5,8 @@ import 'package:html/dom.dart' as dom; typedef CustomRender = Widget Function(dom.Node node, List children); typedef OnLinkTap = void Function(String url); -const OFFSET_TAGS_FONT_SIZE_FACTOR = 0.7; //The ratio of the parent font for each of the offset tags: sup or sub +const OFFSET_TAGS_FONT_SIZE_FACTOR = + 0.7; //The ratio of the parent font for each of the offset tags: sup or sub class LinkTextSpan extends TextSpan { // Beware! @@ -757,7 +758,7 @@ class HtmlOldParser extends StatelessWidget { this.onLinkTap, this.renderNewlines = false, this.customRender, - this.blockSpacing = 14.0, + this.blockSpacing, this.html, }); @@ -868,7 +869,7 @@ class HtmlOldParser extends StatelessWidget { Widget _parseNode(dom.Node node) { if (customRender != null) { final Widget customWidget = - customRender(node, _parseNodeList(node.nodes)); + customRender(node, _parseNodeList(node.nodes)); if (customWidget != null) { return customWidget; } @@ -981,7 +982,8 @@ 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( @@ -1104,7 +1106,8 @@ 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, @@ -1282,8 +1285,7 @@ class HtmlOldParser extends StatelessWidget { mark, Wrap( crossAxisAlignment: WrapCrossAlignment.center, - children: _parseNodeList(node.nodes) - ) + children: _parseNodeList(node.nodes)) ], ), ); @@ -1334,7 +1336,7 @@ class HtmlOldParser extends StatelessWidget { width: width, child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, - alignment: WrapAlignment.start, //@ominibyte Added this for when the line breaks. I think it looks better + alignment: WrapAlignment.start, children: _parseNodeList(node.nodes), ), ), @@ -1433,19 +1435,32 @@ class HtmlOldParser extends StatelessWidget { ); case "sub": case "sup": - //Use builder to capture the parent font to inherit the font styles - return Builder(builder: (BuildContext context){ + //Use builder to capture the parent font to inherit the font styles + return Builder(builder: (BuildContext context) { final DefaultTextStyle parent = DefaultTextStyle.of(context); TextStyle parentStyle = parent.style; - var painter = new TextPainter(text: new TextSpan(text: node.text, style: parentStyle,), textDirection: TextDirection.ltr); + var painter = new TextPainter( + text: new TextSpan( + text: node.text, + style: parentStyle, + ), + textDirection: TextDirection.ltr); painter.layout(); //print(painter.size); //Get the height from the default text - var height = painter.size.height * 1.35; //compute a higher height for the text to increase the offset of the Positioned text - - painter = new TextPainter(text: new TextSpan(text: node.text, style: parentStyle.merge(TextStyle(fontSize: parentStyle.fontSize * OFFSET_TAGS_FONT_SIZE_FACTOR)),), textDirection: TextDirection.ltr); + var height = painter.size.height * + 1.35; //compute a higher height for the text to increase the offset of the Positioned text + + painter = new TextPainter( + text: new TextSpan( + text: node.text, + style: parentStyle.merge(TextStyle( + fontSize: + parentStyle.fontSize * OFFSET_TAGS_FONT_SIZE_FACTOR)), + ), + textDirection: TextDirection.ltr); painter.layout(); //print(painter.size); @@ -1463,14 +1478,19 @@ class HtmlOldParser extends StatelessWidget { children: [ //The Stack needs a non-positioned object for the next widget to respect the space so we create //a sized box to fill the required space - SizedBox(width: width, height: height,), + SizedBox( + width: width, + height: height, + ), DefaultTextStyle.merge( child: Positioned( child: Wrap(children: _parseNodeList(node.nodes)), bottom: node.localName == "sub" ? 0 : null, top: node.localName == "sub" ? null : 0, ), - style: TextStyle(fontSize: parentStyle.fontSize * OFFSET_TAGS_FONT_SIZE_FACTOR), + style: TextStyle( + fontSize: parentStyle.fontSize * + OFFSET_TAGS_FONT_SIZE_FACTOR), ) ], ) @@ -1501,7 +1521,7 @@ class HtmlOldParser extends StatelessWidget { ), ); case "template": - //Not usually displayed in HTML + //Not usually displayed in HTML return Container(); case "tfoot": return Column( @@ -1579,7 +1599,7 @@ class HtmlOldParser extends StatelessWidget { return Wrap(); } if (node.text.trim() == "" && node.text.indexOf(" ") != -1) { - node.text = "";//@ominibyte Looks better without the space + node.text = " "; } String finalText = trimStringHtml(node.text); diff --git a/pubspec.yaml b/pubspec.yaml index 5450c8afe1..6b561a39b1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) -version: 0.9.0 +version: 0.9.1 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From 66851b1c7138ec152b25fbbbbed1c4b4373bd1a1 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Thu, 31 Jan 2019 19:53:04 -0700 Subject: [PATCH 038/638] Version 0.9.1 (quickfix) --- lib/html_parser.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index cc54c1fc39..1cbd780f95 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -46,13 +46,13 @@ class LinkBlock extends Container { // final OnLinkTap onLinkTap; final List children; - LinkBlock( - {String url, - EdgeInsets padding, - EdgeInsets margin, - OnLinkTap onLinkTap, - List this.children}) - : super( + LinkBlock({ + String url, + EdgeInsets padding, + EdgeInsets margin, + OnLinkTap onLinkTap, + this.children, + }) : super( padding: padding, margin: margin, child: GestureDetector( From b8e65024209a453795e4186b84b90ae7b6a2afb2 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Thu, 31 Jan 2019 20:27:45 -0700 Subject: [PATCH 039/638] Version 0.9.2 --- CHANGELOG.md | 4 ++++ README.md | 4 ++-- lib/html_parser.dart | 5 +++++ pubspec.yaml | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f66a560c9..75a9269423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.9.2] - January 31, 2019: + +* Adds partial support for deprecated `font` tag. + ## [0.9.1] - January 31, 2019: * Adds full support for `sub` and `sup`. ([#46](https://github.com/Sub6Resources/flutter_html/pull/46)) diff --git a/README.md b/README.md index 52279d2e3a..92b9bc2889 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Add the following to your `pubspec.yaml` file: ### Partially supported elements: > These are common elements that aren't yet fully supported, but won't be ignored and will still render somewhat correctly. -`center` +`center`, `font` ### List of _planned_ supported elements: > These are elements that are planned, but present a specific challenge that makes them somewhat difficult to implement. @@ -31,7 +31,7 @@ Add the following to your `pubspec.yaml` file: > Note: These unsupported tags will just be ignored. -`applet`, `area`, `base`, `basefont`, `button`, `canvas`, `col`, `colgroup`, `datalist`, `dialog`, `dir`, `embed`, `font`, `fieldset`, `form`, `frame`, `frameset`, `head`, `iframe`, `input`, `label`, `legend`, `link`, `map`, `meta`, `meter`, `noframe`, `object`, `optgroup`, `option`, `output`, `param`, `picture`, `progress`, `script`, `select`, `style`, `textarea`, `title` +`applet`, `area`, `base`, `basefont`, `button`, `canvas`, `col`, `colgroup`, `datalist`, `dialog`, `dir`, `embed`, `fieldset`, `form`, `frame`, `frameset`, `head`, `iframe`, `input`, `label`, `legend`, `link`, `map`, `meta`, `meter`, `noframe`, `object`, `optgroup`, `option`, `output`, `param`, `picture`, `progress`, `script`, `select`, `style`, `textarea`, `title` ## Why this package? diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 1cbd780f95..6aff70f4f9 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -797,6 +797,7 @@ class HtmlOldParser extends StatelessWidget { "em", "figcaption", "figure", + "font", "footer", "h1", "h2", @@ -1112,6 +1113,10 @@ class HtmlOldParser extends StatelessWidget { children: _parseNodeList(node.nodes), crossAxisAlignment: CrossAxisAlignment.center, )); + case "font": + return Wrap( + children: _parseNodeList(node.nodes), + ); case "footer": return Container( width: width, diff --git a/pubspec.yaml b/pubspec.yaml index 6b561a39b1..94afe8db97 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) -version: 0.9.1 +version: 0.9.2 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From 1f3d137b146ce3695912243b1c46aa1dc2844f27 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Thu, 31 Jan 2019 20:28:48 -0700 Subject: [PATCH 040/638] Version 0.9.2 - Add font partial support --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92b9bc2889..59b069f865 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.9.1 + flutter_html: ^0.9.2 ## Currently Supported HTML Tags: `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` From 42b07c0c9dc89b15b77a71d15f93f037bb6bf592 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Thu, 31 Jan 2019 21:07:27 -0700 Subject: [PATCH 041/638] Version 0.9.3 - Add support for base64 encoded images --- CHANGELOG.md | 4 ++++ README.md | 2 +- lib/html_parser.dart | 14 ++++++++++++-- pubspec.yaml | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75a9269423..49864fb60c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.9.3] - January 31, 2019: + +* Adds support for base64 encoded images + ## [0.9.2] - January 31, 2019: * Adds partial support for deprecated `font` tag. diff --git a/README.md b/README.md index 59b069f865..33b89f8c63 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.9.2 + flutter_html: ^0.9.3 ## Currently Supported HTML Tags: `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` diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 6aff70f4f9..681edc0fe5 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:flutter/gestures.dart'; import 'package:html/parser.dart' as parser; @@ -571,8 +573,13 @@ class HtmlRichTextParser extends StatelessWidget { break; case "img": if (node.attributes['src'] != null) { - parseContext.rootWidgetList - .add(Image.network(node.attributes['src'])); + if(node.attributes['src'].startsWith("data:image") && node.attributes['src'].contains("base64,")) { + parseContext.rootWidgetList + .add(Image.memory(base64.decode(node.attributes['src'].split("base64,")[1].trim()))); + } else { + parseContext.rootWidgetList + .add(Image.network(node.attributes['src'])); + } } else if (node.attributes['alt'] != null) { parseContext.rootWidgetList.add(BlockText( margin: EdgeInsets.symmetric(horizontal: 0.0, vertical: 10.0), @@ -1236,6 +1243,9 @@ class HtmlOldParser extends StatelessWidget { ); case "img": if (node.attributes['src'] != null) { + if(node.attributes['src'].startsWith("data:image") && node.attributes['src'].contains("base64,")) { + return Image.memory(base64.decode(node.attributes['src'].split("base64,")[1].trim())); + } return Image.network(node.attributes['src']); } else if (node.attributes['alt'] != null) { //Temp fix for https://github.com/flutter/flutter/issues/736 diff --git a/pubspec.yaml b/pubspec.yaml index 94afe8db97..f4064d8824 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) -version: 0.9.2 +version: 0.9.3 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From 34a86ab0745e40d1dfe53cd5e7db29fab08f2958 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Thu, 31 Jan 2019 21:08:47 -0700 Subject: [PATCH 042/638] Version 0.9.3 - Add support for base64 images --- lib/html_parser.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 681edc0fe5..54851683cf 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -573,9 +573,10 @@ class HtmlRichTextParser extends StatelessWidget { break; case "img": if (node.attributes['src'] != null) { - if(node.attributes['src'].startsWith("data:image") && node.attributes['src'].contains("base64,")) { - parseContext.rootWidgetList - .add(Image.memory(base64.decode(node.attributes['src'].split("base64,")[1].trim()))); + if (node.attributes['src'].startsWith("data:image") && + node.attributes['src'].contains("base64,")) { + parseContext.rootWidgetList.add(Image.memory(base64.decode( + node.attributes['src'].split("base64,")[1].trim()))); } else { parseContext.rootWidgetList .add(Image.network(node.attributes['src'])); @@ -1243,8 +1244,10 @@ class HtmlOldParser extends StatelessWidget { ); case "img": if (node.attributes['src'] != null) { - if(node.attributes['src'].startsWith("data:image") && node.attributes['src'].contains("base64,")) { - return Image.memory(base64.decode(node.attributes['src'].split("base64,")[1].trim())); + if (node.attributes['src'].startsWith("data:image") && + node.attributes['src'].contains("base64,")) { + return Image.memory(base64 + .decode(node.attributes['src'].split("base64,")[1].trim())); } return Image.network(node.attributes['src']); } else if (node.attributes['alt'] != null) { From c818dd208316ce7992fc0681926875fff684f287 Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Thu, 31 Jan 2019 21:32:24 -0700 Subject: [PATCH 043/638] Add update about Flutters official webview package --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 33b89f8c63..11c8912d02 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,12 @@ This package is designed with simplicity in mind. Flutter currently does not sup into the widget tree. This package is designed to be a reasonable alternative for rendering static web content until official support is added. +### Update: +The official Flutter WebView package has been created and is in a developer preview. It's not stable yet, so I'll continue to support this project at least until webview_flutter hits 1.0.0. + +Check out the official Flutter WebView package here: https://pub.dartlang.org/packages/webview_flutter + + ## Example Usage: Html( @@ -78,4 +84,4 @@ until official support is added. This package has a known issue where text does not wrap correctly. Setting `useRichText` to true fixes the issue by using an alternate parser. The alternate parser, however, does not support the `customRender` callback, and several elements -supported by the default parser are not supported by the alternate parser. \ No newline at end of file +supported by the default parser are not supported by the alternate parser. From 1ead9ca96434a51089d07a955812507dc2e38789 Mon Sep 17 00:00:00 2001 From: Jeff Mikels Date: Sat, 2 Feb 2019 14:15:48 -0500 Subject: [PATCH 044/638] fixed table rendering --- lib/html_parser.dart | 62 ++++++++---- pubspec.lock | 220 +------------------------------------------ 2 files changed, 49 insertions(+), 233 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 8b36b8dac6..7367ec05f0 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -296,30 +296,33 @@ class HtmlRichTextParser extends StatelessWidget { // a text only node is a child of a tag with no inner html if (node is dom.Text) { // WHITESPACE CONSIDERATIONS --- - // truly empty nodes, should just be ignored + // truly empty nodes should just be ignored if (node.text.trim() == "" && node.text.indexOf(" ") == -1) { return; } - // if (node.text.trim() == "" && - // node.text.indexOf(" ") != -1 && - // parseContext.condenseWhitespace) { - // node.text = " "; - // } - // we might want to preserve internal whitespace // empty strings of whitespace might be significant or not, condense it by default String finalText = parseContext.condenseWhitespace ? condenseHtmlWhitespace(node.text) : node.text; - // if this is part of a string of spans, we will preserve leading and trailing whitespace - if (!(parseContext.parentElement is TextSpan || - parseContext.parentElement is LinkTextSpan)) - finalText = finalText.trim(); + // if this is part of a string of spans, we will preserve leading + // and trailing whitespace unless the previous character is whitespace + if (parseContext.parentElement == null) + finalText = finalText.trimLeft(); + else if (parseContext.parentElement is TextSpan || + parseContext.parentElement is LinkTextSpan) { + String lastString = parseContext.parentElement.text ?? ''; + if (!parseContext.parentElement.children.isEmpty) { + lastString = parseContext.parentElement.children.last.text; + } + if (lastString.endsWith(' ') || lastString.endsWith('\n')) + finalText = finalText.trimLeft(); + } // if the finalText is actually empty, just return - if (finalText.isEmpty) return; + if (finalText.trim().isEmpty) return; // NOW WE HAVE OUR TRULY FINAL TEXT // debugPrint("Plain Text Node: '$finalText'"); @@ -372,7 +375,7 @@ class HtmlRichTextParser extends StatelessWidget { .add(BlockText(child: RichText(text: span))); } - // this allows future items to be added as children + // this allows future items to be added as children of this item parseContext.parentElement = span; // if the parent is a LinkTextSpan, keep the main attributes of that span going. @@ -387,8 +390,10 @@ class HtmlRichTextParser extends StatelessWidget { )); // if the parent is a normal span, just add this to that list - } else { + } else if (!(parseContext.parentElement.children is List)) { parseContext.parentElement.children.add(span); + } else { + print('doing nothing'); } return; } @@ -521,33 +526,54 @@ class HtmlRichTextParser extends StatelessWidget { break; case "table": - case "tbody": - case "thead": // new block, so clear out the parent element parseContext.parentElement = null; nextContext.parentElement = Column( crossAxisAlignment: CrossAxisAlignment.start, + children: [], ); nextContext.rootWidgetList.add(nextContext.parentElement); break; + // we don't handle tbody or thead elements separately for now + case "tbody": + case "thead": + break; + + // caption elements throw us off + case "caption": + RichText text = + RichText(text: TextSpan(text: '', children: [])); + Row row = Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + text, + ], + ); + nextContext.parentElement.children.add(row); + nextContext.parentElement = text.text; + break; + case "td": case "th": int colspan = 1; if (node.attributes['colspan'] != null) { colspan = int.tryParse(node.attributes['colspan']); } + RichText text = + RichText(text: TextSpan(text: '', children: [])); Expanded cell = Expanded( flex: colspan, - child: Wrap(), + child: text, ); nextContext.parentElement.children.add(cell); - nextContext.parentElement = cell.child; + nextContext.parentElement = text.text; break; case "tr": Row row = Row( crossAxisAlignment: CrossAxisAlignment.center, + children: [], ); nextContext.parentElement.children.add(row); nextContext.parentElement = row; diff --git a/pubspec.lock b/pubspec.lock index 5bb8a63f39..4d7c167048 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,13 +1,6 @@ # Generated by pub # See https://www.dartlang.org/tools/pub/glossary#lockfile packages: - analyzer: - dependency: transitive - description: - name: analyzer - url: "https://pub.dartlang.org" - source: hosted - version: "0.32.4" args: dependency: transitive description: @@ -43,20 +36,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.11" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.6" csslib: dependency: transitive description: @@ -74,20 +53,6 @@ packages: description: flutter source: sdk version: "0.0.0" - front_end: - dependency: transitive - description: - name: front_end - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.4" - glob: - dependency: transitive - description: - name: glob - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.7" html: dependency: "direct main" description: @@ -95,55 +60,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.13.3+3" - http: - dependency: transitive - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.11.3+17" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.5" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.3" - io: - dependency: transitive - description: - name: io - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.3" - js: - dependency: transitive - description: - name: js - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.1+1" - json_rpc_2: - dependency: transitive - description: - name: json_rpc_2 - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.9" - kernel: - dependency: transitive - description: - name: kernel - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.4" logging: dependency: transitive description: @@ -165,41 +81,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.6" - mime: - dependency: transitive - description: - name: mime - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.6+2" - multi_server_socket: - dependency: transitive - description: - name: multi_server_socket - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - node_preamble: - dependency: transitive - description: - name: node_preamble - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.4" - package_config: - dependency: transitive - description: - name: package_config - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" - package_resolver: - dependency: transitive - description: - name: package_resolver - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" path: dependency: transitive description: @@ -207,81 +88,18 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.2" - plugin: - dependency: transitive - description: - name: plugin - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0+3" - pool: - dependency: transitive - description: - name: pool - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.6" - pub_semver: - dependency: transitive - description: - name: pub_semver - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.2" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.0+1" - shelf: - dependency: transitive - description: - name: shelf - url: "https://pub.dartlang.org" - source: hosted - version: "0.7.3+3" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - shelf_static: - dependency: transitive - description: - name: shelf_static - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.8" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.2+4" + version: "2.0.1" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.5" - source_maps: - dependency: transitive - description: - name: source_maps - url: "https://pub.dartlang.org" - source: hosted - version: "0.10.7" source_span: dependency: transitive description: @@ -317,13 +135,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" - test: + test_api: dependency: transitive description: - name: test + name: test_api url: "https://pub.dartlang.org" source: hosted - version: "1.3.0" + version: "0.2.1" typed_data: dependency: transitive description: @@ -345,34 +163,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" - vm_service_client: - dependency: transitive - description: - name: vm_service_client - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.6" - watcher: - dependency: transitive - description: - name: watcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.9.7+10" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.9" - yaml: - dependency: transitive - description: - name: yaml - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.15" sdks: - dart: ">=2.0.0-dev.68.0 <3.0.0" + dart: ">=2.0.0 <3.0.0" flutter: ">=0.5.0" From b57450b268c5c1aa93e7cceb72e001f5104d93b1 Mon Sep 17 00:00:00 2001 From: Jeff Mikels Date: Sat, 2 Feb 2019 22:08:31 -0500 Subject: [PATCH 045/638] finished basic table rendering --- lib/html_parser.dart | 51 ++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 35ae4a0704..00b70b9e67 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -175,6 +175,7 @@ class HtmlRichTextParser extends StatelessWidget { "br", "table", "tbody", + "caption", "td", "tfoot", "th", @@ -401,7 +402,7 @@ class HtmlRichTextParser extends StatelessWidget { // OTHER ELEMENT NODES else if (node is dom.Element) { assert(() { - // debugPrint("Found ${node.localName}"); + debugPrint("Found ${node.localName}"); // debugPrint(node.outerHtml); return true; }()); @@ -532,26 +533,15 @@ class HtmlRichTextParser extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [], ); - nextContext.rootWidgetList.add(nextContext.parentElement); + nextContext.rootWidgetList.add(Container( + margin: EdgeInsets.symmetric(vertical: 12.0), + child: nextContext.parentElement)); break; - // we don't handle tbody or thead elements separately for now + // we don't handle tbody, thead, or tfoot elements separately for now case "tbody": case "thead": - break; - - // caption elements throw us off - case "caption": - RichText text = - RichText(text: TextSpan(text: '', children: [])); - Row row = Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - text, - ], - ); - nextContext.parentElement.children.add(row); - nextContext.parentElement = text.text; + case "tfoot": break; case "td": @@ -560,11 +550,15 @@ class HtmlRichTextParser extends StatelessWidget { if (node.attributes['colspan'] != null) { 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: [])); Expanded cell = Expanded( flex: colspan, - child: text, + child: Container(padding: EdgeInsets.all(1.0), child: text), ); nextContext.parentElement.children.add(cell); nextContext.parentElement = text.text; @@ -578,6 +572,27 @@ class HtmlRichTextParser extends StatelessWidget { nextContext.parentElement.children.add(row); nextContext.parentElement = row; break; + + // treat captions like a row with one expanded cell + case "caption": + // create the row + Row row = Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [], + ); + + // create an expanded cell + RichText text = RichText( + textAlign: TextAlign.center, + textScaleFactor: 1.2, + text: TextSpan(text: '', children: [])); + Expanded cell = Expanded( + child: Container(padding: EdgeInsets.all(2.0), child: text), + ); + row.children.add(cell); + nextContext.parentElement.children.add(row); + nextContext.parentElement = text.text; + break; } } From 52376e5149c51c88fdb5a23d5eeb8f351f3211b8 Mon Sep 17 00:00:00 2001 From: Jeff Mikels Date: Sat, 2 Feb 2019 22:12:43 -0500 Subject: [PATCH 046/638] removed whitespace at beginning of block elements --- lib/html_parser.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 00b70b9e67..3ed9a22328 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -318,8 +318,9 @@ class HtmlRichTextParser extends StatelessWidget { if (!parseContext.parentElement.children.isEmpty) { lastString = parseContext.parentElement.children.last.text; } - if (lastString.endsWith(' ') || lastString.endsWith('\n')) - finalText = finalText.trimLeft(); + if (lastString == '' || + lastString.endsWith(' ') || + lastString.endsWith('\n')) finalText = finalText.trimLeft(); } // if the finalText is actually empty, just return From c3cc2481ed6797a6c764d975b70c9bb914406b02 Mon Sep 17 00:00:00 2001 From: Jeff Mikels Date: Sat, 2 Feb 2019 22:17:51 -0500 Subject: [PATCH 047/638] ...but preserve whitespace inside 'pre' tag --- lib/html_parser.dart | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 3ed9a22328..14b35eea55 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -304,23 +304,24 @@ class HtmlRichTextParser extends StatelessWidget { // we might want to preserve internal whitespace // empty strings of whitespace might be significant or not, condense it by default - String finalText = parseContext.condenseWhitespace - ? condenseHtmlWhitespace(node.text) - : node.text; - - // if this is part of a string of spans, we will preserve leading - // and trailing whitespace unless the previous character is whitespace - if (parseContext.parentElement == null) - finalText = finalText.trimLeft(); - else if (parseContext.parentElement is TextSpan || - parseContext.parentElement is LinkTextSpan) { - String lastString = parseContext.parentElement.text ?? ''; - if (!parseContext.parentElement.children.isEmpty) { - lastString = parseContext.parentElement.children.last.text; + String finalText = node.text; + if (parseContext.condenseWhitespace) { + finalText = condenseHtmlWhitespace(node.text); + + // if this is part of a string of spans, we will preserve leading + // and trailing whitespace unless the previous character is whitespace + if (parseContext.parentElement == null) + finalText = finalText.trimLeft(); + else if (parseContext.parentElement is TextSpan || + parseContext.parentElement is LinkTextSpan) { + String lastString = parseContext.parentElement.text ?? ''; + if (!parseContext.parentElement.children.isEmpty) { + lastString = parseContext.parentElement.children.last.text; + } + if (lastString == '' || + lastString.endsWith(' ') || + lastString.endsWith('\n')) finalText = finalText.trimLeft(); } - if (lastString == '' || - lastString.endsWith(' ') || - lastString.endsWith('\n')) finalText = finalText.trimLeft(); } // if the finalText is actually empty, just return @@ -403,7 +404,7 @@ class HtmlRichTextParser extends StatelessWidget { // OTHER ELEMENT NODES else if (node is dom.Element) { assert(() { - debugPrint("Found ${node.localName}"); + // debugPrint("Found ${node.localName}"); // debugPrint(node.outerHtml); return true; }()); From ea627c52546b85d62f41b96c19080ac42a7480ba Mon Sep 17 00:00:00 2001 From: Jeff Mikels Date: Mon, 4 Feb 2019 17:11:16 -0500 Subject: [PATCH 048/638] removing a print statement --- lib/html_parser.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 14b35eea55..82b13a209c 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -396,7 +396,7 @@ class HtmlRichTextParser extends StatelessWidget { } else if (!(parseContext.parentElement.children is List)) { parseContext.parentElement.children.add(span); } else { - print('doing nothing'); + // Doing nothing... we shouldn't ever get here } return; } From 27fc0b0e3809f4b6e6103b540ac517897924571b Mon Sep 17 00:00:00 2001 From: Jeff Mikels Date: Tue, 5 Feb 2019 14:25:06 -0500 Subject: [PATCH 049/638] handle possible null string condition --- lib/html_parser.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 82b13a209c..3b0ef5ab6f 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -316,7 +316,7 @@ class HtmlRichTextParser extends StatelessWidget { parseContext.parentElement is LinkTextSpan) { String lastString = parseContext.parentElement.text ?? ''; if (!parseContext.parentElement.children.isEmpty) { - lastString = parseContext.parentElement.children.last.text; + lastString = parseContext.parentElement.children.last.text ?? ''; } if (lastString == '' || lastString.endsWith(' ') || From a8aa2f920bb01bcfc72d3473a816b5bd6832d290 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Tue, 5 Feb 2019 13:41:19 -0700 Subject: [PATCH 050/638] Version 0.9.4 --- CHANGELOG.md | 4 ++++ README.md | 2 +- lib/html_parser.dart | 1 + pubspec.yaml | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49864fb60c..cb64166d82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.9.4] - February 5, 2019: + +* Fixes `table` error in `RichText` parser. ([#58](https://github.com/Sub6Resources/flutter_html/issues/58)) + ## [0.9.3] - January 31, 2019: * Adds support for base64 encoded images diff --git a/README.md b/README.md index 11c8912d02..9baeea502f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.9.3 + flutter_html: ^0.9.4 ## Currently Supported HTML Tags: `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` diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 2208c2f0d8..2355cd86ea 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -7,6 +7,7 @@ import 'package:html/dom.dart' as dom; typedef CustomRender = Widget Function(dom.Node node, List children); typedef OnLinkTap = void Function(String url); + const OFFSET_TAGS_FONT_SIZE_FACTOR = 0.7; //The ratio of the parent font for each of the offset tags: sup or sub diff --git a/pubspec.yaml b/pubspec.yaml index f4064d8824..9eca8564c9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) -version: 0.9.3 +version: 0.9.4 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From d6ea2e1b511ddd72ea17b7addbc83b4adc632635 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Tue, 5 Feb 2019 13:57:47 -0700 Subject: [PATCH 051/638] Setup CI --- .circleci/config.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000000..4377b96e1d --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,8 @@ +version: 2 +jobs: + test: + docker: + - image: cirrusci/flutter + steps: + - checkout + - run: flutter test \ No newline at end of file From a07c9e4f1900f42bea2e920de89cc47e75d4631a Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Tue, 5 Feb 2019 13:58:37 -0700 Subject: [PATCH 052/638] Setup CI - Fix config --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4377b96e1d..dc93507cce 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,6 @@ version: 2 jobs: - test: + build: docker: - image: cirrusci/flutter steps: From 83998fd2780e9f4d3a7e60492befdda357b338aa Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Tue, 5 Feb 2019 14:15:06 -0700 Subject: [PATCH 053/638] Add CI Badge to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9baeea502f..a1898c6eb8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # flutter_html [![pub package](https://img.shields.io/pub/v/flutter_html.svg)](https://pub.dartlang.org/packages/flutter_html) +[![CircleCI](https://circleci.com/gh/Sub6Resources/flutter_html.svg?style=svg)](https://circleci.com/gh/Sub6Resources/flutter_html) A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) From 65ace8080d3d9c0bbe21582c166ce5ebbb961d5d Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Tue, 5 Feb 2019 14:34:07 -0700 Subject: [PATCH 054/638] Start adding more tests --- test/html_parser_test.dart | 115 ++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 65 deletions(-) diff --git a/test/html_parser_test.dart b/test/html_parser_test.dart index c86c4662f1..36f88fae45 100644 --- a/test/html_parser_test.dart +++ b/test/html_parser_test.dart @@ -1,91 +1,76 @@ import 'package:flutter/material.dart'; -import 'package:flutter_html/html_parser.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_html/flutter_html.dart'; void main() { - // test('Checks that `parse` does not throw an exception', () { - // final elementList = HtmlOldParser(width: 42.0).parse("Bold Text"); - // expect(elementList, isNotNull); - // }); - testWidgets('Tests some plain old text', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( + testWidgets("Check that default parser does not fail on empty data", (tester) async { + await tester.pumpWidget( + MaterialApp( home: Scaffold( - body: Html(data: "This is some plain text"), - ))); - - expect(find.text("This is some plain text"), findsOneWidget); - }); - - testWidgets('Tests that a element gets rendered correctly', - (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( - home: Scaffold( - body: Html( - data: "Bold Text", + body: Html( + data: "", + ), ), ), - )); - - expect(find.byType(RichText), findsOneWidget); + ); }); - testWidgets( - 'Tests that a combination of elements and text nodes gets rendered', - (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( - home: Scaffold( - body: Html( - data: "Bold Text and plain text", + testWidgets("Check that RichText parser does not fail on empty data", (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: "", + useRichText: true, + ), ), ), - )); - - expect(find.byType(RichText), findsNWidgets(2)); - expect(find.text(" and plain text"), findsOneWidget); + ); }); - testWidgets('Tests that a element gets rendered correctly', - (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( - home: Scaffold( - body: Html( - data: "Bold Text, Italic Text and plain text", + testWidgets("Check that `a` tag is rendered by both parsers", (tester) async { + String html = "Test link"; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), ), ), - )); + ); - expect(find.byType(RichText), findsNWidgets(4)); - expect(find.text(", "), findsOneWidget); - expect(find.text(" and plain text"), findsOneWidget); - }); - - testWidgets('Tests that nested elements get rendered correctly', - (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( - home: Scaffold( - body: Html( - data: - "Bold Text and Italic bold text and Underlined italic bold text", + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), ), ), - )); - - expect(find.byType(RichText), findsNWidgets(3)); + ); }); - testWidgets('Tests that the header elements (h1-h6) get rendered correctly', - (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( - home: Scaffold( - body: Html( - data: - "

H1

H2

H3

H4

H5
H6
", + testWidgets("Check that tapping on the `a` tag calls the callback", (tester) async { + String html = "Test link"; + String urlTapped; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + onLinkTap: (url) { + urlTapped = url; + }, + ), ), ), - )); - - expect(find.byType(RichText), findsNWidgets(6)); + ); + await tester.tap(find.text("Test link")); + expect(urlTapped, "https://github.com"); }); + } From d3546048156061dd5c27a1f134dfb85f4fcbb298 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Tue, 5 Feb 2019 21:23:57 -0700 Subject: [PATCH 055/638] Add dozens of new tests --- lib/html_parser.dart | 4 +- test/html_parser_test.dart | 937 ++++++++++++++++++++++++++++++++++++- 2 files changed, 936 insertions(+), 5 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 2355cd86ea..9abe77bbc2 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -193,8 +193,8 @@ class HtmlRichTextParser extends StatelessWidget { // is found inside another block level element, // we simply treat it as a new block level element static const _supportedBlockElements = [ - "article" - "body", + "article", + "body", "center", "dd", "dfn", diff --git a/test/html_parser_test.dart b/test/html_parser_test.dart index 36f88fae45..c60ea77da5 100644 --- a/test/html_parser_test.dart +++ b/test/html_parser_test.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_html/html_parser.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_html/flutter_html.dart'; @@ -29,28 +30,33 @@ void main() { ); }); + //`a` tag tests + testWidgets("Check that `a` tag is rendered by both parsers", (tester) async { - String html = "Test link"; await tester.pumpWidget( MaterialApp( home: Scaffold( body: Html( - data: html, + data: "Test link", ), ), ), ); + expect(find.text("Test link"), findsOneWidget); + await tester.pumpWidget( MaterialApp( home: Scaffold( body: Html( - data: html, + data: "Test link", useRichText: true, ), ), ), ); + + expect(find.byType(RichText), findsOneWidget); }); testWidgets("Check that tapping on the `a` tag calls the callback", (tester) async { @@ -73,4 +79,929 @@ void main() { expect(urlTapped, "https://github.com"); }); + testWidgets("Check that tapping on the `a` tag calls the callback `RichText` parser", (tester) async { + String html = "Test link"; + String urlTapped; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + onLinkTap: (url) { + urlTapped = url; + }, + useRichText: true, + ), + ), + ), + ); + await tester.tap(find.byType(RichText)); + expect(urlTapped, "https://github.com"); + }); + + // `abbr` tag tests + testWidgets("Check that `abbr` tag renders", (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: "Abbreviation", + ), + ), + ), + ); + + + expect(find.text("Abbreviation"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: "Abbreviation", + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `acronym` tag renders", (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: "Acronym", + ), + ), + ), + ); + + + expect(find.text("Acronym"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: "Acronym", + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `address` tag renders", (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: "
Address
", + ), + ), + ), + ); + + + expect(find.text("Address"), findsOneWidget); + + //Not supported in `RichText` parser. +// await tester.pumpWidget( +// MaterialApp( +// home: Scaffold( +// body: Html( +// data: "
Address
", +// useRichText: true, +// ), +// ), +// ), +// ); +// +// expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `article` tag renders", (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: "
Article
", + ), + ), + ), + ); + + + expect(find.text("Article"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: "
Article
", + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(BlockText), findsOneWidget); + }); + + testWidgets("Check that `aside` tag renders", (tester) async { + String html = ""; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Aside"), findsOneWidget); + + //Not supported in `RichText` parser. +// await tester.pumpWidget( +// MaterialApp( +// home: Scaffold( +// body: Html( +// data: html, +// useRichText: true, +// ), +// ), +// ), +// ); +// +// expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `b` tag renders", (tester) async { + String html = "B"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("B"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `bdi` tag renders", (tester) async { + String html = "Bdi"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Bdi"), findsOneWidget); + + //Not supported in `RichText` parser. +// await tester.pumpWidget( +// MaterialApp( +// home: Scaffold( +// body: Html( +// data: html, +// useRichText: true, +// ), +// ), +// ), +// ); +// +// expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `bdo` tag renders", (tester) async { + String html = "Bdo"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Bdo"), findsOneWidget); + + //Not supported in `RichText` parser. +// await tester.pumpWidget( +// MaterialApp( +// home: Scaffold( +// body: Html( +// data: html, +// useRichText: true, +// ), +// ), +// ), +// ); +// +// expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `big` tag renders", (tester) async { + String html = "Big"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Big"), findsOneWidget); + + //Not supported in `RichText` parser. +// await tester.pumpWidget( +// MaterialApp( +// home: Scaffold( +// body: Html( +// data: html, +// useRichText: true, +// ), +// ), +// ), +// ); +// +// expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `blockquote` tag renders", (tester) async { + String html = "
Blockquote
"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Blockquote"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(BlockText), findsOneWidget); + }); + + testWidgets("Check that `body` tag renders", (tester) async { + String html = "Body"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Body"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(BlockText), findsOneWidget); + }); + + testWidgets("Check that `br` tag renders", (tester) async { + String html = "Text
broken"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Text"), findsOneWidget); + expect(find.text("broken"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `caption` tag renders", (tester) async { + String html = "
"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Caption"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `cite` tag renders", (tester) async { + String html = "Cite"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Cite"), findsOneWidget); + + //Not supported in `RichText` parser. +// await tester.pumpWidget( +// MaterialApp( +// home: Scaffold( +// body: Html( +// data: html, +// useRichText: true, +// ), +// ), +// ), +// ); +// +// expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `code` tag renders", (tester) async { + String html = "Code"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Code"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `data` tag renders", (tester) async { + String html = "Data"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Data"), findsOneWidget); + + //Not supported in `RichText` parser. +// await tester.pumpWidget( +// MaterialApp( +// home: Scaffold( +// body: Html( +// data: html, +// useRichText: true, +// ), +// ), +// ), +// ); +// +// expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `dd` tag renders", (tester) async { + String html = "
DD
"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("DD"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `del` tag renders", (tester) async { + String html = "Dfn"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Dfn"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `div` tag renders", (tester) async { + String html = "
Div
"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Div"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(BlockText), findsOneWidget); + }); + + testWidgets("Check that `dl` tag renders", (tester) async { + String html = "
Dl
"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Dl"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `dt` tag renders", (tester) async { + String html = "
Dt
"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Dt"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `em` tag renders", (tester) async { + String html = "Em"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Em"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `figcaption` tag renders", (tester) async { + String html = "
Figcaption
"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Figcaption"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `figure` tag renders", (tester) async { + String html = "
Figure
"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Figure"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets("Check that `footer` tag renders", (tester) async { + String html = "Footer"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("Footer"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(BlockText), findsOneWidget); + }); + + testWidgets("Check that `h1` tag renders", (tester) async { + String html = "

h1

"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("h1"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(BlockText), findsOneWidget); + }); + + testWidgets("Check that `h2` tag renders", (tester) async { + String html = "

h2

"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("h2"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(BlockText), findsOneWidget); + }); + + testWidgets("Check that `h3` tag renders", (tester) async { + String html = "

h3

"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("h3"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(BlockText), findsOneWidget); + }); + + testWidgets("Check that `h4` tag renders", (tester) async { + String html = "

h4

"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("h4"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(BlockText), findsOneWidget); + }); + + testWidgets("Check that `h5` tag renders", (tester) async { + String html = "
h5
"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("h5"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(BlockText), findsOneWidget); + }); + + testWidgets("Check that `h6` tag renders", (tester) async { + String html = "
h6
"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("h6"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(BlockText), findsOneWidget); + }); + } From 175c0a522042d078e7210755b2add6bea8dfed19 Mon Sep 17 00:00:00 2001 From: gregor Date: Wed, 13 Feb 2019 15:09:41 +0100 Subject: [PATCH 056/638] used preCacheImage to catch errors while image loading --- lib/html_parser.dart | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 9abe77bbc2..6206fd5fa0 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -260,7 +260,8 @@ class HtmlRichTextParser extends StatelessWidget { ); // ignore the top level "body" - body.nodes.forEach((dom.Node node) => _parseNode(node, parseContext)); + body.nodes + .forEach((dom.Node node) => _parseNode(node, parseContext, context)); // _parseNode(body, parseContext); // filter out empty widgets @@ -297,7 +298,8 @@ 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) { + 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) { @@ -619,9 +621,27 @@ class HtmlRichTextParser extends StatelessWidget { 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: (_, stacktrace) { + print(stacktrace); + }, + ); parseContext.rootWidgetList.add(Image.memory(base64.decode( node.attributes['src'].split("base64,")[1].trim()))); } else { + precacheImage( + NetworkImage(node.attributes['src']), + buildContext, + onError: (_, stacktrace) { + print(stacktrace); + }, + ); parseContext.rootWidgetList .add(Image.network(node.attributes['src'])); } @@ -738,7 +758,7 @@ class HtmlRichTextParser extends StatelessWidget { } node.nodes.forEach((dom.Node childNode) { - _parseNode(childNode, nextContext); + _parseNode(childNode, nextContext, buildContext); }); } } From 40a0b4a4b9680385f7aff3242a892626c50f7bcc Mon Sep 17 00:00:00 2001 From: Luke Pighetti Date: Mon, 4 Mar 2019 20:33:24 -0500 Subject: [PATCH 057/638] add linkStyle argument to HtmlRichTextParser and HtmlOldParser --- lib/flutter_html.dart | 7 +++++++ lib/html_parser.dart | 23 +++++++++++++---------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 49399c2fd4..ab3ec5ad0d 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -15,6 +15,10 @@ class Html extends StatelessWidget { this.customRender, this.blockSpacing = 14.0, this.useRichText = false, + this.linkStyle = const TextStyle( + decoration: TextDecoration.underline, + color: Colors.blueAccent, + decorationColor: Colors.blueAccent), }) : super(key: key); final String data; @@ -25,6 +29,7 @@ class Html extends StatelessWidget { final bool renderNewlines; final double blockSpacing; final bool useRichText; + final TextStyle linkStyle; /// Either return a custom widget for specific node types or return null to /// fallback to the default rendering. @@ -46,6 +51,7 @@ class Html extends StatelessWidget { onLinkTap: onLinkTap, renderNewlines: renderNewlines, html: data, + linkStyle: linkStyle, ) : HtmlOldParser( width: width, @@ -54,6 +60,7 @@ class Html extends StatelessWidget { customRender: customRender, html: data, blockSpacing: blockSpacing, + linkStyle: linkStyle, ), ), ); diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 9abe77bbc2..4408df033a 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -144,6 +144,10 @@ class HtmlRichTextParser extends StatelessWidget { this.onLinkTap, this.renderNewlines = false, this.html, + this.linkStyle = const TextStyle( + decoration: TextDecoration.underline, + color: Colors.blueAccent, + decorationColor: Colors.blueAccent), }); final double indentSize = 10.0; @@ -152,6 +156,7 @@ class HtmlRichTextParser extends StatelessWidget { final onLinkTap; final bool renderNewlines; final String html; + final TextStyle linkStyle; // style elements set a default style // for all child nodes @@ -497,13 +502,9 @@ class HtmlRichTextParser extends StatelessWidget { nextContext.parentElement = linkContainer; nextContext.rootWidgetList.add(linkContainer); } else { - TextStyle linkStyle = parseContext.childStyle.merge(TextStyle( - decoration: TextDecoration.underline, - color: Colors.blueAccent, - decorationColor: Colors.blueAccent, - )); + TextStyle _linkStyle = parseContext.childStyle.merge(linkStyle); LinkTextSpan span = LinkTextSpan( - style: linkStyle, + style: _linkStyle, url: url, onLinkTap: onLinkTap, children: [], @@ -812,6 +813,10 @@ class HtmlOldParser extends StatelessWidget { this.customRender, this.blockSpacing, this.html, + this.linkStyle = const TextStyle( + decoration: TextDecoration.underline, + color: Colors.blueAccent, + decorationColor: Colors.blueAccent), }); final double width; @@ -820,6 +825,7 @@ class HtmlOldParser extends StatelessWidget { final CustomRender customRender; final double blockSpacing; final String html; + final TextStyle linkStyle; static const _supportedElements = [ "a", @@ -940,10 +946,7 @@ class HtmlOldParser extends StatelessWidget { child: Wrap( children: _parseNodeList(node.nodes), ), - style: const TextStyle( - decoration: TextDecoration.underline, - color: Colors.blueAccent, - decorationColor: Colors.blueAccent), + style: linkStyle, ), onTap: () { if (node.attributes.containsKey('href') && onLinkTap != null) { From af77caa033c278346fd375dcfef17441cf43ab71 Mon Sep 17 00:00:00 2001 From: Lucas Reiners Date: Fri, 8 Mar 2019 13:35:05 +0100 Subject: [PATCH 058/638] Added custom textstyle and edgeinsets callback --- lib/flutter_html.dart | 6 ++++++ lib/html_parser.dart | 25 +++++++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 49399c2fd4..f1554ba09a 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -13,6 +13,8 @@ class Html extends StatelessWidget { this.onLinkTap, this.renderNewlines = false, this.customRender, + this.customEdgeInsets, + this.customTextStyle, this.blockSpacing = 14.0, this.useRichText = false, }) : super(key: key); @@ -29,6 +31,8 @@ class Html extends StatelessWidget { /// Either return a custom widget for specific node types or return null to /// fallback to the default rendering. final CustomRender customRender; + final CustomEdgeInsets customEdgeInsets; + final CustomTextStyle customTextStyle; @override Widget build(BuildContext context) { @@ -45,6 +49,8 @@ class Html extends StatelessWidget { width: width, onLinkTap: onLinkTap, renderNewlines: renderNewlines, + customEdgeInsets: customEdgeInsets, + customTextStyle: customTextStyle, html: data, ) : HtmlOldParser( diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 9abe77bbc2..8365a56e19 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -6,6 +6,8 @@ import 'package:html/parser.dart' as parser; import 'package:html/dom.dart' as dom; typedef CustomRender = Widget Function(dom.Node node, List children); +typedef CustomTextStyle = TextStyle Function(dom.Node node, TextStyle baseStyle); +typedef CustomEdgeInsets = EdgeInsets Function(dom.Node node); typedef OnLinkTap = void Function(String url); const OFFSET_TAGS_FONT_SIZE_FACTOR = @@ -144,6 +146,8 @@ class HtmlRichTextParser extends StatelessWidget { this.onLinkTap, this.renderNewlines = false, this.html, + this.customTextStyle, + this.customEdgeInsets, }); final double indentSize = 10.0; @@ -152,6 +156,8 @@ class HtmlRichTextParser extends StatelessWidget { final onLinkTap; final bool renderNewlines; final String html; + final CustomTextStyle customTextStyle; + final CustomEdgeInsets customEdgeInsets; // style elements set a default style // for all child nodes @@ -472,6 +478,14 @@ class HtmlRichTextParser extends StatelessWidget { nextContext.blockType = 'blockquote'; break; } + + if (customTextStyle != null) { + final TextStyle customStyle = customTextStyle(node, childStyle); + if (customStyle != null) { + childStyle = customStyle; + } + } + nextContext.childStyle = childStyle; } @@ -610,6 +624,11 @@ class HtmlRichTextParser extends StatelessWidget { parseContext.parentElement = null; TextAlign textAlign = TextAlign.left; + EdgeInsets _customEdgeInsets; + if (customEdgeInsets != null) { + _customEdgeInsets = customEdgeInsets(node); + } + switch (node.localName) { case "hr": parseContext.rootWidgetList @@ -715,10 +734,8 @@ class HtmlRichTextParser extends StatelessWidget { )); } BlockText blockText = BlockText( - margin: EdgeInsets.only( - top: 8.0, - bottom: 8.0, - left: parseContext.indentLevel * indentSize), + margin: _customEdgeInsets ?? + EdgeInsets.only(top: 8.0, bottom: 8.0, left: parseContext.indentLevel * indentSize), padding: EdgeInsets.all(2.0), decoration: decoration, child: RichText( From 90c0daacbf65c79561f7d7aae80c321b06dee5e2 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Mon, 11 Mar 2019 22:54:19 -0600 Subject: [PATCH 059/638] Version 0.9.5 --- CHANGELOG.md | 6 +++ README.md | 7 ++- example/main.dart | 5 ++ lib/html_parser.dart | 11 ++-- pubspec.yaml | 2 +- test/html_parser_test.dart | 104 +++++++++++++++++++++++++++++++++---- 6 files changed, 118 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb64166d82..6c119847a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [0.9.5] - March 11, 2019: + +* Add support for `span` in `RichText` parser. ([#61](https://github.com/Sub6Resources/flutter_html/issues/61)) +* Adds `linkStyle` attribute. ([#70](https://github.com/Sub6Resources/flutter_html/pull/70)) +* Adds tests for `header`, `hr`, and `i` ([#62](https://github.com/Sub6Resources/flutter_html/issues/62)) + ## [0.9.4] - February 5, 2019: * Fixes `table` error in `RichText` parser. ([#58](https://github.com/Sub6Resources/flutter_html/issues/58)) diff --git a/README.md b/README.md index a1898c6eb8..d1bfe8c383 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.9.4 + flutter_html: ^0.9.5 ## Currently Supported HTML Tags: `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 padding: EdgeInsets.all(8.0), backgroundColor: Colors.white70, defaultTextStyle: TextStyle(fontFamily: 'serif'), + linkStyle: const TextStyle( + color: Colors.redAccent, + ), onLinkTap: (url) { // open url in a webview }, @@ -85,4 +88,4 @@ Check out the official Flutter WebView package here: https://pub.dartlang.org/pa This package has a known issue where text does not wrap correctly. Setting `useRichText` to true fixes the issue by using an alternate parser. The alternate parser, however, does not support the `customRender` callback, and several elements -supported by the default parser are not supported by the alternate parser. +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). diff --git a/example/main.dart b/example/main.dart index 55035f0847..9f125b29f9 100644 --- a/example/main.dart +++ b/example/main.dart @@ -121,6 +121,11 @@ class _MyHomePageState extends State { """, //Optional parameters: padding: EdgeInsets.all(8.0), + linkStyle: const TextStyle( + color: Colors.redAccent, + decorationColor: Colors.redAccent, + decoration: TextDecoration.underline, + ), onLinkTap: (url) { print("Opening $url..."); }, diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 4408df033a..a420198929 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -173,7 +173,8 @@ class HtmlRichTextParser extends StatelessWidget { "acronym", "ol", "ul", - "blockquote" + "blockquote", + "span", ]; // specialty elements require unique handling @@ -476,6 +477,9 @@ class HtmlRichTextParser extends StatelessWidget { nextContext.indentLevel += 1; nextContext.blockType = 'blockquote'; break; + case "span": + //No additional styles + break; } nextContext.childStyle = childStyle; } @@ -1275,10 +1279,7 @@ class HtmlOldParser extends StatelessWidget { case "hr": return Padding( padding: EdgeInsets.only(top: 7.0, bottom: 7.0), - child: Container( - height: 0.0, - decoration: BoxDecoration(border: Border.all()), - ), + child: Divider(height: 1.0, color: Colors.black38), ); case "i": return DefaultTextStyle.merge( diff --git a/pubspec.yaml b/pubspec.yaml index 9eca8564c9..989f2f90c2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) -version: 0.9.4 +version: 0.9.5 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html diff --git a/test/html_parser_test.dart b/test/html_parser_test.dart index c60ea77da5..348fe5e48e 100644 --- a/test/html_parser_test.dart +++ b/test/html_parser_test.dart @@ -4,8 +4,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_html/flutter_html.dart'; void main() { - - testWidgets("Check that default parser does not fail on empty data", (tester) async { + testWidgets("Check that default parser does not fail on empty data", + (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -17,7 +17,8 @@ void main() { ); }); - testWidgets("Check that RichText parser does not fail on empty data", (tester) async { + testWidgets("Check that RichText parser does not fail on empty data", + (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -59,7 +60,8 @@ void main() { expect(find.byType(RichText), findsOneWidget); }); - testWidgets("Check that tapping on the `a` tag calls the callback", (tester) async { + testWidgets("Check that tapping on the `a` tag calls the callback", + (tester) async { String html = "Test link"; String urlTapped; @@ -79,7 +81,9 @@ void main() { expect(urlTapped, "https://github.com"); }); - testWidgets("Check that tapping on the `a` tag calls the callback `RichText` parser", (tester) async { + testWidgets( + "Check that tapping on the `a` tag calls the callback `RichText` parser", + (tester) async { String html = "Test link"; String urlTapped; @@ -112,7 +116,6 @@ void main() { ), ); - expect(find.text("Abbreviation"), findsOneWidget); await tester.pumpWidget( @@ -140,7 +143,6 @@ void main() { ), ); - expect(find.text("Acronym"), findsOneWidget); await tester.pumpWidget( @@ -168,7 +170,6 @@ void main() { ), ); - expect(find.text("Address"), findsOneWidget); //Not supported in `RichText` parser. @@ -197,7 +198,6 @@ void main() { ), ); - expect(find.text("Article"), findsOneWidget); await tester.pumpWidget( @@ -1004,4 +1004,90 @@ void main() { expect(find.byType(BlockText), findsOneWidget); }); + testWidgets("Check that `header` tag renders", (tester) async { + String html = "
header
"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("header"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(BlockText), findsOneWidget); + }); + + testWidgets("Check that `hr` tag renders", (tester) async { + String html = "
"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.byType(Divider), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(Divider), findsOneWidget); + }); + + testWidgets("Check that `i` tag renders", (tester) async { + String html = "i"; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + ), + ), + ), + ); + + expect(find.text("i"), findsOneWidget); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Html( + data: html, + useRichText: true, + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); } From f8cb45208a0b609a5430fce109b1b77c0b4aab85 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Mon, 11 Mar 2019 23:19:41 -0600 Subject: [PATCH 060/638] Version 0.9.6 --- CHANGELOG.md | 4 ++++ README.md | 2 +- lib/html_parser.dart | 10 +++++----- pubspec.yaml | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c119847a1..9f1c3d49fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.9.6] - March 11, 2019: + +* Fix whitespace issue. ([#59](https://github.com/Sub6Resources/flutter_html/issues/59)) + ## [0.9.5] - March 11, 2019: * Add support for `span` in `RichText` parser. ([#61](https://github.com/Sub6Resources/flutter_html/issues/61)) diff --git a/README.md b/README.md index d1bfe8c383..30da6a339e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A Flutter widget for rendering static html tags as Flutter widgets. (Will render Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.9.5 + flutter_html: ^0.9.6 ## Currently Supported HTML Tags: `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` diff --git a/lib/html_parser.dart b/lib/html_parser.dart index a420198929..1b4b944165 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -329,14 +329,14 @@ class HtmlRichTextParser extends StatelessWidget { if (!parseContext.parentElement.children.isEmpty) { lastString = parseContext.parentElement.children.last.text ?? ''; } - if (lastString == '' || - lastString.endsWith(' ') || - lastString.endsWith('\n')) finalText = finalText.trimLeft(); + if (lastString.endsWith(' ') || lastString.endsWith('\n')) { + finalText = finalText.trimLeft(); + } } } - // if the finalText is actually empty, just return - if (finalText.trim().isEmpty) return; + // if the finalText is actually empty, just return (unless it's just a space) + if (finalText.trim().isEmpty && finalText != " ") return; // NOW WE HAVE OUR TRULY FINAL TEXT // debugPrint("Plain Text Node: '$finalText'"); diff --git a/pubspec.yaml b/pubspec.yaml index 989f2f90c2..dfd2232f8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget for rendering static html tags as Flutter widgets. (Will render over 70 different html tags!) -version: 0.9.5 +version: 0.9.6 author: Matthew Whitaker homepage: https://github.com/Sub6Resources/flutter_html From 9ee86eec781b4d8d992a1cee328f9baec2ff9fa1 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Tue, 12 Mar 2019 18:31:51 -0600 Subject: [PATCH 061/638] Add initial structure for new parser --- lib/html_parser.dart | 46 ++++++++++++++++++++++++++++ lib/html_types.dart | 71 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 lib/html_types.dart diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 1b4b944165..e2ee7e9e10 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/gestures.dart'; import 'package:html/parser.dart' as parser; import 'package:html/dom.dart' as dom; +import 'package:flutter_html/html_types.dart'; typedef CustomRender = Widget Function(dom.Node node, List children); typedef OnLinkTap = void Function(String url); @@ -1722,3 +1723,48 @@ class HtmlOldParser extends StatelessWidget { return false; } } + +class HtmlParser extends StatelessWidget { + final String html; + + HtmlParser(this.html); + + @override + Widget build(BuildContext context) { + return parseTree(context, lexDomTree(parseHTML(html))); + } + + /// [parseHTML] converts a string to a DOM document using the dart `html` library. + static dom.Document parseHTML(String data) { + return parser.parse(data); + } + + /// [lexDomTree] converts a DOM document to a simplified tree of [StyledElement]s. + static StyledElement lexDomTree(dom.Document html) { + //TODO + } + + /// [cleanTree] optimizes the [StyledElement] tree so all [BlockElement]s are + /// on the first level and redundant levels are collapsed. + static StyledElement cleanTree(StyledElement tree) { + //TODO + } + + /// [parseTree] converts a tree of [StyledElement]s to a Flutter [Widget] tree. + static Widget parseTree(BuildContext context, StyledElement tree) { + //TODO + } +} + +/// A [CustomRenderer] is used to render html tags in your own way or implement unsupported tags. +class CustomRenderer { + final String name; + final Widget Function(BuildContext context) render; + final ElementType renderAs; + + CustomRenderer( + this.name, { + this.render, + this.renderAs, + }); +} diff --git a/lib/html_types.dart b/lib/html_types.dart new file mode 100644 index 0000000000..5ba997d090 --- /dev/null +++ b/lib/html_types.dart @@ -0,0 +1,71 @@ +const STYLED_ELEMENTS = [ + "b", +]; + +const INTERACTABLE_ELEMENTS = [ + "a", +]; + +const BLOCK_ELEMENTS = [ + "div", + +]; + +const CONTENT_ELEMENTS = [ + "img", +]; + +/// A [StyledElement] applies a style to all of its children. +class StyledElement { + final String name; + final List children; + final dynamic style; //TODO + + StyledElement({ + this.name, + this.children, + this.style, + }); +} + +/// A [Gesture] indicates the type of interaction by a user. +enum Gesture { + TAP, +} + +/// An [InteractableElement] is a [StyledElement] that takes user gestures (e.g. tap). +class InteractableElement extends StyledElement { + final void Function(Gesture gesture, dynamic data) onGesture; + + InteractableElement({ + this.onGesture, + }); +} + +/// A [Block] contains information about a [BlockElement] (width, height, padding, margins) +class Block { + //TODO +} + +/// A [BlockElement] is a [StyledElement] that wraps before and after the its [children]. +/// +/// A [BlockElement] may have a margin/padding or be a set width/height. +class BlockElement extends StyledElement { + final Block block; + + BlockElement({ + this.block, + }); +} + +/// A [ContentElement] is a type of [TextElement] that renders itself, but none of its [children]. +/// +/// A [ContentElement] may use its [children] to determine how it should render (e.g.
This is the table's caption
Head 1Head 2Head 3
Head 1*Head 2Head 3
Data 1Long Data 2Really, realllllly, long data 3
Data 1Long Data 2Really, realllllly, long data 3
Data 1Long Data 2Really, realllllly, long data 3
Caption
+ + + + + + + +
OneTwoThree
DataDataData
DataDataData
DataDataData
DataDataData
+ + + + + +
-

Pricing

-

Lorem ipsum dolor sit amet.

-
- This is some center text... ABBR and ACRONYM -
-

The Team

-

There isn't really a team...

-

Installation

-

You cannot install a nonexistent product!

-

Don't ask me to find x in

-

log2(x2 - 6x) = 3 + log2(1 - x)

-
-

bdi and bdo Test:

-

- In the example below, usernames are shown along with the number of points in a contest. - If the bdi element is not supported in the browser, the username of the Arabic user would confuse the text (the bidirectional algorithm would put the colon and the number "90" next to the word "User" rather than next to the word "points"). -

- + Flutter Website
+ +
    +
  1. This
  2. +
  3. is
  4. +
  5. an
  6. +
  7. + ordered
      -
    • User hrefs: 60 points
    • -
    • User jdoe: 80 points
    • -
    • User إيان: 90 points
    • - Swapped! - This text will go left to right! - With bdi: User إيان: 90 points - Without bdi: User إيان: 90 points - ltr w/ bdi: User إيان: 90 points - ltr w/o bdi: User إيان: 90 points +
    • With
    • +
    • a
    • +
    • nested
    • +
    • unordered
    • +
    • list
    -
-
- - - - - - - - - - - -
This is the table's caption
Head 1*Head 2Head 3
Data 1Long Data 2Really, realllllly, long data 3
Data 1Long Data 2Really, realllllly, long data 3
Data 1Long Data 2Really, realllllly, long data 3
Different 1Different reallllllly long 2Diff 3
This spans 2 columnsNormal td
In foot 1In foot 2In foot long 2
-
-
Nested div
-
-
-            jQuery("#monkey");
-            
-
-

This is a fancy quote

-
-
- Second nested div
-
- -
Available on GitHub
-
-
-
Third nested div
-
-

Second header

-

Third header

-
Fourth div
+ +
  • list!
  • + +
    + + """, - //Optional parameters: - style: { - "ul": Style( - fontFamily: "monospace", - fontSize: 24, - margin: EdgeInsets.zero, -// block: Block( -// border: Border.all(width: 2), -// ), - backgroundColor: Colors.blue, - ), - }, - onLinkTap: (url) { - print("Opening $url..."); - }, - onImageTap: (src) { - print(src); - }, - ), + //Optional parameters: + style: { + "html": Style.fromTextStyle(TextStyle(fontFamily: 'monospace')), + "a": Style( + color: Colors.red, + ), + }, + customRender: { + "flutter": (RenderContext context, Widget child, attributes) { + return FlutterLogo( + style: (attributes['horizontal'] != null)? FlutterLogoStyle.horizontal: FlutterLogoStyle.markOnly, + textColor: context.style.color, + size: context.style.fontSize * 5, + ); + } + }, + onLinkTap: (url) { + print("Opening $url..."); + }, + onImageTap: (src) { + print(src); + }, ), ), ); diff --git a/example/pubspec.lock b/example/pubspec.lock index 6457c9a3e9..3e2d4c9b4b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -22,6 +22,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.2" + chewie: + dependency: transitive + description: + name: chewie + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.8" + chewie_audio: + dependency: transitive + description: + name: chewie_audio + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0+1" collection: dependency: transitive description: @@ -29,6 +43,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" css_colors: dependency: transitive description: @@ -62,6 +83,13 @@ packages: relative: true source: path version: "1.0.0-pre.1" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.1" flutter_test: dependency: "direct dev" description: flutter @@ -88,6 +116,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.7" + open_iconic_flutter: + dependency: transitive + description: + name: open_iconic_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" path: dependency: transitive description: @@ -95,6 +130,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.4" + path_drawing: + dependency: transitive + description: + name: path_drawing + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" pedantic: dependency: transitive description: @@ -102,6 +151,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0+1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" quiver: dependency: transitive description: @@ -109,6 +165,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.5" + screen: + dependency: transitive + description: + name: screen + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.5" sky_engine: dependency: transitive description: flutter @@ -170,6 +233,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + video_player: + dependency: transitive + description: + name: video_player + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.2+1" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.14+1" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "3.5.0" sdks: - dart: ">=2.2.2 <3.0.0" - flutter: ">=0.7.4 <2.0.0" + dart: ">=2.4.0 <3.0.0" + flutter: ">=1.7.4 <2.0.0" diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 3b32ae92fb..6d4dd1e08a 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -12,10 +12,10 @@ class Html extends StatelessWidget { @required this.data, this.css = "", @deprecated this.padding, - this.backgroundColor, + @deprecated this.backgroundColor, @deprecated this.defaultTextStyle, this.onLinkTap, - this.renderNewlines = false, + @deprecated this.renderNewlines = false, //TODO(Sub6Resources): Document alternatives this.customRender, @deprecated this.customEdgeInsets, @deprecated this.customTextStyle, @@ -30,7 +30,8 @@ class Html extends StatelessWidget { this.shrinkToFit = false, this.imageProperties, this.onImageTap, - this.showImages = true, + @deprecated this.showImages = true, + this.blacklistedElements = const [], this.style, }) : super(key: key); @@ -52,9 +53,11 @@ class Html extends StatelessWidget { final OnImageTap onImageTap; final bool showImages; + final List blacklistedElements; + /// Either return a custom widget for specific node types or return null to /// fallback to the default rendering. - final CustomRender customRender; + final Map customRender; final CustomEdgeInsets customEdgeInsets; final CustomTextStyle customTextStyle; final CustomTextAlign customTextAlign; @@ -99,6 +102,8 @@ class Html extends StatelessWidget { cssData: css, onLinkTap: onLinkTap, style: style, + customRender: customRender, + blacklistedElements: blacklistedElements, ), ); } diff --git a/lib/html_parser.dart b/lib/html_parser.dart index e3a42ce98b..b56a1cf3fc 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -1,3 +1,5 @@ +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_html/src/layout_element.dart'; import 'package:flutter_html/style.dart'; import 'package:flutter/material.dart'; import 'package:csslib/visitor.dart' as css; @@ -7,32 +9,37 @@ import 'package:html/parser.dart' as htmlparser; import 'package:csslib/parser.dart' as cssparser; typedef OnLinkTap = void Function(String url); +typedef CustomRender = Widget Function( + RenderContext context, Widget child, Map elementAttributes); class HtmlParser extends StatelessWidget { final String htmlData; final String cssData; final OnLinkTap onLinkTap; final Map style; + final Map customRender; + final List blacklistedElements; HtmlParser({ @required this.htmlData, @required this.cssData, this.onLinkTap, this.style, + this.customRender, + this.blacklistedElements, }); @override Widget build(BuildContext context) { dom.Document document = parseHTML(htmlData); css.StyleSheet sheet = parseCSS(cssData); - StyledElement lexedTree = lexDomTree(document); + StyledElement lexedTree = lexDomTree(document, customRender?.keys?.toList() ?? [], blacklistedElements); StyledElement styledTree = applyCSS(lexedTree, sheet); StyledElement inlineStyledTree = applyInlineStyles(styledTree); StyledElement customStyledTree = _applyCustomStyles(inlineStyledTree); StyledElement cleanedTree = cleanTree(customStyledTree); - print(cleanedTree); InlineSpan parsedTree = parseTree( - RenderContext(style: Theme.of(context).textTheme.body1), + RenderContext(style: Style.fromTextStyle(Theme.of(context).textTheme.body1)), cleanedTree, ); @@ -49,7 +56,8 @@ class HtmlParser extends StatelessWidget { } /// [lexDomTree] converts a DOM document to a simplified tree of [StyledElement]s. - static StyledElement lexDomTree(dom.Document html) { + static StyledElement lexDomTree( + dom.Document html, List customRenderTags, List blacklistedElements) { StyledElement tree = StyledElement( name: "[Tree Root]", children: new List(), @@ -57,21 +65,26 @@ class HtmlParser extends StatelessWidget { ); html.nodes.forEach((node) { - tree.children.add(_recursiveLexer(node)); + tree.children.add(_recursiveLexer(node, customRenderTags, blacklistedElements)); }); return tree; } //TODO(Sub6Resources): Apply inline styles - static StyledElement _recursiveLexer(dom.Node node) { + static StyledElement _recursiveLexer( + dom.Node node, List customRenderTags, List blacklistedElements) { List children = List(); node.nodes.forEach((childNode) { - children.add(_recursiveLexer(childNode)); + children.add(_recursiveLexer(childNode, customRenderTags, blacklistedElements)); }); + //TODO(Sub6Resources): There's probably a more efficient way to look this up. if (node is dom.Element) { + if (blacklistedElements?.contains(node.localName) ?? false) { + return EmptyContentElement(); + } if (STYLED_ELEMENTS.contains(node.localName)) { return parseStyledElement(node, children); } else if (INTERACTABLE_ELEMENTS.contains(node.localName)) { @@ -80,6 +93,10 @@ class HtmlParser extends StatelessWidget { return parseBlockElement(node, children); } else if (REPLACED_ELEMENTS.contains(node.localName)) { return parseReplacedElement(node); + } else if (LAYOUT_ELEMENTS.contains(node.localName)) { + return parseLayoutElement(node, children); + } else if (customRenderTags.contains(node.localName)) { + return parseStyledElement(node, children); } else { return EmptyContentElement(); } @@ -93,8 +110,8 @@ class HtmlParser extends StatelessWidget { static StyledElement applyCSS(StyledElement tree, css.StyleSheet sheet) { sheet.topLevels.forEach((treeNode) { if (treeNode is css.RuleSet) { - print(treeNode.selectorGroup.selectors.first.simpleSelectorSequences - .first.simpleSelector.name); + print(treeNode + .selectorGroup.selectors.first.simpleSelectorSequences.first.simpleSelector.name); } }); @@ -142,20 +159,37 @@ class HtmlParser extends StatelessWidget { // Merge this element's style into the context so that children // inherit the correct style RenderContext newContext = RenderContext( - style: context.style.merge(tree.style?.generateTextStyle()), + style: context.style.merge(tree.style), ); + if (customRender?.containsKey(tree.name) ?? false) { + return WidgetSpan( + child: ContainerSpan( + thisContext: context, + newContext: newContext, + style: tree.style, + child: customRender[tree.name].call( + newContext, + ContainerSpan( + thisContext: context, + newContext: newContext, + style: tree.style, + children: tree.children?.map((tree) => parseTree(newContext, tree))?.toList() ?? [], + ), + tree.attributes, + ), + ), + ); + } + //Return the correct InlineSpan based on the element type. if (tree.style?.display == Display.BLOCK) { return WidgetSpan( child: ContainerSpan( - style: tree.style, - thisContext: context, newContext: newContext, - children: tree.children - ?.map((tree) => parseTree(newContext, tree)) - ?.toList() ?? - [], + thisContext: context, + style: tree.style, + children: tree.children?.map((tree) => parseTree(newContext, tree))?.toList() ?? [], ), ); } else if (tree is ReplacedElement) { @@ -174,21 +208,21 @@ class HtmlParser extends StatelessWidget { onTap: () => onLinkTap(tree.href), child: RichText( text: TextSpan( - style: context.style.merge(tree.style?.generateTextStyle()), - children: tree.children - .map((tree) => parseTree(newContext, tree)) - .toList() ?? - [], + style: newContext.style.generateTextStyle(), + children: tree.children.map((tree) => parseTree(newContext, tree)).toList() ?? [], ), ), ), ); + } else if (tree is LayoutElement) { + return WidgetSpan( + child: tree.toWidget(context), + ); } else { ///[tree] is an inline element, as such, it can only have horizontal margins. return TextSpan( - style: context.style.merge(tree.style?.generateTextStyle()), - children: - tree.children.map((tree) => parseTree(newContext, tree)).toList(), + style: newContext.style.generateTextStyle(), + children: tree.children.map((tree) => parseTree(newContext, tree)).toList(), ); } } @@ -220,8 +254,7 @@ class HtmlParser extends StatelessWidget { /// [processListCharacters] adds list characters to the front of all list items. static StyledElement _processListCharacters(StyledElement tree) { - if (tree.style?.display == Display.BLOCK && - (tree.name == "ol" || tree.name == "ul")) { + if (tree.style?.display == Display.BLOCK && (tree.name == "ol" || tree.name == "ul")) { for (int i = 0; i < tree.children?.length; i++) { if (tree.children[i].name == "li") { tree.children[i].children?.insert( @@ -266,32 +299,21 @@ class HtmlParser extends StatelessWidget { } } -/// A [CustomRenderer] is used to render html tags in your own way or implement unsupported tags. -class CustomRenderer { - final String name; - final Widget Function(BuildContext context) render; - final ElementType renderAs; - - CustomRenderer( - this.name, { - this.render, - this.renderAs, - }); -} - class RenderContext { - TextStyle style; + final Style style; RenderContext({this.style}); } class ContainerSpan extends StatelessWidget { + final Widget child; final List children; final Style style; final RenderContext thisContext; final RenderContext newContext; ContainerSpan({ + this.child, this.children, this.style, this.thisContext, @@ -299,7 +321,7 @@ class ContainerSpan extends StatelessWidget { }); @override - Widget build(BuildContext context) { + Widget build(BuildContext _) { return Container( decoration: BoxDecoration( border: style?.block?.border, @@ -310,12 +332,13 @@ class ContainerSpan extends StatelessWidget { padding: style?.padding, margin: style?.margin, alignment: style?.block?.alignment, - child: RichText( - text: TextSpan( - style: thisContext.style.merge(style?.generateTextStyle()), - children: children, - ), - ), + child: child ?? + RichText( + text: TextSpan( + style: thisContext.style.merge(style).generateTextStyle(), + children: children, + ), + ), ); } } diff --git a/lib/rich_text_parser.dart b/lib/rich_text_parser.dart index 87350c1781..a8c8762994 100644 --- a/lib/rich_text_parser.dart +++ b/lib/rich_text_parser.dart @@ -7,7 +7,6 @@ import 'package:flutter/material.dart'; import 'package:html/dom.dart' as dom; import 'package:html/parser.dart' as parser; -typedef CustomRender = Widget Function(dom.Node node, List children); typedef CustomTextStyle = TextStyle Function( dom.Node node, TextStyle baseStyle, diff --git a/lib/src/html_elements.dart b/lib/src/html_elements.dart index 5382473efc..6ad7751040 100644 --- a/lib/src/html_elements.dart +++ b/lib/src/html_elements.dart @@ -87,17 +87,16 @@ const REPLACED_ELEMENTS = [ "br", "head", "iframe", - "img", //TODO display inline + "img", + "svg", "template", "video", ]; -enum ElementType { - REPLACED, - INLINE, - BLOCK, - INTERACTABLE, -} +const LAYOUT_ELEMENTS = [ + "table", + "tr", +]; /** Here is a list of elements with planned support: @@ -168,7 +167,7 @@ enum ElementType { strong - s [x] sub - s [x] sup - s [x] - svg - c [ ] + svg - c [x] table - b [x] tbody - b [x] td - s [ ] diff --git a/lib/src/interactable_element.dart b/lib/src/interactable_element.dart index 7a98b7b873..8ebd2ce22a 100644 --- a/lib/src/interactable_element.dart +++ b/lib/src/interactable_element.dart @@ -12,7 +12,8 @@ class InteractableElement extends StyledElement { List children, Style style, this.href, - }) : super(name: name, children: children, style: style); + dom.Node node, + }) : super(name: name, children: children, style: style, node: node); } /// A [Gesture] indicates the type of interaction by a user. @@ -25,6 +26,7 @@ InteractableElement parseInteractableElement( InteractableElement interactableElement = InteractableElement( name: element.localName, children: children, + node: element, ); switch (element.localName) { diff --git a/lib/src/layout_element.dart b/lib/src/layout_element.dart new file mode 100644 index 0000000000..d33c55c62d --- /dev/null +++ b/lib/src/layout_element.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/html_parser.dart'; +import 'package:flutter_html/src/styled_element.dart'; +import 'package:flutter_html/style.dart'; +import 'package:html/dom.dart' as dom; + +/// A [LayoutElement] is an element that breaks the normal Inline flow of +/// an html document with a more complex layout. LayoutElements handle +abstract class LayoutElement extends StyledElement { + LayoutElement({ + String name, + List children, + Style style, + dom.Element node, + }) : super(name: name, children: children, style: style, node: node); + + Widget toWidget(RenderContext context); +} + +class TableLayoutElement extends LayoutElement { + TableLayoutElement({ + @required List children, + }) : super(children: children); + + @override + Widget toWidget(RenderContext context) { + return Table( +// children: children.where((e) => e.name == 'tr').map(), + ); + } +} + +class TableRowLayoutElement extends LayoutElement { + TableRowLayoutElement({ + @required List children, +}) : super(children: children); + + @override + Widget toWidget(RenderContext context) { + return Container(child: Text("TABLE ROW")); + } + + TableRow toTableRow(RenderContext context) { + + } +} + +LayoutElement parseLayoutElement(dom.Element element, List children) { + switch (element.localName) { + case "table": + return TableLayoutElement( + children: children, + ); + break; + case "tr": + return TableLayoutElement( + children: children, + ); + break; + default: + return null; + } +} diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index b8f047a18f..6fded7bdc4 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -1,5 +1,12 @@ import 'dart:convert'; +import 'package:chewie/chewie.dart'; +import 'package:chewie_audio/chewie_audio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:video_player/video_player.dart'; +import 'package:webview_flutter/webview_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_html/html_parser.dart'; @@ -7,9 +14,9 @@ import 'package:flutter_html/src/html_elements.dart'; import 'package:flutter_html/style.dart'; import 'package:html/dom.dart' as dom; -/// A [ReplacedElement] is a type of [StyledElement] that renders none of its [children]. +/// A [ReplacedElement] is a type of [StyledElement] that does not require its [children] to be rendered. /// -/// A [ContentElement] may use its children nodes to determine relevant information +/// A [ReplacedElement] may use its children nodes to determine relevant information /// (e.g.