From c5f396dd29b9ede943d1e908b48d4b80ac2d2542 Mon Sep 17 00:00:00 2001 From: Zak Barbuto Date: Mon, 25 Oct 2021 17:43:30 +1030 Subject: [PATCH 01/75] 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 1c2412a24374c704af3ed39e0c21a0404b7925c3 Mon Sep 17 00:00:00 2001 From: wangbax Date: Mon, 28 Mar 2022 21:31:02 +0800 Subject: [PATCH 02/75] fix: ol use default style --- lib/custom_render.dart | 2 +- lib/html_parser.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/custom_render.dart b/lib/custom_render.dart index 9b4abd8890..1115c3ae78 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -173,7 +173,7 @@ CustomRender listElementRender({ 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("\t", textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.w400)), + Text("\u0020", textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.w400)), Expanded( child: Padding( padding: (style?.listStylePosition ?? context.tree.style.listStylePosition) == ListStylePosition.INSIDE ? diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 4f15b9a8a1..226f4223c4 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -606,6 +606,7 @@ class HtmlParser extends StatelessWidget { tree.style.markerContent = Text( marker, textAlign: TextAlign.right, + style: tree.style.generateTextStyle(), ); } From 1f7105359f96ced0ca53c5853a75004b7efaea5a Mon Sep 17 00:00:00 2001 From: Jonathan Friesen Date: Mon, 11 Apr 2022 20:18:59 -0700 Subject: [PATCH 03/75] WIP - add feature/async-parsing updates to latest from master --- example/lib/main.dart | 2 +- lib/custom_render.dart | 40 +- lib/flutter_html.dart | 36 +- lib/html_parser.dart | 541 ++++++++++++++---- lib/src/layout_element.dart | 4 +- lib/src/replaced_element.dart | 4 +- lib/style.dart | 4 +- .../lib/flutter_html_math.dart | 2 +- test/html_parser_test.dart | 37 +- 9 files changed, 499 insertions(+), 171 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 550ad15bd1..094f488728 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -274,7 +274,7 @@ class _MyHomePageState extends State { tagMatcher("tex"): CustomRender.widget(widget: (context, buildChildren) => Math.tex( context.tree.element?.innerHtml ?? '', mathStyle: MathStyle.display, - textStyle: context.style.generateTextStyle(), + textStyle: context.style.generateTextStyle(context.buildContext), onErrorFallback: (FlutterMathException e) { return Text(e.message); }, diff --git a/lib/custom_render.dart b/lib/custom_render.dart index 9b4abd8890..82b63e7908 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -108,7 +108,7 @@ CustomRender blockElementRender({ CustomRender.inlineSpan(inlineSpan: (context, buildChildren) { if (context.parser.selectable) { return TextSpan( - style: context.style.generateTextStyle(), + style: context.style.generateTextStyle(context.buildContext), children: (children as List?) ?? context.tree.children .expandIndexed((i, childTree) => [ if (childTree.style.display == Display.BLOCK && @@ -186,7 +186,7 @@ CustomRender listElementRender({ [ WidgetSpan(alignment: PlaceholderAlignment.middle, child: style?.markerContent ?? context.style.markerContent ?? Container(height: 0, width: 0)) ] : []), - style: style?.generateTextStyle() ?? context.style.generateTextStyle(), + style: style?.generateTextStyle(context.buildContext) ?? context.style.generateTextStyle(context.buildContext), ), style: style ?? context.style, renderContext: context, @@ -222,7 +222,7 @@ CustomRender base64ImageRender() => CustomRender.widget(widget: (context, buildC decodedImage, frameBuilder: (ctx, child, frame, _) { if (frame == null) { - return Text(_alt(context.tree.element!.attributes.cast()) ?? "", style: context.style.generateTextStyle()); + return Text(_alt(context.tree.element!.attributes.cast()) ?? "", style: context.style.generateTextStyle(context.buildContext)); } return child; }, @@ -259,7 +259,7 @@ CustomRender assetImageRender({ 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 Text(_alt(context.tree.element!.attributes.cast()) ?? "", style: context.style.generateTextStyle(context.buildContext)); } return child; }, @@ -351,7 +351,7 @@ CustomRender networkImageRender({ if (frame == null) { return altWidget?.call(_alt(attributes)) ?? Text(_alt(attributes) ?? "", - style: context.style.generateTextStyle()); + style: context.style.generateTextStyle(context.buildContext)); } return child; }, @@ -361,7 +361,7 @@ CustomRender networkImageRender({ } else if (snapshot.hasError) { return altWidget?.call(_alt(context.tree.element!.attributes.cast())) ?? Text(_alt(context.tree.element!.attributes.cast()) - ?? "", style: context.style.generateTextStyle()); + ?? "", style: context.style.generateTextStyle(context.buildContext)); } else { return loadingWidget?.call() ?? const CircularProgressIndicator(); } @@ -394,7 +394,7 @@ CustomRender interactableElementRender({List? children}) => .map((tree) => context.parser.parseTree(context, tree)) .map((childSpan) { return _getInteractableChildren(context, context.tree as InteractableElement, childSpan, - context.style.generateTextStyle().merge(childSpan.style)); + context.style.generateTextStyle(context.buildContext).merge(childSpan.style)); }).toList(), )); @@ -413,7 +413,7 @@ CustomRender verticalAlignRender({ offset: Offset(0, verticalOffset ?? _getVerticalOffset(context.tree)), child: StyledText( textSpan: TextSpan( - style: style?.generateTextStyle() ?? context.style.generateTextStyle(), + style: style?.generateTextStyle(context.buildContext) ?? context.style.generateTextStyle(context.buildContext), children: children ?? buildChildren.call(), ), style: context.style, @@ -424,7 +424,7 @@ CustomRender verticalAlignRender({ CustomRender fallbackRender({Style? style, List? children}) => CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan( - style: style?.generateTextStyle() ?? context.style.generateTextStyle(), + style: style?.generateTextStyle(context.buildContext) ?? context.style.generateTextStyle(context.buildContext), children: context.tree.children .expand((tree) => [ context.parser.parseTree(context, tree), @@ -438,6 +438,26 @@ CustomRender fallbackRender({Style? style, List? children}) => .toList(), )); +// CustomRender fallbackRender({Style? style, List? children}) => CustomRender.inlineSpan( +// inlineSpan: (renderContext, buildChildren) => WidgetSpan( +// child: Builder( +// builder: (context) => StyledText( +// renderContext: renderContext, +// style: renderContext.style, +// textSpan: TextSpan( +// style: style?.generateTextStyle(context) ?? renderContext.style.generateTextStyle(context), +// children: renderContext.tree.children +// .expand((tree) => [ +// renderContext.parser.parseTree(renderContext, 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(), listElementMatcher(): listElementRender(), @@ -470,7 +490,7 @@ InlineSpan _getInteractableChildren(RenderContext context, InteractableElement t children: childSpan.children ?.map((e) => _getInteractableChildren(context, tree, e, childStyle.merge(childSpan.style))) .toList(), - style: context.style.generateTextStyle().merge( + style: context.style.generateTextStyle(context.buildContext).merge( childSpan.style == null ? childStyle : childStyle.merge(childSpan.style)), diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index f0b33d547d..0b5cd2e673 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -9,15 +9,18 @@ import 'package:html/dom.dart' as dom; //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'; export 'package:flutter_html/src/layout_element.dart'; export 'package:flutter_html/src/replaced_element.dart'; export 'package:flutter_html/src/styled_element.dart'; + //export style api export 'package:flutter_html/style.dart'; @@ -60,8 +63,11 @@ class Html extends StatefulWidget { this.onImageTap, this.tagsList = const [], this.style = const {}, - }) : documentElement = null, - assert (data != null), + this.loadingPlaceholder, + this.onContentRendered, + this.textScaleFactor, + }) : documentElement = null, + assert(data != null), _anchorKey = anchorKey ?? GlobalKey(), super(key: key); @@ -78,7 +84,10 @@ class Html extends StatefulWidget { this.onImageTap, this.tagsList = const [], this.style = const {}, - }) : data = null, + this.loadingPlaceholder, + this.onContentRendered, + this.textScaleFactor, + }) : data = null, assert(document != null), this.documentElement = document!.documentElement, _anchorKey = anchorKey ?? GlobalKey(), @@ -97,7 +106,10 @@ class Html extends StatefulWidget { this.onImageTap, this.tagsList = const [], this.style = const {}, - }) : data = null, + this.loadingPlaceholder, + this.onContentRendered, + this.textScaleFactor, + }) : data = null, assert(documentElement != null), _anchorKey = anchorKey ?? GlobalKey(), super(key: key); @@ -141,6 +153,10 @@ class Html extends StatefulWidget { /// An API that allows you to override the default style for any HTML element final Map style; + final Widget? loadingPlaceholder; + final OnContentRendered? onContentRendered; + final double? textScaleFactor; + static List get tags => new List.from(STYLED_ELEMENTS) ..addAll(INTERACTABLE_ELEMENTS) ..addAll(REPLACED_ELEMENTS) @@ -159,8 +175,7 @@ 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 @@ -182,6 +197,9 @@ class _HtmlState extends State { ..addAll(widget.customRenders) ..addAll(defaultRenders), tagsList: widget.tagsList.isEmpty ? Html.tags : widget.tagsList, + loadingPlaceholder: widget.loadingPlaceholder, + onContentRendered: widget.onContentRendered, + textScaleFactor: widget.textScaleFactor, ), ); } @@ -232,7 +250,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 +268,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 +287,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); diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 4f15b9a8a1..e86bf106c9 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -1,9 +1,11 @@ +import 'dart:async'; import 'dart:collection'; import 'dart:math'; import 'package:collection/collection.dart'; import 'package:csslib/parser.dart' as cssparser; import 'package:csslib/visitor.dart' as css; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/src/css_parser.dart'; @@ -23,8 +25,9 @@ typedef OnCssParseError = String? Function( String css, List errors, ); +typedef OnContentRendered = Function(Size size); -class HtmlParser extends StatelessWidget { +class HtmlParser extends StatefulWidget { final Key? key; final dom.Element htmlData; final OnTap? onLinkTap; @@ -42,6 +45,9 @@ class HtmlParser extends StatelessWidget { final Html? root; final TextSelectionControls? selectionControls; final ScrollPhysics? scrollPhysics; + final Widget? loadingPlaceholder; + final OnContentRendered? onContentRendered; + final double? textScaleFactor; final Map cachedImageSizes = {}; @@ -61,6 +67,9 @@ class HtmlParser extends StatelessWidget { this.root, this.selectionControls, this.scrollPhysics, + this.loadingPlaceholder, + this.onContentRendered, + this.textScaleFactor, }) : this.internalOnAnchorTap = onAnchorTap != null ? onAnchorTap : key != null @@ -69,64 +78,7 @@ class HtmlParser extends StatelessWidget { super(key: key); @override - Widget build(BuildContext context) { - Map>> declarations = _getExternalCssDeclarations(htmlData.getElementsByTagName("style"), onCssParseError); - StyledElement lexedTree = lexDomTree( - htmlData, - customRenders.keys.toList(), - tagsList, - context, - this, - ); - StyledElement? externalCssStyledTree; - if (declarations.isNotEmpty) { - externalCssStyledTree = _applyExternalCss(declarations, lexedTree); - } - StyledElement inlineStyledTree = _applyInlineStyles(externalCssStyledTree ?? lexedTree, onCssParseError); - StyledElement customStyledTree = _applyCustomStyles(style, inlineStyledTree); - StyledElement cascadedStyledTree = _cascadeStyles(style, customStyledTree); - StyledElement cleanedTree = cleanTree(cascadedStyledTree); - InlineSpan parsedTree = parseTree( - RenderContext( - buildContext: context, - parser: this, - tree: cleanedTree, - style: cleanedTree.style, - ), - cleanedTree, - ); - - // This is the final scaling that assumes any other StyledText instances are - // using textScaleFactor = 1.0 (which is the default). This ensures the correct - // scaling is used, but relies on https://github.com/flutter/flutter/pull/59711 - // to wrap everything when larger accessibility fonts are used. - if (selectable) { - return StyledText.selectable( - textSpan: parsedTree as TextSpan, - style: cleanedTree.style, - textScaleFactor: MediaQuery.of(context).textScaleFactor, - renderContext: RenderContext( - buildContext: context, - parser: this, - tree: cleanedTree, - style: cleanedTree.style, - ), - selectionControls: selectionControls, - scrollPhysics: scrollPhysics, - ); - } - return StyledText( - textSpan: parsedTree, - style: cleanedTree.style, - textScaleFactor: MediaQuery.of(context).textScaleFactor, - renderContext: RenderContext( - buildContext: context, - parser: this, - tree: cleanedTree, - style: cleanedTree.style, - ), - ); - } + State createState() => _HtmlParserState(); /// [parseHTML] converts a string of HTML to a DOM element using the dart `html` library. static dom.Element parseHTML(String data) { @@ -139,13 +91,13 @@ class HtmlParser extends StatelessWidget { } /// [lexDomTree] converts a DOM document to a simplified tree of [StyledElement]s. - static StyledElement lexDomTree( - dom.Element html, - List customRenderMatchers, - List tagsList, - BuildContext context, - HtmlParser parser, - ) { + static StyledElement lexDomTree(List args) { + dom.Element html = args[0]; + List customRenderMatchers = args[1]; + List tagsList = args[2]; + BuildContext context = args[3]; + HtmlParser parser = args[4]; + StyledElement tree = StyledElement( name: "[Tree Root]", children: [], @@ -229,7 +181,10 @@ class HtmlParser extends StatelessWidget { } } - static Map>> _getExternalCssDeclarations(List styles, OnCssParseError? errorHandler) { + static Map>> _getExternalCssDeclarations(List args) { + List styles = args[0]; + OnCssParseError? errorHandler = args[1]; + String fullCss = ""; for (final e in styles) { fullCss = fullCss + e.innerHtml; @@ -242,7 +197,10 @@ class HtmlParser extends StatelessWidget { } } - static StyledElement _applyExternalCss(Map>> declarations, StyledElement tree) { + static StyledElement _applyExternalCss(List args) { + Map>> declarations = args[0]; + StyledElement tree = args[1]; + declarations.forEach((key, style) { try { if (tree.matchesSelector(key)) { @@ -251,12 +209,15 @@ class HtmlParser extends StatelessWidget { } catch (_) {} }); - tree.children.forEach((e) => _applyExternalCss(declarations, e)); + tree.children.forEach((e) => _applyExternalCss([declarations, e])); return tree; } - static StyledElement _applyInlineStyles(StyledElement tree, OnCssParseError? errorHandler) { + static StyledElement _applyInlineStyles(List args) { + StyledElement tree = args[0]; + OnCssParseError? errorHandler = args[1]; + if (tree.attributes.containsKey("style")) { final newStyle = inlineCssToStyle(tree.attributes['style'], errorHandler); if (newStyle != null) { @@ -264,13 +225,16 @@ class HtmlParser extends StatelessWidget { } } - tree.children.forEach((e) => _applyInlineStyles(e, errorHandler)); + tree.children.forEach((e) => _applyInlineStyles([e, errorHandler])); return tree; } /// [applyCustomStyles] applies the [Style] objects passed into the [Html] /// widget onto the [StyledElement] tree, no cascading of styles is done at this point. - static StyledElement _applyCustomStyles(Map style, StyledElement tree) { + static StyledElement _applyCustomStyles(List args) { + Map style = args[0]; + StyledElement tree = args[1]; + style.forEach((key, style) { try { if (tree.matchesSelector(key)) { @@ -278,17 +242,20 @@ class HtmlParser extends StatelessWidget { } } catch (_) {} }); - tree.children.forEach((e) => _applyCustomStyles(style, e)); + tree.children.forEach((e) => _applyCustomStyles([style, e])); return tree; } /// [_cascadeStyles] cascades all of the inherited styles down the tree, applying them to each /// child that doesn't specify a different style. - static StyledElement _cascadeStyles(Map style, StyledElement tree) { + static StyledElement _cascadeStyles(List args) { + Map style = args[0]; + StyledElement tree = args[1]; + tree.children.forEach((child) { child.style = tree.style.copyOnlyInherited(child.style); - _cascadeStyles(style, child); + _cascadeStyles([style, child]); }); return tree; @@ -297,7 +264,9 @@ class HtmlParser extends StatelessWidget { /// [cleanTree] optimizes the [StyledElement] tree so all [BlockElement]s are /// on the first level, redundant levels are collapsed, empty elements are /// removed, and specialty elements are processed. - static StyledElement cleanTree(StyledElement tree) { + static StyledElement cleanTree(List args) { + StyledElement tree = args[0]; + tree = _processInternalWhitespace(tree); tree = _processInlineWhitespace(tree); tree = _removeEmptyElements(tree); @@ -308,47 +277,6 @@ class HtmlParser extends StatelessWidget { return tree; } - /// [parseTree] converts a tree of [StyledElement]s to an [InlineSpan] tree. - /// - /// [parseTree] is responsible for handling the [customRenders] parameter and - /// deciding what different `Style.display` options look like as Widgets. - InlineSpan parseTree(RenderContext context, StyledElement tree) { - // Merge this element's style into the context so that children - // inherit the correct style - RenderContext newContext = RenderContext( - buildContext: context.buildContext, - parser: this, - tree: tree, - style: context.style.copyOnlyInherited(tree.style), - key: AnchorKey.of(key, tree), - ); - - for (final entry in customRenders.keys) { - if (entry.call(newContext)) { - final buildChildren = () => tree.children.map((tree) => parseTree(newContext, tree)).toList(); - if (newContext.parser.selectable && customRenders[entry] is SelectableCustomRender) { - final selectableBuildChildren = () => tree.children.map((tree) => parseTree(newContext, tree) as TextSpan).toList(); - return (customRenders[entry] as SelectableCustomRender).textSpan.call(newContext, selectableBuildChildren); - } - if (newContext.parser.selectable) { - return customRenders[entry]!.inlineSpan!.call(newContext, buildChildren) as TextSpan; - } - if (customRenders[entry]?.inlineSpan != null) { - return customRenders[entry]!.inlineSpan!.call(newContext, buildChildren); - } - return WidgetSpan( - child: ContainerSpan( - newContext: newContext, - style: tree.style, - shrinkWrap: newContext.parser.shrinkWrap, - child: customRenders[entry]!.widget!.call(newContext, buildChildren), - ), - ); - } - } - return WidgetSpan(child: Container(height: 0, width: 0)); - } - static OnTap _handleAnchorTap(Key key, OnTap? onLinkTap) => (String? url, RenderContext context, Map attributes, dom.Element? element) { if (url?.startsWith("#") == true) { @@ -801,6 +729,368 @@ class HtmlParser extends StatelessWidget { }); return tree; } + + /// [parseTree] converts a tree of [StyledElement]s to an [InlineSpan] tree. + /// + /// [parseTree] is responsible for handling the [customRenders] parameter and + /// deciding what different `Style.display` options look like as Widgets. + InlineSpan parseTree(RenderContext context, StyledElement tree) { + // Merge this element's style into the context so that children + // inherit the correct style + RenderContext newContext = RenderContext( + buildContext: context.buildContext, + parser: this, + tree: tree, + style: context.style.copyOnlyInherited(tree.style), + key: AnchorKey.of(key, tree), + ); + + for (final entry in customRenders.keys) { + if (entry.call(newContext)) { + final buildChildren = () => tree.children.map((tree) => parseTree(newContext, tree)).toList(); + if (newContext.parser.selectable && customRenders[entry] is SelectableCustomRender) { + final selectableBuildChildren = () => tree.children.map((tree) => parseTree(newContext, tree) as TextSpan).toList(); + return (customRenders[entry] as SelectableCustomRender).textSpan.call(newContext, selectableBuildChildren); + } + if (newContext.parser.selectable) { + return customRenders[entry]!.inlineSpan!.call(newContext, buildChildren) as TextSpan; + } + if (customRenders[entry]?.inlineSpan != null) { + return customRenders[entry]!.inlineSpan!.call(newContext, buildChildren); + } + return WidgetSpan( + child: ContainerSpan( + newContext: newContext, + style: tree.style, + shrinkWrap: newContext.parser.shrinkWrap, + child: customRenders[entry]!.widget!.call(newContext, buildChildren), + ), + ); + } + } + return WidgetSpan(child: Container(height: 0, width: 0)); + } +} + +class _HtmlParserState extends State { + static final _renderQueue = []; + + final GlobalKey _htmlGlobalKey = GlobalKey(); + final GlobalKey _animatedSwitcherKey = GlobalKey(); + + Completer? _completer; + bool _isOffstage = true; + ParseResult? _parseResult; + + ThemeData? _themeData; + InlineSpan? _parsedTree; + StyledElement? _cleanedTree; + bool _disposed = false; + + @override + void dispose() { + _disposed = true; + + // If we're still waiting to run, just cancel and remove us from the render queue + if (_completer?.isCompleted == false) { + _completer!.completeError(Exception('disposed')); + _renderQueue.remove(_completer); + } + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final themeData = Theme.of(context); +// if (themeData != _themeData) { +// _themeData = themeData; +//// _parseResult = null; +// _parseTree(context); +// } + + if (_parseResult == null) { + try { + _parseTree(context); + } catch (error) { + print(error); + } + } + + if (_parseResult != null && _isOffstage) { + WidgetsBinding.instance?.addPostFrameCallback(_afterLayout); + } + + final children = []; + + children.add( + Visibility( + visible: _isOffstage || _parseResult == null, + maintainSize: true, + maintainAnimation: true, + maintainState: true, + child: widget.loadingPlaceholder ?? Container(height: 1000), + ), + ); + + if (!_isOffstage && _parseResult != null) { + children.add( + _buildStyledText( + parsedTree: _parsedTree, + cleanedTree: _cleanedTree, + ), + ); + } + + return Stack( + children: [ + if (_isOffstage && _parseResult != null) + Offstage( + offstage: true, + child: _buildStyledText( + parsedTree: _parsedTree, + cleanedTree: _cleanedTree, + ), + ), + AnimatedSwitcher( + key: _animatedSwitcherKey, + layoutBuilder: (currentChild, previousChildren) { + return Stack( + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + alignment: Alignment.topLeft, + ); + }, + duration: kThemeAnimationDuration, + child: Stack( + alignment: Alignment.topLeft, + fit: StackFit.loose, + key: ValueKey(_isOffstage), + children: children, + ), + ), + ], + ); + } + + StyledText _buildStyledText({ + parsedTree, + cleanedTree, + }) { + // This is the final scaling that assumes any other StyledText instances are + // using textScaleFactor = 1.0 (which is the default). This ensures the correct + // scaling is used, but relies on https://github.com/flutter/flutter/pull/59711 + // to wrap everything when larger accessibility fonts are used. + if (widget.selectable) { + return StyledText.selectable( + key: _htmlGlobalKey, + textSpan: parsedTree as TextSpan, + style: cleanedTree.style, + textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context).textScaleFactor, + renderContext: RenderContext( + buildContext: context, + parser: widget, + tree: cleanedTree, + style: cleanedTree.style, + ), + selectionControls: widget.selectionControls, + scrollPhysics: widget.scrollPhysics, + ); + } + return StyledText( + key: _htmlGlobalKey, + textSpan: parsedTree, + style: cleanedTree.style, + textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context).textScaleFactor, + renderContext: RenderContext( + buildContext: context, + parser: widget, + tree: cleanedTree, + style: cleanedTree.style, + ), + ); + } + + void _afterLayout(Duration timeStamp) { + final RenderBox renderBox = _htmlGlobalKey.currentContext?.findRenderObject() as RenderBox; + final size = renderBox.size; + + // print("html size: $size"); + + widget.onContentRendered?.call(size); + + setState(() { + _isOffstage = false; + }); + } + + Future _parseTree(BuildContext context) async { + if (_completer != null) { + if (_renderQueue.isNotEmpty) { + _renderQueue.remove(_completer!); + } + + if (!_completer!.isCompleted) { + _completer!.completeError(Exception('replaced')); + } + } + + _completer = Completer(); + _renderQueue.add(_completer!); + + try { + if (_renderQueue.length > 1) { + print('_parseTree waiting for queue'); + await _completer!.future; + } else { + _completer!.complete(); + } + } catch (exception) { + print('_parseTree ${exception.toString()}'); + return null; + } + + print('_parseTree parsing'); + + InlineSpan? parsedTree; + + try { + if (_cleanedTree == null) { + Map>> declarations = await compute(HtmlParser._getExternalCssDeclarations, [widget.htmlData.getElementsByTagName("style"), widget.onCssParseError]); + StyledElement lexedTree = HtmlParser.lexDomTree([ + widget.htmlData, + widget.customRenders.keys.toList(), + widget.tagsList, + context, + widget, + ]); + StyledElement? externalCssStyledTree; + if (declarations.isNotEmpty) { + externalCssStyledTree = await compute(HtmlParser._applyExternalCss, [declarations, lexedTree]); + } + StyledElement inlineStyledTree = await compute(HtmlParser._applyInlineStyles, [externalCssStyledTree ?? lexedTree, widget.onCssParseError]); + StyledElement customStyledTree = await compute(HtmlParser._applyCustomStyles, [widget.style, inlineStyledTree]); + StyledElement cascadedStyledTree = await compute(HtmlParser._cascadeStyles, [widget.style, customStyledTree]); + _cleanedTree = await compute(HtmlParser.cleanTree, [cascadedStyledTree]); + _parsedTree = widget.parseTree( + RenderContext( + buildContext: context, + parser: widget, + tree: _cleanedTree!, + style: _cleanedTree!.style, + ), + _cleanedTree!, + ); + } + + // parsedTree = await parseTree( + // RenderContext( + // buildContext: context, + // parser: widget, + // style: Style.fromTextStyle(Theme.of(context).textTheme.bodyText2), + // ), + // _cleanedTree, + // ); + + print('_parseTree parsed'); + } catch (exception) { + print(exception); + return null; + } finally { + _renderQueue.remove(_completer); + _completer = null; + + if (_renderQueue.isNotEmpty && !_renderQueue[0].isCompleted) { + _renderQueue[0].complete(); + } + } + + if (!_disposed) { + setState(() { + _parseResult = ParseResult(parsedTree, _cleanedTree?.style); + }); + } + } + + // /// [applyCustomStyles] applies the [Style] objects passed into the [Html] + // /// widget onto the [StyledElement] tree, no cascading of styles is done at this point. + // StyledElement _applyCustomStyles(StyledElement tree) { + // if (widget.style == null) return tree; + // widget.style.forEach((key, style) { + // if (tree.matchesSelector(key)) { + // if (tree.style == null) { + // tree.style = style; + // } else { + // tree.style = tree.style.merge(style); + // } + // } + // }); + // tree.children?.forEach(_applyCustomStyles); + // + // return tree; + // } + + // @override + // Widget build(BuildContext context) { + // Map>> declarations = HtmlParser._getExternalCssDeclarations(widget.htmlData.getElementsByTagName("style"), widget.onCssParseError); + // StyledElement lexedTree = HtmlParser.lexDomTree( + // widget.htmlData, + // widget.customRenders.keys.toList(), + // widget.tagsList, + // context, + // widget, + // ); + // StyledElement? externalCssStyledTree; + // if (declarations.isNotEmpty) { + // externalCssStyledTree = HtmlParser._applyExternalCss(declarations, lexedTree); + // } + // StyledElement inlineStyledTree = HtmlParser._applyInlineStyles(externalCssStyledTree ?? lexedTree, widget.onCssParseError); + // StyledElement customStyledTree = HtmlParser._applyCustomStyles(widget.style, inlineStyledTree); + // StyledElement cascadedStyledTree = HtmlParser._cascadeStyles(widget.style, customStyledTree); + // StyledElement cleanedTree = HtmlParser.cleanTree(cascadedStyledTree); + // InlineSpan parsedTree = parseTree( + // RenderContext( + // buildContext: context, + // parser: widget, + // tree: cleanedTree, + // style: cleanedTree.style, + // ), + // cleanedTree, + // ); + // + // // This is the final scaling that assumes any other StyledText instances are + // // using textScaleFactor = 1.0 (which is the default). This ensures the correct + // // scaling is used, but relies on https://github.com/flutter/flutter/pull/59711 + // // to wrap everything when larger accessibility fonts are used. + // if (widget.selectable) { + // return StyledText.selectable( + // textSpan: parsedTree as TextSpan, + // style: cleanedTree.style, + // textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context).textScaleFactor, + // renderContext: RenderContext( + // buildContext: context, + // parser: widget, + // tree: cleanedTree, + // style: cleanedTree.style, + // ), + // selectionControls: widget.selectionControls, + // scrollPhysics: widget.scrollPhysics, + // ); + // } + // return StyledText( + // textSpan: parsedTree, + // style: cleanedTree.style, + // textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context).textScaleFactor, + // renderContext: RenderContext( + // buildContext: context, + // parser: widget, + // tree: cleanedTree, + // style: cleanedTree.style, + // ), + // ); + // } } /// The [RenderContext] is available when parsing the tree. It contains information @@ -845,7 +1135,7 @@ class ContainerSpan extends StatelessWidget { }): super(key: key); @override - Widget build(BuildContext _) { + Widget build(BuildContext context) { return Container( decoration: BoxDecoration( border: style.border, @@ -859,7 +1149,7 @@ class ContainerSpan extends StatelessWidget { child: child ?? StyledText( textSpan: TextSpan( - style: newContext.style.generateTextStyle(), + style: newContext.style.generateTextStyle(context), children: children, ), style: newContext.style, @@ -874,7 +1164,7 @@ class StyledText extends StatelessWidget { final Style style; final double textScaleFactor; final RenderContext renderContext; - final AnchorKey? key; + final GlobalKey? key; final bool _selectable; final TextSelectionControls? selectionControls; final ScrollPhysics? scrollPhysics; @@ -907,7 +1197,7 @@ class StyledText extends StatelessWidget { if (_selectable) { return SelectableText.rich( textSpan as TextSpan, - style: style.generateTextStyle(), + style: style.generateTextStyle(context), textAlign: style.textAlign, textDirection: style.direction, textScaleFactor: textScaleFactor, @@ -920,7 +1210,7 @@ class StyledText extends StatelessWidget { width: consumeExpandedBlock(style.display, renderContext), child: Text.rich( textSpan, - style: style.generateTextStyle(), + style: style.generateTextStyle(context), textAlign: style.textAlign, textDirection: style.direction, textScaleFactor: textScaleFactor, @@ -958,3 +1248,10 @@ extension IterateLetters on String { } } } + +class ParseResult { + final InlineSpan? inlineSpan; + final Style? style; + + ParseResult(this.inlineSpan, this.style); +} diff --git a/lib/src/layout_element.dart b/lib/src/layout_element.dart index 33093e7493..d677d6d46e 100644 --- a/lib/src/layout_element.dart +++ b/lib/src/layout_element.dart @@ -148,7 +148,7 @@ class DetailsContentElement extends LayoutElement { expandedAlignment: Alignment.centerLeft, title: elementList.isNotEmpty == true && elementList.first.localName == "summary" ? StyledText( textSpan: TextSpan( - style: style.generateTextStyle(), + style: style.generateTextStyle(context.buildContext), children: firstChild == null ? [] : [firstChild], ), style: style, @@ -157,7 +157,7 @@ class DetailsContentElement extends LayoutElement { children: [ StyledText( textSpan: TextSpan( - style: style.generateTextStyle(), + style: style.generateTextStyle(context.buildContext), children: getChildren(childrenList, context, elementList.isNotEmpty == true && elementList.first.localName == "summary" ? firstChild : null) ), style: style, diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index 81cc5d58ee..5021f7e688 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -111,14 +111,14 @@ class RubyElement extends ReplacedElement { style: c.style, child: Text(c.element!.innerHtml, style: c.style - .generateTextStyle() + .generateTextStyle(context.buildContext) .copyWith(fontSize: rubySize)), )))), ContainerSpan( newContext: context, style: context.style, child: node is TextContentElement ? Text((node as TextContentElement).text?.trim() ?? "", - style: context.style.generateTextStyle()) : null, + style: context.style.generateTextStyle(context.buildContext)) : null, children: node is TextContentElement ? null : [context.parser.parseTree(context, node!)]), ], ); diff --git a/lib/style.dart b/lib/style.dart index 121319e918..2c54a4075d 100644 --- a/lib/style.dart +++ b/lib/style.dart @@ -254,10 +254,10 @@ class Style { return styleMap; } - TextStyle generateTextStyle() { + TextStyle generateTextStyle(BuildContext context) { return TextStyle( backgroundColor: backgroundColor, - color: color, + color: Theme.of(context).brightness == Brightness.light ? (color == Colors.white ? Colors.black : color) : Colors.white, // TODO make this smarter, decoration: textDecoration, decorationColor: textDecorationColor, decorationStyle: textDecorationStyle, diff --git a/packages/flutter_html_math/lib/flutter_html_math.dart b/packages/flutter_html_math/lib/flutter_html_math.dart index cd1ca088cf..e9c1dda5f5 100644 --- a/packages/flutter_html_math/lib/flutter_html_math.dart +++ b/packages/flutter_html_math/lib/flutter_html_math.dart @@ -12,7 +12,7 @@ CustomRender mathRender({OnMathError? onMathError}) => CustomRender.widget(widge child: Math.tex( texStr, mathStyle: MathStyle.display, - textStyle: context.style.generateTextStyle(), + textStyle: context.style.generateTextStyle(context.buildContext), onErrorFallback: (FlutterMathException e) { if (onMathError != null) { return onMathError.call(texStr, e.message, e.messageWithType); diff --git a/test/html_parser_test.dart b/test/html_parser_test.dart index c351ab2a36..cc93277dae 100644 --- a/test/html_parser_test.dart +++ b/test/html_parser_test.dart @@ -3,8 +3,7 @@ import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - testWidgets("Check that default parser does not fail on empty data", - (tester) async { + testWidgets("Check that default parser does not fail on empty data", (tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( @@ -32,16 +31,14 @@ void main() { void testNewParser(BuildContext context) { HtmlParser.parseHTML("Hello, World!"); - StyledElement tree = HtmlParser.lexDomTree( - HtmlParser.parseHTML( - "Hello! Hello, World!Hello, New World!"), + StyledElement tree = HtmlParser.lexDomTree([ + HtmlParser.parseHTML("Hello! Hello, World!Hello, New World!"), [], Html.tags, context, HtmlParser( key: null, - htmlData: HtmlParser.parseHTML( - "Hello! Hello, World!Hello, New World!"), + htmlData: HtmlParser.parseHTML("Hello! Hello, World!Hello, New World!"), onLinkTap: null, onAnchorTap: null, onImageTap: null, @@ -55,19 +52,17 @@ void testNewParser(BuildContext context) { selectionControls: null, scrollPhysics: null, ) - ); + ]); print(tree.toString()); - tree = HtmlParser.lexDomTree( - HtmlParser.parseHTML( - "Hello, World! This is a link"), + tree = HtmlParser.lexDomTree([ + HtmlParser.parseHTML("Hello, World! This is a link"), [], Html.tags, context, HtmlParser( key: null, - htmlData: HtmlParser.parseHTML( - "Hello, World! This is a link"), + htmlData: HtmlParser.parseHTML("Hello, World! This is a link"), onLinkTap: null, onAnchorTap: null, onImageTap: null, @@ -81,10 +76,10 @@ void testNewParser(BuildContext context) { selectionControls: null, scrollPhysics: null, ) - ); + ]); print(tree.toString()); - tree = HtmlParser.lexDomTree( + tree = HtmlParser.lexDomTree([ HtmlParser.parseHTML(""), [], Html.tags, @@ -105,19 +100,17 @@ void testNewParser(BuildContext context) { selectionControls: null, scrollPhysics: null, ) - ); + ]); print(tree.toString()); - tree = HtmlParser.lexDomTree( - HtmlParser.parseHTML( - "

Link
Hello, World! Bold and Italic
"), + tree = HtmlParser.lexDomTree([ + HtmlParser.parseHTML("
Link
Hello, World! Bold and Italic
"), [], Html.tags, context, HtmlParser( key: null, - htmlData: HtmlParser.parseHTML( - "
Link
Hello, World! Bold and Italic
"), + htmlData: HtmlParser.parseHTML("
Link
Hello, World! Bold and Italic
"), onLinkTap: null, onAnchorTap: null, onImageTap: null, @@ -131,7 +124,7 @@ void testNewParser(BuildContext context) { selectionControls: null, scrollPhysics: null, ) - ); + ]); print(tree.toString()); /*ReplacedElement videoContentElement = parseReplacedElement( From b4ff166a64f9fe6a012698be60f0776977a7646d Mon Sep 17 00:00:00 2001 From: Jonathan Friesen Date: Tue, 12 Apr 2022 16:23:53 -0700 Subject: [PATCH 04/75] Add feature/async-parsing updates to latest from master --- lib/custom_render.dart | 20 -------------------- lib/html_parser.dart | 10 ---------- lib/src/css_parser.dart | 40 ++++++++++++++++++++++++++++++++++++++-- pubspec.yaml | 5 +++++ 4 files changed, 43 insertions(+), 32 deletions(-) diff --git a/lib/custom_render.dart b/lib/custom_render.dart index 82b63e7908..44b23aaaa1 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -438,26 +438,6 @@ CustomRender fallbackRender({Style? style, List? children}) => .toList(), )); -// CustomRender fallbackRender({Style? style, List? children}) => CustomRender.inlineSpan( -// inlineSpan: (renderContext, buildChildren) => WidgetSpan( -// child: Builder( -// builder: (context) => StyledText( -// renderContext: renderContext, -// style: renderContext.style, -// textSpan: TextSpan( -// style: style?.generateTextStyle(context) ?? renderContext.style.generateTextStyle(context), -// children: renderContext.tree.children -// .expand((tree) => [ -// renderContext.parser.parseTree(renderContext, 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(), listElementMatcher(): listElementRender(), diff --git a/lib/html_parser.dart b/lib/html_parser.dart index e86bf106c9..df7607c4b4 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -782,7 +782,6 @@ class _HtmlParserState extends State { bool _isOffstage = true; ParseResult? _parseResult; - ThemeData? _themeData; InlineSpan? _parsedTree; StyledElement? _cleanedTree; bool _disposed = false; @@ -985,15 +984,6 @@ class _HtmlParserState extends State { ); } - // parsedTree = await parseTree( - // RenderContext( - // buildContext: context, - // parser: widget, - // style: Style.fromTextStyle(Theme.of(context).textTheme.bodyText2), - // ), - // _cleanedTree, - // ); - print('_parseTree parsed'); } catch (exception) { print(exception); diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index 1f5bbd90ed..cee63bc666 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -192,6 +192,22 @@ Style declarationsToStyle(Map> declarations) { case 'font-weight': style.fontWeight = ExpressionMapping.expressionToFontWeight(value.first); break; + case '-epub-text-align-last': + if (value.first is css.Identifier) { + css.Identifier identifier = value.first as css.Identifier; + switch (identifier.name) { + case 'left': + style.alignment = Alignment.centerLeft; + break; + case 'right': + style.alignment = Alignment.centerRight; + break; + case 'center': + style.alignment = Alignment.center; + break; + } + } + break; case 'list-style': css.LiteralTerm? position = value.firstWhereOrNull((e) => e is css.LiteralTerm && (e.text == "outside" || e.text == "inside")) as css.LiteralTerm?; css.UriTerm? image = value.firstWhereOrNull((e) => e is css.UriTerm) as css.UriTerm?; @@ -348,6 +364,9 @@ Style declarationsToStyle(Map> declarations) { style.textTransform = TextTransform.none; } break; + case 'vertical-align': + style.verticalAlign = ExpressionMapping.expressionToVerticalAlignTerm((value.first as css.LiteralTerm)); + break; case 'width': style.width = ExpressionMapping.expressionToPaddingLength(value.first) ?? style.width; break; @@ -433,6 +452,7 @@ class DeclarationVisitor extends css.Visitor { //Mapping functions class ExpressionMapping { + static final _leadingOrTrailingQuoteRegexp = RegExp(r'^"|"$'); static Border expressionToBorder(List? borderWidths, List? borderStyles, List? borderColors) { CustomBorderSide left = CustomBorderSide(); @@ -698,8 +718,11 @@ class ExpressionMapping { } static String? expressionToFontFamily(css.Expression value) { - if (value is css.LiteralTerm) return value.text; - return null; + if (value is css.LiteralTerm) { + return value.text.replaceAll(_leadingOrTrailingQuoteRegexp, ''); + } else { + return value.span?.text.replaceAll(_leadingOrTrailingQuoteRegexp, ''); + } } static LineHeight expressionToLineHeight(css.Expression value) { @@ -978,4 +1001,17 @@ class ExpressionMapping { return stringToColor(namedColors[namedColor]!); } else return null; } + + static VerticalAlign expressionToVerticalAlignTerm(css.LiteralTerm value) { + switch (value.text) { + case 'sub': + return VerticalAlign.SUB; + case 'super': + return VerticalAlign.SUPER; + case 'baseline': + return VerticalAlign.BASELINE; + } + + return VerticalAlign.BASELINE; + } } diff --git a/pubspec.yaml b/pubspec.yaml index f10c8fce5a..a5e5d8bc30 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,11 @@ dependencies: flutter: sdk: flutter +dependency_overrides: + csslib: + git: + url: https://github.com/jfri/csslib.git + dev_dependencies: flutter_test: sdk: flutter From 283c3afa8222687e0cdc3c1657be36c68e8dd944 Mon Sep 17 00:00:00 2001 From: Eric Kok Date: Thu, 14 Apr 2022 13:51:00 +0200 Subject: [PATCH 05/75] Don't crash when video ir iframe uses unsupported height/width; fixes #1033 --- .../lib/iframe_mobile.dart | 8 ++--- .../flutter_html_iframe/lib/iframe_web.dart | 8 ++--- .../lib/flutter_html_video.dart | 30 +++++++++---------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/flutter_html_iframe/lib/iframe_mobile.dart b/packages/flutter_html_iframe/lib/iframe_mobile.dart index 2bd5b4608c..a75a98362c 100644 --- a/packages/flutter_html_iframe/lib/iframe_mobile.dart +++ b/packages/flutter_html_iframe/lib/iframe_mobile.dart @@ -7,11 +7,11 @@ import 'package:webview_flutter/webview_flutter.dart'; CustomRender iframeRender({NavigationDelegate? navigationDelegate}) => CustomRender.widget(widget: (context, buildChildren) { final sandboxMode = context.tree.element?.attributes["sandbox"]; final UniqueKey key = UniqueKey(); + final givenWidth = double.tryParse(context.tree.element?.attributes['width'] ?? ""); + final givenHeight = double.tryParse(context.tree.element?.attributes['height'] ?? ""); return Container( - width: double.tryParse(context.tree.element?.attributes['width'] ?? "") - ?? (double.tryParse(context.tree.element?.attributes['height'] ?? "") ?? 150) * 2, - height: double.tryParse(context.tree.element?.attributes['height'] ?? "") - ?? (double.tryParse(context.tree.element?.attributes['width'] ?? "") ?? 300) / 2, + width: givenWidth ?? (givenHeight ?? 150) * 2, + height: givenHeight ?? (givenWidth ?? 300) / 2, child: ContainerSpan( style: context.style, newContext: context, diff --git a/packages/flutter_html_iframe/lib/iframe_web.dart b/packages/flutter_html_iframe/lib/iframe_web.dart index 1e9c50598f..4b09ec7a6c 100644 --- a/packages/flutter_html_iframe/lib/iframe_web.dart +++ b/packages/flutter_html_iframe/lib/iframe_web.dart @@ -10,11 +10,11 @@ import 'dart:html' as html; import 'package:webview_flutter/webview_flutter.dart'; CustomRender iframeRender({NavigationDelegate? navigationDelegate}) => CustomRender.widget(widget: (context, buildChildren) { + final givenWidth = double.tryParse(context.tree.element?.attributes['width'] ?? ""); + final givenHeight = double.tryParse(context.tree.element?.attributes['height'] ?? ""); final html.IFrameElement iframe = html.IFrameElement() - ..width = (double.tryParse(context.tree.element?.attributes['width'] ?? "") - ?? (double.tryParse(context.tree.element?.attributes['height'] ?? "") ?? 150) * 2).toString() - ..height = (double.tryParse(context.tree.element?.attributes['height'] ?? "") - ?? (double.tryParse(context.tree.element?.attributes['width'] ?? "") ?? 300) / 2).toString() + ..width = (givenWidth ?? (givenHeight ?? 150) * 2).toString() + ..height = (givenHeight ?? (givenWidth ?? 300) / 2).toString() ..src = context.tree.element?.attributes['src'] ..style.border = 'none'; final String createdViewId = getRandString(10); diff --git a/packages/flutter_html_video/lib/flutter_html_video.dart b/packages/flutter_html_video/lib/flutter_html_video.dart index 90591d5c55..78ccfe8936 100644 --- a/packages/flutter_html_video/lib/flutter_html_video.dart +++ b/packages/flutter_html_video/lib/flutter_html_video.dart @@ -30,11 +30,10 @@ class VideoWidget extends StatefulWidget { } class _VideoWidgetState extends State { - ChewieController? chewieController; - VideoPlayerController? videoController; + ChewieController? _chewieController; + VideoPlayerController? _videoController; double? _width; double? _height; - late final List sources; @override void initState() { @@ -44,12 +43,14 @@ class _VideoWidgetState extends State { attributes['src'], ...ReplacedElement.parseMediaSources(widget.context.tree.element!.children), ]; + final givenWidth = double.tryParse(attributes['width'] ?? ""); + final givenHeight = double.tryParse(attributes['height'] ?? ""); if (sources.isNotEmpty && sources.first != null) { - _width = double.tryParse(attributes['width'] ?? (attributes['height'] ?? 150) * 2); - _height = double.tryParse(attributes['height'] ?? (attributes['width'] ?? 300) / 2); - videoController = VideoPlayerController.network(sources.first!); - chewieController = ChewieController( - videoPlayerController: videoController!, + _width = givenWidth ?? (givenHeight ?? 150) * 2; + _height = givenHeight ?? (givenWidth ?? 300) / 2; + _videoController = VideoPlayerController.network(sources.first!); + _chewieController = ChewieController( + videoPlayerController: _videoController!, placeholder: attributes['poster'] != null && attributes['poster']!.isNotEmpty ? Image.network(attributes['poster']!) : Container(color: Colors.black), @@ -59,32 +60,29 @@ class _VideoWidgetState extends State { autoInitialize: true, aspectRatio: _width == null || _height == null ? null : _width! / _height!, ); - widget.callback?.call(widget.context.tree.element, chewieController!, videoController!); + widget.callback?.call(widget.context.tree.element, _chewieController!, _videoController!); } super.initState(); } @override void dispose() { - chewieController?.dispose(); - videoController?.dispose(); + _chewieController?.dispose(); + _videoController?.dispose(); super.dispose(); } @override Widget build(BuildContext bContext) { - if (sources.isEmpty || sources.first == null) { + if (_chewieController == null) { return Container(height: 0, width: 0); } final child = Container( key: widget.context.key, child: Chewie( - controller: chewieController!, + controller: _chewieController!, ), ); - if (_width == null || _height == null) { - return child; - } return AspectRatio( aspectRatio: _width! / _height!, child: child, From ec3ac1cf8f04e9b2d8e43c6646740f9836d445df Mon Sep 17 00:00:00 2001 From: Eric Kok Date: Thu, 14 Apr 2022 14:07:17 +0200 Subject: [PATCH 06/75] 3.0.0-alpha.3 release --- CHANGELOG.md | 4 ++++ README.md | 2 +- packages/flutter_html_all/pubspec.yaml | 16 ++++++++-------- packages/flutter_html_audio/pubspec.yaml | 4 ++-- packages/flutter_html_iframe/CHANGELOG.md | 3 +++ packages/flutter_html_iframe/pubspec.yaml | 4 ++-- packages/flutter_html_math/pubspec.yaml | 4 ++-- packages/flutter_html_svg/pubspec.yaml | 4 ++-- packages/flutter_html_table/pubspec.yaml | 4 ++-- packages/flutter_html_video/CHANGELOG.md | 3 +++ packages/flutter_html_video/pubspec.yaml | 4 ++-- pubspec.yaml | 2 +- 12 files changed, 32 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5879aafab7..78ad789752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [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 + ## [3.0.0-alpha.2] - January 5, 2022: * **BREAKING** Full modularization using split packages; see our upgrade guide or use flutter_html_all diff --git a/README.md b/README.md index 326ac6ccb7..d7aacfe45e 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ 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.2 + flutter_html: ^3.0.0-alpha.3 ## Currently Supported HTML Tags: | | | | | | | | | | | | diff --git a/packages/flutter_html_all/pubspec.yaml b/packages/flutter_html_all/pubspec.yaml index 9bea10a832..7f5b9305de 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.2 +version: 3.0.0-alpha.3 homepage: https://github.com/Sub6Resources/flutter_html environment: @@ -11,13 +11,13 @@ dependencies: flutter: sdk: flutter html: '>=0.15.0 <1.0.0' - flutter_html: '>=3.0.0-alpha.2 <4.0.0' - flutter_html_audio: '>=3.0.0-alpha.2 <4.0.0' - flutter_html_iframe: '>=3.0.0-alpha.2 <4.0.0' - flutter_html_math: '>=3.0.0-alpha.2 <4.0.0' - flutter_html_svg: '>=3.0.0-alpha.2 <4.0.0' - flutter_html_table: '>=3.0.0-alpha.2 <4.0.0' - flutter_html_video: '>=3.0.0-alpha.2 <4.0.0' + flutter_html: '>=3.0.0-alpha.3 <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' + flutter_html_svg: '>=3.0.0-alpha.3 <4.0.0' + flutter_html_table: '>=3.0.0-alpha.3 <4.0.0' + flutter_html_video: '>=3.0.0-alpha.3 <4.0.0' # flutter_html_audio: # path: ../flutter_html_audio # flutter_html_iframe: diff --git a/packages/flutter_html_audio/pubspec.yaml b/packages/flutter_html_audio/pubspec.yaml index 16076e952b..adcd592e80 100644 --- a/packages/flutter_html_audio/pubspec.yaml +++ b/packages/flutter_html_audio/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_html_audio description: Audio widget for flutter_html. -version: 3.0.0-alpha.2 +version: 3.0.0-alpha.3 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.2 <4.0.0' + flutter_html: '>=3.0.0-alpha.3 <4.0.0' # flutter_html: # path: ../.. diff --git a/packages/flutter_html_iframe/CHANGELOG.md b/packages/flutter_html_iframe/CHANGELOG.md index 31b4275bb4..7c233e36a8 100644 --- a/packages/flutter_html_iframe/CHANGELOG.md +++ b/packages/flutter_html_iframe/CHANGELOG.md @@ -1,2 +1,5 @@ +## [3.0.0-alpha.3] - April 14, 2022: +* Fixed crash when iframe or video tags used unsupported/incorrect height or width + ## [3.0.0-alpha.2] - January 5, 2022: * Initial modularized flutter_html release; use flutter_html_iframe if you need support for the `