From d833de11d03909feb359f5a2e5fb7a7c73e0fd57 Mon Sep 17 00:00:00 2001 From: Nguyen Dat Date: Mon, 8 Feb 2021 11:26:29 +0700 Subject: [PATCH 001/361] add custom --- lib/html_parser.dart | 7 ++++--- lib/src/interactable_element.dart | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index e728ee7f6a..3ce9d0f08e 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -16,6 +16,7 @@ import 'package:html/parser.dart' as htmlparser; import 'package:webview_flutter/webview_flutter.dart'; typedef OnTap = void Function(String url); +typedef OnLinkTap = void Function(String url, String onEventClick); typedef CustomRender = dynamic Function( RenderContext context, Widget parsedChild, @@ -25,7 +26,7 @@ typedef CustomRender = dynamic Function( class HtmlParser extends StatelessWidget { final String htmlData; - final OnTap onLinkTap; + final OnLinkTap onLinkTap; final OnTap onImageTap; final ImageErrorListener onImageError; final bool shrinkWrap; @@ -349,7 +350,7 @@ class HtmlParser extends StatelessWidget { : childStyle.merge(childSpan.style)), semanticsLabel: childSpan.semanticsLabel, recognizer: TapGestureRecognizer() - ..onTap = () => onLinkTap?.call(tree.href), + ..onTap = () => onLinkTap?.call(tree.href, tree.onClick), ); } else { return WidgetSpan( @@ -360,7 +361,7 @@ class HtmlParser extends StatelessWidget { MultipleTapGestureRecognizer>( () => MultipleTapGestureRecognizer(), (instance) { - instance..onTap = () => onLinkTap?.call(tree.href); + instance..onTap = () => onLinkTap?.call(tree.href, tree.onClick); }, ), }, diff --git a/lib/src/interactable_element.dart b/lib/src/interactable_element.dart index e19351df94..21100224fc 100644 --- a/lib/src/interactable_element.dart +++ b/lib/src/interactable_element.dart @@ -12,6 +12,7 @@ class InteractableElement extends StyledElement { List children, Style style, this.href, + this.onClick, dom.Node node, }) : super(name: name, children: children, style: style, node: node); } @@ -36,6 +37,7 @@ InteractableElement parseInteractableElement( color: Colors.blue, textDecoration: TextDecoration.underline, ); + interactableElement.onClick = element.attributes['onclick']; break; } From 062d2fd9fb63d1707434adc9f6fdb353c2457755 Mon Sep 17 00:00:00 2001 From: Nguyen Dat Date: Mon, 8 Feb 2021 11:28:26 +0700 Subject: [PATCH 002/361] update sample --- example/lib/main.dart | 2 +- lib/flutter_html.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index eda1888c5c..37300a0cfc 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -185,7 +185,7 @@ class _MyHomePageState extends State { ); }, }, - onLinkTap: (url) { + onLinkTap: (url, event) { print("Opening $url..."); }, onImageTap: (src) { diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index b90e8459ee..2290177626 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -45,7 +45,7 @@ class Html extends StatelessWidget { }) : super(key: key); final String data; - final OnTap onLinkTap; + final OnLinkTap onLinkTap; final ImageErrorListener onImageError; final bool shrinkWrap; From 1719cfe8b0b78ed2476f9b31395d44fe4e588e24 Mon Sep 17 00:00:00 2001 From: Nguyen Dat Date: Mon, 8 Feb 2021 12:13:15 +0700 Subject: [PATCH 003/361] add missing param --- lib/src/interactable_element.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/interactable_element.dart b/lib/src/interactable_element.dart index 21100224fc..fc766fa56d 100644 --- a/lib/src/interactable_element.dart +++ b/lib/src/interactable_element.dart @@ -6,6 +6,7 @@ import 'package:html/dom.dart' as dom; /// An [InteractableElement] is a [StyledElement] that takes user gestures (e.g. tap). class InteractableElement extends StyledElement { String href; + String onClick; InteractableElement({ String name, From eab4eac4cb4fb71e1dab581665d71c63b925bdf1 Mon Sep 17 00:00:00 2001 From: Eric Kok Date: Tue, 9 Feb 2021 11:19:47 +0100 Subject: [PATCH 004/361] Update documentation and bump version in advance of 1.3.0 release --- CHANGELOG.md | 12 ++++++++++++ README.md | 2 +- pubspec.yaml | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 844ae65c37..483dd86918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## [1.3.0] - February 9, 2021: +* New image loading API +* Image loading with request headers, from relative paths and custom loading widget +* SVG image support from network or local assets +* Support for `
`/`` tags +* Allow returning spans from custom tag renders +* Inline font styling +* Content-based table column sizing +* Respect iframe sandbox attribute +* Fixed text flow and styling when using tags inside `` links +* Updated dependencies for Flutter 1.26+ + ## [1.2.0] - January 14, 2021: * Support irregular table sizes * Allow for returning `null` from a customRender function to disable the widget diff --git a/README.md b/README.md index 79f9852641..02d4a971c0 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets. Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^1.2.0 + flutter_html: ^1.3.0 ## Currently Supported HTML Tags: | | | | | | | | | | | | diff --git a/pubspec.yaml b/pubspec.yaml index b3965e7241..f41b5669e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html description: A Flutter widget rendering static HTML and CSS as Flutter widgets. -version: 1.2.0 +version: 1.3.0 homepage: https://github.com/Sub6Resources/flutter_html environment: From b45b827a1206170266fdfed027adbf8de7e714f8 Mon Sep 17 00:00:00 2001 From: tanay Date: Wed, 10 Feb 2021 12:13:52 -0500 Subject: [PATCH 005/361] Support padding and 'start' attribute for lists, support RTL direction for lists --- lib/html_parser.dart | 56 ++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 91cec7a880..fb76741452 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -299,35 +299,35 @@ class HtmlParser extends StatelessWidget { newContext: newContext, style: tree.style, shrinkWrap: context.parser.shrinkWrap, - child: Stack( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + textDirection: tree.style?.direction, children: [ - if (tree.style?.listStylePosition == ListStylePosition.OUTSIDE || - tree.style?.listStylePosition == null) - PositionedDirectional( - width: 30, //TODO derive this from list padding. - start: 0, - child: Text('${newContext.style.markerContent}\t', - textAlign: TextAlign.right, - style: newContext.style.generateTextStyle()), - ), + tree.style?.listStylePosition == ListStylePosition.OUTSIDE || + tree.style?.listStylePosition == null ? Padding( - padding: EdgeInsetsDirectional.only( - start: 30), //TODO derive this from list padding. - child: StyledText( - textSpan: TextSpan( - text: (tree.style?.listStylePosition == - ListStylePosition.INSIDE) - ? '${newContext.style.markerContent}\t' - : null, - children: tree.children - ?.map((tree) => parseTree(newContext, tree)) - ?.toList() ?? - [], - style: newContext.style.generateTextStyle(), - ), - style: newContext.style, - renderContext: context, - ), + padding: tree.style?.padding ?? EdgeInsets.only(left: tree.style?.direction != TextDirection.rtl ? 10.0 : 0.0, right: tree.style?.direction == TextDirection.rtl ? 10.0 : 0.0), + child: Text('${newContext.style.markerContent}\t', + textAlign: TextAlign.right, + style: newContext.style.generateTextStyle()), + ) : Container(height: 0, width: 0), + Expanded( + child: StyledText( + textSpan: TextSpan( + text: (tree.style?.listStylePosition == + ListStylePosition.INSIDE) + ? '${newContext.style.markerContent}\t' + : null, + children: tree.children + ?.map((tree) => parseTree(newContext, tree)) + ?.toList() ?? + [], + style: newContext.style.generateTextStyle(), + ), + style: newContext.style, + renderContext: context, + ) ) ], ), @@ -518,7 +518,7 @@ class HtmlParser extends StatelessWidget { static StyledElement _processListCharactersRecursive( StyledElement tree, ListQueue> olStack) { if (tree.name == 'ol') { - olStack.add(Context(0)); + olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']) ?? 1 : 1) - 1)); } else if (tree.style.display == Display.LIST_ITEM) { switch (tree.style.listStyleType) { case ListStyleType.DISC: From 7c3b33a01d9d98ea24661dfff788f6696ec554bf Mon Sep 17 00:00:00 2001 From: Tanay Neotia Date: Wed, 10 Feb 2021 15:56:26 -0500 Subject: [PATCH 006/361] Fix "unknown character" box showing up when font-weight is below w400 on iOS --- lib/html_parser.dart | 45 ++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index fb76741452..06a067ae27 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -294,6 +294,15 @@ class HtmlParser extends StatelessWidget { ), ); } else if (tree.style?.display == Display.LIST_ITEM) { + List getChildren(StyledElement tree) { + InlineSpan tabSpan = WidgetSpan(child: Text("\t", textAlign: TextAlign.right)); + List children = tree.children?.map((tree) => parseTree(newContext, tree))?.toList() ?? []; + if (tree.style?.listStylePosition == ListStylePosition.INSIDE) { + children.insert(0, tabSpan); + } + return children; + } + return WidgetSpan( child: ContainerSpan( newContext: newContext, @@ -308,25 +317,29 @@ class HtmlParser extends StatelessWidget { tree.style?.listStylePosition == null ? Padding( padding: tree.style?.padding ?? EdgeInsets.only(left: tree.style?.direction != TextDirection.rtl ? 10.0 : 0.0, right: tree.style?.direction == TextDirection.rtl ? 10.0 : 0.0), - child: Text('${newContext.style.markerContent}\t', + child: Text( + newContext.style.markerContent, textAlign: TextAlign.right, - style: newContext.style.generateTextStyle()), + style: newContext.style.generateTextStyle() + ), ) : Container(height: 0, width: 0), + Text("\t", textAlign: TextAlign.right), Expanded( - child: StyledText( - textSpan: TextSpan( - text: (tree.style?.listStylePosition == - ListStylePosition.INSIDE) - ? '${newContext.style.markerContent}\t' - : null, - children: tree.children - ?.map((tree) => parseTree(newContext, tree)) - ?.toList() ?? - [], - style: newContext.style.generateTextStyle(), - ), - style: newContext.style, - renderContext: context, + child: Padding( + padding: tree.style?.listStylePosition == ListStylePosition.INSIDE ? + EdgeInsets.only(left: tree.style?.direction != TextDirection.rtl ? 10.0 : 0.0, right: tree.style?.direction == TextDirection.rtl ? 10.0 : 0.0) : EdgeInsets.zero, + child: StyledText( + textSpan: TextSpan( + text: (tree.style?.listStylePosition == + ListStylePosition.INSIDE) + ? '${newContext.style.markerContent}' + : null, + children: getChildren(tree), + style: newContext.style.generateTextStyle(), + ), + style: newContext.style, + renderContext: context, + ) ) ) ], From 8d4d5293e0eefef90cdeaee67bdbeb2b9c3af8ad Mon Sep 17 00:00:00 2001 From: tanay Date: Thu, 11 Feb 2021 12:48:34 -0500 Subject: [PATCH 007/361] Add the ability for iframe to switch url if HTML data changes --- lib/src/replaced_element.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index 76a8db944c..7456b75cb3 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -90,6 +90,11 @@ class ImageContentElement extends ReplacedElement { } } +/// Create a webview controller and old src independent of [IframeContentElement] +/// to make sure it doesn't reset when the html string is updated +WebViewController controller; +String oldUrl; + /// [IframeContentElement is a [ReplacedElement] with web content. class IframeContentElement extends ReplacedElement { final String src; @@ -110,11 +115,18 @@ class IframeContentElement extends ReplacedElement { @override Widget toWidget(RenderContext context) { final sandboxMode = attributes["sandbox"]; + if (oldUrl != null && src != oldUrl && controller != null) { + controller.loadUrl(src); + } + oldUrl = src; return Container( width: width ?? (height ?? 150) * 2, height: height ?? (width ?? 300) / 2, child: WebView( initialUrl: src, + onWebViewCreated: (WebViewController webController) { + controller = webController; + }, javascriptMode: sandboxMode == null || sandboxMode == "allow-scripts" ? JavascriptMode.unrestricted : JavascriptMode.disabled, From 900b68562555dba36fd346d320a9a99606b946ba Mon Sep 17 00:00:00 2001 From: Eric Kok Date: Fri, 12 Feb 2021 00:02:27 +0100 Subject: [PATCH 008/361] Removed the 'example' entries from the readme index as they don't really help with the navigation --- README.md | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/README.md b/README.md index 02d4a971c0..f98c2de72d 100644 --- a/README.md +++ b/README.md @@ -35,43 +35,27 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets. - [Data](#data) - - [Example](#example-usage---data) - - [onLinkTap](#onlinktap) - - [Example](#example-usage---onlinktap) - - [customRender](#customrender) - - [Example](#example-usages---customrender) - - [onImageError](#onimageerror) - - [Example](#example-usage---onimageerror) - - [onImageTap](#onimagetap) - - [Example](#example-usage---onimagetap) - - [blacklistedElements](#blacklistedelements) - - [Example](#example-usage---blacklistedelements) - - [style](#style) - - [Example](#example-usage---style) - - [navigationDelegateForIframe](#navigationdelegateforiframe) - - [Example](#example-usage---navigationdelegateforiframe) - - [customImageRender](#customimagerender) - [typedef ImageSourceMatcher (with examples)](#typedef-imagesourcematcher) - [typedef ImageRender (with examples)](#typedef-imagerender) - - [Examples](#example-usages---customimagerender) + - [Extended examples](#example-usages---customimagerender) - [Rendering Reference](#rendering-reference) From 037429ef07dffb80cdfc117671e86ff0223d19d5 Mon Sep 17 00:00:00 2001 From: tanay Date: Thu, 11 Feb 2021 18:37:42 -0500 Subject: [PATCH 009/361] Support switching multiple iframes --- lib/src/replaced_element.dart | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index 7456b75cb3..6d6a6f03fd 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -90,17 +90,13 @@ class ImageContentElement extends ReplacedElement { } } -/// Create a webview controller and old src independent of [IframeContentElement] -/// to make sure it doesn't reset when the html string is updated -WebViewController controller; -String oldUrl; - /// [IframeContentElement is a [ReplacedElement] with web content. class IframeContentElement extends ReplacedElement { final String src; final double width; final double height; final NavigationDelegate navigationDelegate; + final GlobalKey key = GlobalKey(); IframeContentElement({ String name, @@ -115,18 +111,12 @@ class IframeContentElement extends ReplacedElement { @override Widget toWidget(RenderContext context) { final sandboxMode = attributes["sandbox"]; - if (oldUrl != null && src != oldUrl && controller != null) { - controller.loadUrl(src); - } - oldUrl = src; return Container( width: width ?? (height ?? 150) * 2, height: height ?? (width ?? 300) / 2, child: WebView( initialUrl: src, - onWebViewCreated: (WebViewController webController) { - controller = webController; - }, + key: key, javascriptMode: sandboxMode == null || sandboxMode == "allow-scripts" ? JavascriptMode.unrestricted : JavascriptMode.disabled, From 8db0104ca333f1a98fea45fdeea5fb10dab95191 Mon Sep 17 00:00:00 2001 From: tanay Date: Sat, 13 Feb 2021 14:59:15 -0500 Subject: [PATCH 010/361] Upgrade link functions to provide more control over onImageTap and onLinkTap --- lib/html_parser.dart | 11 ++++++++--- lib/src/replaced_element.dart | 12 +++++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 60732287d4..546bb54e99 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -16,7 +16,12 @@ import 'package:html/dom.dart' as dom; import 'package:html/parser.dart' as htmlparser; import 'package:webview_flutter/webview_flutter.dart'; -typedef OnTap = void Function(String url); +typedef OnTap = void Function( + String url, + RenderContext context, + Map attributes, + dom.Element element, +); typedef CustomRender = dynamic Function( RenderContext context, Widget parsedChild, @@ -358,7 +363,7 @@ class HtmlParser extends StatelessWidget { : childStyle.merge(childSpan.style)), semanticsLabel: childSpan.semanticsLabel, recognizer: TapGestureRecognizer() - ..onTap = () => onLinkTap?.call(tree.href), + ..onTap = () => onLinkTap?.call(tree.href, context, tree.attributes, tree.element), ); } else { return WidgetSpan( @@ -369,7 +374,7 @@ class HtmlParser extends StatelessWidget { MultipleTapGestureRecognizer>( () => MultipleTapGestureRecognizer(), (instance) { - instance..onTap = () => onLinkTap?.call(tree.href); + instance..onTap = () => onLinkTap?.call(tree.href, context, tree.attributes, tree.element); }, ), }, diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index 76a8db944c..9853084ae8 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_html/html_parser.dart'; import 'package:flutter_html/src/html_elements.dart'; +import 'package:flutter_html/src/utils.dart'; import 'package:flutter_html/style.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:html/dom.dart' as dom; @@ -82,7 +83,16 @@ class ImageContentElement extends ReplacedElement { if (entry.key.call(attributes, element)) { final widget = entry.value.call(context, attributes, element); if (widget != null) { - return widget; + return RawGestureDetector( + child: widget, + gestures: { + MultipleTapGestureRecognizer: GestureRecognizerFactoryWithHandlers( + () => MultipleTapGestureRecognizer(), (instance) { + instance..onTap = () => context.parser.onImageTap?.call(src, context, attributes, element); + }, + ), + }, + ); } } } From e54a7584d1a0d277c5784ce0dc38a2e03c442613 Mon Sep 17 00:00:00 2001 From: tanay Date: Sat, 13 Feb 2021 20:37:42 -0500 Subject: [PATCH 011/361] Support border inline style and hsl & named colors --- lib/src/css_parser.dart | 158 ++++++++++++++++++++++++++++++++++++++-- lib/src/utils.dart | 35 +++++++++ 2 files changed, 187 insertions(+), 6 deletions(-) diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index dd5e08c034..2d60f7549c 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -1,8 +1,11 @@ +import 'dart:math'; import 'dart:ui'; import 'package:csslib/visitor.dart' as css; import 'package:csslib/parser.dart' as cssparser; import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_html/src/utils.dart'; import 'package:flutter_html/style.dart'; Style declarationsToStyle(Map> declarations) { @@ -12,6 +15,19 @@ Style declarationsToStyle(Map> declarations) { case 'background-color': style.backgroundColor = ExpressionMapping.expressionToColor(value.first); break; + case 'border': + List borderWidths = value.whereType().toList(); + /// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping] + borderWidths.removeWhere((element) => element.text != "thin" && element.text != "medium" && element.text != "thick" && !(element is css.LengthTerm)); + List borderColors = value.where((element) => ExpressionMapping.expressionToColor(element) != null).toList(); + List temp = value.whereType().toList(); + /// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future. + List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"]; + /// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping] + temp.removeWhere((element) => !possibleBorderValues.contains(element.text)); + List borderStyles = temp; + style.border = ExpressionMapping.expressionToBorder(borderWidths, borderStyles, borderColors); + break; case 'color': style.color = ExpressionMapping.expressionToColor(value.first); break; @@ -46,7 +62,7 @@ Style declarationsToStyle(Map> declarations) { List textDecorationList = value.whereType().toList(); /// List might include other values than the ones we want for [textDecorationList], so make sure to remove those before passing it to [ExpressionMapping] textDecorationList.removeWhere((element) => element.text != "none" && element.text != "overline" && element.text != "underline" && element.text != "line-through"); - css.Expression textDecorationColor = value.firstWhere((element) => element is css.HexColorTerm || element is css.FunctionTerm, orElse: null); + css.Expression textDecorationColor = value.firstWhere((element) => element is css.HexColorTerm || element is css.FunctionTerm, orElse: () => null); List temp = value.whereType().toList(); /// List might include other values than the ones we want for [textDecorationStyle], so make sure to remove those before passing it to [ExpressionMapping] temp.removeWhere((element) => element.text != "solid" && element.text != "double" && element.text != "dashed" && element.text != "dotted" && element.text != "wavy"); @@ -105,15 +121,120 @@ class DeclarationVisitor extends css.Visitor { //Mapping functions class ExpressionMapping { + + static Border expressionToBorder(List borderWidths, List borderStyles, List borderColors) { + CustomBorderSide left = CustomBorderSide(); + CustomBorderSide top = CustomBorderSide(); + CustomBorderSide right = CustomBorderSide(); + CustomBorderSide bottom = CustomBorderSide(); + if (borderWidths != null) { + top.width = expressionToBorderWidth(borderWidths.first); + if (borderWidths.length == 4) { + right.width = expressionToBorderWidth(borderWidths[1]); + bottom.width = expressionToBorderWidth(borderWidths[2]); + left.width = expressionToBorderWidth(borderWidths.last); + } + if (borderWidths.length == 3) { + left.width = expressionToBorderWidth(borderWidths[1]); + right.width = expressionToBorderWidth(borderWidths[1]); + bottom.width = expressionToBorderWidth(borderWidths.last); + } + if (borderWidths.length == 2) { + bottom.width = expressionToBorderWidth(borderWidths.first); + left.width = expressionToBorderWidth(borderWidths.last); + right.width = expressionToBorderWidth(borderWidths.last); + } + if (borderWidths.length == 1) { + bottom.width = expressionToBorderWidth(borderWidths.first); + left.width = expressionToBorderWidth(borderWidths.first); + right.width = expressionToBorderWidth(borderWidths.first); + } + } + if (borderStyles != null) { + top.style = expressionToBorderStyle(borderStyles.first); + if (borderStyles.length == 4) { + right.style = expressionToBorderStyle(borderStyles[1]); + bottom.style = expressionToBorderStyle(borderStyles[2]); + left.style = expressionToBorderStyle(borderStyles.last); + } + if (borderStyles.length == 3) { + left.style = expressionToBorderStyle(borderStyles[1]); + right.style = expressionToBorderStyle(borderStyles[1]); + bottom.style = expressionToBorderStyle(borderStyles.last); + } + if (borderStyles.length == 2) { + bottom.style = expressionToBorderStyle(borderStyles.first); + left.style = expressionToBorderStyle(borderStyles.last); + right.style = expressionToBorderStyle(borderStyles.last); + } + if (borderStyles.length == 1) { + bottom.style = expressionToBorderStyle(borderStyles.first); + left.style = expressionToBorderStyle(borderStyles.first); + right.style = expressionToBorderStyle(borderStyles.first); + } + } + if (borderColors != null) { + top.color = expressionToColor(borderColors.first); + if (borderColors.length == 4) { + right.color = expressionToColor(borderColors[1]); + bottom.color = expressionToColor(borderColors[2]); + left.color = expressionToColor(borderColors.last); + } + if (borderColors.length == 3) { + left.color = expressionToColor(borderColors[1]); + right.color = expressionToColor(borderColors[1]); + bottom.color = expressionToColor(borderColors.last); + } + if (borderColors.length == 2) { + bottom.color = expressionToColor(borderColors.first); + left.color = expressionToColor(borderColors.last); + right.color = expressionToColor(borderColors.last); + } + if (borderColors.length == 1) { + bottom.color = expressionToColor(borderColors.first); + left.color = expressionToColor(borderColors.first); + right.color = expressionToColor(borderColors.first); + } + } + return Border( + top: BorderSide(width: top.width, color: top.color, style: top.style), + right: BorderSide(width: right.width, color: right.color, style: right.style), + bottom: BorderSide(width: bottom.width, color: bottom.color, style: bottom.style), + left: BorderSide(width: left.width, color: left.color, style: left.style) + ); + } + + static double expressionToBorderWidth(css.LiteralTerm value) { + switch(value.text) { + case "thin": + return 2.0; + case "medium": + return 4.0; + case "thick": + return 6.0; + default: + return double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')); + } + } + + static BorderStyle expressionToBorderStyle(css.LiteralTerm value) { + if (value.text != "none" && value.text != "hidden") { + return BorderStyle.solid; + } + return BorderStyle.none; + } + static Color expressionToColor(css.Expression value) { if (value is css.HexColorTerm) { return stringToColor(value.text); } else if (value is css.FunctionTerm) { - if (value.text == 'rgba') { - return rgbOrRgbaToColor(value.span.text); - } else if (value.text == 'rgb') { + if (value.text == 'rgba' || value.text == 'rgb') { return rgbOrRgbaToColor(value.span.text); + } else if (value.text == 'hsla' || value.text == 'hsl') { + return hslToRgbToColor(value.span.text); } + } else if (value is css.LiteralTerm) { + return namedColorToColor(value.text); } return null; } @@ -350,13 +471,13 @@ class ExpressionMapping { css.LiteralTerm exp4 = list.length > 3 ? list[3] : null; RegExp nonNumberRegex = RegExp(r'\s+(\d+\.\d+)\s+'); if (exp is css.LiteralTerm && exp2 is css.LiteralTerm) { - if (exp3 != null && (exp3 is css.HexColorTerm || exp3 is css.FunctionTerm)) { + if (exp3 != null && ExpressionMapping.expressionToColor(exp3) != null) { shadow.add(Shadow( color: expressionToColor(exp3), offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, '')), double.tryParse(exp2.text.replaceAll(nonNumberRegex, ''))) )); } else if (exp3 != null && exp3 is css.LiteralTerm) { - if (exp4 != null && (exp4 is css.HexColorTerm || exp4 is css.FunctionTerm)) { + if (exp4 != null && ExpressionMapping.expressionToColor(exp4) != null) { shadow.add(Shadow( color: expressionToColor(exp4), offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, '')), double.tryParse(exp2.text.replaceAll(nonNumberRegex, ''))), @@ -418,4 +539,29 @@ class ExpressionMapping { return null; } } + + static Color hslToRgbToColor(String text) { + final hslText = text.replaceAll(')', '').replaceAll(' ', ''); + final hslValues = hslText.split(',').toList(); + List parsedHsl = []; + hslValues.forEach((element) { + if (element.contains("%")) { + parsedHsl.add(double.tryParse(element.replaceAll("%", "")) * 0.01); + } else { + parsedHsl.add(double.tryParse(element)); + } + }); + if (parsedHsl.length == 4) { + return HSLColor.fromAHSL(parsedHsl.last, parsedHsl.first, parsedHsl[1], parsedHsl[2]).toColor(); + } + return HSLColor.fromAHSL(1.0, parsedHsl.first, parsedHsl[1], parsedHsl.last).toColor(); + } + + static Color namedColorToColor(String text) { + String namedColor = namedColors.keys.firstWhere((element) => element.toLowerCase() == text.toLowerCase(), orElse: () => null); + if (namedColor != null) { + return stringToColor(namedColors[namedColor]); + } + return null; + } } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index fddb87d88e..c84a8df0bf 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,4 +1,24 @@ import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +Map namedColors = { + "White": "#FFFFFF", + "Silver": "#C0C0C0", + "Gray": "#808080", + "Black": "#000000", + "Red": "#FF0000", + "Maroon": "#800000", + "Yellow": "#FFFF00", + "Olive": "#808000", + "Lime": "#00FF00", + "Green": "#008000", + "Aqua": "#00FFFF", + "Teal": "#008080", + "Blue": "#0000FF", + "Navy": "#000080", + "Fuchsia": "#FF00FF", + "Purple":"#800080", +}; class Context { T data; @@ -43,3 +63,18 @@ class MultipleTapGestureRecognizer extends TapGestureRecognizer { } } } + +class CustomBorderSide { + CustomBorderSide({ + this.color = const Color(0xFF000000), + this.width = 4.0, + this.style = BorderStyle.none, + }) : assert(color != null), + assert(width != null), + assert(width >= 0.0), + assert(style != null); + + Color color; + double width; + BorderStyle style; +} From fd978abf8c2e9b532eec38a2307a0cfec1abb093 Mon Sep 17 00:00:00 2001 From: tanay Date: Sat, 13 Feb 2021 20:49:03 -0500 Subject: [PATCH 012/361] Update README to include supported inline styles --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index fc917c7dd6..742a26a05f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets. - [Currently Supported CSS Attributes](#currently-supported-css-attributes) +- [Currently Supported Inline CSS Attributes](#currently-supported-inline-css-attributes) + - [Why flutter_html?](#why-this-package) - [API Reference](#api-reference) @@ -122,6 +124,13 @@ Add the following to your `pubspec.yaml` file: |`padding` | `margin`| `text-align`| `text-decoration`| `text-decoration-color`| `text-decoration-style`| `text-decoration-thickness`| |`text-shadow` | `vertical-align`| `white-space`| `width` | `word-spacing`| | | +## Currently Supported Inline CSS Attributes: +| | | | | | | | +|------------------|--------|------------|----------|--------------|------------------------|------------| +|`background-color`| `border` | `color`| `direction`| `display`| `font-family`| `font-feature-settings` | +| `font-size`|`font-style` | `font-weight`| `line-height` | `list-style-type` | `list-style-position`|`padding` | +| `margin`| `text-align`| `text-decoration`| `text-decoration-color`| `text-decoration-style`| `text-shadow` | | + Don't see a tag or attribute you need? File a feature request or contribute to the project! ## Why this package? From 5ee28d145929c0a27c5e560622a46552c3d2e396 Mon Sep 17 00:00:00 2001 From: tanay Date: Mon, 15 Feb 2021 19:13:55 -0500 Subject: [PATCH 013/361] Replace GlobalKey with UniqueKey --- lib/src/replaced_element.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index 6d6a6f03fd..47d4618c40 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -96,7 +96,7 @@ class IframeContentElement extends ReplacedElement { final double width; final double height; final NavigationDelegate navigationDelegate; - final GlobalKey key = GlobalKey(); + final UniqueKey key = UniqueKey(); IframeContentElement({ String name, From 98c725cb56f6fcb6cd741b30547c45980437cde3 Mon Sep 17 00:00:00 2001 From: tanay Date: Tue, 16 Feb 2021 09:34:41 -0500 Subject: [PATCH 014/361] Support creating Html widget from dom.Document --- README.md | 47 ++++++++++++++++++++++++++-- lib/flutter_html.dart | 73 ++++++++++++++++++++++++++++++++----------- lib/html_parser.dart | 6 +++- 3 files changed, 104 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index fc917c7dd6..5c6f02df3a 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,15 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets. - [API Reference](#api-reference) + - [Constructors](#constructors) + - [Parameters Table](#parameters) - [Data](#data) - [Example](#example-usage---data) + + - [Document](#document) - [onLinkTap](#onlinktap) @@ -138,11 +142,20 @@ For a full example, see [here](https://github.com/Sub6Resources/flutter_html/tre Below, you will find brief descriptions of the parameters the`Html` widget accepts and some code snippets to help you use this package. +## Constructors: + +The package currently has two different constructors - `Html()` and `Html.fromDom()`. + +The `Html()` constructor is for those who would like to directly pass HTML from the source to the package to be rendered. + +If you would like to modify or sanitize the HTML before rendering it, then `Html.fromDom()` is for you - you can convert the HTML string to a `Document` and use its methods to modify the HTML as you wish. Then, you can directly pass the modified `Document` to the package. This eliminates the need to parse the modified `Document` back to a string, pass to `Html()`, and convert back to a `Document`, thus cutting down on load times. + ### Parameters: | Parameters | Description | |--------------|-----------------| -| `data` | The HTML data passed to the `Html` widget. This is required and cannot be null. | +| `data` | The HTML data passed to the `Html` widget. This is required and cannot be null when using `Html()`. | +| `document` | The DOM document passed to the `Html` widget. This is required and cannot be null when using `Html.fromDom()`. | | `onLinkTap` | A function that defines what the widget should do when a link is tapped. The function exposes the `src` of the link as a `String` to use in your implementation. | | `customRender` | A powerful API that allows you to customize everything when rendering a specific HTML tag. | | `onImageError` | A function that defines what the widget should do when an image fails to load. The function exposes the exception `Object` and `StackTrace` to use in your implementation. | @@ -155,7 +168,7 @@ Below, you will find brief descriptions of the parameters the`Html` widget accep ### Data: -The HTML data passed to the `Html` widget as a `String`. This is required and cannot be null. +The HTML data passed to the `Html` widget as a `String`. This is required and cannot be null when using `Html`. Any HTML tags in the `String` that are not supported by the package will not be rendered. #### Example Usage - Data: @@ -176,6 +189,36 @@ Widget html = Html( ); ``` +### Document: + +The DOM document passed to the `Html` widget as a `Document`. This is required and cannot be null when using `Html.fromDom()`. +Any HTML tags in the document that are not supported by the package will not be rendered. +Using the `Html.fromDom()` constructor can be useful when you would like to sanitize the HTML string yourself before passing it to the package. + +#### Example Usage - Document: + +```dart +import 'package:html/parser.dart' as htmlparser; +import 'package:html/dom.dart' as dom; +... +String htmlData = """
+

Demo Page

+

This is a fantastic product that you should buy!

+

Features

+
    +
  • It actually works
  • +
  • It exists
  • +
  • It doesn't cost much!
  • +
+ +
"""; +dom.Document document = htmlparser.parse(htmlData); +/// sanitize or query document here +Widget html = Html( + document: document, +); +``` + ### onLinkTap: A function that defines what the widget should do when a link is tapped. diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index faf8468d3a..3aa3ff4fe5 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -5,14 +5,15 @@ import 'package:flutter_html/html_parser.dart'; import 'package:flutter_html/image_render.dart'; import 'package:flutter_html/style.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import 'package:html/dom.dart' as dom; class Html extends StatelessWidget { /// The `Html` widget takes HTML as input and displays a RichText /// tree of the parsed HTML content. /// /// **Attributes** - /// **data** *required* takes in a String of HTML data. - /// + /// **data** *required* takes in a String of HTML data (required only for `Html` constructor). + /// **document** *required* takes in a Document of HTML data (required only for `Html.fromDom` constructor). /// /// **onLinkTap** This function is called whenever a link (`
`) /// is tapped. @@ -34,7 +35,7 @@ class Html extends StatelessWidget { /// See [its wiki page](https://github.com/Sub6Resources/flutter_html/wiki/Style) for more info. Html({ Key key, - @required this.data, + @required String data, this.onLinkTap, this.customRender, this.customImageRenders = const {}, @@ -44,9 +45,54 @@ class Html extends StatelessWidget { this.blacklistedElements = const [], this.style, this.navigationDelegateForIframe, - }) : super(key: key); + }) : htmlParserWidget = HtmlParser( + htmlData: data, + userDocument: null, + onLinkTap: onLinkTap, + onImageTap: onImageTap, + onImageError: onImageError, + shrinkWrap: shrinkWrap, + style: style, + customRender: customRender, + imageRenders: {} + ..addAll(customImageRenders) + ..addAll(defaultImageRenders), + blacklistedElements: blacklistedElements, + navigationDelegateForIframe: navigationDelegateForIframe, + ), + assert (data != null), + super(key: key); + + Html.fromDom({ + Key key, + @required dom.Document document, + this.onLinkTap, + this.customRender, + this.customImageRenders = const {}, + this.onImageError, + this.shrinkWrap = false, + this.onImageTap, + this.blacklistedElements = const [], + this.style, + this.navigationDelegateForIframe, + }) : htmlParserWidget = HtmlParser( + htmlData: null, + userDocument: document, + onLinkTap: onLinkTap, + onImageTap: onImageTap, + onImageError: onImageError, + shrinkWrap: shrinkWrap, + style: style, + customRender: customRender, + imageRenders: {} + ..addAll(customImageRenders) + ..addAll(defaultImageRenders), + blacklistedElements: blacklistedElements, + navigationDelegateForIframe: navigationDelegateForIframe, + ), + assert(document != null), + super(key: key); - final String data; final OnTap onLinkTap; final Map customImageRenders; final ImageErrorListener onImageError; @@ -69,26 +115,15 @@ class Html extends StatelessWidget { /// to use NavigationDelegate. final NavigationDelegate navigationDelegateForIframe; + final Widget htmlParserWidget; + @override Widget build(BuildContext context) { final double width = shrinkWrap ? null : MediaQuery.of(context).size.width; return Container( width: width, - child: HtmlParser( - htmlData: data, - onLinkTap: onLinkTap, - onImageTap: onImageTap, - onImageError: onImageError, - shrinkWrap: shrinkWrap, - style: style, - customRender: customRender, - imageRenders: {} - ..addAll(customImageRenders) - ..addAll(defaultImageRenders), - blacklistedElements: blacklistedElements, - navigationDelegateForIframe: navigationDelegateForIframe, - ), + child: htmlParserWidget, ); } } diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 60732287d4..125a3a0bef 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -26,6 +26,7 @@ typedef CustomRender = dynamic Function( class HtmlParser extends StatelessWidget { final String htmlData; + final dom.Document userDocument; final OnTap onLinkTap; final OnTap onImageTap; final ImageErrorListener onImageError; @@ -39,6 +40,7 @@ class HtmlParser extends StatelessWidget { HtmlParser({ @required this.htmlData, + @required this.userDocument, this.onLinkTap, this.onImageTap, this.onImageError, @@ -52,7 +54,9 @@ class HtmlParser extends StatelessWidget { @override Widget build(BuildContext context) { - dom.Document document = parseHTML(htmlData); + dom.Document document; + if (userDocument == null) document = parseHTML(htmlData); + if (htmlData == null) document = userDocument; StyledElement lexedTree = lexDomTree( document, customRender?.keys?.toList() ?? [], From 4f1306f98a1cea5c03f21cb2cd4d7ca425ee8f69 Mon Sep 17 00:00:00 2001 From: tanay Date: Tue, 16 Feb 2021 10:18:21 -0500 Subject: [PATCH 015/361] Minor refactor to reduce the number of changes --- lib/flutter_html.dart | 56 ++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 3aa3ff4fe5..fd770d993c 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -35,7 +35,7 @@ class Html extends StatelessWidget { /// See [its wiki page](https://github.com/Sub6Resources/flutter_html/wiki/Style) for more info. Html({ Key key, - @required String data, + @required this.data, this.onLinkTap, this.customRender, this.customImageRenders = const {}, @@ -45,27 +45,13 @@ class Html extends StatelessWidget { this.blacklistedElements = const [], this.style, this.navigationDelegateForIframe, - }) : htmlParserWidget = HtmlParser( - htmlData: data, - userDocument: null, - onLinkTap: onLinkTap, - onImageTap: onImageTap, - onImageError: onImageError, - shrinkWrap: shrinkWrap, - style: style, - customRender: customRender, - imageRenders: {} - ..addAll(customImageRenders) - ..addAll(defaultImageRenders), - blacklistedElements: blacklistedElements, - navigationDelegateForIframe: navigationDelegateForIframe, - ), + }) : document = null, assert (data != null), super(key: key); Html.fromDom({ Key key, - @required dom.Document document, + @required this.document, this.onLinkTap, this.customRender, this.customImageRenders = const {}, @@ -75,24 +61,12 @@ class Html extends StatelessWidget { this.blacklistedElements = const [], this.style, this.navigationDelegateForIframe, - }) : htmlParserWidget = HtmlParser( - htmlData: null, - userDocument: document, - onLinkTap: onLinkTap, - onImageTap: onImageTap, - onImageError: onImageError, - shrinkWrap: shrinkWrap, - style: style, - customRender: customRender, - imageRenders: {} - ..addAll(customImageRenders) - ..addAll(defaultImageRenders), - blacklistedElements: blacklistedElements, - navigationDelegateForIframe: navigationDelegateForIframe, - ), + }) : data = null, assert(document != null), super(key: key); + final String data; + final dom.Document document; final OnTap onLinkTap; final Map customImageRenders; final ImageErrorListener onImageError; @@ -115,15 +89,27 @@ class Html extends StatelessWidget { /// to use NavigationDelegate. final NavigationDelegate navigationDelegateForIframe; - final Widget htmlParserWidget; - @override Widget build(BuildContext context) { final double width = shrinkWrap ? null : MediaQuery.of(context).size.width; return Container( width: width, - child: htmlParserWidget, + child: HtmlParser( + htmlData: data, + userDocument: document, + onLinkTap: onLinkTap, + onImageTap: onImageTap, + onImageError: onImageError, + shrinkWrap: shrinkWrap, + style: style, + customRender: customRender, + imageRenders: {} + ..addAll(customImageRenders) + ..addAll(defaultImageRenders), + blacklistedElements: blacklistedElements, + navigationDelegateForIframe: navigationDelegateForIframe, + ), ); } } From 083248dcf08e0676c983b8cf06bbb3d58e8f4f90 Mon Sep 17 00:00:00 2001 From: tanay Date: Tue, 16 Feb 2021 10:27:07 -0500 Subject: [PATCH 016/361] Minor update to changelog for new changes before 1.3.0 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 483dd86918..0cc21292ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## [1.3.0] - February 9, 2021: +## [1.3.0] - February 16, 2021: * New image loading API * Image loading with request headers, from relative paths and custom loading widget * SVG image support from network or local assets @@ -8,6 +8,9 @@ * Content-based table column sizing * Respect iframe sandbox attribute * Fixed text flow and styling when using tags inside `` links +* Fixed issue where `shrinkWrap` property would not constrain the widget to take up the space it needs + * See the [Notes](https://github.com/Sub6Resources/flutter_html#notes) for an example usage with `shrinkWrap` +* Fixed issue where iframes would not update when their `src`s changed in the HTML data * Updated dependencies for Flutter 1.26+ ## [1.2.0] - January 14, 2021: From d9f04b1928ba1f7f041817b320730cbda0fcfa08 Mon Sep 17 00:00:00 2001 From: tanay Date: Wed, 17 Feb 2021 16:39:39 -0500 Subject: [PATCH 017/361] Pre-nullsafety changes --- lib/flutter_html.dart | 4 +- lib/html_parser.dart | 43 ++++++------ lib/src/css_parser.dart | 2 +- lib/src/interactable_element.dart | 41 +++++++----- lib/src/layout_element.dart | 72 +++++++++++--------- lib/src/replaced_element.dart | 105 ++++++++++++++---------------- lib/src/styled_element.dart | 13 ++-- pubspec.yaml | 26 +++++--- test/golden_test.dart | 2 - 9 files changed, 162 insertions(+), 146 deletions(-) diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index faf8468d3a..7e51453658 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -36,13 +36,13 @@ class Html extends StatelessWidget { Key key, @required this.data, this.onLinkTap, - this.customRender, + this.customRender = const {}, this.customImageRenders = const {}, this.onImageError, this.shrinkWrap = false, this.onImageTap, this.blacklistedElements = const [], - this.style, + this.style = const {}, this.navigationDelegateForIframe, }) : super(key: key); diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 60732287d4..c762c29d66 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -39,15 +39,15 @@ class HtmlParser extends StatelessWidget { HtmlParser({ @required this.htmlData, - this.onLinkTap, - this.onImageTap, - this.onImageError, - this.shrinkWrap, - this.style, - this.customRender, - this.imageRenders, - this.blacklistedElements, - this.navigationDelegateForIframe, + @required this.onLinkTap, + @required this.onImageTap, + @required this.onImageError, + @required this.shrinkWrap, + @required this.style, + @required this.customRender, + @required this.imageRenders, + @required this.blacklistedElements, + @required this.navigationDelegateForIframe, }); @override @@ -108,8 +108,9 @@ class HtmlParser extends StatelessWidget { ) { StyledElement tree = StyledElement( name: "[Tree Root]", - children: new List(), + children: [], node: html.documentElement, + style: Style(), ); html.nodes.forEach((node) { @@ -134,7 +135,7 @@ class HtmlParser extends StatelessWidget { List blacklistedElements, NavigationDelegate navigationDelegateForIframe, ) { - List children = List(); + List children = []; node.nodes.forEach((childNode) { children.add(_recursiveLexer( @@ -168,7 +169,7 @@ class HtmlParser extends StatelessWidget { return EmptyContentElement(); } } else if (node is dom.Text) { - return TextContentElement(text: node.text); + return TextContentElement(text: node.text, style: Style()); } else { return EmptyContentElement(); } @@ -667,7 +668,7 @@ class HtmlParser extends StatelessWidget { /// or any block-level [TextContentElement] that contains only whitespace and doesn't follow /// a block element or a line break. static StyledElement _removeEmptyElements(StyledElement tree) { - List toRemove = new List(); + List toRemove = []; bool lastChildBlock = true; tree.children?.forEach((child) { if (child is EmptyContentElement || child is EmptyLayoutElement) { @@ -723,9 +724,9 @@ class RenderContext { final Style style; RenderContext({ - this.buildContext, - this.parser, - this.style, + @required this.buildContext, + @required this.parser, + @required this.style, }); } @@ -743,8 +744,8 @@ class ContainerSpan extends StatelessWidget { ContainerSpan({ this.child, this.children, - this.style, - this.newContext, + @required this.style, + @required this.newContext, this.shrinkWrap = false, }); @@ -780,10 +781,10 @@ class StyledText extends StatelessWidget { final RenderContext renderContext; const StyledText({ - this.textSpan, - this.style, + @required this.textSpan, + @required this.style, this.textScaleFactor = 1.0, - this.renderContext, + @required this.renderContext, }); @override diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index dd5e08c034..90a9f4bfbb 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -91,7 +91,7 @@ class DeclarationVisitor extends css.Visitor { @override void visitDeclaration(css.Declaration node) { _currentProperty = node.property; - _result[_currentProperty] = new List(); + _result[_currentProperty] = []; node.expression.visit(this); } diff --git a/lib/src/interactable_element.dart b/lib/src/interactable_element.dart index 98bdd3df41..6fbcf03918 100644 --- a/lib/src/interactable_element.dart +++ b/lib/src/interactable_element.dart @@ -8,11 +8,11 @@ class InteractableElement extends StyledElement { String href; InteractableElement({ - String name, - List children, - Style style, - this.href, - dom.Node node, + @required String name, + @required List children, + @required Style style, + @required this.href, + @required dom.Node node, }) : super(name: name, children: children, style: style, node: node); } @@ -23,21 +23,26 @@ enum Gesture { InteractableElement parseInteractableElement( dom.Element element, List children) { - InteractableElement interactableElement = InteractableElement( - name: element.localName, - children: children, - node: element, - ); - switch (element.localName) { case "a": - interactableElement.href = element.attributes['href']; - interactableElement.style = Style( - color: Colors.blue, - textDecoration: TextDecoration.underline, + return InteractableElement( + name: element.localName, + children: children, + href: element.attributes['href'], + style: Style( + color: Colors.blue, + textDecoration: TextDecoration.underline, + ), + node: element, + ); + /// will never be called, just to suppress missing return warning + default: + return InteractableElement( + name: element.localName, + children: children, + node: element, + href: '', + style: Style(), ); - break; } - - return interactableElement; } \ No newline at end of file diff --git a/lib/src/layout_element.dart b/lib/src/layout_element.dart index 0950b51c18..49e58bf424 100644 --- a/lib/src/layout_element.dart +++ b/lib/src/layout_element.dart @@ -12,22 +12,20 @@ import 'package:html/dom.dart' as dom; /// an html document with a more complex layout. LayoutElements handle abstract class LayoutElement extends StyledElement { LayoutElement({ - String name, - List children, - Style style, + String name = "[[No Name]]", + @required List children, dom.Element node, - }) : super(name: name, children: children, style: style, node: node); + }) : super(name: name, children: children, style: Style(), node: node); Widget toWidget(RenderContext context); } class TableLayoutElement extends LayoutElement { TableLayoutElement({ - String name, - Style style, + @required String name, @required List children, - dom.Element node, - }) : super(name: name, style: style, children: children, node: node); + @required dom.Element node, + }) : super(name: name, children: children, node: node); @override Widget toWidget(RenderContext context) { @@ -51,8 +49,7 @@ class TableLayoutElement extends LayoutElement { columnSizes = child.children .where((c) => c.name == "col") .map((c) { - final span = - int.parse(c.attributes["span"] ?? "1", onError: (_) => 1); + final span = int.tryParse(c.attributes["span"] ?? "1") ?? 1; final colWidth = c.attributes["width"]; return List.generate(span, (index) { if (colWidth != null && colWidth.endsWith("%")) { @@ -145,8 +142,8 @@ class TableLayoutElement extends LayoutElement { return LayoutGrid( gridFit: GridFit.loose, - templateColumnSizes: finalColumnSizes, - templateRowSizes: rowSizes, + columnSizes: finalColumnSizes, + rowSizes: rowSizes, children: cells, ); } @@ -154,7 +151,7 @@ class TableLayoutElement extends LayoutElement { class TableSectionLayoutElement extends LayoutElement { TableSectionLayoutElement({ - String name, + @required String name, @required List children, }) : super(name: name, children: children); @@ -167,9 +164,9 @@ class TableSectionLayoutElement extends LayoutElement { class TableRowLayoutElement extends LayoutElement { TableRowLayoutElement({ - String name, + @required String name, @required List children, - dom.Element node, + @required dom.Element node, }) : super(name: name, children: children, node: node); @override @@ -184,12 +181,12 @@ class TableCellElement extends StyledElement { int rowspan = 1; TableCellElement({ - String name, - String elementId, - List elementClasses, + @required String name, + @required String elementId, + @required List elementClasses, @required List children, - Style style, - dom.Element node, + @required Style style, + @required dom.Element node, }) : super( name: name, elementId: elementId, @@ -217,6 +214,7 @@ TableCellElement parseTableCellElement( elementClasses: element.classes.toList(), children: children, node: element, + style: Style(), ); if (element.localName == "th") { cell.style = Style( @@ -228,10 +226,10 @@ TableCellElement parseTableCellElement( class TableStyleElement extends StyledElement { TableStyleElement({ - String name, - List children, - Style style, - dom.Element node, + @required String name, + @required List children, + @required Style style, + @required dom.Element node, }) : super(name: name, children: children, style: style, node: node); } @@ -246,9 +244,15 @@ TableStyleElement parseTableDefinitionElement( name: element.localName, children: children, node: element, + style: Style(), ); default: - return TableStyleElement(); + return TableStyleElement( + name: "[[No Name]]", + children: children, + node: element, + style: Style(), + ); } } @@ -256,10 +260,10 @@ class DetailsContentElement extends LayoutElement { List elementList; DetailsContentElement({ - String name, - List children, - dom.Element node, - this.elementList, + @required String name, + @required List children, + @required dom.Element node, + @required this.elementList, }) : super(name: name, node: node, children: children); @override @@ -311,7 +315,7 @@ class DetailsContentElement extends LayoutElement { } class EmptyLayoutElement extends LayoutElement { - EmptyLayoutElement({String name = "empty"}) : super(name: name); + EmptyLayoutElement({@required String name}) : super(name: name, children: []); @override Widget toWidget(_) => null; @@ -324,7 +328,7 @@ LayoutElement parseLayoutElement( switch (element.localName) { case "details": if (children?.isEmpty ?? false) { - return EmptyLayoutElement(); + return EmptyLayoutElement(name: "empty"); } return DetailsContentElement( node: element, @@ -355,6 +359,10 @@ LayoutElement parseLayoutElement( ); break; default: - return TableLayoutElement(children: children); + return TableLayoutElement( + children: children, + name: "[[No Name]]", + node: element + ); } } diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index 7074060333..e6d92241f1 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -21,12 +21,12 @@ import 'package:webview_flutter/webview_flutter.dart'; abstract class ReplacedElement extends StyledElement { PlaceholderAlignment alignment; - ReplacedElement( - {String name, - Style style, - dom.Element node, - this.alignment = PlaceholderAlignment.aboveBaseline}) - : super(name: name, children: null, style: style, node: node); + ReplacedElement({ + @required String name, + @required Style style, + dom.Element node, + this.alignment = PlaceholderAlignment.aboveBaseline + }) : super(name: name, children: [], style: style, node: node); static List parseMediaSources(List elements) { return elements @@ -44,8 +44,8 @@ class TextContentElement extends ReplacedElement { String text; TextContentElement({ - Style style, - this.text, + @required Style style, + @required this.text, }) : super(name: "[text]", style: style); @override @@ -64,17 +64,11 @@ class ImageContentElement extends ReplacedElement { final String alt; ImageContentElement({ - String name, - Style style, - this.src, - this.alt, - dom.Element node, - }) : super( - name: name, - style: style, - node: node, - alignment: PlaceholderAlignment.middle, - ); + @required String name, + @required this.src, + @required this.alt, + @required dom.Element node, + }) : super(name: name, style: Style(), node: node, alignment: PlaceholderAlignment.middle); @override Widget toWidget(RenderContext context) { @@ -99,14 +93,13 @@ class IframeContentElement extends ReplacedElement { final UniqueKey key = UniqueKey(); IframeContentElement({ - String name, - Style style, - this.src, - this.width, - this.height, - dom.Element node, - this.navigationDelegate, - }) : super(name: name, style: style, node: node); + @required String name, + @required this.src, + @required this.width, + @required this.height, + @required dom.Element node, + @required this.navigationDelegate, + }) : super(name: name, style: Style(), node: node); @override Widget toWidget(RenderContext context) { @@ -138,15 +131,14 @@ class AudioContentElement extends ReplacedElement { final bool muted; AudioContentElement({ - String name, - Style style, - this.src, - this.showControls, - this.autoplay, - this.loop, - this.muted, - dom.Element node, - }) : super(name: name, style: style, node: node); + @required String name, + @required this.src, + @required this.showControls, + @required this.autoplay, + @required this.loop, + @required this.muted, + @required dom.Element node, + }) : super(name: name, style: Style(), node: node); @override Widget toWidget(RenderContext context) { @@ -179,18 +171,17 @@ class VideoContentElement extends ReplacedElement { final double height; VideoContentElement({ - String name, - Style style, - this.src, - this.poster, - this.showControls, - this.autoplay, - this.loop, - this.muted, - this.width, - this.height, - dom.Element node, - }) : super(name: name, style: style, node: node); + @required String name, + @required this.src, + @required this.poster, + @required this.showControls, + @required this.autoplay, + @required this.loop, + @required this.muted, + @required this.width, + @required this.height, + @required dom.Element node, + }) : super(name: name, style: Style(), node: node); @override Widget toWidget(RenderContext context) { @@ -226,10 +217,12 @@ class SvgContentElement extends ReplacedElement { final double height; SvgContentElement({ - this.data, - this.width, - this.height, - }); + @required String name, + @required this.data, + @required this.width, + @required this.height, + @required dom.Node node, + }) : super(name: name, style: Style(), node: node); @override Widget toWidget(RenderContext context) { @@ -242,7 +235,7 @@ class SvgContentElement extends ReplacedElement { } class EmptyContentElement extends ReplacedElement { - EmptyContentElement({String name = "empty"}) : super(name: name); + EmptyContentElement({String name = "empty"}) : super(name: name, style: Style()); @override Widget toWidget(_) => null; @@ -252,12 +245,12 @@ class RubyElement extends ReplacedElement { dom.Element element; RubyElement({@required this.element, String name = "ruby"}) - : super(name: name, alignment: PlaceholderAlignment.middle); + : super(name: name, alignment: PlaceholderAlignment.middle, style: Style()); @override Widget toWidget(RenderContext context) { dom.Node textNode; - List widgets = List(); + List widgets = []; //TODO calculate based off of parent font size. final rubySize = max(9.0, context.style.fontSize.size / 2); final rubyYPos = rubySize + rubySize / 2; @@ -362,9 +355,11 @@ ReplacedElement parseReplacedElement( ); case "svg": return SvgContentElement( + name: "svg", data: element.outerHtml, width: double.tryParse(element.attributes['width'] ?? ""), height: double.tryParse(element.attributes['height'] ?? ""), + node: element, ); case "ruby": return RubyElement( diff --git a/lib/src/styled_element.dart b/lib/src/styled_element.dart index 1d9ffb970e..569d16db39 100644 --- a/lib/src/styled_element.dart +++ b/lib/src/styled_element.dart @@ -15,11 +15,11 @@ class StyledElement { StyledElement({ this.name = "[[No name]]", - this.elementId, - this.elementClasses, - this.children, - this.style, - dom.Element node, + this.elementId = "[[No ID]]", + this.elementClasses = const [], + @required this.children, + @required this.style, + @required dom.Element node, }) : this._node = node; bool matchesSelector(String selector) => @@ -27,7 +27,7 @@ class StyledElement { Map get attributes => _node?.attributes?.map((key, value) { - return MapEntry(key, value); + return MapEntry(key.toString(), value); }) ?? Map(); @@ -53,6 +53,7 @@ StyledElement parseStyledElement( elementClasses: element.classes.toList(), children: children, node: element, + style: Style(), ); switch (element.localName) { diff --git a/pubspec.yaml b/pubspec.yaml index f41b5669e6..b60cc735c4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,29 +4,37 @@ version: 1.3.0 homepage: https://github.com/Sub6Resources/flutter_html environment: - sdk: '>=2.2.2 <3.0.0' + sdk: '>=2.11.0 <3.0.0' flutter: '>=1.17.0' dependencies: # Plugin for parsing html - html: ^0.14.0+4 + html: ^0.15.0 # Plugins for parsing css - csslib: ^0.16.2 - css_colors: ^1.0.2 + csslib: ^0.17.0 + css_colors: ^1.1.0 # Plugins for rendering the tag. - flutter_layout_grid: ^0.10.5 + flutter_layout_grid: ^1.0.0-nullsafety.5 # Plugins for rendering the