From c5f396dd29b9ede943d1e908b48d4b80ac2d2542 Mon Sep 17 00:00:00 2001 From: Zak Barbuto Date: Mon, 25 Oct 2021 17:43:30 +1030 Subject: [PATCH 001/107] Add support for auto horizontal margins Allow centering images with auto-margins --- example/lib/main.dart | 18 +++- lib/html_parser.dart | 53 ++++++----- lib/src/css_parser.dart | 68 ++++++++++--- lib/src/styled_element.dart | 32 +++---- lib/style.dart | 95 ++++++++++++++++++- .../lib/flutter_html_table.dart | 2 +- 6 files changed, 209 insertions(+), 59 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 550ad15bd1..cd5f87704c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -55,10 +55,20 @@ const htmlData = r"""

The should be BLACK with 10% alpha style='color: rgba(0, 0, 0, 0.10);

The should be GREEN style='color: rgb(0, 97, 0);

The should be GREEN style='color: rgb(0, 97, 0);

-

blasdafjklasdlkjfkl

-

blasdafjklasdlkjfkl

-

blasdafjklasdlkjfkl

-

blasdafjklasdlkjfkl

+

Text Alignment

+

Center Aligned Text

+

Right Aligned Text

+

Justified Text

+

Center Aligned Text

+

Auto Margins

+
Default Div
+
margin: auto
+
margin: 15px auto
+
margin-left: auto
+

With an image - non-block (should not center):

+ +

block image (should center):

+

Table support (with custom styling!):

Famous quote... diff --git a/lib/html_parser.dart b/lib/html_parser.dart index fdfcf4cbd1..e2eabfb967 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -650,7 +650,7 @@ class HtmlParser extends StatelessWidget { if (tree.children.isEmpty) { // Handle case (4) from above. if ((tree.style.height ?? 0) == 0) { - tree.style.margin = EdgeInsets.zero; + tree.style.margin = tree.style.margin?.collapse() ?? Margins.zero; } return tree; } @@ -666,47 +666,47 @@ class HtmlParser extends StatelessWidget { // Handle case (1) from above. // Top margins cannot collapse if the element has padding if ((tree.style.padding?.top ?? 0) == 0) { - final parentTop = tree.style.margin?.top ?? 0; - final firstChildTop = tree.children.first.style.margin?.top ?? 0; + final parentTop = tree.style.margin?.top?.value ?? 0; + final firstChildTop = tree.children.first.style.margin?.top?.value ?? 0; final newOuterMarginTop = max(parentTop, firstChildTop); // Set the parent's margin if (tree.style.margin == null) { - tree.style.margin = EdgeInsets.only(top: newOuterMarginTop); + tree.style.margin = Margins.only(top: newOuterMarginTop); } else { - tree.style.margin = tree.style.margin!.copyWith(top: newOuterMarginTop); + tree.style.margin = tree.style.margin!.copyWithEdge(top: newOuterMarginTop); } // And remove the child's margin if (tree.children.first.style.margin == null) { - tree.children.first.style.margin = EdgeInsets.zero; + tree.children.first.style.margin = Margins.zero; } else { tree.children.first.style.margin = - tree.children.first.style.margin!.copyWith(top: 0); + tree.children.first.style.margin!.copyWithEdge(top: 0); } } // Handle case (3) from above. // Bottom margins cannot collapse if the element has padding if ((tree.style.padding?.bottom ?? 0) == 0) { - final parentBottom = tree.style.margin?.bottom ?? 0; - final lastChildBottom = tree.children.last.style.margin?.bottom ?? 0; + final parentBottom = tree.style.margin?.bottom?.value ?? 0; + final lastChildBottom = tree.children.last.style.margin?.bottom?.value ?? 0; final newOuterMarginBottom = max(parentBottom, lastChildBottom); // Set the parent's margin if (tree.style.margin == null) { - tree.style.margin = EdgeInsets.only(bottom: newOuterMarginBottom); + tree.style.margin = Margins.only(bottom: newOuterMarginBottom); } else { tree.style.margin = - tree.style.margin!.copyWith(bottom: newOuterMarginBottom); + tree.style.margin!.copyWithEdge(bottom: newOuterMarginBottom); } // And remove the child's margin if (tree.children.last.style.margin == null) { - tree.children.last.style.margin = EdgeInsets.zero; + tree.children.last.style.margin = Margins.zero; } else { tree.children.last.style.margin = - tree.children.last.style.margin!.copyWith(bottom: 0); + tree.children.last.style.margin!.copyWithEdge(bottom: 0); } } @@ -714,24 +714,24 @@ class HtmlParser extends StatelessWidget { if (tree.children.length > 1) { for (int i = 1; i < tree.children.length; i++) { final previousSiblingBottom = - tree.children[i - 1].style.margin?.bottom ?? 0; - final thisTop = tree.children[i].style.margin?.top ?? 0; + tree.children[i - 1].style.margin?.bottom?.value ?? 0; + final thisTop = tree.children[i].style.margin?.top?.value ?? 0; final newInternalMargin = max(previousSiblingBottom, thisTop) / 2; if (tree.children[i - 1].style.margin == null) { tree.children[i - 1].style.margin = - EdgeInsets.only(bottom: newInternalMargin); + Margins.only(bottom: newInternalMargin); } else { tree.children[i - 1].style.margin = tree.children[i - 1].style.margin! - .copyWith(bottom: newInternalMargin); + .copyWithEdge(bottom: newInternalMargin); } if (tree.children[i].style.margin == null) { tree.children[i].style.margin = - EdgeInsets.only(top: newInternalMargin); + Margins.only(top: newInternalMargin); } else { tree.children[i].style.margin = - tree.children[i].style.margin!.copyWith(top: newInternalMargin); + tree.children[i].style.margin!.copyWithEdge(top: newInternalMargin); } } } @@ -840,7 +840,14 @@ class ContainerSpan extends StatelessWidget { @override Widget build(BuildContext _) { - return Container( + + // Elements that are inline should ignore margin: auto for alignment. + var alignment = shrinkWrap ? null : style.alignment; + if(style.display == Display.BLOCK) { + alignment = style.margin?.alignment ?? alignment; + } + + Widget container = Container( decoration: BoxDecoration( border: style.border, color: style.backgroundColor, @@ -848,8 +855,8 @@ class ContainerSpan extends StatelessWidget { height: style.height, width: style.width, padding: style.padding?.nonNegative, - margin: style.margin?.nonNegative, - alignment: shrinkWrap ? null : style.alignment, + margin: style.margin?.asInsets.nonNegative, + alignment: alignment, child: child ?? StyledText( textSpan: TextSpan( @@ -860,6 +867,8 @@ class ContainerSpan extends StatelessWidget { renderContext: newContext, ), ); + + return container; } } diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index 66c622a8cf..7551da7cd4 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -244,30 +244,31 @@ Style declarationsToStyle(Map> declarations) { && !(element is css.EmTerm) && !(element is css.RemTerm) && !(element is css.NumberTerm) + && !(element.text == 'auto') ); - List margin = ExpressionMapping.expressionToPadding(marginLengths); - style.margin = (style.margin ?? EdgeInsets.zero).copyWith( - left: margin[0], - right: margin[1], - top: margin[2], - bottom: margin[3], + Margins margin = ExpressionMapping.expressionToMargins(marginLengths); + style.margin = (style.margin ?? Margins.all(0)).copyWith( + left: margin.left, + right: margin.right, + top: margin.top, + bottom: margin.bottom, ); break; case 'margin-left': - style.margin = (style.margin ?? EdgeInsets.zero).copyWith( - left: ExpressionMapping.expressionToPaddingLength(value.first)); + style.margin = (style.margin ?? Margins.zero).copyWith( + left: ExpressionMapping.expressionToMargin(value.first)); break; case 'margin-right': - style.margin = (style.margin ?? EdgeInsets.zero).copyWith( - right: ExpressionMapping.expressionToPaddingLength(value.first)); + style.margin = (style.margin ?? Margins.zero).copyWith( + right: ExpressionMapping.expressionToMargin(value.first)); break; case 'margin-top': - style.margin = (style.margin ?? EdgeInsets.zero).copyWith( - top: ExpressionMapping.expressionToPaddingLength(value.first)); + style.margin = (style.margin ?? Margins.zero).copyWith( + top: ExpressionMapping.expressionToMargin(value.first)); break; case 'margin-bottom': - style.margin = (style.margin ?? EdgeInsets.zero).copyWith( - bottom: ExpressionMapping.expressionToPaddingLength(value.first)); + style.margin = (style.margin ?? Margins.zero).copyWith( + bottom: ExpressionMapping.expressionToMargin(value.first)); break; case 'padding': List? paddingLengths = value.whereType().toList(); @@ -748,6 +749,45 @@ class ExpressionMapping { return null; } + static Margin? expressionToMargin(css.Expression value) { + if ((value is css.LiteralTerm) && value.text == 'auto') { + return AutoMargin(); + } else { + return InsetMargin(expressionToPaddingLength(value) ?? 0); + } + } + + static Margins expressionToMargins(List? lengths) { + Margin? left; + Margin? right; + Margin? top; + Margin? bottom; + if (lengths != null && lengths.isNotEmpty) { + top = expressionToMargin(lengths.first); + if (lengths.length == 4) { + right = expressionToMargin(lengths[1]); + bottom = expressionToMargin(lengths[2]); + left = expressionToMargin(lengths.last); + } + if (lengths.length == 3) { + left = expressionToMargin(lengths[1]); + right = expressionToMargin(lengths[1]); + bottom = expressionToMargin(lengths.last); + } + if (lengths.length == 2) { + bottom = expressionToMargin(lengths.first); + left = expressionToMargin(lengths.last); + right = expressionToMargin(lengths.last); + } + if (lengths.length == 1) { + bottom = expressionToMargin(lengths.first); + left = expressionToMargin(lengths.first); + right = expressionToMargin(lengths.first); + } + } + return Margins(left: left, right: right, top: top, bottom: bottom); + } + static List expressionToPadding(List? lengths) { double? left; double? right; diff --git a/lib/src/styled_element.dart b/lib/src/styled_element.dart index 9561d8f228..9531f5bcf9 100644 --- a/lib/src/styled_element.dart +++ b/lib/src/styled_element.dart @@ -102,19 +102,19 @@ StyledElement parseStyledElement( //TODO(Sub6Resources) this is a workaround for collapsing margins. Remove. if (element.parent!.localName == "blockquote") { styledElement.style = Style( - margin: const EdgeInsets.only(left: 40.0, right: 40.0, bottom: 14.0), + margin: Margins.only(left: 40.0, right: 40.0, bottom: 14.0), display: Display.BLOCK, ); } else { styledElement.style = Style( - margin: const EdgeInsets.symmetric(horizontal: 40.0, vertical: 14.0), + margin: Margins.symmetric(horizontal: 40.0, vertical: 14.0), display: Display.BLOCK, ); } break; case "body": styledElement.style = Style( - margin: EdgeInsets.all(8.0), + margin: Margins.all(8.0), display: Display.BLOCK, ); break; @@ -134,7 +134,7 @@ StyledElement parseStyledElement( break; case "dd": styledElement.style = Style( - margin: EdgeInsets.only(left: 40.0), + margin: Margins.only(left: 40.0), display: Display.BLOCK, ); break; @@ -148,13 +148,13 @@ StyledElement parseStyledElement( continue italics; case "div": styledElement.style = Style( - margin: EdgeInsets.all(0), + margin: Margins.all(0), display: Display.BLOCK, ); break; case "dl": styledElement.style = Style( - margin: EdgeInsets.symmetric(vertical: 14.0), + margin: Margins.symmetric(vertical: 14.0), display: Display.BLOCK, ); break; @@ -172,7 +172,7 @@ StyledElement parseStyledElement( break; case "figure": styledElement.style = Style( - margin: EdgeInsets.symmetric(vertical: 14.0, horizontal: 40.0), + margin: Margins.symmetric(vertical: 14.0, horizontal: 40.0), display: Display.BLOCK, ); break; @@ -196,7 +196,7 @@ StyledElement parseStyledElement( styledElement.style = Style( fontSize: FontSize.xxLarge, fontWeight: FontWeight.bold, - margin: EdgeInsets.symmetric(vertical: 18.67), + margin: Margins.symmetric(vertical: 18.67), display: Display.BLOCK, ); break; @@ -204,7 +204,7 @@ StyledElement parseStyledElement( styledElement.style = Style( fontSize: FontSize.xLarge, fontWeight: FontWeight.bold, - margin: EdgeInsets.symmetric(vertical: 17.5), + margin: Margins.symmetric(vertical: 17.5), display: Display.BLOCK, ); break; @@ -212,7 +212,7 @@ StyledElement parseStyledElement( styledElement.style = Style( fontSize: FontSize(16.38), fontWeight: FontWeight.bold, - margin: EdgeInsets.symmetric(vertical: 16.5), + margin: Margins.symmetric(vertical: 16.5), display: Display.BLOCK, ); break; @@ -220,7 +220,7 @@ StyledElement parseStyledElement( styledElement.style = Style( fontSize: FontSize.medium, fontWeight: FontWeight.bold, - margin: EdgeInsets.symmetric(vertical: 18.5), + margin: Margins.symmetric(vertical: 18.5), display: Display.BLOCK, ); break; @@ -228,7 +228,7 @@ StyledElement parseStyledElement( styledElement.style = Style( fontSize: FontSize(11.62), fontWeight: FontWeight.bold, - margin: EdgeInsets.symmetric(vertical: 19.25), + margin: Margins.symmetric(vertical: 19.25), display: Display.BLOCK, ); break; @@ -236,7 +236,7 @@ StyledElement parseStyledElement( styledElement.style = Style( fontSize: FontSize(9.38), fontWeight: FontWeight.bold, - margin: EdgeInsets.symmetric(vertical: 22), + margin: Margins.symmetric(vertical: 22), display: Display.BLOCK, ); break; @@ -247,7 +247,7 @@ StyledElement parseStyledElement( break; case "hr": styledElement.style = Style( - margin: EdgeInsets.symmetric(vertical: 7.0), + margin: Margins.symmetric(vertical: 7.0), width: double.infinity, height: 1, backgroundColor: Colors.black, @@ -318,14 +318,14 @@ StyledElement parseStyledElement( break; case "p": styledElement.style = Style( - margin: EdgeInsets.symmetric(vertical: 14.0), + margin: Margins.symmetric(vertical: 14.0), display: Display.BLOCK, ); break; case "pre": styledElement.style = Style( fontFamily: 'monospace', - margin: EdgeInsets.symmetric(vertical: 14.0), + margin: Margins.symmetric(vertical: 14.0), whiteSpace: WhiteSpace.PRE, display: Display.BLOCK, ); diff --git a/lib/style.dart b/lib/style.dart index fc2c3f6d21..3ac2ff7ba8 100644 --- a/lib/style.dart +++ b/lib/style.dart @@ -95,7 +95,7 @@ class Style { /// /// Inherited: no, /// Default: EdgeInsets.zero - EdgeInsets? margin; + Margins? margin; /// CSS attribute "`text-align`" /// @@ -378,7 +378,7 @@ class Style { ListStyleType? listStyleType, ListStylePosition? listStylePosition, EdgeInsets? padding, - EdgeInsets? margin, + Margins? margin, TextAlign? textAlign, TextDecoration? textDecoration, Color? textDecorationColor, @@ -466,6 +466,97 @@ enum Display { NONE, } +abstract class Margin { + const Margin(); + + double get value => this is InsetMargin ? this.value : 0; + } + +class AutoMargin extends Margin { + const AutoMargin(); +} + +class InsetMargin extends Margin { + final double value; + const InsetMargin(this.value); +} + +class Margins { + final Margin? left; + final Margin? right; + final Margin? top; + final Margin? bottom; + + const Margins({ this.left, this.right, this.top, this.bottom }); + + /// Auto margins already have a "value" of zero so can be considered collapsed. + Margins collapse() => Margins( + left: left is AutoMargin ? left : InsetMargin(0), + right: right is AutoMargin ? right : InsetMargin(0), + top: top is AutoMargin ? top : InsetMargin(0), + bottom: bottom is AutoMargin ? bottom : InsetMargin(0), + ); + + Margins copyWith({ Margin? left, Margin? right, Margin? top, Margin? bottom }) => Margins( + left: left ?? this.left, + right: right ?? this.right, + top: top ?? this.top, + bottom: bottom ?? this.bottom, + ); + + Margins copyWithEdge({ double? left, double? right, double? top, double? bottom }) => Margins( + left: left != null ? InsetMargin(left) : this.left, + right: right != null ? InsetMargin(right) : this.right, + top: top != null ? InsetMargin(top) : this.top, + bottom: bottom != null ? InsetMargin(bottom) : this.bottom, + ); + + bool get isAutoHorizontal => (left is AutoMargin) || (right is AutoMargin); + + Alignment? get alignment { + if((left is AutoMargin) && (right is AutoMargin)) { + return Alignment.center; + } else if(left is AutoMargin) { + return Alignment.topRight; + } + } + + /// Analogous to [EdgeInsets.zero] + static Margins get zero => Margins.all(0); + + /// Analogous to [EdgeInsets.all] + static Margins all(double value) => Margins( + left: InsetMargin(value), + right: InsetMargin(value), + top: InsetMargin(value), + bottom: InsetMargin(value), + ); + + /// Analogous to [EdgeInsets.only] + static Margins only({ double? left, double? right, double? top, double? bottom }) => Margins( + left: InsetMargin(left ?? 0), + right: InsetMargin(right ?? 0), + top: InsetMargin(top ?? 0), + bottom: InsetMargin(bottom ?? 0), + ); + + + /// Analogous to [EdgeInsets.symmetric] + static Margins symmetric({double? horizontal, double? vertical}) => Margins( + left: InsetMargin(horizontal ?? 0), + right: InsetMargin(horizontal ?? 0), + top: InsetMargin(vertical ?? 0), + bottom: InsetMargin(vertical ?? 0), + ); + + EdgeInsets get asInsets => EdgeInsets.zero.copyWith( + left: left?.value ?? 0, + right: right?.value ?? 0, + top: top?.value ?? 0, + bottom: bottom?.value ?? 0, + ); +} + class FontSize { final double? size; final String units; diff --git a/packages/flutter_html_table/lib/flutter_html_table.dart b/packages/flutter_html_table/lib/flutter_html_table.dart index 7791f36f2c..7a48613fc0 100644 --- a/packages/flutter_html_table/lib/flutter_html_table.dart +++ b/packages/flutter_html_table/lib/flutter_html_table.dart @@ -9,7 +9,7 @@ import 'package:flutter_html/flutter_html.dart'; CustomRender tableRender() => CustomRender.widget(widget: (context, buildChildren) { return Container( key: context.key, - margin: context.style.margin?.nonNegative, + margin: context.style.margin?.asInsets.nonNegative, padding: context.style.padding?.nonNegative, alignment: context.style.alignment, decoration: BoxDecoration( From c1d70aca0912e7fa679d9de3740036847635f07d Mon Sep 17 00:00:00 2001 From: Michal Srutek Date: Sat, 23 Apr 2022 09:16:16 +0200 Subject: [PATCH 002/107] Fix commented dependency --- packages/flutter_html_iframe/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flutter_html_iframe/pubspec.yaml b/packages/flutter_html_iframe/pubspec.yaml index fb0fa876cc..69a5b80433 100644 --- a/packages/flutter_html_iframe/pubspec.yaml +++ b/packages/flutter_html_iframe/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: sdk: flutter html: '>=0.15.0 <1.0.0' flutter_html: '>=3.0.0-alpha.3 <4.0.0' -# flutter_ht +# flutter_html: # path: ../.. webview_flutter: '>=2.0.4 <4.0.0' From 9bf38c2968d3b8491a7d3aa920eb872c45ad9ef2 Mon Sep 17 00:00:00 2001 From: Michal Srutek Date: Sat, 23 Apr 2022 09:18:26 +0200 Subject: [PATCH 003/107] Fix newlines in table --- README.md | 2 +- example/lib/main.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d7aacfe45e..ebc4cd044d 100644 --- a/README.md +++ b/README.md @@ -497,7 +497,7 @@ Widget html = Html( - Rowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspanDataData + Rowspan
Rowspan
Rowspan
Rowspan
Rowspan
Rowspan
Rowspan
Rowspan
Rowspan
RowspanDataData Google diff --git a/example/lib/main.dart b/example/lib/main.dart index 550ad15bd1..971022f1c9 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -73,7 +73,7 @@ const htmlData = r""" - Rowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspanDataData + Rowspan
Rowspan
Rowspan
Rowspan
Rowspan
Rowspan
Rowspan
Rowspan
Rowspan
RowspanDataData Google From 6198f1413083498683ca7f6ed12eb4fd16cae1b8 Mon Sep 17 00:00:00 2001 From: Michal Srutek Date: Sat, 23 Apr 2022 09:35:32 +0200 Subject: [PATCH 004/107] Remove example test --- example/test/widget_test.dart | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 example/test/widget_test.dart diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart deleted file mode 100644 index 747db1da35..0000000000 --- a/example/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} From 45bc4703256fe3e0a3a822070b593281363d572b Mon Sep 17 00:00:00 2001 From: Michal Srutek Date: Sat, 23 Apr 2022 09:50:59 +0200 Subject: [PATCH 005/107] Fix custom renders in readme --- packages/flutter_html_audio/README.md | 2 +- packages/flutter_html_iframe/README.md | 4 ++-- packages/flutter_html_math/README.md | 4 ++-- packages/flutter_html_svg/README.md | 2 +- packages/flutter_html_table/README.md | 2 +- packages/flutter_html_video/README.md | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/flutter_html_audio/README.md b/packages/flutter_html_audio/README.md index 70680b78d0..8c63614c36 100644 --- a/packages/flutter_html_audio/README.md +++ b/packages/flutter_html_audio/README.md @@ -10,7 +10,7 @@ The package considers the attributes `controls`, `loop`, `src`, `autoplay`, `wid ```dart Widget html = Html( - customRender: { + customRenders: { audioMatcher(): audioRender(), } ); diff --git a/packages/flutter_html_iframe/README.md b/packages/flutter_html_iframe/README.md index c58427a27c..2c46edc0e7 100644 --- a/packages/flutter_html_iframe/README.md +++ b/packages/flutter_html_iframe/README.md @@ -12,7 +12,7 @@ Sandbox controls the JavaScript mode of the webview - a value of `null` or `allo ```dart Widget html = Html( - customRender: { + customRenders: { iframeMatcher(): iframeRender(), } ); @@ -23,7 +23,7 @@ You can set the `navigationDelegate` of the webview with the `navigationDelegate ```dart Widget html = Html( - customRender: { + customRenders: { iframeMatcher(): iframeRender(navigationDelegate: (NavigationRequest request) { if (request.url.contains("google.com/images")) { return NavigationDecision.prevent; diff --git a/packages/flutter_html_math/README.md b/packages/flutter_html_math/README.md index 320ec123ce..4dd30c1da1 100644 --- a/packages/flutter_html_math/README.md +++ b/packages/flutter_html_math/README.md @@ -12,7 +12,7 @@ Because this package is parsing MathML to Tex, it may not support some functiona ```dart Widget html = Html( - customRender: { + customRenders: { mathMatcher(): mathRender(), } ); @@ -28,7 +28,7 @@ You can analyze the error and the parsed string, and finally return a new instan ```dart Widget html = Html( - customRender: { + customRenders: { mathMatcher(): mathRender(onMathError: (tex, exception, exceptionWithType) { print(exception); //optionally try and correct the Tex string here diff --git a/packages/flutter_html_svg/README.md b/packages/flutter_html_svg/README.md index 104c31db5e..712da67ba7 100644 --- a/packages/flutter_html_svg/README.md +++ b/packages/flutter_html_svg/README.md @@ -12,7 +12,7 @@ The package also exposes a few ways to render SVGs within an `` tag, specif ```dart Widget html = Html( - customRender: { + customRenders: { svgTagMatcher(): svgTagRender(), svgDataUriMatcher(): svgDataImageRender(), svgAssetUriMatcher(): svgAssetImageRender(), diff --git a/packages/flutter_html_table/README.md b/packages/flutter_html_table/README.md index 7c9220749c..f2ef51789b 100644 --- a/packages/flutter_html_table/README.md +++ b/packages/flutter_html_table/README.md @@ -10,7 +10,7 @@ When rendering table elements, the package tries to calculate the best fit for e ```dart Widget html = Html( - customRender: { + customRenders: { tableMatcher(): tableRender(), } ); diff --git a/packages/flutter_html_video/README.md b/packages/flutter_html_video/README.md index ebfd64c406..62b7551e4b 100644 --- a/packages/flutter_html_video/README.md +++ b/packages/flutter_html_video/README.md @@ -10,7 +10,7 @@ The package considers the attributes `controls`, `loop`, `src`, `autoplay`, `pos ```dart Widget html = Html( - customRender: { + customRenders: { videoMatcher(): videoRender(), } ); From 5a381bc263c29640453c6968439adacc8ad6b554 Mon Sep 17 00:00:00 2001 From: Eric Kok Date: Thu, 19 May 2022 13:18:32 +0200 Subject: [PATCH 006/107] Fix onLinkTap not being called when no widget (anchor) key is provided --- 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 226f4223c4..2f66cc9a36 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -65,7 +65,7 @@ class HtmlParser extends StatelessWidget { ? onAnchorTap : key != null ? _handleAnchorTap(key, onLinkTap) - : null, + : onLinkTap, super(key: key); @override From 77ffe7cbcdb67fdf49c27c86db0babc8c2ce4a4d Mon Sep 17 00:00:00 2001 From: Kaique Gazola Date: Fri, 3 Jun 2022 09:03:02 -0300 Subject: [PATCH 007/107] fix: fix textShadow color declaration handler --- lib/src/css_parser.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index 1f5bbd90ed..acc4724cc2 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -870,10 +870,10 @@ class ExpressionMapping { css.Expression? offsetX; css.Expression? offsetY; css.Expression? blurRadius; - css.HexColorTerm? color; + css.Expression? color; int expressionIndex = 0; list.forEach((element) { - if (element is css.HexColorTerm) { + if (element is css.HexColorTerm || element is css.FunctionTerm) { color = element; } else if (expressionIndex == 0) { offsetX = element; From 1d65aafd49f2abecb3a99e1a04c6a7d5ecb2dabb Mon Sep 17 00:00:00 2001 From: Kaique Gazola Date: Sat, 4 Jun 2022 19:41:53 -0300 Subject: [PATCH 008/107] feat: exposes fontFamilyFallback parameter --- lib/style.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/style.dart b/lib/style.dart index 121319e918..95bde15088 100644 --- a/lib/style.dart +++ b/lib/style.dart @@ -37,6 +37,14 @@ class Style { /// Default: Theme.of(context).style.textTheme.body1.fontFamily String? fontFamily; + + /// The list of font families to fall back on when a glyph cannot be found in default font family. + /// + /// Inherited: yes, + /// Default: null + List? fontFamilyFallback; + + /// CSS attribute "`font-feature-settings`" /// /// Inherited: yes, @@ -199,6 +207,7 @@ class Style { this.direction, this.display, this.fontFamily, + this.fontFamilyFallback, this.fontFeatureSettings, this.fontSize, this.fontStyle, @@ -263,6 +272,7 @@ class Style { decorationStyle: textDecorationStyle, decorationThickness: textDecorationThickness, fontFamily: fontFamily, + fontFamilyFallback: fontFamilyFallback, fontFeatures: fontFeatureSettings, fontSize: fontSize?.size, fontStyle: fontStyle, @@ -288,6 +298,7 @@ class Style { direction: other.direction, display: other.display, fontFamily: other.fontFamily, + fontFamilyFallback: other.fontFamilyFallback, fontFeatureSettings: other.fontFeatureSettings, fontSize: other.fontSize, fontStyle: other.fontStyle, @@ -341,6 +352,7 @@ class Style { direction: child.direction ?? direction, display: display == Display.NONE ? display : child.display, fontFamily: child.fontFamily ?? fontFamily, + fontFamilyFallback: child.fontFamilyFallback ?? fontFamilyFallback, fontFeatureSettings: child.fontFeatureSettings ?? fontFeatureSettings, fontSize: finalFontSize, fontStyle: child.fontStyle ?? fontStyle, @@ -368,6 +380,7 @@ class Style { TextDirection? direction, Display? display, String? fontFamily, + List? fontFamilyFallback, List? fontFeatureSettings, FontSize? fontSize, FontStyle? fontStyle, @@ -405,6 +418,7 @@ class Style { direction: direction ?? this.direction, display: display ?? this.display, fontFamily: fontFamily ?? this.fontFamily, + fontFamilyFallback: fontFamilyFallback ?? this.fontFamilyFallback, fontFeatureSettings: fontFeatureSettings ?? this.fontFeatureSettings, fontSize: fontSize ?? this.fontSize, fontStyle: fontStyle ?? this.fontStyle, @@ -446,6 +460,7 @@ class Style { this.textDecorationStyle = textStyle.decorationStyle; this.textDecorationThickness = textStyle.decorationThickness; this.fontFamily = textStyle.fontFamily; + this.fontFamilyFallback = textStyle.fontFamilyFallback; this.fontFeatureSettings = textStyle.fontFeatures; this.fontSize = FontSize(textStyle.fontSize); this.fontStyle = textStyle.fontStyle; From f9a4503b04fb14eca8350dc705b29751dd7a0835 Mon Sep 17 00:00:00 2001 From: Arjan Mels Date: Mon, 6 Jun 2022 21:29:52 +0200 Subject: [PATCH 009/107] Fixes dynamic changing of data/documentElement Signed-off-by: Arjan Mels --- lib/flutter_html.dart | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index f0b33d547d..9496d9263a 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -7,11 +7,11 @@ import 'package:flutter_html/src/html_elements.dart'; import 'package:flutter_html/style.dart'; import 'package:html/dom.dart' as dom; +export 'package:flutter_html/custom_render.dart'; //export render context api export 'package:flutter_html/html_parser.dart'; //export render context api export 'package:flutter_html/html_parser.dart'; -export 'package:flutter_html/custom_render.dart'; //export src for advanced custom render uses (e.g. casting context.tree) export 'package:flutter_html/src/anchor.dart'; export 'package:flutter_html/src/interactable_element.dart'; @@ -60,8 +60,8 @@ class Html extends StatefulWidget { this.onImageTap, this.tagsList = const [], this.style = const {}, - }) : documentElement = null, - assert (data != null), + }) : documentElement = null, + assert(data != null), _anchorKey = anchorKey ?? GlobalKey(), super(key: key); @@ -78,7 +78,7 @@ class Html extends StatefulWidget { this.onImageTap, this.tagsList = const [], this.style = const {}, - }) : data = null, + }) : data = null, assert(document != null), this.documentElement = document!.documentElement, _anchorKey = anchorKey ?? GlobalKey(), @@ -97,7 +97,7 @@ class Html extends StatefulWidget { this.onImageTap, this.tagsList = const [], this.style = const {}, - }) : data = null, + }) : data = null, assert(documentElement != null), _anchorKey = anchorKey ?? GlobalKey(), super(key: key); @@ -154,13 +154,20 @@ class Html extends StatefulWidget { } class _HtmlState extends State { - late final dom.Element documentElement; + late dom.Element documentElement; @override void initState() { super.initState(); - documentElement = - widget.data != null ? HtmlParser.parseHTML(widget.data!) : widget.documentElement!; + documentElement = widget.data != null ? HtmlParser.parseHTML(widget.data!) : widget.documentElement!; + } + + @override + void didUpdateWidget(Html oldWidget) { + super.didUpdateWidget(oldWidget); + if ((widget.data != null && oldWidget.data != widget.data) || oldWidget.documentElement != widget.documentElement) { + documentElement = widget.data != null ? HtmlParser.parseHTML(widget.data!) : widget.documentElement!; + } } @override @@ -232,7 +239,7 @@ class SelectableHtml extends StatefulWidget { this.tagsList = const [], this.selectionControls, this.scrollPhysics, - }) : documentElement = null, + }) : documentElement = null, assert(data != null), _anchorKey = anchorKey ?? GlobalKey(), super(key: key); @@ -250,7 +257,7 @@ class SelectableHtml extends StatefulWidget { this.tagsList = const [], this.selectionControls, this.scrollPhysics, - }) : data = null, + }) : data = null, assert(document != null), this.documentElement = document!.documentElement, _anchorKey = anchorKey ?? GlobalKey(), @@ -269,7 +276,7 @@ class SelectableHtml extends StatefulWidget { this.tagsList = const [], this.selectionControls, this.scrollPhysics, - }) : data = null, + }) : data = null, assert(documentElement != null), _anchorKey = anchorKey ?? GlobalKey(), super(key: key); From d391e28adb3abe3b0a22934dfc57d0469e78503b Mon Sep 17 00:00:00 2001 From: Eric Kok Date: Thu, 9 Jun 2022 15:08:05 +0200 Subject: [PATCH 010/107] Release 3.0.0-alpha.5 with fixed link taps and fixed hot reload --- CHANGELOG.md | 5 +++ README.md | 36 ++++++++++++++++++-- example/lib/generated_plugin_registrant.dart | 1 + example/lib/main.dart | 12 +++++++ packages/flutter_html_all/pubspec.yaml | 4 +-- pubspec.yaml | 2 +- 6 files changed, 55 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ad789752..44cc2c1cb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [3.0.0-alpha.5] - June 9, 2022: +* Fixed hot reloads, thanks @arjenmels +* Fixed link taps not working +* Improvements in README + ## [3.0.0-alpha.3] - April 14, 2022: * Fixed styling not being applied to list item markers * [video] Fixed crash when iframe or video tags used unsupported/incorrect height or width diff --git a/README.md b/README.md index ebc4cd044d..9210a56651 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,8 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets. Add the following to your `pubspec.yaml` file: dependencies: - flutter_html: ^3.0.0-alpha.3 + flutter_html: ^3.0.0-alpha.5 + // Or flutter_html_all: ^3.0.0-alpha.5 to include table, video, audio, iframe... ## Currently Supported HTML Tags: | | | | | | | | | | | | @@ -566,6 +567,11 @@ The package considers the attributes `controls`, `loop`, `src`, `autoplay`, `wid #### Registering the `CustomRender`: +Add the dependency to your pubspec.yaml: + + dependencies: + flutter_html_audio: ^3.0.0-alpha.3 + ```dart Widget html = Html( customRenders: { @@ -584,6 +590,11 @@ Sandbox controls the JavaScript mode of the webview - a value of `null` or `allo #### Registering the `CustomRender`: +Add the dependency to your pubspec.yaml: + + dependencies: + flutter_html_iframe: ^3.0.0-alpha.3 + ```dart Widget html = Html( customRenders: { @@ -620,6 +631,11 @@ Because this package is parsing MathML to Tex, it may not support some functiona #### Registering the `CustomRender`: +Add the dependency to your pubspec.yaml: + + dependencies: + flutter_html_math: ^3.0.0-alpha.3 + ```dart Widget html = Html( customRenders: { @@ -688,6 +704,11 @@ The package also exposes a few ways to render SVGs within an `` tag, specif #### Registering the `CustomRender`: +Add the dependency to your pubspec.yaml: + + dependencies: + flutter_html_svg: ^3.0.0-alpha.3 + ```dart Widget html = Html( customRenders: { @@ -707,6 +728,11 @@ When rendering table elements, the package tries to calculate the best fit for e #### Registering the `CustomRender`: +Add the dependency to your pubspec.yaml: + + dependencies: + flutter_html_table: ^3.0.0-alpha.3 + ```dart Widget html = Html( customRenders: { @@ -723,6 +749,11 @@ The package considers the attributes `controls`, `loop`, `src`, `autoplay`, `pos #### Registering the `CustomRender`: +Add the dependency to your pubspec.yaml: + + dependencies: + flutter_html_video: ^3.0.0-alpha.3 + ```dart Widget html = Html( customRenders: { @@ -750,7 +781,8 @@ Widget row = Row( ``` ## Migration Guides -- For Version 1.0 - [Guide](https://github.com/Sub6Resources/flutter_html/wiki/1.0.0-Migration-Guide) +- For Version 1.0/2.0 - [Guide](https://github.com/Sub6Resources/flutter_html/wiki/1.0.0-Migration-Guide) +- For Version 3.0 - **TODO** ## Contribution Guide > Coming soon! diff --git a/example/lib/generated_plugin_registrant.dart b/example/lib/generated_plugin_registrant.dart index fd95b4ff6c..fc500d7eff 100644 --- a/example/lib/generated_plugin_registrant.dart +++ b/example/lib/generated_plugin_registrant.dart @@ -4,6 +4,7 @@ // ignore_for_file: directives_ordering // ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: depend_on_referenced_packages import 'package:video_player_web/video_player_web.dart'; import 'package:wakelock_web/wakelock_web.dart'; diff --git a/example/lib/main.dart b/example/lib/main.dart index 971022f1c9..e0489a248c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -241,6 +241,8 @@ const htmlData = r"""

Scroll to top

"""; +final staticAnchorKey = GlobalKey(); + class _MyHomePageState extends State { @override Widget build(BuildContext context) { @@ -249,8 +251,18 @@ class _MyHomePageState extends State { title: Text('flutter_html Example'), centerTitle: true, ), + floatingActionButton: FloatingActionButton( + child: Icon(Icons.arrow_downward), + onPressed: () { + final anchorContext = AnchorKey.forId(staticAnchorKey, "bottom")?.currentContext; + if (anchorContext != null) { + Scrollable.ensureVisible(anchorContext); + } + }, + ), body: SingleChildScrollView( child: Html( + anchorKey: staticAnchorKey, data: htmlData, style: { "table": Style( diff --git a/packages/flutter_html_all/pubspec.yaml b/packages/flutter_html_all/pubspec.yaml index 7f5b9305de..ab76f12164 100644 --- a/packages/flutter_html_all/pubspec.yaml +++ b/packages/flutter_html_all/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html_all description: All optional flutter_html widgets, bundled into a single package. -version: 3.0.0-alpha.3 +version: 3.0.0-alpha.5 homepage: https://github.com/Sub6Resources/flutter_html environment: @@ -11,7 +11,7 @@ dependencies: flutter: sdk: flutter html: '>=0.15.0 <1.0.0' - flutter_html: '>=3.0.0-alpha.3 <4.0.0' + flutter_html: '>=3.0.0-alpha.5 <4.0.0' flutter_html_audio: '>=3.0.0-alpha.3 <4.0.0' flutter_html_iframe: '>=3.0.0-alpha.3 <4.0.0' flutter_html_math: '>=3.0.0-alpha.3 <4.0.0' diff --git a/pubspec.yaml b/pubspec.yaml index 5c1d6aebb5..8842d4ba76 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: 3.0.0-alpha.3 +version: 3.0.0-alpha.5 homepage: https://github.com/Sub6Resources/flutter_html environment: From 03b87028c2b3a9e33f67b1df867da2d9bd58d927 Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Sat, 11 Jun 2022 00:53:32 -0600 Subject: [PATCH 011/107] Fix image source matcher tests so they actually run correctly --- ...svg_image_matcher_source_matcher_test.dart | 110 ++++++ test/image_render_source_matcher_test.dart | 328 ++++++++++-------- 2 files changed, 294 insertions(+), 144 deletions(-) create mode 100644 packages/flutter_html_svg/test/svg_image_matcher_source_matcher_test.dart diff --git a/packages/flutter_html_svg/test/svg_image_matcher_source_matcher_test.dart b/packages/flutter_html_svg/test/svg_image_matcher_source_matcher_test.dart new file mode 100644 index 0000000000..ed136f0e18 --- /dev/null +++ b/packages/flutter_html_svg/test/svg_image_matcher_source_matcher_test.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_html_svg/flutter_html_svg.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:meta/meta.dart'; + +void main() { + group("custom image data uri matcher", () { + CustomRenderMatcher matcher = svgDataUriMatcher(encoding: null, mime: 'image/svg+xml'); + testImgSrcMatcher( + "matches an svg data uri with base64 encoding", + matcher, + imgSrc: + 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2aWV3Qm94PSIwIDAgMzAgMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxjaXJjbGUgY3g9IjE1IiBjeT0iMTAiIHI9IjEwIiBmaWxsPSJncmVlbiIvPgo8L3N2Zz4=', + shouldMatch: true, + ); + testImgSrcMatcher( + "matches an svg data uri without specified encoding", + matcher, + imgSrc: + 'data:image/svg+xml,%3C?xml version="1.0" encoding="UTF-8"?%3E%3Csvg viewBox="0 0 30 20" xmlns="http://www.w3.org/2000/svg"%3E%3Ccircle cx="15" cy="10" r="10" fill="green"/%3E%3C/svg%3E', + shouldMatch: true, + ); + testImgSrcMatcher( + "matches base64 data uri without data", + matcher, + imgSrc: 'data:image/svg+xml;base64,', + shouldMatch: true, + ); + testImgSrcMatcher( + "doesn't match non-base64 image data uri", + matcher, + imgSrc: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==', + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't match different mime data uri", + matcher, + imgSrc: 'data:text/plain;base64,', + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't non-data schema", + matcher, + imgSrc: 'http:', + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't match null", + matcher, + imgSrc: null, + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't match empty", + matcher, + imgSrc: '', + shouldMatch: false, + ); + }); +} + +String _fakeElement(String? src) { + return """ + + """; +} + +@isTest +void testImgSrcMatcher( + String name, + CustomRenderMatcher matcher, { + required String? imgSrc, + required bool shouldMatch, + }) { + testWidgets(name, (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + Html( + data: _fakeElement(imgSrc), + customRenders: { + matcher: CustomRender.widget( + widget: (RenderContext context, _) { + return Text("Success"); + }, + ), + }, + ), + ), + ); + await expectLater(find.text("Success"), shouldMatch ? findsOneWidget : findsNothing); + }); +} + +class TestApp extends StatelessWidget { + final Widget body; + + TestApp(this.body); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: body, + appBar: AppBar(title: Text('flutter_html')), + ), + ); + } +} \ No newline at end of file diff --git a/test/image_render_source_matcher_test.dart b/test/image_render_source_matcher_test.dart index 0cbea40ce5..a259d161d2 100644 --- a/test/image_render_source_matcher_test.dart +++ b/test/image_render_source_matcher_test.dart @@ -1,47 +1,82 @@ -import 'package:flutter_html/custom_render.dart'; +import 'package:meta/meta.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:html/dom.dart' as dom; + +import 'golden_test.dart'; void main() { group("asset uri matcher", () { CustomRenderMatcher matcher = assetUriMatcher(); - test("matches a full asset: uri", () { - expect(_match(matcher, 'asset:some/asset.png'), isTrue); - }); - test("matches asset: schema without path", () { - expect(_match(matcher, 'asset:'), isTrue); - }); - test("doesn't match literal host 'asset'", () { - expect(_match(matcher, 'asset/faulty.path'), isFalse); - }); - test("doesn't match null", () { - expect(_match(matcher, null), isFalse); - }); - test("doesn't match empty", () { - expect(_match(matcher, ''), isFalse); - }); + testImgSrcMatcher( + "matches a full asset: uri", + matcher, + imgSrc: 'asset:some/asset.png', + shouldMatch: true, + ); + testImgSrcMatcher( + "matches asset: schema without path", + matcher, + imgSrc: 'asset:', + shouldMatch: true, + ); + testImgSrcMatcher( + "doesn't match literal host 'asset'", + matcher, + imgSrc: 'asset/faulty.path', + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't match null", + matcher, + imgSrc: null, + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't match empty", + matcher, + imgSrc: '', + shouldMatch: false, + ); }); group("default network source matcher", () { CustomRenderMatcher matcher = networkSourceMatcher(); - test("matches a full http uri", () { - expect(_match(matcher, 'http://legacy.http/uri.png'), isTrue); - }); - test("matches a full https uri", () { - expect(_match(matcher, 'https://proper.https/uri'), isTrue); - }); - test("matches http: schema without path", () { - expect(_match(matcher, 'http:'), isTrue); - }); - test("matches https: schema without path", () { - expect(_match(matcher, 'http:'), isTrue); - }); - test("doesn't match null", () { - expect(_match(matcher, null), isFalse); - }); - test("doesn't match empty", () { - expect(_match(matcher, ''), isFalse); - }); + testImgSrcMatcher( + "matches a full http uri", + matcher, + imgSrc: 'http://legacy.http/uri.png', + shouldMatch: true, + ); + testImgSrcMatcher( + "matches a full https uri", + matcher, + imgSrc: 'https://proper.https/uri', + shouldMatch: true, + ); + testImgSrcMatcher( + "matches http: schema without path", + matcher, + imgSrc: 'http:', + shouldMatch: true, + ); + testImgSrcMatcher( + "matches https: schema without path", + matcher, + imgSrc: 'http:', + shouldMatch: true, + ); + testImgSrcMatcher( + "doesn't match null", + matcher, + imgSrc: null, + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't match empty", + matcher, + imgSrc: '', + shouldMatch: false, + ); }); group("custom network source matcher", () { CustomRenderMatcher matcher = networkSourceMatcher( @@ -49,122 +84,127 @@ void main() { domains: ['www.google.com'], extension: 'png', ); - test("matches schema, domain and extension", () { - expect( - _match(matcher, - 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png'), - isTrue); - }); - test("doesn't match if schema is different", () { - expect( - _match(matcher, - 'http://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png'), - isFalse); - }); - test("doesn't match if domain is different", () { - expect( - _match(matcher, - 'https://google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png'), - isFalse); - }); - test("doesn't match if file extension is different", () { - expect( - _match(matcher, - 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dppng'), - isFalse); - }); - test("doesn't match null", () { - expect(_match(matcher, null), isFalse); - }); - test("doesn't match empty", () { - expect(_match(matcher, ''), isFalse); - }); + testImgSrcMatcher( + "matches schema, domain and extension", + matcher, + imgSrc: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png', + shouldMatch: true, + ); + testImgSrcMatcher( + "doesn't match if schema is different", + matcher, + imgSrc: 'http://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png', + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't match if domain is different", + matcher, + imgSrc: 'https://google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png', + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't match if file extension is different", + matcher, + imgSrc: 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dppng', + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't match null", + matcher, + imgSrc: null, + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't match empty", + matcher, + imgSrc: '', + shouldMatch: false, + ); }); group("default (base64) image data uri matcher", () { CustomRenderMatcher matcher = dataUriMatcher(); - test("matches a full png base64 data uri", () { - expect( - _match(matcher, - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='), - isTrue); - }); - test("matches a full jpeg base64 data uri", () { - expect( - _match(matcher, - 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDADIiJSwlHzIsKSw4NTI7S31RS0VFS5ltc1p9tZ++u7Kfr6zI4f/zyNT/16yv+v/9////////wfD/////////////2wBDATU4OEtCS5NRUZP/zq/O////////////////////////////////////////////////////////////////////wAARCAAYAEADAREAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAQMAAgQF/8QAJRABAAIBBAEEAgMAAAAAAAAAAQIRAAMSITEEEyJBgTORUWFx/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AOgM52xQDrjvAV5Xv0vfKUALlTQfeBm0HThMNHXkL0Lw/swN5qgA8yT4MCS1OEOJV8mBz9Z05yfW8iSx7p4j+jA1aD6Wj7ZMzstsfvAas4UyRHvjrAkC9KhpLMClQntlqFc2X1gUj4viwVObKrddH9YDoHvuujAEuNV+bLwFS8XxdSr+Cq3Vf+4F5RgQl6ZR2p1eAzU/HX80YBYyJLCuexwJCO2O1bwCRidAfWBSctswbI12GAJT3yiwFR7+MBjGK2g/WAJR3FdF84E2rK5VR0YH/9k='), - isTrue); - }); - test("matches base64 data uri without data", () { - expect(_match(matcher, 'data:image/png;base64,'), isTrue); - }); - test("doesn't match non-base64 image data uri", () { - expect( - _match(matcher, - 'data:image/png;hex,89504e470d0a1a0a0000000d49484452000000050000000508060000008d6f26e50000001c4944415408d763f8ffff3fc37f062005c3201284d031f18258cd04000ef535cbd18e0e1f0000000049454e44ae426082'), - isFalse); - }); - test("doesn't match base64 non-image data uri", () { - expect(_match(matcher, 'data:text/plain;base64,'), isFalse); - }); - test("doesn't non-data schema", () { - expect(_match(matcher, 'http:'), isFalse); - }); - test("doesn't match null", () { - expect(_match(matcher, null), isFalse); - }); - test("doesn't match empty", () { - expect(_match(matcher, ''), isFalse); - }); - }); - group("custom image data uri matcher", () { - CustomRenderMatcher matcher = - dataUriMatcher(encoding: null, mime: 'image/svg+xml'); - test("matches an svg data uri with base64 encoding", () { - expect( - _match(matcher, - 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2aWV3Qm94PSIwIDAgMzAgMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxjaXJjbGUgY3g9IjE1IiBjeT0iMTAiIHI9IjEwIiBmaWxsPSJncmVlbiIvPgo8L3N2Zz4='), - isTrue); - }); - test("matches an svg data uri without specified encoding", () { - expect( - _match(matcher, - 'data:image/svg+xml,%3C?xml version="1.0" encoding="UTF-8"?%3E%3Csvg viewBox="0 0 30 20" xmlns="http://www.w3.org/2000/svg"%3E%3Ccircle cx="15" cy="10" r="10" fill="green"/%3E%3C/svg%3E'), - isTrue); - }); - test("matches base64 data uri without data", () { - expect(_match(matcher, 'data:image/svg+xml;base64,'), isTrue); - }); - test("doesn't match non-base64 image data uri", () { - expect( - _match(matcher, - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='), - isFalse); - }); - test("doesn't match different mime data uri", () { - expect(_match(matcher, 'data:text/plain;base64,'), isFalse); - }); - test("doesn't non-data schema", () { - expect(_match(matcher, 'http:'), isFalse); - }); - test("doesn't match null", () { - expect(_match(matcher, null), isFalse); - }); - test("doesn't match empty", () { - expect(_match(matcher, ''), isFalse); - }); + testImgSrcMatcher( + "matches a full png base64 data uri", + matcher, + imgSrc: + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==', + shouldMatch: true, + ); + testImgSrcMatcher( + "matches a full jpeg base64 data uri", + matcher, + imgSrc: + 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDADIiJSwlHzIsKSw4NTI7S31RS0VFS5ltc1p9tZ++u7Kfr6zI4f/zyNT/16yv+v/9////////wfD/////////////2wBDATU4OEtCS5NRUZP/zq/O////////////////////////////////////////////////////////////////////wAARCAAYAEADAREAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAQMAAgQF/8QAJRABAAIBBAEEAgMAAAAAAAAAAQIRAAMSITEEEyJBgTORUWFx/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AOgM52xQDrjvAV5Xv0vfKUALlTQfeBm0HThMNHXkL0Lw/swN5qgA8yT4MCS1OEOJV8mBz9Z05yfW8iSx7p4j+jA1aD6Wj7ZMzstsfvAas4UyRHvjrAkC9KhpLMClQntlqFc2X1gUj4viwVObKrddH9YDoHvuujAEuNV+bLwFS8XxdSr+Cq3Vf+4F5RgQl6ZR2p1eAzU/HX80YBYyJLCuexwJCO2O1bwCRidAfWBSctswbI12GAJT3yiwFR7+MBjGK2g/WAJR3FdF84E2rK5VR0YH/9k=', + shouldMatch: true, + ); + testImgSrcMatcher( + "matches base64 data uri without data", + matcher, + imgSrc: 'data:image/png;base64,', + shouldMatch: true, + ); + testImgSrcMatcher( + "doesn't match non-base64 image data uri", + matcher, + imgSrc: + 'data:image/png;hex,89504e470d0a1a0a0000000d49484452000000050000000508060000008d6f26e50000001c4944415408d763f8ffff3fc37f062005c3201284d031f18258cd04000ef535cbd18e0e1f0000000049454e44ae426082', + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't match base64 non-image data uri", + matcher, + imgSrc: 'data:text/plain;base64,', + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't non-data schema", + matcher, + imgSrc: 'http:', + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't match null", + matcher, + imgSrc: null, + shouldMatch: false, + ); + testImgSrcMatcher( + "doesn't match empty", + matcher, + imgSrc: '', + shouldMatch: false, + ); }); } -dom.Element _fakeElement(String? src) { - return dom.Element.html(""" +String _fakeElement(String? src) { + return """ - """); + """; } -bool _match(CustomRenderMatcher matcher, String? src) { - final element = _fakeElement(src); - //todo find a way to use RenderContext for tests - return /*matcher.call( - element.attributes.map((key, value) => MapEntry(key.toString(), value)), - element);*/ true; +@isTest +void testImgSrcMatcher( + String name, + CustomRenderMatcher matcher, { + required String? imgSrc, + required bool shouldMatch, +}) { + testWidgets(name, (WidgetTester tester) async { + await tester.pumpWidget( + TestApp( + Html( + data: _fakeElement(imgSrc), + customRenders: { + matcher: CustomRender.widget( + widget: (RenderContext context, _) { + return Text("Success"); + }, + ), + }, + ), + ), + ); + await expectLater(find.text("Success"), shouldMatch ? findsOneWidget : findsNothing); + }); } From a1a1cc94e816eb046abfa696c68f5d3fcfcdfca1 Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Mon, 13 Jun 2022 17:41:17 -0600 Subject: [PATCH 012/107] Fix issues brought up by pub.dev score analyzer --- lib/custom_render.dart | 801 ++++++++++-------- lib/flutter_html.dart | 18 +- packages/flutter_html_all/example/example.md | 3 + .../lib/flutter_html_all.dart | 2 + .../flutter_html_audio/example/example.md | 3 + .../lib/flutter_html_audio.dart | 34 +- packages/flutter_html_audio/pubspec.yaml | 2 +- .../flutter_html_iframe/example/example.md | 3 + .../lib/flutter_html_iframe.dart | 8 +- .../lib/iframe_mobile.dart | 55 +- .../lib/iframe_unsupported.dart | 11 +- .../flutter_html_iframe/lib/iframe_web.dart | 68 +- packages/flutter_html_iframe/pubspec.yaml | 2 +- packages/flutter_html_math/example/example.md | 3 + .../lib/flutter_html_math.dart | 116 +-- packages/flutter_html_math/pubspec.yaml | 2 +- packages/flutter_html_svg/example/example.md | 3 + .../lib/flutter_html_svg.dart | 268 +++--- packages/flutter_html_svg/pubspec.yaml | 2 +- .../flutter_html_table/example/example.md | 3 + .../lib/flutter_html_table.dart | 108 +-- packages/flutter_html_table/pubspec.yaml | 3 +- .../flutter_html_video/example/example.md | 3 + .../lib/flutter_html_video.dart | 38 +- packages/flutter_html_video/pubspec.yaml | 2 +- pubspec.yaml | 2 +- 26 files changed, 886 insertions(+), 677 deletions(-) create mode 100644 packages/flutter_html_all/example/example.md create mode 100644 packages/flutter_html_audio/example/example.md create mode 100644 packages/flutter_html_iframe/example/example.md create mode 100644 packages/flutter_html_math/example/example.md create mode 100644 packages/flutter_html_svg/example/example.md create mode 100644 packages/flutter_html_table/example/example.md create mode 100644 packages/flutter_html_video/example/example.md diff --git a/lib/custom_render.dart b/lib/custom_render.dart index 1115c3ae78..a497010cf4 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -11,39 +11,44 @@ import 'package:flutter_html/src/utils.dart'; typedef CustomRenderMatcher = bool Function(RenderContext context); CustomRenderMatcher tagMatcher(String tag) => (context) { - return context.tree.element?.localName == tag; -}; + return context.tree.element?.localName == tag; + }; CustomRenderMatcher blockElementMatcher() => (context) { - return context.tree.style.display == Display.BLOCK && - (context.tree.children.isNotEmpty || context.tree.element?.localName == "hr"); -}; + return context.tree.style.display == Display.BLOCK && + (context.tree.children.isNotEmpty || + context.tree.element?.localName == "hr"); + }; CustomRenderMatcher listElementMatcher() => (context) { - return context.tree.style.display == Display.LIST_ITEM; -}; + return context.tree.style.display == Display.LIST_ITEM; + }; CustomRenderMatcher replacedElementMatcher() => (context) { - return context.tree is ReplacedElement; -}; + return context.tree is ReplacedElement; + }; -CustomRenderMatcher dataUriMatcher({String? encoding = 'base64', String? mime}) => (context) { - if (context.tree.element?.attributes == null - || _src(context.tree.element!.attributes.cast()) == null) return false; - final dataUri = _dataUriFormat.firstMatch(_src(context.tree.element!.attributes.cast())!); - return dataUri != null && dataUri.namedGroup('mime') != "image/svg+xml" && - (mime == null || dataUri.namedGroup('mime') == mime) && - (encoding == null || dataUri.namedGroup('encoding') == ';$encoding'); -}; +CustomRenderMatcher dataUriMatcher( + {String? encoding = 'base64', String? mime}) => + (context) { + if (context.tree.element?.attributes == null || + _src(context.tree.element!.attributes.cast()) == null) return false; + final dataUri = _dataUriFormat + .firstMatch(_src(context.tree.element!.attributes.cast())!); + return dataUri != null && + dataUri.namedGroup('mime') != "image/svg+xml" && + (mime == null || dataUri.namedGroup('mime') == mime) && + (encoding == null || dataUri.namedGroup('encoding') == ';$encoding'); + }; CustomRenderMatcher networkSourceMatcher({ List schemas: const ["https", "http"], List? domains, String? extension, }) => - (context) { - if (context.tree.element?.attributes.cast() == null - || _src(context.tree.element!.attributes.cast()) == null) return false; + (context) { + if (context.tree.element?.attributes.cast() == null || + _src(context.tree.element!.attributes.cast()) == null) return false; try { final src = Uri.parse(_src(context.tree.element!.attributes.cast())!); return schemas.contains(src.scheme) && @@ -55,34 +60,35 @@ CustomRenderMatcher networkSourceMatcher({ }; CustomRenderMatcher assetUriMatcher() => (context) => - context.tree.element?.attributes.cast() != null - && _src(context.tree.element!.attributes.cast()) != null - && _src(context.tree.element!.attributes.cast())!.startsWith("asset:") - && !_src(context.tree.element!.attributes.cast())!.endsWith(".svg"); + context.tree.element?.attributes.cast() != null && + _src(context.tree.element!.attributes.cast()) != null && + _src(context.tree.element!.attributes.cast())!.startsWith("asset:") && + !_src(context.tree.element!.attributes.cast())!.endsWith(".svg"); CustomRenderMatcher textContentElementMatcher() => (context) { - return context.tree is TextContentElement; -}; + return context.tree is TextContentElement; + }; CustomRenderMatcher interactableElementMatcher() => (context) { - return context.tree is InteractableElement; -}; + return context.tree is InteractableElement; + }; CustomRenderMatcher layoutElementMatcher() => (context) { - return context.tree is LayoutElement; -}; + return context.tree is LayoutElement; + }; CustomRenderMatcher verticalAlignMatcher() => (context) { - return context.tree.style.verticalAlign != null - && context.tree.style.verticalAlign != VerticalAlign.BASELINE; -}; + return context.tree.style.verticalAlign != null && + context.tree.style.verticalAlign != VerticalAlign.BASELINE; + }; CustomRenderMatcher fallbackMatcher() => (context) { - return true; -}; + return true; + }; class CustomRender { - final InlineSpan Function(RenderContext, List Function())? inlineSpan; + final InlineSpan Function(RenderContext, List Function())? + inlineSpan; final Widget Function(RenderContext, List Function())? widget; CustomRender.inlineSpan({ @@ -102,188 +108,242 @@ class SelectableCustomRender extends CustomRender { }) : super.inlineSpan(inlineSpan: null); } -CustomRender blockElementRender({ - Style? style, - List? children}) => +CustomRender blockElementRender({Style? style, List? children}) => CustomRender.inlineSpan(inlineSpan: (context, buildChildren) { - if (context.parser.selectable) { - return TextSpan( - style: context.style.generateTextStyle(), - children: (children as List?) ?? context.tree.children - .expandIndexed((i, childTree) => [ - if (childTree.style.display == Display.BLOCK && - i > 0 && - context.tree.children[i - 1] is ReplacedElement) - TextSpan(text: "\n"), - context.parser.parseTree(context, childTree), - if (i != context.tree.children.length - 1 && - childTree.style.display == Display.BLOCK && - childTree.element?.localName != "html" && - childTree.element?.localName != "body") - TextSpan(text: "\n"), - ]) - .toList(), - ); - } - return WidgetSpan( + if (context.parser.selectable) { + return TextSpan( + style: context.style.generateTextStyle(), + children: (children as List?) ?? + context.tree.children + .expandIndexed((i, childTree) => [ + if (childTree.style.display == Display.BLOCK && + i > 0 && + context.tree.children[i - 1] is ReplacedElement) + TextSpan(text: "\n"), + context.parser.parseTree(context, childTree), + if (i != context.tree.children.length - 1 && + childTree.style.display == Display.BLOCK && + childTree.element?.localName != "html" && + childTree.element?.localName != "body") + TextSpan(text: "\n"), + ]) + .toList(), + ); + } + return WidgetSpan( child: ContainerSpan( - key: context.key, - newContext: context, - style: style ?? context.tree.style, - shrinkWrap: context.parser.shrinkWrap, - children: children ?? context.tree.children + key: context.key, + newContext: context, + style: style ?? context.tree.style, + shrinkWrap: context.parser.shrinkWrap, + children: children ?? + context.tree.children .expandIndexed((i, childTree) => [ - if (context.parser.shrinkWrap && - childTree.style.display == Display.BLOCK && - i > 0 && - context.tree.children[i - 1] is ReplacedElement) - TextSpan(text: "\n"), - context.parser.parseTree(context, childTree), - if (context.parser.shrinkWrap && - i != context.tree.children.length - 1 && - childTree.style.display == Display.BLOCK && - childTree.element?.localName != "html" && - childTree.element?.localName != "body") - TextSpan(text: "\n"), - ]) + if (context.parser.shrinkWrap && + childTree.style.display == Display.BLOCK && + i > 0 && + context.tree.children[i - 1] is ReplacedElement) + TextSpan(text: "\n"), + context.parser.parseTree(context, childTree), + if (context.parser.shrinkWrap && + i != context.tree.children.length - 1 && + childTree.style.display == Display.BLOCK && + childTree.element?.localName != "html" && + childTree.element?.localName != "body") + TextSpan(text: "\n"), + ]) .toList(), - )); + )); }); -CustomRender listElementRender({ - Style? style, - Widget? child, - List? children}) => - CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => - WidgetSpan( - child: ContainerSpan( - key: context.key, - newContext: context, - style: style ?? context.tree.style, - shrinkWrap: context.parser.shrinkWrap, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - textDirection: style?.direction ?? context.tree.style.direction, - children: [ - (style?.listStylePosition ?? context.tree.style.listStylePosition) == ListStylePosition.OUTSIDE ? - Padding( - padding: style?.padding?.nonNegative ?? context.tree.style.padding?.nonNegative - ?? EdgeInsets.only(left: (style?.direction ?? context.tree.style.direction) != TextDirection.rtl ? 10.0 : 0.0, - right: (style?.direction ?? context.tree.style.direction) == TextDirection.rtl ? 10.0 : 0.0), - child: style?.markerContent ?? context.style.markerContent - ) : Container(height: 0, width: 0), - Text("\u0020", textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.w400)), - Expanded( - child: Padding( - padding: (style?.listStylePosition ?? context.tree.style.listStylePosition) == ListStylePosition.INSIDE ? - EdgeInsets.only(left: (style?.direction ?? context.tree.style.direction) != TextDirection.rtl ? 10.0 : 0.0, - right: (style?.direction ?? context.tree.style.direction) == TextDirection.rtl ? 10.0 : 0.0) : EdgeInsets.zero, - child: StyledText( - textSpan: TextSpan( - children: _getListElementChildren(style?.listStylePosition ?? context.tree.style.listStylePosition, buildChildren) - ..insertAll(0, context.tree.style.listStylePosition == ListStylePosition.INSIDE ? - [ - WidgetSpan(alignment: PlaceholderAlignment.middle, child: style?.markerContent ?? context.style.markerContent ?? Container(height: 0, width: 0)) - ] : []), - style: style?.generateTextStyle() ?? context.style.generateTextStyle(), - ), - style: style ?? context.style, - renderContext: context, - ) - ) - ) - ], - ), - ), -)); - -CustomRender replacedElementRender({PlaceholderAlignment? alignment, TextBaseline? baseline, Widget? child}) => - CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => WidgetSpan( - alignment: alignment ?? (context.tree as ReplacedElement).alignment, - baseline: baseline ?? TextBaseline.alphabetic, - child: child ?? (context.tree as ReplacedElement).toWidget(context)!, -)); +CustomRender listElementRender( + {Style? style, Widget? child, List? children}) => + CustomRender.inlineSpan( + inlineSpan: (context, buildChildren) => WidgetSpan( + child: ContainerSpan( + key: context.key, + newContext: context, + style: style ?? context.tree.style, + shrinkWrap: context.parser.shrinkWrap, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + textDirection: + style?.direction ?? context.tree.style.direction, + children: [ + (style?.listStylePosition ?? + context.tree.style.listStylePosition) == + ListStylePosition.OUTSIDE + ? Padding( + padding: style?.padding?.nonNegative ?? + context.tree.style.padding?.nonNegative ?? + EdgeInsets.only( + left: (style?.direction ?? + context.tree.style.direction) != + TextDirection.rtl + ? 10.0 + : 0.0, + right: (style?.direction ?? + context.tree.style.direction) == + TextDirection.rtl + ? 10.0 + : 0.0), + child: style?.markerContent ?? + context.style.markerContent) + : Container(height: 0, width: 0), + Text("\u0020", + textAlign: TextAlign.right, + style: TextStyle(fontWeight: FontWeight.w400)), + Expanded( + child: Padding( + padding: (style?.listStylePosition ?? + context.tree.style.listStylePosition) == + ListStylePosition.INSIDE + ? EdgeInsets.only( + left: (style?.direction ?? + context.tree.style.direction) != + TextDirection.rtl + ? 10.0 + : 0.0, + right: (style?.direction ?? + context.tree.style.direction) == + TextDirection.rtl + ? 10.0 + : 0.0) + : EdgeInsets.zero, + child: StyledText( + textSpan: TextSpan( + children: _getListElementChildren( + style?.listStylePosition ?? + context.tree.style.listStylePosition, + buildChildren) + ..insertAll( + 0, + context.tree.style.listStylePosition == + ListStylePosition.INSIDE + ? [ + WidgetSpan( + alignment: + PlaceholderAlignment + .middle, + child: style?.markerContent ?? + context.style + .markerContent ?? + Container( + height: 0, width: 0)) + ] + : []), + style: style?.generateTextStyle() ?? + context.style.generateTextStyle(), + ), + style: style ?? context.style, + renderContext: context, + ))) + ], + ), + ), + )); + +CustomRender replacedElementRender( + {PlaceholderAlignment? alignment, + TextBaseline? baseline, + Widget? child}) => + CustomRender.inlineSpan( + inlineSpan: (context, buildChildren) => WidgetSpan( + alignment: + alignment ?? (context.tree as ReplacedElement).alignment, + baseline: baseline ?? TextBaseline.alphabetic, + child: + child ?? (context.tree as ReplacedElement).toWidget(context)!, + )); CustomRender textContentElementRender({String? text}) => - CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => - TextSpan(text: (text ?? (context.tree as TextContentElement).text).transformed(context.tree.style.textTransform))); - -CustomRender base64ImageRender() => CustomRender.widget(widget: (context, buildChildren) { - final decodedImage = base64.decode(_src(context.tree.element!.attributes.cast())!.split("base64,")[1].trim()); - precacheImage( - MemoryImage(decodedImage), - context.buildContext, - onError: (exception, StackTrace? stackTrace) { - context.parser.onImageError?.call(exception, stackTrace); - }, - ); - final widget = Image.memory( - decodedImage, - frameBuilder: (ctx, child, frame, _) { - if (frame == null) { - return Text(_alt(context.tree.element!.attributes.cast()) ?? "", style: context.style.generateTextStyle()); - } - return child; - }, - ); - return Builder( - key: context.key, - builder: (buildContext) { - return GestureDetector( - child: widget, - onTap: () { - if (MultipleTapGestureDetector.of(buildContext) != null) { - MultipleTapGestureDetector.of(buildContext)!.onTap?.call(); - } - context.parser.onImageTap?.call( - _src(context.tree.element!.attributes.cast())!.split("base64,")[1].trim(), - context, - context.tree.element!.attributes.cast(), - context.tree.element + CustomRender.inlineSpan( + inlineSpan: (context, buildChildren) => TextSpan( + text: (text ?? (context.tree as TextContentElement).text) + .transformed(context.tree.style.textTransform))); + +CustomRender base64ImageRender() => + CustomRender.widget(widget: (context, buildChildren) { + final decodedImage = base64.decode( + _src(context.tree.element!.attributes.cast())! + .split("base64,")[1] + .trim()); + precacheImage( + MemoryImage(decodedImage), + context.buildContext, + onError: (exception, StackTrace? stackTrace) { + context.parser.onImageError?.call(exception, stackTrace); + }, + ); + final widget = Image.memory( + decodedImage, + frameBuilder: (ctx, child, frame, _) { + if (frame == null) { + return Text(_alt(context.tree.element!.attributes.cast()) ?? "", + style: context.style.generateTextStyle()); + } + return child; + }, + ); + return Builder( + key: context.key, + builder: (buildContext) { + return GestureDetector( + child: widget, + onTap: () { + if (MultipleTapGestureDetector.of(buildContext) != null) { + MultipleTapGestureDetector.of(buildContext)!.onTap?.call(); + } + context.parser.onImageTap?.call( + _src(context.tree.element!.attributes.cast())! + .split("base64,")[1] + .trim(), + context, + context.tree.element!.attributes.cast(), + context.tree.element); + }, ); - }, - ); - } - ); -}); + }); + }); CustomRender assetImageRender({ double? width, double? height, -}) => CustomRender.widget(widget: (context, buildChildren) { - final assetPath = _src(context.tree.element!.attributes.cast())!.replaceFirst('asset:', ''); - final widget = Image.asset( - assetPath, - width: width ?? _width(context.tree.element!.attributes.cast()), - height: height ?? _height(context.tree.element!.attributes.cast()), - frameBuilder: (ctx, child, frame, _) { - if (frame == null) { - return Text(_alt(context.tree.element!.attributes.cast()) ?? "", style: context.style.generateTextStyle()); - } - return child; - }, - ); - return Builder( - key: context.key, - builder: (buildContext) { - return GestureDetector( - child: widget, - onTap: () { - if (MultipleTapGestureDetector.of(buildContext) != null) { - MultipleTapGestureDetector.of(buildContext)!.onTap?.call(); - } - context.parser.onImageTap?.call( - assetPath, - context, - context.tree.element!.attributes.cast(), - context.tree.element +}) => + CustomRender.widget(widget: (context, buildChildren) { + final assetPath = _src(context.tree.element!.attributes.cast())! + .replaceFirst('asset:', ''); + final widget = Image.asset( + assetPath, + width: width ?? _width(context.tree.element!.attributes.cast()), + height: height ?? _height(context.tree.element!.attributes.cast()), + frameBuilder: (ctx, child, frame, _) { + if (frame == null) { + return Text(_alt(context.tree.element!.attributes.cast()) ?? "", + style: context.style.generateTextStyle()); + } + return child; + }, + ); + return Builder( + key: context.key, + builder: (buildContext) { + return GestureDetector( + child: widget, + onTap: () { + if (MultipleTapGestureDetector.of(buildContext) != null) { + MultipleTapGestureDetector.of(buildContext)!.onTap?.call(); + } + context.parser.onImageTap?.call( + assetPath, + context, + context.tree.element!.attributes.cast(), + context.tree.element); + }, ); - }, - ); - } - ); -}); + }); + }); CustomRender networkImageRender({ Map? headers, @@ -292,151 +352,164 @@ CustomRender networkImageRender({ double? height, Widget Function(String?)? altWidget, Widget Function()? loadingWidget, -}) => CustomRender.widget(widget: (context, buildChildren) { - final src = mapUrl?.call(_src(context.tree.element!.attributes.cast())) - ?? _src(context.tree.element!.attributes.cast())!; - Completer completer = Completer(); - if (context.parser.cachedImageSizes[src] != null) { - completer.complete(context.parser.cachedImageSizes[src]); - } else { - Image image = Image.network(src, frameBuilder: (ctx, child, frame, _) { - if (frame == null) { - if (!completer.isCompleted) { - completer.completeError("error"); - } - return child; +}) => + CustomRender.widget(widget: (context, buildChildren) { + final src = mapUrl?.call(_src(context.tree.element!.attributes.cast())) ?? + _src(context.tree.element!.attributes.cast())!; + Completer completer = Completer(); + if (context.parser.cachedImageSizes[src] != null) { + completer.complete(context.parser.cachedImageSizes[src]); } else { - return child; - } - }); - - ImageStreamListener? listener; - listener = ImageStreamListener((ImageInfo imageInfo, bool synchronousCall) { - var myImage = imageInfo.image; - Size size = Size(myImage.width.toDouble(), myImage.height.toDouble()); - if (!completer.isCompleted) { - context.parser.cachedImageSizes[src] = size; - completer.complete(size); - image.image.resolve(ImageConfiguration()).removeListener(listener!); - } - }, onError: (object, stacktrace) { - if (!completer.isCompleted) { - completer.completeError(object); - image.image.resolve(ImageConfiguration()).removeListener(listener!); + Image image = Image.network(src, frameBuilder: (ctx, child, frame, _) { + if (frame == null) { + if (!completer.isCompleted) { + completer.completeError("error"); + } + return child; + } else { + return child; + } + }); + + ImageStreamListener? listener; + listener = + ImageStreamListener((ImageInfo imageInfo, bool synchronousCall) { + var myImage = imageInfo.image; + Size size = Size(myImage.width.toDouble(), myImage.height.toDouble()); + if (!completer.isCompleted) { + context.parser.cachedImageSizes[src] = size; + completer.complete(size); + image.image.resolve(ImageConfiguration()).removeListener(listener!); + } + }, onError: (object, stacktrace) { + if (!completer.isCompleted) { + completer.completeError(object); + image.image.resolve(ImageConfiguration()).removeListener(listener!); + } + }); + + image.image.resolve(ImageConfiguration()).addListener(listener); } - }); - - image.image.resolve(ImageConfiguration()).addListener(listener); - } - final attributes = context.tree.element!.attributes.cast(); - final widget = FutureBuilder( - future: completer.future, - initialData: context.parser.cachedImageSizes[src], - builder: (BuildContext buildContext, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return Container( - constraints: BoxConstraints( - maxWidth: width ?? _width(attributes) ?? snapshot.data!.width, - maxHeight: - (width ?? _width(attributes) ?? snapshot.data!.width) / - _aspectRatio(attributes, snapshot)), - child: AspectRatio( - aspectRatio: _aspectRatio(attributes, snapshot), - child: Image.network( - src, - headers: headers, - width: width ?? _width(attributes) ?? snapshot.data!.width, - height: height ?? _height(attributes), - frameBuilder: (ctx, child, frame, _) { - if (frame == null) { - return altWidget?.call(_alt(attributes)) ?? - Text(_alt(attributes) ?? "", - style: context.style.generateTextStyle()); + final attributes = + context.tree.element!.attributes.cast(); + final widget = FutureBuilder( + future: completer.future, + initialData: context.parser.cachedImageSizes[src], + builder: (BuildContext buildContext, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return Container( + constraints: BoxConstraints( + maxWidth: width ?? _width(attributes) ?? snapshot.data!.width, + maxHeight: + (width ?? _width(attributes) ?? snapshot.data!.width) / + _aspectRatio(attributes, snapshot)), + child: AspectRatio( + aspectRatio: _aspectRatio(attributes, snapshot), + child: Image.network( + src, + headers: headers, + width: width ?? _width(attributes) ?? snapshot.data!.width, + height: height ?? _height(attributes), + frameBuilder: (ctx, child, frame, _) { + if (frame == null) { + return altWidget?.call(_alt(attributes)) ?? + Text(_alt(attributes) ?? "", + style: context.style.generateTextStyle()); + } + return child; + }, + ), + ), + ); + } else if (snapshot.hasError) { + return altWidget + ?.call(_alt(context.tree.element!.attributes.cast())) ?? + Text(_alt(context.tree.element!.attributes.cast()) ?? "", + style: context.style.generateTextStyle()); + } else { + return loadingWidget?.call() ?? const CircularProgressIndicator(); + } + }, + ); + return Builder( + key: context.key, + builder: (buildContext) { + return GestureDetector( + child: widget, + onTap: () { + if (MultipleTapGestureDetector.of(buildContext) != null) { + MultipleTapGestureDetector.of(buildContext)!.onTap?.call(); } - return child; + context.parser.onImageTap?.call( + src, + context, + context.tree.element!.attributes.cast(), + context.tree.element); }, - ), - ), - ); - } else if (snapshot.hasError) { - return altWidget?.call(_alt(context.tree.element!.attributes.cast())) ?? - Text(_alt(context.tree.element!.attributes.cast()) - ?? "", style: context.style.generateTextStyle()); - } else { - return loadingWidget?.call() ?? const CircularProgressIndicator(); - } - }, - ); - return Builder( - key: context.key, - builder: (buildContext) { - return GestureDetector( - child: widget, - onTap: () { - if (MultipleTapGestureDetector.of(buildContext) != null) { - MultipleTapGestureDetector.of(buildContext)!.onTap?.call(); - } - context.parser.onImageTap?.call( - src, - context, - context.tree.element!.attributes.cast(), - context.tree.element ); - }, - ); - } - ); -}); + }); + }); CustomRender interactableElementRender({List? children}) => - CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan( - children: children ?? (context.tree as InteractableElement).children - .map((tree) => context.parser.parseTree(context, tree)) - .map((childSpan) { - return _getInteractableChildren(context, context.tree as InteractableElement, childSpan, - context.style.generateTextStyle().merge(childSpan.style)); - }).toList(), -)); - -CustomRender layoutElementRender({Widget? child}) => - CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => WidgetSpan( - child: child ?? (context.tree as LayoutElement).toWidget(context)!, -)); - -CustomRender verticalAlignRender({ - double? verticalOffset, - Style? style, - List? children}) => - CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => WidgetSpan( - child: Transform.translate( - key: context.key, - offset: Offset(0, verticalOffset ?? _getVerticalOffset(context.tree)), - child: StyledText( - textSpan: TextSpan( - style: style?.generateTextStyle() ?? context.style.generateTextStyle(), - children: children ?? buildChildren.call(), - ), - style: context.style, - renderContext: context, - ), - ), -)); + CustomRender.inlineSpan( + inlineSpan: (context, buildChildren) => TextSpan( + children: children ?? + (context.tree as InteractableElement) + .children + .map((tree) => context.parser.parseTree(context, tree)) + .map((childSpan) { + return _getInteractableChildren( + context, + context.tree as InteractableElement, + childSpan, + context.style + .generateTextStyle() + .merge(childSpan.style)); + }).toList(), + )); + +CustomRender layoutElementRender({Widget? child}) => CustomRender.inlineSpan( + inlineSpan: (context, buildChildren) => WidgetSpan( + child: child ?? (context.tree as LayoutElement).toWidget(context)!, + )); + +CustomRender verticalAlignRender( + {double? verticalOffset, Style? style, List? children}) => + CustomRender.inlineSpan( + inlineSpan: (context, buildChildren) => WidgetSpan( + child: Transform.translate( + key: context.key, + offset: Offset( + 0, verticalOffset ?? _getVerticalOffset(context.tree)), + child: StyledText( + textSpan: TextSpan( + style: style?.generateTextStyle() ?? + context.style.generateTextStyle(), + children: children ?? buildChildren.call(), + ), + style: context.style, + renderContext: context, + ), + ), + )); CustomRender fallbackRender({Style? style, List? children}) => - CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan( - style: style?.generateTextStyle() ?? context.style.generateTextStyle(), - children: context.tree.children - .expand((tree) => [ - context.parser.parseTree(context, tree), - if (tree.style.display == Display.BLOCK && - tree.element?.parent?.localName != "th" && - tree.element?.parent?.localName != "td" && - tree.element?.localName != "html" && - tree.element?.localName != "body") - TextSpan(text: "\n"), - ]) - .toList(), -)); + CustomRender.inlineSpan( + inlineSpan: (context, buildChildren) => TextSpan( + style: style?.generateTextStyle() ?? + context.style.generateTextStyle(), + children: context.tree.children + .expand((tree) => [ + context.parser.parseTree(context, tree), + if (tree.style.display == Display.BLOCK && + tree.element?.parent?.localName != "th" && + tree.element?.parent?.localName != "td" && + tree.element?.localName != "html" && + tree.element?.localName != "body") + TextSpan(text: "\n"), + ]) + .toList(), + )); final Map defaultRenders = { blockElementMatcher(): blockElementRender(), @@ -452,45 +525,51 @@ final Map defaultRenders = { fallbackMatcher(): fallbackRender(), }; -List _getListElementChildren(ListStylePosition? position, Function() buildChildren) { +List _getListElementChildren( + ListStylePosition? position, Function() buildChildren) { List children = buildChildren.call(); if (position == ListStylePosition.INSIDE) { final tabSpan = WidgetSpan( - child: Text("\t", textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.w400)), + child: Text("\t", + textAlign: TextAlign.right, + style: TextStyle(fontWeight: FontWeight.w400)), ); children.insert(0, tabSpan); } return children; } -InlineSpan _getInteractableChildren(RenderContext context, InteractableElement tree, InlineSpan childSpan, TextStyle childStyle) { +InlineSpan _getInteractableChildren(RenderContext context, + InteractableElement tree, InlineSpan childSpan, TextStyle childStyle) { if (childSpan is TextSpan) { return TextSpan( text: childSpan.text, children: childSpan.children - ?.map((e) => _getInteractableChildren(context, tree, e, childStyle.merge(childSpan.style))) + ?.map((e) => _getInteractableChildren( + context, tree, e, childStyle.merge(childSpan.style))) .toList(), - style: context.style.generateTextStyle().merge( - childSpan.style == null - ? childStyle - : childStyle.merge(childSpan.style)), + style: context.style.generateTextStyle().merge(childSpan.style == null + ? childStyle + : childStyle.merge(childSpan.style)), semanticsLabel: childSpan.semanticsLabel, recognizer: TapGestureRecognizer() - ..onTap = - context.parser.internalOnAnchorTap != null ? - () => context.parser.internalOnAnchorTap!(tree.href, context, tree.attributes, tree.element) - : null, + ..onTap = context.parser.internalOnAnchorTap != null + ? () => context.parser.internalOnAnchorTap!( + tree.href, context, tree.attributes, tree.element) + : null, ); } else { return WidgetSpan( child: MultipleTapGestureDetector( onTap: context.parser.internalOnAnchorTap != null - ? () => context.parser.internalOnAnchorTap!(tree.href, context, tree.attributes, tree.element) + ? () => context.parser.internalOnAnchorTap!( + tree.href, context, tree.attributes, tree.element) : null, child: GestureDetector( key: context.key, onTap: context.parser.internalOnAnchorTap != null - ? () => context.parser.internalOnAnchorTap!(tree.href, context, tree.attributes, tree.element) + ? () => context.parser.internalOnAnchorTap!( + tree.href, context, tree.attributes, tree.element) : null, child: (childSpan as WidgetSpan).child, ), @@ -499,7 +578,8 @@ InlineSpan _getInteractableChildren(RenderContext context, InteractableElement t } } -final _dataUriFormat = RegExp("^(?data):(?image\/[\\w\+\-\.]+)(?;base64)?\,(?.*)"); +final _dataUriFormat = RegExp( + "^(?data):(?image\/[\\w\+\-\.]+)(?;base64)?\,(?.*)"); double _getVerticalOffset(StyledElement tree) { switch (tree.style.verticalAlign) { @@ -522,12 +602,16 @@ String? _alt(Map attributes) { double? _height(Map attributes) { final heightString = attributes["height"]; - return heightString == null ? heightString as double? : double.tryParse(heightString); + return heightString == null + ? heightString as double? + : double.tryParse(heightString); } double? _width(Map attributes) { final widthString = attributes["width"]; - return widthString == null ? widthString as double? : double.tryParse(widthString); + return widthString == null + ? widthString as double? + : double.tryParse(widthString); } double _aspectRatio( @@ -545,5 +629,6 @@ double _aspectRatio( } extension ClampedEdgeInsets on EdgeInsetsGeometry { - EdgeInsetsGeometry get nonNegative => this.clamp(EdgeInsets.zero, const EdgeInsets.all(double.infinity)); + EdgeInsetsGeometry get nonNegative => + this.clamp(EdgeInsets.zero, const EdgeInsets.all(double.infinity)); } diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 9496d9263a..3db68efb2b 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -159,14 +159,19 @@ class _HtmlState extends State { @override void initState() { super.initState(); - documentElement = widget.data != null ? HtmlParser.parseHTML(widget.data!) : widget.documentElement!; + documentElement = widget.data != null + ? HtmlParser.parseHTML(widget.data!) + : widget.documentElement!; } @override void didUpdateWidget(Html oldWidget) { super.didUpdateWidget(oldWidget); - if ((widget.data != null && oldWidget.data != widget.data) || oldWidget.documentElement != widget.documentElement) { - documentElement = widget.data != null ? HtmlParser.parseHTML(widget.data!) : widget.documentElement!; + if ((widget.data != null && oldWidget.data != widget.data) || + oldWidget.documentElement != widget.documentElement) { + documentElement = widget.data != null + ? HtmlParser.parseHTML(widget.data!) + : widget.documentElement!; } } @@ -333,7 +338,9 @@ class _SelectableHtmlState extends State { @override void initState() { super.initState(); - documentElement = widget.data != null ? HtmlParser.parseHTML(widget.data!) : widget.documentElement!; + documentElement = widget.data != null + ? HtmlParser.parseHTML(widget.data!) + : widget.documentElement!; } @override @@ -354,7 +361,8 @@ class _SelectableHtmlState extends State { customRenders: {} ..addAll(widget.customRenders) ..addAll(defaultRenders), - tagsList: widget.tagsList.isEmpty ? SelectableHtml.tags : widget.tagsList, + tagsList: + widget.tagsList.isEmpty ? SelectableHtml.tags : widget.tagsList, selectionControls: widget.selectionControls, scrollPhysics: widget.scrollPhysics, ), diff --git a/packages/flutter_html_all/example/example.md b/packages/flutter_html_all/example/example.md new file mode 100644 index 0000000000..aff9f75ad4 --- /dev/null +++ b/packages/flutter_html_all/example/example.md @@ -0,0 +1,3 @@ +# Example + +### Coming soon... \ No newline at end of file diff --git a/packages/flutter_html_all/lib/flutter_html_all.dart b/packages/flutter_html_all/lib/flutter_html_all.dart index 9fb8378795..2b34957522 100644 --- a/packages/flutter_html_all/lib/flutter_html_all.dart +++ b/packages/flutter_html_all/lib/flutter_html_all.dart @@ -1,3 +1,5 @@ +/// Package flutter_html_all is used to get access to all +/// of the extended features of the flutter_html package. library flutter_html_all; export 'package:flutter_html_audio/flutter_html_audio.dart'; diff --git a/packages/flutter_html_audio/example/example.md b/packages/flutter_html_audio/example/example.md new file mode 100644 index 0000000000..aff9f75ad4 --- /dev/null +++ b/packages/flutter_html_audio/example/example.md @@ -0,0 +1,3 @@ +# Example + +### Coming soon... \ No newline at end of file diff --git a/packages/flutter_html_audio/lib/flutter_html_audio.dart b/packages/flutter_html_audio/lib/flutter_html_audio.dart index 51fc3d70df..2484b8e45d 100644 --- a/packages/flutter_html_audio/lib/flutter_html_audio.dart +++ b/packages/flutter_html_audio/lib/flutter_html_audio.dart @@ -6,16 +6,24 @@ import 'package:flutter_html/flutter_html.dart'; import 'package:video_player/video_player.dart'; import 'package:html/dom.dart' as dom; -typedef AudioControllerCallback = void Function(dom.Element?, ChewieAudioController, VideoPlayerController); +typedef AudioControllerCallback = void Function( + dom.Element?, ChewieAudioController, VideoPlayerController); -CustomRender audioRender({AudioControllerCallback? onControllerCreated}) - => CustomRender.widget(widget: (context, buildChildren) - => AudioWidget(context: context, callback: onControllerCreated)); +/// The CustomRender function for the `