From 0a858db221169ca181cbc12b8509b14e70d01074 Mon Sep 17 00:00:00 2001 From: MatthewWhitaker Date: Wed, 15 Aug 2018 09:43:12 -0600 Subject: [PATCH] Version 0.3.0 --- CHANGELOG.md | 15 +- README.md | 132 ++++++++- lib/html_parser.dart | 569 ++++++++++++++++++++++++++++++++++++- pubspec.yaml | 2 +- test/html_parser_test.dart | 40 ++- 5 files changed, 724 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcecb70292..1026a49833 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,21 @@ +## [0.3.0] - August 15, 2018: + +* Adds support for `abbr`, `address`, `article`, `aside`, `blockquote`, `br`, `cite`, `code`, `data`, `dd`, +`del`, `dfn`, `dl`, `dt`, `figcaption`, `figure`, `footer`, `header`, `hr`, `img`, `ins`, `kbd`, `li`, +`main`, `mark`, `nav`, `noscript`, `pre`, `q`, `rp`, `rt`, `ruby`, `s`, `samp`, `section`, `small`, `span`, +`template`, `time`, and `var` + +* Adds partial support for `a`, `ol`, and `ul` + +## [0.2.0] - August 14, 2018: + +* Adds support for `img`. + ## [0.1.1] - August 14, 2018: * Fixed `b` to be bold, not italic... * Adds support for `em`, and `strong` -* Add support for a default `TextStyle` +* Adds support for a default `TextStyle` ## [0.1.0] - August 14, 2018: diff --git a/README.md b/README.md index f9250e0758..f2124d6804 100644 --- a/README.md +++ b/README.md @@ -7,26 +7,148 @@ A Flutter widget for rendering static html tags as Flutter widgets. Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^0.1.1 + flutter_html: ^0.3.0 ## Currently Supported HTML Tags: + * `abbr` + * `address` + * `article` + * `aside` * `b` + * `blockquote` * `body` + * `br` + * `cite` + * `code` + * `data` + * `dd` + * `del` + * `dfn` * `div` + * `dl` + * `dt` * `em` - * `h1` - `h6` + * `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`, * `strong` + * `template` + * `time` * `u` + * `var` + +### 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` + +### List of _planned_ supported elements: +> These are elements that are planned, but present a specific challenge that makes them somewhat difficult to implement. -Here are a list of elements that this package will never support: + * `audio` + * `bdi` + * `bdo` + * `caption` + * `col` + * `colgroup` + * `details` + * `source` + * `sub` + * `summary` + * `sup` + * `svg` + * `table` + * `tbody` + * `td` + * `tfoot` + * `th` + * `thead` + * `tr` + * `track` + * `video` + * `wbr` - * `script` +### Here are a 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, + though I cannot promise I will merge them. + +> 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` + * `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) + * `strike` (deprecated) + * `style` + * `textarea` (`form` elements are outside the scope of this package) + * `title` (`head` elements are not rendered) + * `tt` (deprecated) -> Note: Unsupported tags will not be rendered ## Why this package? diff --git a/lib/html_parser.dart b/lib/html_parser.dart index ed6e7184dc..1ea635b0c6 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -8,20 +8,63 @@ class HtmlParser { final TextStyle defaultTextStyle; static const _supportedElements = [ + "a", + "abbr", + "address", + "article", + "aside", "b", + "blockquote", "body", + "br", + "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", //partial + "main", + "mark", + "nav", + "noscript", + "ol", //partial "p", + "pre", + "q", + "rp", + "rt", + "ruby", + "s", + "samp", + "section", + "small", + "span", "strong", + "template", + "time", "u", + "ul", //partial + "var", ]; ///Parses an html string and returns a list of widgets that represent the body of your html document. @@ -40,6 +83,51 @@ class HtmlParser { return Container(); } switch (node.localName) { + case "a": + return RichText( + text: TextSpan( + children: [ + TextSpan( + children: _parseInlineElement(node), + style: TextStyle( + decoration: TextDecoration.underline, + ), + ) + ], + style: defaultTextStyle, + )); + case "abbr": + return RichText( + text: TextSpan( + children: [ + TextSpan( + children: _parseInlineElement(node), + style: TextStyle( + decoration: TextDecoration.underline, + decorationStyle: TextDecorationStyle.dotted, + ), + ) + ], + style: defaultTextStyle, + )); + case "address": + return RichText( + text: TextSpan(children: [ + TextSpan( + children: _parseInlineElement(node), + style: TextStyle(fontStyle: FontStyle.italic), + ) + ], style: defaultTextStyle)); + case "article": + return Column( + children: _parseNodeList(node.nodes), + crossAxisAlignment: CrossAxisAlignment.start, + ); + case "aside": + return Column( + children: _parseNodeList(node.nodes), + crossAxisAlignment: CrossAxisAlignment.start, + ); case "b": return RichText( text: TextSpan(children: [ @@ -48,16 +136,99 @@ class HtmlParser { style: TextStyle(fontWeight: FontWeight.bold), ) ], style: defaultTextStyle)); + case "blockquote": + return Padding( + padding: EdgeInsets.fromLTRB(40.0, 14.0, 40.0, 14.0), + child: Column( + children: _parseNodeList(node.nodes), + crossAxisAlignment: CrossAxisAlignment.start, + )); case "body": return Column( children: _parseNodeList(node.nodes), crossAxisAlignment: CrossAxisAlignment.start, ); + case "br": + return Container(height: 14.0); + case "cite": + return RichText( + text: TextSpan( + children: [ + TextSpan( + children: _parseInlineElement(node), + style: TextStyle( + fontStyle: FontStyle.italic, + )) + ], + style: defaultTextStyle, + )); + case "code": + return RichText( + text: TextSpan( + children: [ + TextSpan( + children: _parseInlineElement(node), + style: TextStyle( + fontFamily: 'monospace', + )) + ], + style: defaultTextStyle, + )); + case "data": + return RichText( + text: TextSpan( + children: _parseInlineElement(node), + style: defaultTextStyle, + )); + case "dd": + return Padding( + padding: EdgeInsets.only(left: 40.0), + child: Column( + children: _parseNodeList(node.nodes), + crossAxisAlignment: CrossAxisAlignment.start, + )); + case "del": + return RichText( + text: TextSpan( + children: [ + TextSpan( + children: _parseInlineElement(node), + style: TextStyle( + decoration: TextDecoration.lineThrough, + )) + ], + style: defaultTextStyle, + )); + case "dfn": + return RichText( + text: TextSpan( + children: [ + TextSpan( + children: _parseInlineElement(node), + style: TextStyle( + fontStyle: FontStyle.italic, + )) + ], + style: defaultTextStyle, + )); case "div": return Column( children: _parseNodeList(node.nodes), crossAxisAlignment: CrossAxisAlignment.start, ); + case "dl": + return Padding( + padding: EdgeInsets.only(top: 16.0, bottom: 16.0), + child: Column( + children: _parseNodeList(node.nodes), + crossAxisAlignment: CrossAxisAlignment.start, + )); + case "dt": + return RichText( + text: TextSpan( + children: _parseInlineElement(node), + style: defaultTextStyle, + )); case "em": return RichText( text: TextSpan( @@ -70,6 +241,24 @@ class HtmlParser { ], style: defaultTextStyle, )); + case "figcaption": + return RichText( + text: TextSpan( + children: _parseInlineElement(node), + style: defaultTextStyle, + )); + case "figure": + return Padding( + padding: EdgeInsets.fromLTRB(40.0, 14.0, 40.0, 14.0), + child: Column( + children: _parseNodeList(node.nodes), + crossAxisAlignment: CrossAxisAlignment.start, + )); + case "footer": + return Column( + children: _parseNodeList(node.nodes), + crossAxisAlignment: CrossAxisAlignment.start, + ); case "h1": return RichText( text: TextSpan( @@ -148,6 +337,19 @@ class HtmlParser { ], style: defaultTextStyle, )); + case "header": + return Column( + children: _parseNodeList(node.nodes), + crossAxisAlignment: CrossAxisAlignment.start, + ); + case "hr": + return Padding( + padding: EdgeInsets.only(top: 7.0, bottom: 7.0), + child: Container( + height: 0.0, + decoration: BoxDecoration(border: Border.all()), + ), + ); case "i": return RichText( text: TextSpan( @@ -159,11 +361,168 @@ class HtmlParser { ], style: defaultTextStyle, )); + 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, + )); + case "kbd": + return RichText( + text: TextSpan( + children: [ + TextSpan( + children: _parseInlineElement(node), + style: TextStyle( + fontFamily: 'monospace', + )) + ], + style: defaultTextStyle, + )); + case "li": + return RichText( + text: TextSpan( + children: _parseInlineElement(node), + style: defaultTextStyle, + )); + case "main": + return Column( + children: _parseNodeList(node.nodes), + crossAxisAlignment: CrossAxisAlignment.start, + ); + case "mark": + return RichText( + text: TextSpan( + children: [ + TextSpan( + children: _parseInlineElement(node), + style: TextStyle( + color: Colors.black, background: _getPaint(Colors.yellow)), + ) + ], + style: defaultTextStyle, + )); + case "nav": + return Column( + children: _parseNodeList(node.nodes), + crossAxisAlignment: CrossAxisAlignment.start, + ); + case "noscript": + return Column( + children: _parseNodeList(node.nodes), + crossAxisAlignment: CrossAxisAlignment.start, + ); + case "ol": + return Column( + children: _parseNodeList(node.nodes), + crossAxisAlignment: CrossAxisAlignment.start, + ); case "p": return RichText( text: TextSpan( children: _parseInlineElement(node), )); + 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, + )), + ); + case "q": + return RichText( + text: TextSpan( + children: [ + TextSpan(text: "\""), + TextSpan( + children: _parseInlineElement(node), + ), + TextSpan(text: "\"") + ], + style: defaultTextStyle, + )); + case "rp": + return RichText( + text: TextSpan( + children: _parseInlineElement(node), + style: defaultTextStyle, + )); + case "rt": + return RichText( + text: TextSpan( + children: _parseInlineElement(node), + style: defaultTextStyle, + )); + case "ruby": + return RichText( + text: TextSpan( + children: _parseInlineElement(node), + style: defaultTextStyle, + )); + case "s": + return RichText( + text: TextSpan( + children: [ + TextSpan( + children: _parseInlineElement(node), + style: TextStyle( + decoration: TextDecoration.lineThrough, + )) + ], + style: defaultTextStyle, + )); + case "samp": + return RichText( + text: TextSpan( + children: [ + TextSpan( + children: _parseInlineElement(node), + style: TextStyle( + fontFamily: 'monospace', + )) + ], + style: defaultTextStyle, + )); + case "section": + return Column( + children: _parseNodeList(node.nodes), + crossAxisAlignment: CrossAxisAlignment.start, + ); + case "small": + return RichText( + text: TextSpan( + children: [ + TextSpan( + children: _parseInlineElement(node), + style: TextStyle( + fontSize: 10.0, + )) + ], + style: defaultTextStyle, + )); + case "span": + return RichText( + text: TextSpan( + children: _parseInlineElement(node), + style: defaultTextStyle, + )); case "strong": return RichText( text: TextSpan( @@ -176,6 +535,14 @@ class HtmlParser { ], style: defaultTextStyle, )); + case "template": + return Container(); + case "time": + return RichText( + text: TextSpan( + children: _parseInlineElement(node), + style: defaultTextStyle, + )); case "u": return RichText( text: TextSpan( @@ -189,6 +556,22 @@ class HtmlParser { ], style: defaultTextStyle, )); + 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, + )); } } else if (node is dom.Text) { if (node.text.trim() == "") { @@ -207,8 +590,18 @@ class HtmlParser { } static const _supportedInlineElements = [ + "a", + "abbr", + "address", "b", + "br", + "cite", + "code", + "data", + "dfn", + "dt", "em", + "figcaption", "h1", "h2", "h3", @@ -216,9 +609,23 @@ class HtmlParser { "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) { @@ -231,15 +638,83 @@ class HtmlParser { 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))); + 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))); + style: TextStyle(fontStyle: FontStyle.italic), + children: _parseInlineElement(node), + )); + break; + case "figcaption": + textSpanList.add(TextSpan( + children: _parseInlineElement(node), + )); break; case "h1": textSpanList.add(TextSpan( @@ -294,19 +769,99 @@ class HtmlParser { 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 { @@ -317,4 +872,10 @@ class HtmlParser { return textSpanList; } + + Paint _getPaint(Color color) { + Paint paint = new Paint(); + paint.color = color; + return paint; + } } diff --git a/pubspec.yaml b/pubspec.yaml index 8afb3376d0..90e8403587 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. -version: 0.1.1 +version: 0.3.0 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 a2461bd95f..856c1f53e0 100644 --- a/test/html_parser_test.dart +++ b/test/html_parser_test.dart @@ -12,16 +12,14 @@ void main() { testWidgets('Tests some plain old text', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( - body: Html(data: "This is some plain text"), - ) - )); + 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 { - + testWidgets('Tests that a element gets rendered correctly', + (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: Html( @@ -31,11 +29,11 @@ void main() { )); expect(find.byType(RichText), findsOneWidget); - }); - testWidgets('Tests that a combination of elements and text nodes gets rendered', (WidgetTester tester) async { - + testWidgets( + 'Tests that a combination of elements and text nodes gets rendered', + (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: Html( @@ -46,11 +44,10 @@ void main() { expect(find.byType(RichText), findsNWidgets(2)); expect(find.text(" and plain text"), findsOneWidget); - }); - testWidgets('Tests that a element gets rendered correctly', (WidgetTester tester) async { - + testWidgets('Tests that a element gets rendered correctly', + (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: Html( @@ -62,39 +59,37 @@ void main() { 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 { - + 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", + data: + "Bold Text and Italic bold text and Underlined italic bold text", ), ), )); expect(find.byType(RichText), findsOneWidget); - }); - testWidgets('Tests that the header elements (h1-h6) get rendered correctly', (WidgetTester tester) async { - + 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
", + data: + "

H1

H2

H3

H4

H5
H6
", ), ), )); expect(find.byType(RichText), findsNWidgets(6)); - }); testWidgets('Tests the provided example', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( home: Scaffold( body: Html( @@ -123,6 +118,5 @@ void main() { //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)); - }); }