From c5f396dd29b9ede943d1e908b48d4b80ac2d2542 Mon Sep 17 00:00:00 2001 From: Zak Barbuto Date: Mon, 25 Oct 2021 17:43:30 +1030 Subject: [PATCH 01/14] 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 d87ef3a1cab44b2a9915393ce21374549df45261 Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Tue, 23 Aug 2022 09:38:31 -0600 Subject: [PATCH 02/14] WIP: Add support for different Units and enhance the Margins class --- example/pubspec.yaml | 2 +- lib/html_parser.dart | 16 +++- lib/src/css_parser.dart | 31 ++++++- lib/src/style/length.dart | 56 +++++++++++ lib/src/style/margin.dart | 60 ++++++++++++ lib/src/styled_element.dart | 1 + lib/style.dart | 93 +------------------ .../lib/flutter_html_table.dart | 8 +- pubspec.yaml | 2 +- test/style/dimension_test.dart | 44 +++++++++ 10 files changed, 215 insertions(+), 98 deletions(-) create mode 100644 lib/src/style/length.dart create mode 100644 lib/src/style/margin.dart create mode 100644 test/style/dimension_test.dart diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 79e23feab9..ffde7654f9 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none version: 1.0.0+1 environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.17.0 <3.0.0' dependencies: flutter_html: diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 1b401ae435..f34f9d9b13 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -8,6 +8,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/src/css_parser.dart'; import 'package:flutter_html/src/html_elements.dart'; +import 'package:flutter_html/src/style/length.dart'; +import 'package:flutter_html/src/style/margin.dart'; import 'package:flutter_html/src/utils.dart'; import 'package:html/dom.dart' as dom; import 'package:html/parser.dart' as htmlparser; @@ -850,8 +852,12 @@ class ContainerSpan extends StatelessWidget { // Elements that are inline should ignore margin: auto for alignment. var alignment = shrinkWrap ? null : style.alignment; + // TODO(Sub6Resources): This needs to follow the CSS spec for computing auto values! if(style.display == Display.BLOCK) { - alignment = style.margin?.alignment ?? alignment; + if(style.margin?.left?.unit == Unit.auto && style.margin?.right?.unit == Unit.auto) + alignment = Alignment.bottomCenter; + else if(style.margin?.left?.unit == Unit.auto) + alignment = Alignment.bottomRight; } Widget container = Container( @@ -862,7 +868,13 @@ class ContainerSpan extends StatelessWidget { height: style.height, width: style.width, padding: style.padding?.nonNegative, - margin: style.margin?.asInsets.nonNegative, + // TODO(Sub6Resources): These need to be computed!! + margin: EdgeInsets.only( + left: style.margin?.left?.value.abs() ?? 0, + right: style.margin?.right?.value.abs() ?? 0, + bottom: style.margin?.bottom?.value.abs() ?? 0, + top: style.margin?.bottom?.value.abs() ?? 0, + ), alignment: alignment, child: child ?? StyledText( diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index 5ac92caf27..4b9d498d6a 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -5,6 +5,8 @@ import 'package:csslib/visitor.dart' as css; import 'package:csslib/parser.dart' as cssparser; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_html/src/style/length.dart'; +import 'package:flutter_html/src/style/margin.dart'; import 'package:flutter_html/src/utils.dart'; Style declarationsToStyle(Map> declarations) { @@ -751,9 +753,9 @@ class ExpressionMapping { static Margin? expressionToMargin(css.Expression value) { if ((value is css.LiteralTerm) && value.text == 'auto') { - return AutoMargin(); + return Margin.auto(); } else { - return InsetMargin(expressionToPaddingLength(value) ?? 0); + return Margin(expressionToPaddingLength(value) ?? 0); } } @@ -832,6 +834,31 @@ class ExpressionMapping { return null; } + static LengthOrPercent expressionToLengthOrPercent(css.Expression value) { + if (value is css.NumberTerm) { + return LengthOrPercent(double.parse(value.text)); + } else if (value is css.EmTerm) { + return LengthOrPercent(double.parse(value.text), Unit.em); + // } else if (value is css.RemTerm) { + // return LengthOrPercent(double.parse(value.text), Unit.rem); + // TODO there are several other available terms processed by the CSS parser + } else if (value is css.LengthTerm) { + double number = double.parse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')); + Unit unit = _unitMap(value.unit); + return LengthOrPercent(number, unit); + } + + //Ignore unparsable input + return LengthOrPercent(0); + } + + static Unit _unitMap(int cssParserUnitToken) { + switch(cssParserUnitToken) { + + default: return Unit.px; + } + } + static TextAlign expressionToTextAlign(css.Expression value) { if (value is css.LiteralTerm) { switch(value.text) { diff --git a/lib/src/style/length.dart b/lib/src/style/length.dart new file mode 100644 index 0000000000..24632a319e --- /dev/null +++ b/lib/src/style/length.dart @@ -0,0 +1,56 @@ +const int _percent = 0x1; +const int _length = 0x2; +const int _auto = 0x4; +const int _lengthPercent = _length | _percent; +const int _margin = _lengthPercent | _auto; + +//TODO there are more unit-types that need support +enum Unit { + //ch, + em(_length), + //ex, + percent(_percent), + px(_length), + //rem, + //Q, + //vh, + //vw, + auto(_auto); + const Unit(this.unitType); + final int unitType; +} + +/// Represents a CSS dimension https://drafts.csswg.org/css-values/#dimensions +abstract class Dimension { + final double value; + final Unit unit; + + Dimension(this.value, this.unit) { + assert((unit.unitType | _unitType) == _unitType, "You used a unit for the property that was not allowed"); + } + + int get _unitType; +} + +class Length extends Dimension { + Length(double value, [Unit unit = Unit.px]) : super(value, unit); + + @override + int get _unitType => _length; +} + +class LengthOrPercent extends Dimension { + LengthOrPercent(double value, [Unit unit = Unit.px]) : super(value, unit); + + @override + int get _unitType => _lengthPercent; +} + +class Margin extends Dimension { + Margin(double value, [Unit? unit = Unit.px]): super(value, unit ?? Unit.px); + + Margin.auto(): super(0, Unit.auto); + + @override + int get _unitType => _margin; +} \ No newline at end of file diff --git a/lib/src/style/margin.dart b/lib/src/style/margin.dart new file mode 100644 index 0000000000..23cd887f3d --- /dev/null +++ b/lib/src/style/margin.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/src/style/length.dart'; + +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?.unit == Unit.auto ? left : Margin(0, Unit.px), + right: right?.unit == Unit.auto ? right : Margin(0, Unit.px), + top: top?.unit == Unit.auto ? top : Margin(0, Unit.px), + bottom: bottom?.unit == Unit.auto ? bottom : Margin(0, Unit.px), + ); + + 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 ? Margin(left, this.left?.unit) : this.left, + right: right != null ? Margin(right, this.right?.unit) : this.right, + top: top != null ? Margin(top, this.top?.unit) : this.top, + bottom: bottom != null ? Margin(bottom, this.bottom?.unit) : this.bottom, + ); + + // bool get isAutoHorizontal => (left is MarginAuto) || (right is MarginAuto); + + /// Analogous to [EdgeInsets.zero] + static Margins get zero => Margins.all(0); + + /// Analogous to [EdgeInsets.all] + Margins.all(double value, {Unit? unit}): + left = Margin(value, unit), + right = Margin(value, unit), + top = Margin(value, unit), + bottom = Margin(value, unit); + + /// Analogous to [EdgeInsets.only] + Margins.only({ double? left, double? right, double? top, double? bottom, Unit? unit}): + left = Margin(left ?? 0, unit), + right = Margin(right ?? 0, unit), + top = Margin(top ?? 0, unit), + bottom = Margin(bottom ?? 0, unit); + + + /// Analogous to [EdgeInsets.symmetric] + Margins.symmetric({double? horizontal, double? vertical, Unit? unit}): + left = Margin(horizontal ?? 0, unit), + right = Margin(horizontal ?? 0, unit), + top = Margin(vertical ?? 0, unit), + bottom = Margin(vertical ?? 0, unit); +} diff --git a/lib/src/styled_element.dart b/lib/src/styled_element.dart index 9531f5bcf9..95bb298dfc 100644 --- a/lib/src/styled_element.dart +++ b/lib/src/styled_element.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/src/css_parser.dart'; +import 'package:flutter_html/src/style/margin.dart'; import 'package:flutter_html/style.dart'; import 'package:html/dom.dart' as dom; //TODO(Sub6Resources): don't use the internal code of the html package as it may change unexpectedly. diff --git a/lib/style.dart b/lib/style.dart index 12f777ae37..6f7a8ade5d 100644 --- a/lib/style.dart +++ b/lib/style.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/src/css_parser.dart'; +import 'package:flutter_html/src/style/margin.dart'; ///This class represents all the available CSS attributes ///for this package. @@ -481,97 +482,7 @@ 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, - ); -} - +//TODO implement dimensionality 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 451e0dd505..ec34ae68a9 100644 --- a/packages/flutter_html_table/lib/flutter_html_table.dart +++ b/packages/flutter_html_table/lib/flutter_html_table.dart @@ -11,7 +11,13 @@ CustomRender tableRender() => CustomRender.widget(widget: (context, buildChildren) { return Container( key: context.key, - margin: context.style.margin?.asInsets.nonNegative, + //TODO(Sub6Resources): This needs to be computed with Units!! + margin: EdgeInsets.only( + left: context.style.margin?.left?.value.abs() ?? 0, + right: context.style.margin?.right?.value.abs() ?? 0, + bottom: context.style.margin?.bottom?.value.abs() ?? 0, + top: context.style.margin?.bottom?.value.abs() ?? 0, + ), padding: context.style.padding?.nonNegative, alignment: context.style.alignment, decoration: BoxDecoration( diff --git a/pubspec.yaml b/pubspec.yaml index f47df5c0d3..6f1baadd9f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 3.0.0-alpha.5 homepage: https://github.com/Sub6Resources/flutter_html environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.17.0 <3.0.0' flutter: '>=2.2.0' dependencies: diff --git a/test/style/dimension_test.dart b/test/style/dimension_test.dart new file mode 100644 index 0000000000..cf239249f2 --- /dev/null +++ b/test/style/dimension_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter_html/src/style/length.dart'; +import 'package:flutter_test/flutter_test.dart'; + +const nonZeroNumber = 16.0; + +void main() { + test("Basic length unspecified units test", () { + final length = Length(nonZeroNumber); + expect(length.value, equals(nonZeroNumber)); + expect(length.unit, equals(Unit.px)); + }); + + test("Basic length-percent unspecified units test", () { + final lengthPercent = LengthOrPercent(nonZeroNumber); + expect(lengthPercent.value, equals(nonZeroNumber)); + expect(lengthPercent.unit, equals(Unit.px)); + }); + + test("Zero-length unspecified units test", () { + final length = Length(0); + expect(length.value, equals(0)); + expect(length.unit, equals(Unit.px)); + }); + + test("Zero-percent-length unspecified units test", () { + final lengthPercent = LengthOrPercent(0); + expect(lengthPercent.value, equals(0)); + expect(lengthPercent.unit, equals(Unit.px)); + }); + + test("Pass in invalid unit", () { + expect(() => Length(nonZeroNumber, Unit.percent), throwsAssertionError); + }); + + test("Pass in invalid unit with zero", () { + expect(() => Length(0, Unit.percent), throwsAssertionError); + }); + + test("Pass in a valid unit", () { + final lengthPercent = LengthOrPercent(nonZeroNumber, Unit.percent); + expect(lengthPercent.value, equals(nonZeroNumber)); + expect(lengthPercent.unit, equals(Unit.percent)); + }); +} \ No newline at end of file From 65c434d4f94d6ff941e02d5df3b084da1181f090 Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Tue, 23 Aug 2022 18:09:03 -0600 Subject: [PATCH 03/14] WIP: Compute margins and cleanup --- lib/custom_render.dart | 13 +- lib/flutter_html.dart | 76 +++++----- lib/html_parser.dart | 136 ++++++++++++------ lib/src/interactable_element.dart | 23 +-- lib/src/layout_element.dart | 63 ++++---- lib/src/replaced_element.dart | 26 ++-- lib/src/style/compute_style.dart | 22 +++ lib/src/style/length.dart | 4 +- lib/src/styled_element.dart | 7 +- lib/style.dart | 7 +- .../lib/iframe_mobile.dart | 2 +- .../flutter_html_iframe/lib/iframe_web.dart | 2 +- test/html_parser_test.dart | 16 ++- test/style/dimension_test.dart | 26 ++-- 14 files changed, 268 insertions(+), 155 deletions(-) create mode 100644 lib/src/style/compute_style.dart diff --git a/lib/custom_render.dart b/lib/custom_render.dart index e421f3c931..8241a781f3 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -116,10 +116,6 @@ CustomRender blockElementRender({Style? style, List? children}) => 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 && @@ -133,17 +129,12 @@ CustomRender blockElementRender({Style? style, List? children}) => return WidgetSpan( child: ContainerSpan( key: context.key, - newContext: context, + renderContext: 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 (i != context.tree.children.length - 1 && childTree.style.display == Display.BLOCK && @@ -161,7 +152,7 @@ CustomRender listElementRender( inlineSpan: (context, buildChildren) => WidgetSpan( child: ContainerSpan( key: context.key, - newContext: context, + renderContext: context, style: style ?? context.tree.style, shrinkWrap: context.parser.shrinkWrap, child: Row( diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 3db68efb2b..c7af31ba9b 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -179,21 +179,26 @@ class _HtmlState extends State { Widget build(BuildContext context) { return Container( width: widget.shrinkWrap ? null : MediaQuery.of(context).size.width, - child: HtmlParser( - key: widget._anchorKey, - htmlData: documentElement, - onLinkTap: widget.onLinkTap, - onAnchorTap: widget.onAnchorTap, - onImageTap: widget.onImageTap, - onCssParseError: widget.onCssParseError, - onImageError: widget.onImageError, - shrinkWrap: widget.shrinkWrap, - selectable: false, - style: widget.style, - customRenders: {} - ..addAll(widget.customRenders) - ..addAll(defaultRenders), - tagsList: widget.tagsList.isEmpty ? Html.tags : widget.tagsList, + child: LayoutBuilder( + builder: (context, constraints) { + return HtmlParser( + key: widget._anchorKey, + htmlData: documentElement, + onLinkTap: widget.onLinkTap, + onAnchorTap: widget.onAnchorTap, + onImageTap: widget.onImageTap, + onCssParseError: widget.onCssParseError, + onImageError: widget.onImageError, + shrinkWrap: widget.shrinkWrap, + selectable: false, + style: widget.style, + customRenders: {} + ..addAll(widget.customRenders) + ..addAll(defaultRenders), + tagsList: widget.tagsList.isEmpty ? Html.tags : widget.tagsList, + constraints: constraints, + ); + } ), ); } @@ -347,24 +352,29 @@ class _SelectableHtmlState extends State { Widget build(BuildContext context) { return Container( width: widget.shrinkWrap ? null : MediaQuery.of(context).size.width, - child: HtmlParser( - key: widget._anchorKey, - htmlData: documentElement, - onLinkTap: widget.onLinkTap, - onAnchorTap: widget.onAnchorTap, - onImageTap: null, - onCssParseError: widget.onCssParseError, - onImageError: null, - shrinkWrap: widget.shrinkWrap, - selectable: true, - style: widget.style, - customRenders: {} - ..addAll(widget.customRenders) - ..addAll(defaultRenders), - tagsList: - widget.tagsList.isEmpty ? SelectableHtml.tags : widget.tagsList, - selectionControls: widget.selectionControls, - scrollPhysics: widget.scrollPhysics, + child: LayoutBuilder( + builder: (context, constraints) { + return HtmlParser( + key: widget._anchorKey, + htmlData: documentElement, + onLinkTap: widget.onLinkTap, + onAnchorTap: widget.onAnchorTap, + onImageTap: null, + onCssParseError: widget.onCssParseError, + onImageError: null, + shrinkWrap: widget.shrinkWrap, + selectable: true, + style: widget.style, + customRenders: {} + ..addAll(widget.customRenders) + ..addAll(defaultRenders), + tagsList: + widget.tagsList.isEmpty ? SelectableHtml.tags : widget.tagsList, + selectionControls: widget.selectionControls, + scrollPhysics: widget.scrollPhysics, + constraints: constraints, + ); + } ), ); } diff --git a/lib/html_parser.dart b/lib/html_parser.dart index f34f9d9b13..2d0ed3a782 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -8,8 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/src/css_parser.dart'; import 'package:flutter_html/src/html_elements.dart'; -import 'package:flutter_html/src/style/length.dart'; -import 'package:flutter_html/src/style/margin.dart'; +import 'package:flutter_html/src/style/compute_style.dart'; import 'package:flutter_html/src/utils.dart'; import 'package:html/dom.dart' as dom; import 'package:html/parser.dart' as htmlparser; @@ -44,6 +43,7 @@ class HtmlParser extends StatelessWidget { final Html? root; final TextSelectionControls? selectionControls; final ScrollPhysics? scrollPhysics; + final BoxConstraints constraints; final Map cachedImageSizes = {}; @@ -60,6 +60,7 @@ class HtmlParser extends StatelessWidget { required this.style, required this.customRenders, required this.tagsList, + required this.constraints, this.root, this.selectionControls, this.scrollPhysics, @@ -70,32 +71,37 @@ class HtmlParser extends StatelessWidget { : onLinkTap, super(key: key); + /// As the widget [build]s, the HTML data is processed into a tree of [StyledElement]s, + /// which are then parsed into an [InlineSpan] tree that is then rendered to the screen by Flutter + //TODO Lazy processing of data. We don't need the processing steps done every build phase unless the data has changed. @override Widget build(BuildContext context) { - Map>> declarations = _getExternalCssDeclarations(htmlData.getElementsByTagName("style"), onCssParseError); + + // Lexing Step StyledElement lexedTree = lexDomTree( htmlData, customRenders.keys.toList(), tagsList, context, this, + constraints, ); - 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); + + // Styling Step + StyledElement styledTree = styleTree(lexedTree, htmlData, style, onCssParseError); + + // Processing Step + StyledElement processedTree = processTree(styledTree); + + // Parsing Step InlineSpan parsedTree = parseTree( RenderContext( buildContext: context, parser: this, - tree: cleanedTree, - style: cleanedTree.style, + tree: processedTree, + style: processedTree.style, ), - cleanedTree, + processedTree, ); // This is the final scaling that assumes any other StyledText instances are @@ -105,13 +111,13 @@ class HtmlParser extends StatelessWidget { if (selectable) { return StyledText.selectable( textSpan: parsedTree as TextSpan, - style: cleanedTree.style, + style: processedTree.style, textScaleFactor: MediaQuery.of(context).textScaleFactor, renderContext: RenderContext( buildContext: context, parser: this, - tree: cleanedTree, - style: cleanedTree.style, + tree: processedTree, + style: processedTree.style, ), selectionControls: selectionControls, scrollPhysics: scrollPhysics, @@ -119,13 +125,13 @@ class HtmlParser extends StatelessWidget { } return StyledText( textSpan: parsedTree, - style: cleanedTree.style, + style: processedTree.style, textScaleFactor: MediaQuery.of(context).textScaleFactor, renderContext: RenderContext( buildContext: context, parser: this, - tree: cleanedTree, - style: cleanedTree.style, + tree: processedTree, + style: processedTree.style, ), ); } @@ -147,12 +153,16 @@ class HtmlParser extends StatelessWidget { List tagsList, BuildContext context, HtmlParser parser, + BoxConstraints constraints, ) { StyledElement tree = StyledElement( name: "[Tree Root]", children: [], node: html, + //TODO(Sub6Resources): This seems difficult to customize style: Style.fromTextStyle(Theme.of(context).textTheme.bodyText2!), + //TODO(Sub6Resources): Okay, how about shrinkWrap? + containingBlockSize: Size(constraints.maxWidth, constraints.maxHeight), ); html.nodes.forEach((node) { @@ -162,6 +172,7 @@ class HtmlParser extends StatelessWidget { tagsList, context, parser, + constraints, )); }); @@ -178,6 +189,7 @@ class HtmlParser extends StatelessWidget { List tagsList, BuildContext context, HtmlParser parser, + BoxConstraints constraints, ) { List children = []; @@ -188,28 +200,32 @@ class HtmlParser extends StatelessWidget { tagsList, context, parser, + constraints, )); }); + //TODO(Sub6Resources): Okay, how about shrinkWrap? How to calculate this for children? + final maxSize = Size(constraints.maxWidth, constraints.maxHeight); + //TODO(Sub6Resources): There's probably a more efficient way to look this up. if (node is dom.Element) { if (!tagsList.contains(node.localName)) { return EmptyContentElement(); } if (STYLED_ELEMENTS.contains(node.localName)) { - return parseStyledElement(node, children); + return parseStyledElement(node, children, maxSize); } else if (INTERACTABLE_ELEMENTS.contains(node.localName)) { - return parseInteractableElement(node, children); + return parseInteractableElement(node, children, maxSize); } else if (REPLACED_ELEMENTS.contains(node.localName)) { - return parseReplacedElement(node, children); + return parseReplacedElement(node, children, maxSize); } else if (LAYOUT_ELEMENTS.contains(node.localName)) { - return parseLayoutElement(node, children); + return parseLayoutElement(node, children, maxSize); } else if (TABLE_CELL_ELEMENTS.contains(node.localName)) { - return parseTableCellElement(node, children); + return parseTableCellElement(node, children, maxSize); } else if (TABLE_DEFINITION_ELEMENTS.contains(node.localName)) { - return parseTableDefinitionElement(node, children); + return parseTableDefinitionElement(node, children, maxSize); } else { - final StyledElement tree = parseStyledElement(node, children); + final StyledElement tree = parseStyledElement(node, children, maxSize); for (final entry in customRenderMatchers) { if (entry.call( RenderContext( @@ -225,7 +241,7 @@ class HtmlParser extends StatelessWidget { return EmptyContentElement(); } } else if (node is dom.Text) { - return TextContentElement(text: node.text, style: Style(), element: node.parent, node: node); + return TextContentElement(text: node.text, style: Style(), element: node.parent, node: node, containingBlockSize: maxSize); } else { return EmptyContentElement(); } @@ -296,10 +312,25 @@ class HtmlParser extends StatelessWidget { return tree; } - /// [cleanTree] optimizes the [StyledElement] tree so all [BlockElement]s are + /// [styleTree] takes the lexed [StyleElement] tree and applies external, + /// inline, and custom CSS/Flutter styles, and then cascades the styles down the tree. + static StyledElement styleTree(StyledElement tree, dom.Element htmlData, Map style, OnCssParseError? onCssParseError) { + Map>> declarations = _getExternalCssDeclarations(htmlData.getElementsByTagName("style"), onCssParseError); + + StyledElement? externalCssStyledTree; + if (declarations.isNotEmpty) { + externalCssStyledTree = _applyExternalCss(declarations, tree); + } + tree = _applyInlineStyles(externalCssStyledTree ?? tree, onCssParseError); + tree = _applyCustomStyles(style, tree); + tree = _cascadeStyles(style, tree); + return tree; + } + + /// [processTree] 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 processTree(StyledElement tree) { tree = _processInternalWhitespace(tree); tree = _processInlineWhitespace(tree); tree = _removeEmptyElements(tree); @@ -340,7 +371,7 @@ class HtmlParser extends StatelessWidget { } return WidgetSpan( child: ContainerSpan( - newContext: newContext, + renderContext: newContext, style: tree.style, shrinkWrap: newContext.parser.shrinkWrap, child: customRenders[entry]!.widget!.call(newContext, buildChildren), @@ -627,11 +658,20 @@ class HtmlParser extends StatelessWidget { static StyledElement _processBeforesAndAfters(StyledElement tree) { if (tree.style.before != null) { tree.children.insert( - 0, TextContentElement(text: tree.style.before, style: tree.style.copyWith(beforeAfterNull: true, display: Display.INLINE))); + 0, + TextContentElement( + text: tree.style.before, + style: tree.style.copyWith(beforeAfterNull: true, display: Display.INLINE), + containingBlockSize: Size.infinite, //TODO(Sub6Resources): This can't be right... + ), + ); } if (tree.style.after != null) { - tree.children - .add(TextContentElement(text: tree.style.after, style: tree.style.copyWith(beforeAfterNull: true, display: Display.INLINE))); + tree.children.add(TextContentElement( + text: tree.style.after, + style: tree.style.copyWith(beforeAfterNull: true, display: Display.INLINE), + containingBlockSize: Size.infinite, //TODO(Sub6Resources): This can't be right... + )); } tree.children.forEach(_processBeforesAndAfters); @@ -835,7 +875,7 @@ class ContainerSpan extends StatelessWidget { final Widget? child; final List? children; final Style style; - final RenderContext newContext; + final RenderContext renderContext; final bool shrinkWrap; ContainerSpan({ @@ -843,15 +883,16 @@ class ContainerSpan extends StatelessWidget { this.child, this.children, required this.style, - required this.newContext, + required this.renderContext, this.shrinkWrap = false, }): super(key: key); @override - Widget build(BuildContext _) { + Widget build(BuildContext context) { // Elements that are inline should ignore margin: auto for alignment. var alignment = shrinkWrap ? null : style.alignment; + // TODO(Sub6Resources): This needs to follow the CSS spec for computing auto values! if(style.display == Display.BLOCK) { if(style.margin?.left?.unit == Unit.auto && style.margin?.right?.unit == Unit.auto) @@ -860,6 +901,11 @@ class ContainerSpan extends StatelessWidget { alignment = Alignment.bottomRight; } + //TODO(Sub6Resources): Is there a better value? + double emValue = (style.fontSize?.size ?? 16) * + MediaQuery.of(context).devicePixelRatio * + MediaQuery.of(context).textScaleFactor; + Widget container = Container( decoration: BoxDecoration( border: style.border, @@ -868,22 +914,22 @@ class ContainerSpan extends StatelessWidget { height: style.height, width: style.width, padding: style.padding?.nonNegative, - // TODO(Sub6Resources): These need to be computed!! + //TODO GIVE A VALID AUTO VALUE, maybe move this all to a new method? margin: EdgeInsets.only( - left: style.margin?.left?.value.abs() ?? 0, - right: style.margin?.right?.value.abs() ?? 0, - bottom: style.margin?.bottom?.value.abs() ?? 0, - top: style.margin?.bottom?.value.abs() ?? 0, + left: computeDimensionValue(style.margin?.left ?? Margin.zero(), DimensionComputeContext(emValue: emValue, autoValue: 0)), + right: computeDimensionValue(style.margin?.right ?? Margin.zero(), DimensionComputeContext(emValue: emValue, autoValue: 0)), + bottom: computeDimensionValue(style.margin?.bottom ?? Margin.zero(), DimensionComputeContext(emValue: emValue, autoValue: 0)), + top: computeDimensionValue(style.margin?.top ?? Margin.zero(), DimensionComputeContext(emValue: emValue, autoValue: 0)), ), alignment: alignment, child: child ?? StyledText( textSpan: TextSpan( - style: newContext.style.generateTextStyle(), + style: renderContext.style.generateTextStyle(), children: children, ), - style: newContext.style, - renderContext: newContext, + style: renderContext.style, + renderContext: renderContext, ), ); diff --git a/lib/src/interactable_element.dart b/lib/src/interactable_element.dart index 2aab878a64..49b8baa0fb 100644 --- a/lib/src/interactable_element.dart +++ b/lib/src/interactable_element.dart @@ -8,13 +8,14 @@ class InteractableElement extends StyledElement { String? href; InteractableElement({ - required String name, - required List children, - required Style style, + required super.name, + required super.children, + required super.style, required this.href, required dom.Node node, - required String elementId, - }) : super(name: name, children: children, style: style, node: node as dom.Element?, elementId: elementId); + required super.elementId, + required super.containingBlockSize, + }) : super(node: node as dom.Element?); } /// A [Gesture] indicates the type of interaction by a user. @@ -23,7 +24,10 @@ enum Gesture { } StyledElement parseInteractableElement( - dom.Element element, List children) { + dom.Element element, + List children, + Size containingBlockSize, + ) { switch (element.localName) { case "a": if (element.attributes.containsKey('href')) { @@ -36,7 +40,8 @@ StyledElement parseInteractableElement( textDecoration: TextDecoration.underline, ), node: element, - elementId: element.id + elementId: element.id, + containingBlockSize: containingBlockSize, ); } // When tag have no href, it must be non clickable and without decoration. @@ -46,6 +51,7 @@ StyledElement parseInteractableElement( style: Style(), node: element, elementId: element.id, + containingBlockSize: containingBlockSize, ); /// will never be called, just to suppress missing return warning default: @@ -55,7 +61,8 @@ StyledElement parseInteractableElement( node: element, href: '', style: Style(), - elementId: "[[No ID]]" + elementId: "[[No ID]]", + containingBlockSize: containingBlockSize, ); } } diff --git a/lib/src/layout_element.dart b/lib/src/layout_element.dart index 33093e7493..32b881e683 100644 --- a/lib/src/layout_element.dart +++ b/lib/src/layout_element.dart @@ -11,10 +11,11 @@ import 'package:html/dom.dart' as dom; abstract class LayoutElement extends StyledElement { LayoutElement({ String name = "[[No Name]]", - required List children, + required super.children, String? elementId, - dom.Element? node, - }) : super(name: name, children: children, style: Style(), node: node, elementId: elementId ?? "[[No ID]]"); + super.node, + required super.containingBlockSize, + }) : super(name: name, style: Style(), elementId: elementId ?? "[[No ID]]"); Widget? toWidget(RenderContext context); } @@ -23,6 +24,7 @@ class TableSectionLayoutElement extends LayoutElement { TableSectionLayoutElement({ required String name, required List children, + required super.containingBlockSize, }) : super(name: name, children: children); @override @@ -34,10 +36,11 @@ class TableSectionLayoutElement extends LayoutElement { class TableRowLayoutElement extends LayoutElement { TableRowLayoutElement({ - required String name, - required List children, - required dom.Element node, - }) : super(name: name, children: children, node: node); + required super.name, + required super.children, + required super.node, + required super.containingBlockSize, + }); @override Widget toWidget(RenderContext context) { @@ -51,13 +54,14 @@ class TableCellElement extends StyledElement { int rowspan = 1; TableCellElement({ - required String name, - required String elementId, - required List elementClasses, - required List children, - required Style style, - required dom.Element node, - }) : super(name: name, elementId: elementId, elementClasses: elementClasses, children: children, style: style, node: node) { + required super.name, + required super.elementId, + required super.elementClasses, + required super.children, + required super.style, + required super.node, + required super.containingBlockSize, + }) { colspan = _parseSpan(this, "colspan"); rowspan = _parseSpan(this, "rowspan"); } @@ -71,6 +75,7 @@ class TableCellElement extends StyledElement { TableCellElement parseTableCellElement( dom.Element element, List children, + Size containingBlockSize, ) { final cell = TableCellElement( name: element.localName!, @@ -79,6 +84,7 @@ TableCellElement parseTableCellElement( children: children, node: element, style: Style(), + containingBlockSize: containingBlockSize, ); if (element.localName == "th") { cell.style = Style( @@ -90,16 +96,18 @@ TableCellElement parseTableCellElement( class TableStyleElement extends StyledElement { TableStyleElement({ - required String name, - required List children, - required Style style, - required dom.Element node, - }) : super(name: name, children: children, style: style, node: node); + required super.name, + required super.children, + required super.style, + required super.node, + required super.containingBlockSize, + }); } TableStyleElement parseTableDefinitionElement( dom.Element element, List children, + Size containingBlockSize, ) { switch (element.localName) { case "colgroup": @@ -109,6 +117,7 @@ TableStyleElement parseTableDefinitionElement( children: children, node: element, style: Style(), + containingBlockSize: containingBlockSize, ); default: return TableStyleElement( @@ -116,6 +125,7 @@ TableStyleElement parseTableDefinitionElement( children: children, node: element, style: Style(), + containingBlockSize: containingBlockSize, ); } } @@ -124,11 +134,12 @@ class DetailsContentElement extends LayoutElement { List elementList; DetailsContentElement({ - required String name, - required List children, + required super.name, + required super.children, required dom.Element node, required this.elementList, - }) : super(name: name, node: node, children: children, elementId: node.id); + required super.containingBlockSize, + }) : super(node: node, elementId: node.id); @override Widget toWidget(RenderContext context) { @@ -174,7 +185,7 @@ class DetailsContentElement extends LayoutElement { } class EmptyLayoutElement extends LayoutElement { - EmptyLayoutElement({required String name}) : super(name: name, children: []); + EmptyLayoutElement({required String name}) : super(name: name, children: [], containingBlockSize: Size.zero); @override Widget? toWidget(_) => null; @@ -183,6 +194,7 @@ class EmptyLayoutElement extends LayoutElement { LayoutElement parseLayoutElement( dom.Element element, List children, + Size containingBlockSize, ) { switch (element.localName) { case "details": @@ -193,7 +205,8 @@ LayoutElement parseLayoutElement( node: element, name: element.localName!, children: children, - elementList: element.children + elementList: element.children, + containingBlockSize: containingBlockSize, ); case "thead": case "tbody": @@ -201,12 +214,14 @@ LayoutElement parseLayoutElement( return TableSectionLayoutElement( name: element.localName!, children: children, + containingBlockSize: containingBlockSize, ); case "tr": return TableRowLayoutElement( name: element.localName!, children: children, node: element, + containingBlockSize: containingBlockSize, ); default: return EmptyLayoutElement(name: "[[No Name]]"); diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index 81cc5d58ee..4b45459476 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -17,13 +17,14 @@ abstract class ReplacedElement extends StyledElement { PlaceholderAlignment alignment; ReplacedElement({ - required String name, - required Style style, - required String elementId, + required super.name, + required super.style, + required super.elementId, + required super.containingBlockSize, List? children, - dom.Element? node, + super.node, this.alignment = PlaceholderAlignment.aboveBaseline, - }) : super(name: name, children: children ?? [], style: style, node: node, elementId: elementId); + }) : super(children: children ?? []); static List parseMediaSources(List elements) { return elements @@ -46,6 +47,7 @@ class TextContentElement extends ReplacedElement { required this.text, this.node, dom.Element? element, + required super.containingBlockSize, }) : super(name: "[text]", style: style, node: element, elementId: "[[No ID]]"); @override @@ -58,7 +60,7 @@ class TextContentElement extends ReplacedElement { } class EmptyContentElement extends ReplacedElement { - EmptyContentElement({String name = "empty"}) : super(name: name, style: Style(), elementId: "[[No ID]]"); + EmptyContentElement({String name = "empty"}) : super(name: name, style: Style(), elementId: "[[No ID]]", containingBlockSize: Size.zero); @override Widget? toWidget(_) => null; @@ -70,7 +72,8 @@ class RubyElement extends ReplacedElement { RubyElement({ required this.element, required List children, - String name = "ruby" + String name = "ruby", + required super.containingBlockSize, }) : super(name: name, alignment: PlaceholderAlignment.middle, style: Style(), elementId: element.id, children: children); @override @@ -102,7 +105,7 @@ class RubyElement extends ReplacedElement { transform: Matrix4.translationValues(0, -(rubyYPos), 0), child: ContainerSpan( - newContext: RenderContext( + renderContext: RenderContext( buildContext: context.buildContext, parser: context.parser, style: c.style, @@ -115,7 +118,7 @@ class RubyElement extends ReplacedElement { .copyWith(fontSize: rubySize)), )))), ContainerSpan( - newContext: context, + renderContext: context, style: context.style, child: node is TextContentElement ? Text((node as TextContentElement).text?.trim() ?? "", style: context.style.generateTextStyle()) : null, @@ -146,6 +149,7 @@ class RubyElement extends ReplacedElement { ReplacedElement parseReplacedElement( dom.Element element, List children, + Size containingBlockSize, ) { switch (element.localName) { case "br": @@ -153,12 +157,14 @@ ReplacedElement parseReplacedElement( text: "\n", style: Style(whiteSpace: WhiteSpace.PRE), element: element, - node: element + node: element, + containingBlockSize: containingBlockSize, ); case "ruby": return RubyElement( element: element, children: children, + containingBlockSize: containingBlockSize, ); default: return EmptyContentElement(name: element.localName == null ? "[[No Name]]" : element.localName!); diff --git a/lib/src/style/compute_style.dart b/lib/src/style/compute_style.dart new file mode 100644 index 0000000000..db50e26e74 --- /dev/null +++ b/lib/src/style/compute_style.dart @@ -0,0 +1,22 @@ +import 'package:flutter_html/src/style/length.dart'; + +class DimensionComputeContext { + const DimensionComputeContext({ + required this.emValue, + required this.autoValue, + }); + + final double emValue; + final double autoValue; +} + +/// [computeDimensionUnit] takes a [Dimension] and some information about the +/// context where the Dimension is being used, and returns a "used" value to +/// use in a rendering. +double computeDimensionValue(Dimension dimension, DimensionComputeContext computeContext) { + switch (dimension.unit) { + case Unit.em: return computeContext.emValue * dimension.value; + case Unit.px: return dimension.value; + case Unit.auto: return computeContext.autoValue; + } +} diff --git a/lib/src/style/length.dart b/lib/src/style/length.dart index 24632a319e..e483f847e1 100644 --- a/lib/src/style/length.dart +++ b/lib/src/style/length.dart @@ -9,7 +9,7 @@ enum Unit { //ch, em(_length), //ex, - percent(_percent), + //percent(_percent), px(_length), //rem, //Q, @@ -51,6 +51,8 @@ class Margin extends Dimension { Margin.auto(): super(0, Unit.auto); + Margin.zero(): super(0, Unit.px); + @override int get _unitType => _margin; } \ No newline at end of file diff --git a/lib/src/styled_element.dart b/lib/src/styled_element.dart index 95bb298dfc..3fb52d9984 100644 --- a/lib/src/styled_element.dart +++ b/lib/src/styled_element.dart @@ -14,6 +14,7 @@ class StyledElement { final List elementClasses; List children; Style style; + Size containingBlockSize; final dom.Element? _node; StyledElement({ @@ -23,6 +24,7 @@ class StyledElement { required this.children, required this.style, required dom.Element? node, + required this.containingBlockSize, }) : this._node = node; bool matchesSelector(String selector) => @@ -49,7 +51,7 @@ class StyledElement { } StyledElement parseStyledElement( - dom.Element element, List children) { + dom.Element element, List children, Size containingBlockSize) { StyledElement styledElement = StyledElement( name: element.localName!, elementId: element.id, @@ -57,6 +59,7 @@ StyledElement parseStyledElement( children: children, node: element, style: Style(), + containingBlockSize: containingBlockSize, ); switch (element.localName) { @@ -319,7 +322,7 @@ StyledElement parseStyledElement( break; case "p": styledElement.style = Style( - margin: Margins.symmetric(vertical: 14.0), + margin: Margins.symmetric(vertical: 1, unit: Unit.em), display: Display.BLOCK, ); break; diff --git a/lib/style.dart b/lib/style.dart index 6f7a8ade5d..5f0326913b 100644 --- a/lib/style.dart +++ b/lib/style.dart @@ -3,7 +3,10 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/src/css_parser.dart'; -import 'package:flutter_html/src/style/margin.dart'; + +//Export Margin API +export 'package:flutter_html/src/style/margin.dart'; +export 'package:flutter_html/src/style/length.dart'; ///This class represents all the available CSS attributes ///for this package. @@ -312,7 +315,7 @@ class Style { padding: other.padding, //TODO merge EdgeInsets margin: other.margin, - //TODO merge EdgeInsets + //TODO merge Margins textAlign: other.textAlign, textDecoration: other.textDecoration, textDecorationColor: other.textDecorationColor, diff --git a/packages/flutter_html_iframe/lib/iframe_mobile.dart b/packages/flutter_html_iframe/lib/iframe_mobile.dart index a1c9bbb713..c3a7b01f1b 100644 --- a/packages/flutter_html_iframe/lib/iframe_mobile.dart +++ b/packages/flutter_html_iframe/lib/iframe_mobile.dart @@ -17,7 +17,7 @@ CustomRender iframeRender({NavigationDelegate? navigationDelegate}) => height: givenHeight ?? (givenWidth ?? 300) / 2, child: ContainerSpan( style: context.style, - newContext: context, + renderContext: context, child: WebView( initialUrl: context.tree.element?.attributes['src'], key: key, diff --git a/packages/flutter_html_iframe/lib/iframe_web.dart b/packages/flutter_html_iframe/lib/iframe_web.dart index fd776fe980..7ad82190b1 100644 --- a/packages/flutter_html_iframe/lib/iframe_web.dart +++ b/packages/flutter_html_iframe/lib/iframe_web.dart @@ -38,7 +38,7 @@ CustomRender iframeRender({NavigationDelegate? navigationDelegate}) => 2, child: ContainerSpan( style: context.style, - newContext: context, + renderContext: context, child: Directionality( textDirection: TextDirection.ltr, child: HtmlElementView( diff --git a/test/html_parser_test.dart b/test/html_parser_test.dart index c351ab2a36..98b515aed1 100644 --- a/test/html_parser_test.dart +++ b/test/html_parser_test.dart @@ -54,7 +54,9 @@ void testNewParser(BuildContext context) { tagsList: Html.tags, selectionControls: null, scrollPhysics: null, - ) + constraints: BoxConstraints(), + ), + BoxConstraints(), ); print(tree.toString()); @@ -80,7 +82,9 @@ void testNewParser(BuildContext context) { tagsList: Html.tags, selectionControls: null, scrollPhysics: null, - ) + constraints: BoxConstraints(), + ), + BoxConstraints() ); print(tree.toString()); @@ -104,7 +108,9 @@ void testNewParser(BuildContext context) { tagsList: Html.tags, selectionControls: null, scrollPhysics: null, - ) + constraints: BoxConstraints(), + ), + BoxConstraints() ); print(tree.toString()); @@ -130,7 +136,9 @@ void testNewParser(BuildContext context) { tagsList: Html.tags, selectionControls: null, scrollPhysics: null, - ) + constraints: BoxConstraints(), + ), + BoxConstraints(), ); print(tree.toString()); diff --git a/test/style/dimension_test.dart b/test/style/dimension_test.dart index cf239249f2..b36fa475bc 100644 --- a/test/style/dimension_test.dart +++ b/test/style/dimension_test.dart @@ -28,17 +28,17 @@ void main() { expect(lengthPercent.unit, equals(Unit.px)); }); - test("Pass in invalid unit", () { - expect(() => Length(nonZeroNumber, Unit.percent), throwsAssertionError); - }); - - test("Pass in invalid unit with zero", () { - expect(() => Length(0, Unit.percent), throwsAssertionError); - }); - - test("Pass in a valid unit", () { - final lengthPercent = LengthOrPercent(nonZeroNumber, Unit.percent); - expect(lengthPercent.value, equals(nonZeroNumber)); - expect(lengthPercent.unit, equals(Unit.percent)); - }); + // test("Pass in invalid unit", () { + // expect(() => Length(nonZeroNumber, Unit.percent), throwsAssertionError); + // }); + + // test("Pass in invalid unit with zero", () { + // expect(() => Length(0, Unit.percent), throwsAssertionError); + // }); + + // test("Pass in a valid unit", () { + // final lengthPercent = LengthOrPercent(nonZeroNumber, Unit.percent); + // expect(lengthPercent.value, equals(nonZeroNumber)); + // expect(lengthPercent.unit, equals(Unit.percent)); + // }); } \ No newline at end of file From e427ed8f45921a6f6d231f4da8f55408df1bb2b1 Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Wed, 24 Aug 2022 23:33:03 -0600 Subject: [PATCH 04/14] WIP: margin/width processing --- lib/custom_render.dart | 34 ++-- lib/flutter_html.dart | 4 +- lib/html_parser.dart | 32 +--- lib/src/replaced_element.dart | 2 + lib/src/style/compute_style.dart | 167 ++++++++++++++++-- .../lib/iframe_mobile.dart | 1 + .../flutter_html_iframe/lib/iframe_web.dart | 1 + 7 files changed, 187 insertions(+), 54 deletions(-) diff --git a/lib/custom_render.dart b/lib/custom_render.dart index 8241a781f3..5174c214ad 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -108,7 +108,7 @@ class SelectableCustomRender extends CustomRender { }) : super.inlineSpan(inlineSpan: null); } -CustomRender blockElementRender({Style? style, List? children}) => +CustomRender blockElementRender({Style? style, List? children, required Size containingBlockSize}) => CustomRender.inlineSpan(inlineSpan: (context, buildChildren) { if (context.parser.selectable) { return TextSpan( @@ -132,6 +132,7 @@ CustomRender blockElementRender({Style? style, List? children}) => renderContext: context, style: style ?? context.tree.style, shrinkWrap: context.parser.shrinkWrap, + containingBlockSize: containingBlockSize, children: children ?? context.tree.children .expandIndexed((i, childTree) => [ @@ -147,7 +148,7 @@ CustomRender blockElementRender({Style? style, List? children}) => }); CustomRender listElementRender( - {Style? style, Widget? child, List? children}) => + {Style? style, Widget? child, List? children, required Size containingBlockSize}) => CustomRender.inlineSpan( inlineSpan: (context, buildChildren) => WidgetSpan( child: ContainerSpan( @@ -155,6 +156,7 @@ CustomRender listElementRender( renderContext: context, style: style ?? context.tree.style, shrinkWrap: context.parser.shrinkWrap, + containingBlockSize: containingBlockSize, child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -503,19 +505,21 @@ CustomRender fallbackRender({Style? style, List? children}) => .toList(), )); -final Map defaultRenders = { - blockElementMatcher(): blockElementRender(), - listElementMatcher(): listElementRender(), - textContentElementMatcher(): textContentElementRender(), - dataUriMatcher(): base64ImageRender(), - assetUriMatcher(): assetImageRender(), - networkSourceMatcher(): networkImageRender(), - replacedElementMatcher(): replacedElementRender(), - interactableElementMatcher(): interactableElementRender(), - layoutElementMatcher(): layoutElementRender(), - verticalAlignMatcher(): verticalAlignRender(), - fallbackMatcher(): fallbackRender(), -}; +Map generateDefaultRenders(Size containingBlockSize) { + return { + blockElementMatcher(): blockElementRender(containingBlockSize: containingBlockSize), + listElementMatcher(): listElementRender(containingBlockSize: containingBlockSize), + textContentElementMatcher(): textContentElementRender(), + dataUriMatcher(): base64ImageRender(), + assetUriMatcher(): assetImageRender(), + networkSourceMatcher(): networkImageRender(), + replacedElementMatcher(): replacedElementRender(), + interactableElementMatcher(): interactableElementRender(), + layoutElementMatcher(): layoutElementRender(), + verticalAlignMatcher(): verticalAlignRender(), + fallbackMatcher(): fallbackRender(), + }; +} List _getListElementChildren( ListStylePosition? position, Function() buildChildren) { diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index c7af31ba9b..8e3339dd82 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -194,7 +194,7 @@ class _HtmlState extends State { style: widget.style, customRenders: {} ..addAll(widget.customRenders) - ..addAll(defaultRenders), + ..addAll(generateDefaultRenders(MediaQuery.of(context).size)), tagsList: widget.tagsList.isEmpty ? Html.tags : widget.tagsList, constraints: constraints, ); @@ -367,7 +367,7 @@ class _SelectableHtmlState extends State { style: widget.style, customRenders: {} ..addAll(widget.customRenders) - ..addAll(defaultRenders), + ..addAll(generateDefaultRenders(MediaQuery.of(context).size)), tagsList: widget.tagsList.isEmpty ? SelectableHtml.tags : widget.tagsList, selectionControls: widget.selectionControls, diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 2d0ed3a782..0cbad48bcf 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -375,6 +375,7 @@ class HtmlParser extends StatelessWidget { style: tree.style, shrinkWrap: newContext.parser.shrinkWrap, child: customRenders[entry]!.widget!.call(newContext, buildChildren), + containingBlockSize: tree.containingBlockSize, ), ); } @@ -877,6 +878,7 @@ class ContainerSpan extends StatelessWidget { final Style style; final RenderContext renderContext; final bool shrinkWrap; + final Size containingBlockSize; ContainerSpan({ this.key, @@ -885,26 +887,14 @@ class ContainerSpan extends StatelessWidget { required this.style, required this.renderContext, this.shrinkWrap = false, + required this.containingBlockSize, }): super(key: key); @override Widget build(BuildContext context) { - // Elements that are inline should ignore margin: auto for alignment. - var alignment = shrinkWrap ? null : style.alignment; - - // TODO(Sub6Resources): This needs to follow the CSS spec for computing auto values! - if(style.display == Display.BLOCK) { - if(style.margin?.left?.unit == Unit.auto && style.margin?.right?.unit == Unit.auto) - alignment = Alignment.bottomCenter; - else if(style.margin?.left?.unit == Unit.auto) - alignment = Alignment.bottomRight; - } - - //TODO(Sub6Resources): Is there a better value? - double emValue = (style.fontSize?.size ?? 16) * - MediaQuery.of(context).devicePixelRatio * - MediaQuery.of(context).textScaleFactor; + //Calculate auto widths and margins: + final widthsAndMargins = WidthAndMargins.calculate(style, containingBlockSize, context); Widget container = Container( decoration: BoxDecoration( @@ -912,16 +902,10 @@ class ContainerSpan extends StatelessWidget { color: style.backgroundColor, ), height: style.height, - width: style.width, + width: style.width, //widthsAndMargins.width, padding: style.padding?.nonNegative, - //TODO GIVE A VALID AUTO VALUE, maybe move this all to a new method? - margin: EdgeInsets.only( - left: computeDimensionValue(style.margin?.left ?? Margin.zero(), DimensionComputeContext(emValue: emValue, autoValue: 0)), - right: computeDimensionValue(style.margin?.right ?? Margin.zero(), DimensionComputeContext(emValue: emValue, autoValue: 0)), - bottom: computeDimensionValue(style.margin?.bottom ?? Margin.zero(), DimensionComputeContext(emValue: emValue, autoValue: 0)), - top: computeDimensionValue(style.margin?.top ?? Margin.zero(), DimensionComputeContext(emValue: emValue, autoValue: 0)), - ), - alignment: alignment, + margin: widthsAndMargins.margins, + alignment: shrinkWrap ? null : style.alignment, child: child ?? StyledText( textSpan: TextSpan( diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index 4b45459476..c56831ab35 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -112,6 +112,7 @@ class RubyElement extends ReplacedElement { tree: c, ), style: c.style, + containingBlockSize: containingBlockSize, child: Text(c.element!.innerHtml, style: c.style .generateTextStyle() @@ -120,6 +121,7 @@ class RubyElement extends ReplacedElement { ContainerSpan( renderContext: context, style: context.style, + containingBlockSize: containingBlockSize, child: node is TextContentElement ? Text((node as TextContentElement).text?.trim() ?? "", style: context.style.generateTextStyle()) : null, children: node is TextContentElement ? null : [context.parser.parseTree(context, node!)]), diff --git a/lib/src/style/compute_style.dart b/lib/src/style/compute_style.dart index db50e26e74..a2ae7e8c55 100644 --- a/lib/src/style/compute_style.dart +++ b/lib/src/style/compute_style.dart @@ -1,22 +1,163 @@ -import 'package:flutter_html/src/style/length.dart'; +import 'dart:math'; -class DimensionComputeContext { - const DimensionComputeContext({ - required this.emValue, - required this.autoValue, - }); - - final double emValue; - final double autoValue; -} +import 'package:flutter/material.dart'; +import 'package:flutter_html/style.dart'; /// [computeDimensionUnit] takes a [Dimension] and some information about the /// context where the Dimension is being used, and returns a "used" value to /// use in a rendering. -double computeDimensionValue(Dimension dimension, DimensionComputeContext computeContext) { +double _computeDimensionValue(Dimension dimension, double emValue, double autoValue) { switch (dimension.unit) { - case Unit.em: return computeContext.emValue * dimension.value; + case Unit.em: return emValue * dimension.value; case Unit.px: return dimension.value; - case Unit.auto: return computeContext.autoValue; + case Unit.auto: return autoValue; + } +} + +double _calculateEmValue(Style style, BuildContext buildContext) { + //TODO is there a better value for this? + return (style.fontSize?.size ?? 16) * + MediaQuery.textScaleFactorOf(buildContext) * + MediaQuery.of(buildContext).devicePixelRatio; +} + +/// This class handles the calculation of widths and margins during parsing: +/// See [calculate] within. +class WidthAndMargins { + final double? width; + + final EdgeInsets margins; + + const WidthAndMargins({required this.width, required this.margins}); + + /// [WidthsAndMargins.calculate] calculates any auto values ans resolves any + /// overconstraint for various elements.. + /// See https://drafts.csswg.org/css2/#Computing_widths_and_margins + static WidthAndMargins calculate(Style style, Size containingBlockSize, BuildContext buildContext) { + + final emValue = _calculateEmValue(style, buildContext); + + double? width; + double marginLeft = _computeDimensionValue(style.margin?.left ?? Margin.zero(), emValue, 0); + double marginRight = _computeDimensionValue(style.margin?.right ?? Margin.zero(), emValue, 0); + + switch(style.display ?? Display.BLOCK) { + case Display.BLOCK: + + // TODO: Handle the case of determining the width of replaced block elements in normal flow + // See https://drafts.csswg.org/css2/#block-replaced-width + + bool autoWidth = style.width == null; + bool autoMarginLeft = style.margin?.left?.unit == Unit.auto; + bool autoMarginRight = style.margin?.right?.unit == Unit.auto; + + double? overrideMarginLeft; + double? overrideMarginRight; + double? autoLeftMarginValue; + double? autoRightMarginValue; + final borderWidth = (style.border?.left.width ?? 0) + (style.border?.right.width ?? 0); + final paddingWidth = (style.padding?.left ?? 0) + (style.padding?.right ?? 0); + final nonAutoWidths = borderWidth + paddingWidth; + final nonAutoMarginWidth = marginLeft + marginRight; + + //If width is not auto, check the total width of the containing block: + if(!autoWidth) { + if(nonAutoWidths + style.width! + nonAutoMarginWidth > containingBlockSize.width) { + autoLeftMarginValue = 0; + autoRightMarginValue = 0; + autoMarginLeft = false; + autoMarginRight = false; + } + } + + //If all values are explicit, the box is over-constrained, and we must + //override one of the given margin values (left if the overconstrained + //element has a rtl directionality, and right if the overconstrained + //element has a ltr directionality). Margins must be non-negative in + //Flutter, so we set them to 0 if they go below that. + if(!autoWidth && !autoMarginLeft && !autoMarginRight) { + final difference = containingBlockSize.width - (nonAutoWidths + style.width! + nonAutoMarginWidth); + switch(style.direction) { + case TextDirection.rtl: + overrideMarginLeft = max(marginLeft + difference, 0); + break; + case TextDirection.ltr: + overrideMarginRight = max(marginRight + difference, 0); + break; + case null: + final directionality = Directionality.maybeOf(buildContext); + if(directionality != null) { + switch(directionality) { + case TextDirection.rtl: + overrideMarginLeft = max(marginLeft + difference, 0); + break; + case TextDirection.ltr: + overrideMarginRight = max(marginRight + difference, 0); + break; + } + } else { + overrideMarginRight = max(marginRight + difference, 0); + } + } + } + + //If exactly one unit is auto, calculate it from the equality. + if(autoWidth && !autoMarginLeft && !autoMarginRight) { + width = containingBlockSize.width - (nonAutoWidths + nonAutoMarginWidth); + } else if(!autoWidth && autoMarginLeft && !autoMarginRight) { + overrideMarginLeft = containingBlockSize.width - (nonAutoWidths + style.width! + marginRight); + } else if(!autoWidth && !autoMarginLeft && autoMarginRight) { + overrideMarginRight = containingBlockSize.width - (nonAutoWidths + style.width! + marginLeft); + } + + //If width is auto, set all other auto values to 0, and the width is + //calculated from the equality + if(style.width == null) { + autoLeftMarginValue = 0; + autoRightMarginValue = 0; + autoMarginLeft = false; + autoMarginRight = false; + width = containingBlockSize.width - (nonAutoMarginWidth + nonAutoWidths); + } + + //If margin-left and margin-right are both auto, their values are equal, + // and the element is centered. + if(autoMarginLeft && autoMarginRight) { + final marginWidth = containingBlockSize.width - (nonAutoWidths + style.width!); + overrideMarginLeft = marginWidth / 2; + overrideMarginRight = marginWidth / 2; + } + + marginLeft = overrideMarginLeft ?? _computeDimensionValue(style.margin?.left ?? Margin.zero(), emValue, autoLeftMarginValue ?? 0); + marginRight = overrideMarginRight ?? _computeDimensionValue(style.margin?.right ?? Margin.zero(), emValue, autoRightMarginValue ?? 0); + break; + case Display.INLINE: + case Display.INLINE_BLOCK: + + //All inline elements have a computed auto value for margin-left and right of 0. + marginLeft = _computeDimensionValue(style.margin?.left ?? Margin.zero(), emValue, 0); + marginRight = _computeDimensionValue(style.margin?.right ?? Margin.zero(), emValue, 0); + + // TODO: Handle the case of replaced inline elements and intrinsic ratio + // (See https://drafts.csswg.org/css2/#inline-replaced-width) + break; + case Display.LIST_ITEM: + // TODO: Any handling for this case? + break; + case Display.NONE: + // Do nothing + break; + } + + return WidthAndMargins( + width: width, + margins: EdgeInsets.only( + left: marginLeft, + right: marginRight, + top: _computeDimensionValue(style.margin?.top ?? Margin.zero(), emValue, 0), + bottom: _computeDimensionValue(style.margin?.bottom ?? Margin.zero(), emValue, 0), + ), + ); } + } diff --git a/packages/flutter_html_iframe/lib/iframe_mobile.dart b/packages/flutter_html_iframe/lib/iframe_mobile.dart index c3a7b01f1b..2ae61de59e 100644 --- a/packages/flutter_html_iframe/lib/iframe_mobile.dart +++ b/packages/flutter_html_iframe/lib/iframe_mobile.dart @@ -18,6 +18,7 @@ CustomRender iframeRender({NavigationDelegate? navigationDelegate}) => child: ContainerSpan( style: context.style, renderContext: context, + containingBlockSize: Size.zero, //TODO this is incorrect child: WebView( initialUrl: context.tree.element?.attributes['src'], key: key, diff --git a/packages/flutter_html_iframe/lib/iframe_web.dart b/packages/flutter_html_iframe/lib/iframe_web.dart index 7ad82190b1..fee8a95fac 100644 --- a/packages/flutter_html_iframe/lib/iframe_web.dart +++ b/packages/flutter_html_iframe/lib/iframe_web.dart @@ -39,6 +39,7 @@ CustomRender iframeRender({NavigationDelegate? navigationDelegate}) => child: ContainerSpan( style: context.style, renderContext: context, + containingBlockSize: Size.zero, //TODO this is incorrect child: Directionality( textDirection: TextDirection.ltr, child: HtmlElementView( From edc5df2d1d57c80a77216716890e099e019d6ee2 Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Sun, 28 Aug 2022 00:13:19 -0600 Subject: [PATCH 05/14] WIP: Add CSSBoxWidget to clean up box rendering --- lib/html_parser.dart | 12 +- lib/src/css_box_widget.dart | 552 +++++++++++++++++++++++++++++++ lib/src/css_parser.dart | 27 +- lib/src/html_elements.dart | 2 + lib/src/style/compute_style.dart | 85 +++-- lib/src/style/length.dart | 17 +- lib/src/style/margin.dart | 8 + lib/src/style/size.dart | 18 + lib/src/styled_element.dart | 5 +- lib/style.dart | 15 +- 10 files changed, 688 insertions(+), 53 deletions(-) create mode 100644 lib/src/css_box_widget.dart create mode 100644 lib/src/style/size.dart diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 0cbad48bcf..9d0e965f28 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -893,16 +893,18 @@ class ContainerSpan extends StatelessWidget { @override Widget build(BuildContext context) { + final bool isReplaced = REPLACED_EXTERNAL_ELEMENTS.contains(renderContext.tree.name); + //Calculate auto widths and margins: - final widthsAndMargins = WidthAndMargins.calculate(style, containingBlockSize, context); + final widthsAndMargins = WidthAndMargins.calculate(style, containingBlockSize, isReplaced, context); Widget container = Container( decoration: BoxDecoration( border: style.border, color: style.backgroundColor, ), - height: style.height, - width: style.width, //widthsAndMargins.width, + height: style.height?.value, //TODO + width: widthsAndMargins.width, padding: style.padding?.nonNegative, margin: widthsAndMargins.margins, alignment: shrinkWrap ? null : style.alignment, @@ -917,7 +919,9 @@ class ContainerSpan extends StatelessWidget { ), ); - return container; + return LayoutBuilder(builder: (context, constraints) { + return container; + }); } } diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart new file mode 100644 index 0000000000..5b404b6181 --- /dev/null +++ b/lib/src/css_box_widget.dart @@ -0,0 +1,552 @@ +import 'dart:math' as math; + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_html/flutter_html.dart'; + +class CSSBoxWidget extends StatelessWidget { + CSSBoxWidget({ + this.key, + required this.child, + required this.style, + this.childIsReplaced = false, + this.shrinkWrap = false, + }): super(key: key); + + /// An optional anchor key to use in finding this box + final AnchorKey? key; + + /// The child to be rendered within the CSS Box. + final Widget child; + + /// The style to use to compute this box's margins/padding/box decoration/width/height/etc. + /// + /// Note that this style will only apply to this box, and will not cascade to its child. + final Style style; + + /// Indicates whether this child is a replaced element that manages its own width + /// (e.g. img, video, iframe, audio, etc.) + final bool childIsReplaced; + + /// Whether or not the content should take its minimum possible width + /// TODO TODO TODO + final bool shrinkWrap; + + @override + Widget build(BuildContext context) { + return Container( + child: _CSSBoxRenderer( + width: style.width ?? Width.auto(), + height: style.height ?? Height.auto(), + paddingSize: style.padding?.collapsedSize ?? Size.zero, + borderSize: style.border?.dimensions.collapsedSize ?? Size.zero, + margins: style.margin ?? Margins.zero, + display: style.display ?? Display.BLOCK, + childIsReplaced: childIsReplaced, + emValue: _calculateEmValue(style, context), + child: Container( + decoration: BoxDecoration( + border: style.border, + color: style.backgroundColor, //Colors the padding and content boxes + ), + width: ((style.display == Display.BLOCK || style.display == Display.LIST_ITEM) && !childIsReplaced && !shrinkWrap) + ? double.infinity + : null, + padding: style.padding ?? EdgeInsets.zero, + child: child, + ), + ), + ); + } +} + +class _CSSBoxRenderer extends MultiChildRenderObjectWidget { + _CSSBoxRenderer({ + Key? key, + required Widget child, + required this.display, + required this.margins, + required this.width, + required this.height, + required this.borderSize, + required this.paddingSize, + required this.childIsReplaced, + required this.emValue, + }) : super(key: key, children: [child]); + + /// The Display type of the element + final Display display; + + /// The computed margin values for this element + final Margins margins; + + /// The width of the element + final Width width; + + /// The height of the element + final Height height; + + /// The collapsed size of the element's border + final Size borderSize; + + /// The collapsed size of the element's padding + final Size paddingSize; + + /// Whether or not the child being rendered is a replaced element + /// (this changes the rules for rendering) + final bool childIsReplaced; + + /// The calculated size of 1em in pixels + final double emValue; + + @override + _RenderCSSBox createRenderObject(BuildContext context) { + return _RenderCSSBox( + display: display, + width: width..normalize(emValue), + height: height..normalize(emValue), + margins: _preProcessMargins(margins), + borderSize: borderSize, + paddingSize: paddingSize, + childIsReplaced: childIsReplaced, + ); + } + + @override + void updateRenderObject(BuildContext context, _RenderCSSBox renderObject) { + renderObject + ..display = display + ..width = (width..normalize(emValue)) + ..height = (height..normalize(emValue)) + ..margins = _preProcessMargins(margins) + ..borderSize = borderSize + ..paddingSize = paddingSize + ..childIsReplaced = childIsReplaced; + } + + Margins _preProcessMargins(Margins margins) { + Margin leftMargin = margins.left ?? Margin.zero(); + Margin rightMargin = margins.right ?? Margin.zero(); + Margin topMargin = margins.top ?? Margin.zero(); + Margin bottomMargin = margins.bottom ?? Margin.zero(); + + //Preprocess margins to a pixel value + leftMargin.normalize(emValue); + rightMargin.normalize(emValue); + topMargin.normalize(emValue); + bottomMargin.normalize(emValue); + + // See https://drafts.csswg.org/css2/#inline-width + // and https://drafts.csswg.org/css2/#inline-replaced-width + // and https://drafts.csswg.org/css2/#inlineblock-width + // and https://drafts.csswg.org/css2/#inlineblock-replaced-width + if (display == Display.INLINE || display == Display.INLINE_BLOCK) { + if (margins.left?.unit == Unit.auto) { + leftMargin = Margin.zero(); + } + if (margins.right?.unit == Unit.auto) { + rightMargin = Margin.zero(); + } + } + + return Margins( + top: topMargin, + right: rightMargin, + bottom: bottomMargin, + left: leftMargin, + ); + } +} + +/// Implements the CSS layout algorithm +class _RenderCSSBox extends RenderBox + with + ContainerRenderObjectMixin, + RenderBoxContainerDefaultsMixin { + _RenderCSSBox({ + required Display display, + required Width width, + required Height height, + required Margins margins, + required Size borderSize, + required Size paddingSize, + required bool childIsReplaced, + }) : _display = display, + _width = width, + _height = height, + _margins = margins, + _borderSize = borderSize, + _paddingSize = paddingSize, + _childIsReplaced = childIsReplaced; + + Display _display; + + Display get display => _display; + + set display(Display display) { + _display = display; + markNeedsLayout(); + } + + Width _width; + + Width get width => _width; + + set width(Width width) { + _width = width; + markNeedsLayout(); + } + + Height _height; + + Height get height => _height; + + set height(Height height) { + _height = height; + markNeedsLayout(); + } + + Margins _margins; + + Margins get margins => _margins; + + set margins(Margins margins) { + _margins = margins; + markNeedsLayout(); + } + + Size _borderSize; + + Size get borderSize => _borderSize; + + set borderSize(Size size) { + _borderSize = size; + markNeedsLayout(); + } + + Size _paddingSize; + + Size get paddingSize => _paddingSize; + + set paddingSize(Size size) { + _paddingSize = size; + markNeedsLayout(); + } + + bool _childIsReplaced; + + bool get childIsReplaced => _childIsReplaced; + + set childIsReplaced(bool childIsReplaced) { + _childIsReplaced = childIsReplaced; + markNeedsLayout(); + } + + @override + void setupParentData(RenderBox child) { + if (child.parentData is! CSSBoxParentData) + child.parentData = CSSBoxParentData(); + } + + static double getIntrinsicDimension(RenderBox? firstChild, + double Function(RenderBox child) mainChildSizeGetter) { + double extent = 0.0; + RenderBox? child = firstChild; + while (child != null) { + final CSSBoxParentData childParentData = + child.parentData! as CSSBoxParentData; + extent = math.max(extent, mainChildSizeGetter(child)); + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + } + return extent; + } + + @override + double computeMinIntrinsicWidth(double height) { + return getIntrinsicDimension( + firstChild, (RenderBox child) => child.getMinIntrinsicWidth(height)); + } + + @override + double computeMaxIntrinsicWidth(double height) { + return getIntrinsicDimension( + firstChild, (RenderBox child) => child.getMaxIntrinsicWidth(height)); + } + + @override + double computeMinIntrinsicHeight(double width) { + return getIntrinsicDimension( + firstChild, (RenderBox child) => child.getMinIntrinsicHeight(width)); + } + + @override + double computeMaxIntrinsicHeight(double width) { + return getIntrinsicDimension( + firstChild, (RenderBox child) => child.getMaxIntrinsicHeight(width)); + } + + @override + double? computeDistanceToActualBaseline(TextBaseline baseline) { + return defaultComputeDistanceToHighestActualBaseline(baseline); + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + return _computeSize( + constraints: constraints, + layoutChild: ChildLayoutHelper.dryLayoutChild, + ).parentSize; + } + + _Sizes _computeSize( + {required BoxConstraints constraints, + required ChildLayouter layoutChild}) { + if (childCount == 0) { + return _Sizes(constraints.biggest, Size.zero); + } + + Size containingBlockSize = constraints.biggest; + double width = containingBlockSize.width; + double height = containingBlockSize.height; + + RenderBox? child = firstChild; + assert(child != null); + + // Calculate child size + final childConstraints = constraints.copyWith( + maxWidth: (this.width.unit != Unit.auto) + ? this.width.value + : constraints.maxWidth - + (this.margins.left?.value ?? 0) - + (this.margins.right?.value ?? 0), + maxHeight: constraints.maxHeight - + (this.margins.top?.value ?? 0) - + (this.margins.bottom?.value ?? 0), + minWidth: 0, + minHeight: 0, + ); + final Size childSize = layoutChild(child!, childConstraints); + + // Calculate used values of margins based on rules + final usedMargins = _calculateUsedMargins(childSize, containingBlockSize); + final horizontalMargins = + (usedMargins.left?.value ?? 0) + (usedMargins.right?.value ?? 0); + final verticalMargins = + (usedMargins.top?.value ?? 0) + (usedMargins.bottom?.value ?? 0); + + //Calculate Width and Height of CSS Box + height = childSize.height; + switch (display) { + case Display.BLOCK: + width = containingBlockSize.width; + height = childSize.height + verticalMargins; + break; + case Display.INLINE: + width = childSize.width + horizontalMargins; + height = childSize.height; + break; + case Display.INLINE_BLOCK: + width = childSize.width + horizontalMargins; + height = childSize.height + verticalMargins; + break; + case Display.LIST_ITEM: + width = containingBlockSize.width; + height = childSize.height + verticalMargins; + break; + case Display.NONE: + width = 0; + height = 0; + break; + } + + return _Sizes(constraints.constrain(Size(width, height)), childSize); + } + + @override + void performLayout() { + final BoxConstraints constraints = this.constraints; + + final sizes = _computeSize( + constraints: constraints, + layoutChild: ChildLayoutHelper.layoutChild, + ); + size = sizes.parentSize; + + RenderBox? child = firstChild; + while (child != null) { + final CSSBoxParentData childParentData = + child.parentData! as CSSBoxParentData; + + // Calculate used margins based on constraints and child size + final usedMargins = + _calculateUsedMargins(sizes.childSize, constraints.biggest); + final leftMargin = usedMargins.left?.value ?? 0; + final topMargin = usedMargins.top?.value ?? 0; + + double leftOffset = 0; + double topOffset = 0; + switch (display) { + case Display.BLOCK: + leftOffset = leftMargin; + topOffset = topMargin; + break; + case Display.INLINE: + leftOffset = leftMargin; + break; + case Display.INLINE_BLOCK: + leftOffset = leftMargin; + topOffset = topMargin; + break; + case Display.LIST_ITEM: + leftOffset = leftMargin; + topOffset = topMargin; + break; + case Display.NONE: + //No offset + break; + } + childParentData.offset = Offset(leftOffset, topOffset); + + assert(child.parentData == childParentData); + child = childParentData.nextSibling; + } + } + + Margins _calculateUsedMargins(Size childSize, Size containingBlockSize) { + //We assume that margins have already been preprocessed + // (i.e. they are non-null and either px units or auto. + assert(margins.left != null && margins.right != null); + assert(margins.left!.unit == Unit.px || margins.left!.unit == Unit.auto); + assert(margins.right!.unit == Unit.px || margins.right!.unit == Unit.auto); + + Margin marginLeft = margins.left!; + Margin marginRight = margins.right!; + + bool widthIsAuto = width.unit == Unit.auto; + bool marginLeftIsAuto = marginLeft.unit == Unit.auto; + bool marginRightIsAuto = marginRight.unit == Unit.auto; + + if (display == Display.BLOCK) { + if (childIsReplaced) { + widthIsAuto = false; + } + + //If width is not auto and the width of the margin box is larger than the + // width of the containing block, then consider left and right margins to + // have a 0 value. + if (!widthIsAuto) { + if ((childSize.width + marginLeft.value + marginRight.value) > + containingBlockSize.width) { + //Treat auto values of margin left and margin right as 0 for following rules + marginLeft = Margin(0); + marginRight = Margin(0); + marginLeftIsAuto = false; + marginRightIsAuto = false; + } + } + + // If all values are non-auto, the box is overconstrained. + // One of the margins will need to be ignored. + if (!widthIsAuto && !marginLeftIsAuto && !marginRightIsAuto) { + //TODO ignore either left or right margin based on directionality of parent widgets. + //For now, assume ltr, and just ignore the right margin. + final difference = + containingBlockSize.width - childSize.width - marginLeft.value; + marginRight = Margin(difference); + } + + // If there is exactly one value specified as auto, compute it value from the equality (our widths are already set) + if (widthIsAuto && !marginLeftIsAuto && !marginRightIsAuto) { + widthIsAuto = false; + } else if (!widthIsAuto && marginLeftIsAuto && !marginRightIsAuto) { + marginLeft = Margin( + containingBlockSize.width - childSize.width - marginRight.value); + marginLeftIsAuto = false; + } else if (!widthIsAuto && !marginLeftIsAuto && marginRightIsAuto) { + marginRight = Margin( + containingBlockSize.width - childSize.width - marginLeft.value); + marginRightIsAuto = false; + } + + //If width is set to auto, any other auto values become 0, and width + // follows from the resulting equality. + if (widthIsAuto) { + if (marginLeftIsAuto) { + marginLeft = Margin(0); + marginLeftIsAuto = false; + } + if (marginRightIsAuto) { + marginRight = Margin(0); + marginRightIsAuto = false; + } + widthIsAuto = false; + } + + //If both margin-left and margin-right are auto, their used values are equal. + // This horizontally centers the element within the containing block. + if (marginLeftIsAuto && marginRightIsAuto) { + final newMargin = + Margin((containingBlockSize.width - childSize.width) / 2); + marginLeft = newMargin; + marginRight = newMargin; + marginLeftIsAuto = false; + marginRightIsAuto = false; + } + + //Assert that all auto values have been assigned. + assert(!marginLeftIsAuto && !marginRightIsAuto && !widthIsAuto); + } + + return Margins( + left: marginLeft, + right: marginRight, + top: margins.top, + bottom: margins.bottom); + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + return defaultHitTestChildren(result, position: position); + } + + @override + void paint(PaintingContext context, Offset offset) { + defaultPaint(context, offset); + } + + @override + void dispose() { + super.dispose(); + } +} + +extension Normalize on Dimension { + void normalize(double emValue) { + switch (this.unit) { + case Unit.em: + this.value *= emValue; + this.unit = Unit.px; + return; + case Unit.px: + case Unit.auto: + return; + } + } +} + +double _calculateEmValue(Style style, BuildContext buildContext) { + //TODO is there a better value for this? + return (style.fontSize?.size ?? 16) * + MediaQuery.textScaleFactorOf(buildContext) * + MediaQuery.of(buildContext).devicePixelRatio; +} + +class CSSBoxParentData extends ContainerBoxParentData {} + +class _Sizes { + final Size parentSize; + final Size childSize; + + const _Sizes(this.parentSize, this.childSize); +} diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index 4b9d498d6a..a82e2a9589 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -5,8 +5,6 @@ import 'package:csslib/visitor.dart' as css; import 'package:csslib/parser.dart' as cssparser; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; -import 'package:flutter_html/src/style/length.dart'; -import 'package:flutter_html/src/style/margin.dart'; import 'package:flutter_html/src/utils.dart'; Style declarationsToStyle(Map> declarations) { @@ -232,7 +230,7 @@ Style declarationsToStyle(Map> declarations) { } break; case 'height': - style.height = ExpressionMapping.expressionToPaddingLength(value.first) ?? style.height; + style.height = ExpressionMapping.expressionToHeight(value.first) ?? style.height; break; case 'list-style-type': if (value.first is css.LiteralTerm) { @@ -352,7 +350,7 @@ Style declarationsToStyle(Map> declarations) { } break; case 'width': - style.width = ExpressionMapping.expressionToPaddingLength(value.first) ?? style.width; + style.width = ExpressionMapping.expressionToWidth(value.first) ?? style.width; break; } } @@ -751,11 +749,30 @@ class ExpressionMapping { return null; } + static Width? expressionToWidth(css.Expression value) { + if ((value is css.LiteralTerm) && value.text == 'auto') { + return Width.auto(); + } else { + final computedValue = expressionToLengthOrPercent(value); + return Width(computedValue.value, computedValue.unit); + } + } + + static Height? expressionToHeight(css.Expression value) { + if ((value is css.LiteralTerm) && value.text == 'auto') { + return Height.auto(); + } else { + final computedValue = expressionToLengthOrPercent(value); + return Height(computedValue.value, computedValue.unit); + } + } + static Margin? expressionToMargin(css.Expression value) { if ((value is css.LiteralTerm) && value.text == 'auto') { return Margin.auto(); } else { - return Margin(expressionToPaddingLength(value) ?? 0); + final computedValue = expressionToLengthOrPercent(value); + return Margin(computedValue.value, computedValue.unit); } } diff --git a/lib/src/html_elements.dart b/lib/src/html_elements.dart index 4b096d8f5d..3899d54a06 100644 --- a/lib/src/html_elements.dart +++ b/lib/src/html_elements.dart @@ -131,6 +131,8 @@ const TABLE_DEFINITION_ELEMENTS = ["col", "colgroup"]; const EXTERNAL_ELEMENTS = ["audio", "iframe", "img", "math", "svg", "table", "video"]; +const REPLACED_EXTERNAL_ELEMENTS = ["iframe", "img", "video", "audio"]; + const SELECTABLE_ELEMENTS = [ "br", "a", diff --git a/lib/src/style/compute_style.dart b/lib/src/style/compute_style.dart index a2ae7e8c55..ec550dae24 100644 --- a/lib/src/style/compute_style.dart +++ b/lib/src/style/compute_style.dart @@ -33,28 +33,39 @@ class WidthAndMargins { /// [WidthsAndMargins.calculate] calculates any auto values ans resolves any /// overconstraint for various elements.. /// See https://drafts.csswg.org/css2/#Computing_widths_and_margins - static WidthAndMargins calculate(Style style, Size containingBlockSize, BuildContext buildContext) { + static WidthAndMargins calculate( + Style style, + Size containingBlockSize, + bool isReplaced, + BuildContext buildContext, + ) { final emValue = _calculateEmValue(style, buildContext); - double? width; + double? width = _computeDimensionValue(style.width ?? Width.auto(), emValue, 0); double marginLeft = _computeDimensionValue(style.margin?.left ?? Margin.zero(), emValue, 0); double marginRight = _computeDimensionValue(style.margin?.right ?? Margin.zero(), emValue, 0); + bool autoWidth = style.width?.unit == Unit.auto || style.width == null; + bool autoMarginLeft = style.margin?.left?.unit == Unit.auto; + bool autoMarginRight = style.margin?.right?.unit == Unit.auto; + switch(style.display ?? Display.BLOCK) { case Display.BLOCK: - // TODO: Handle the case of determining the width of replaced block elements in normal flow - // See https://drafts.csswg.org/css2/#block-replaced-width - - bool autoWidth = style.width == null; - bool autoMarginLeft = style.margin?.left?.unit == Unit.auto; - bool autoMarginRight = style.margin?.right?.unit == Unit.auto; + if(isReplaced && autoWidth) { + //TODO calculate width as for inline replaced element + // See https://drafts.csswg.org/css2/#block-replaced-width + //For now, just let the element calculate its own width + width = null; + } double? overrideMarginLeft; double? overrideMarginRight; + double? autoLeftMarginValue; double? autoRightMarginValue; + final borderWidth = (style.border?.left.width ?? 0) + (style.border?.right.width ?? 0); final paddingWidth = (style.padding?.left ?? 0) + (style.padding?.right ?? 0); final nonAutoWidths = borderWidth + paddingWidth; @@ -62,7 +73,7 @@ class WidthAndMargins { //If width is not auto, check the total width of the containing block: if(!autoWidth) { - if(nonAutoWidths + style.width! + nonAutoMarginWidth > containingBlockSize.width) { + if(nonAutoWidths + (width ?? 0) + nonAutoMarginWidth > containingBlockSize.width) { autoLeftMarginValue = 0; autoRightMarginValue = 0; autoMarginLeft = false; @@ -75,8 +86,8 @@ class WidthAndMargins { //element has a rtl directionality, and right if the overconstrained //element has a ltr directionality). Margins must be non-negative in //Flutter, so we set them to 0 if they go below that. - if(!autoWidth && !autoMarginLeft && !autoMarginRight) { - final difference = containingBlockSize.width - (nonAutoWidths + style.width! + nonAutoMarginWidth); + if(!autoWidth && !autoMarginLeft && !autoMarginRight && width != null) { + final difference = containingBlockSize.width - (nonAutoWidths + width + nonAutoMarginWidth); switch(style.direction) { case TextDirection.rtl: overrideMarginLeft = max(marginLeft + difference, 0); @@ -102,28 +113,28 @@ class WidthAndMargins { } //If exactly one unit is auto, calculate it from the equality. - if(autoWidth && !autoMarginLeft && !autoMarginRight) { + if(autoWidth && !autoMarginLeft && !autoMarginRight && width != null) { width = containingBlockSize.width - (nonAutoWidths + nonAutoMarginWidth); - } else if(!autoWidth && autoMarginLeft && !autoMarginRight) { - overrideMarginLeft = containingBlockSize.width - (nonAutoWidths + style.width! + marginRight); - } else if(!autoWidth && !autoMarginLeft && autoMarginRight) { - overrideMarginRight = containingBlockSize.width - (nonAutoWidths + style.width! + marginLeft); + } else if((!autoWidth || width==null) && autoMarginLeft && !autoMarginRight) { + overrideMarginLeft = containingBlockSize.width - (nonAutoWidths + (width ?? 0) + marginRight); + } else if((!autoWidth || width == null) && !autoMarginLeft && autoMarginRight) { + overrideMarginRight = containingBlockSize.width - (nonAutoWidths + (width ?? 0) + marginLeft); } //If width is auto, set all other auto values to 0, and the width is //calculated from the equality - if(style.width == null) { + if(autoWidth && width != null) { autoLeftMarginValue = 0; autoRightMarginValue = 0; autoMarginLeft = false; autoMarginRight = false; - width = containingBlockSize.width - (nonAutoMarginWidth + nonAutoWidths); + width = containingBlockSize.width - (nonAutoWidths + nonAutoMarginWidth); } //If margin-left and margin-right are both auto, their values are equal, // and the element is centered. if(autoMarginLeft && autoMarginRight) { - final marginWidth = containingBlockSize.width - (nonAutoWidths + style.width!); + final marginWidth = containingBlockSize.width - (nonAutoWidths + (width ?? 0)); overrideMarginLeft = marginWidth / 2; overrideMarginRight = marginWidth / 2; } @@ -132,13 +143,41 @@ class WidthAndMargins { marginRight = overrideMarginRight ?? _computeDimensionValue(style.margin?.right ?? Margin.zero(), emValue, autoRightMarginValue ?? 0); break; case Display.INLINE: + //All inline elements have a computed auto value for margin of 0. + if(autoMarginLeft) { + marginLeft = 0; + } + if(autoMarginRight) { + marginRight = 0; + } + if(isReplaced) { + //TODO calculate intrinsic width + //For now, we can just let the element calculate its own width! + width = null; + } + else { + width = null; + } + break; case Display.INLINE_BLOCK: + //All inline elements have a computed auto value for margin of 0. + if(autoMarginLeft) { + marginLeft = 0; + } + if(autoMarginRight) { + marginRight = 0; + } + if(isReplaced) { + //TODO calculate intrinsic width + //For now, we can just let the element calculate its own width! + width = null; + } else { + //TODO calculate shrink-to-fit width for auto widths. + //For now, we can just let the element calculate its own width! + width = null; + } - //All inline elements have a computed auto value for margin-left and right of 0. - marginLeft = _computeDimensionValue(style.margin?.left ?? Margin.zero(), emValue, 0); - marginRight = _computeDimensionValue(style.margin?.right ?? Margin.zero(), emValue, 0); - // TODO: Handle the case of replaced inline elements and intrinsic ratio // (See https://drafts.csswg.org/css2/#inline-replaced-width) break; case Display.LIST_ITEM: diff --git a/lib/src/style/length.dart b/lib/src/style/length.dart index e483f847e1..4853201fd7 100644 --- a/lib/src/style/length.dart +++ b/lib/src/style/length.dart @@ -2,7 +2,7 @@ const int _percent = 0x1; const int _length = 0x2; const int _auto = 0x4; const int _lengthPercent = _length | _percent; -const int _margin = _lengthPercent | _auto; +const int _lengthPercentAuto = _lengthPercent | _auto; //TODO there are more unit-types that need support enum Unit { @@ -22,8 +22,8 @@ enum Unit { /// Represents a CSS dimension https://drafts.csswg.org/css-values/#dimensions abstract class Dimension { - final double value; - final Unit unit; + double value; + Unit unit; Dimension(this.value, this.unit) { assert((unit.unitType | _unitType) == _unitType, "You used a unit for the property that was not allowed"); @@ -46,13 +46,8 @@ class LengthOrPercent extends Dimension { int get _unitType => _lengthPercent; } -class Margin extends Dimension { - Margin(double value, [Unit? unit = Unit.px]): super(value, unit ?? Unit.px); +class AutoOrLengthOrPercent extends Dimension { + AutoOrLengthOrPercent(double value, [Unit unit = Unit.px]): super(value, unit); - Margin.auto(): super(0, Unit.auto); - - Margin.zero(): super(0, Unit.px); - - @override - int get _unitType => _margin; + int get _unitType => _lengthPercentAuto; } \ No newline at end of file diff --git a/lib/src/style/margin.dart b/lib/src/style/margin.dart index 23cd887f3d..91c87f0a86 100644 --- a/lib/src/style/margin.dart +++ b/lib/src/style/margin.dart @@ -1,6 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/src/style/length.dart'; +class Margin extends AutoOrLengthOrPercent { + Margin(double value, [Unit? unit = Unit.px]): super(value, unit ?? Unit.px); + + Margin.auto(): super(0, Unit.auto); + + Margin.zero(): super(0, Unit.px); +} + class Margins { final Margin? left; final Margin? right; diff --git a/lib/src/style/size.dart b/lib/src/style/size.dart new file mode 100644 index 0000000000..9891131e38 --- /dev/null +++ b/lib/src/style/size.dart @@ -0,0 +1,18 @@ +import 'package:flutter_html/flutter_html.dart'; + +/// The [Width] class takes in a value and units, and defaults to px if no +/// units are provided. A helper constructor, [Width.auto] constructor is +/// provided for convenience. +class Width extends AutoOrLengthOrPercent { + Width(super.value, [super.unit = Unit.px]): + assert(value >= 0, 'Width value must be non-negative'); + + Width.auto(): super(0, Unit.auto); +} + +class Height extends AutoOrLengthOrPercent { + Height(super.value, [super.unit = Unit.px]): + assert(value >= 0, 'Height value must be non-negative'); + + Height.auto(): super(0, Unit.auto); +} \ No newline at end of file diff --git a/lib/src/styled_element.dart b/lib/src/styled_element.dart index 3fb52d9984..d02288a118 100644 --- a/lib/src/styled_element.dart +++ b/lib/src/styled_element.dart @@ -251,9 +251,8 @@ StyledElement parseStyledElement( break; case "hr": styledElement.style = Style( - margin: Margins.symmetric(vertical: 7.0), - width: double.infinity, - height: 1, + margin: Margins.symmetric(vertical: 0.5, unit: Unit.em), + height: Height(1), backgroundColor: Colors.black, display: Display.BLOCK, ); diff --git a/lib/style.dart b/lib/style.dart index 5f0326913b..fb1365b527 100644 --- a/lib/style.dart +++ b/lib/style.dart @@ -4,9 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/src/css_parser.dart'; -//Export Margin API +//Export Style value-unit APIs export 'package:flutter_html/src/style/margin.dart'; export 'package:flutter_html/src/style/length.dart'; +export 'package:flutter_html/src/style/size.dart'; ///This class represents all the available CSS attributes ///for this package. @@ -76,8 +77,8 @@ class Style { /// CSS attribute "`height`" /// /// Inherited: no, - /// Default: Unspecified (null), - double? height; + /// Default: Height.auto(), + Height? height; /// CSS attribute "`letter-spacing`" /// @@ -163,8 +164,8 @@ class Style { /// CSS attribute "`width`" /// /// Inherited: no, - /// Default: unspecified (null) - double? width; + /// Default: Width.auto() + Width? width; /// CSS attribute "`word-spacing`" /// @@ -389,7 +390,7 @@ class Style { FontSize? fontSize, FontStyle? fontStyle, FontWeight? fontWeight, - double? height, + Height? height, LineHeight? lineHeight, double? letterSpacing, ListStyleType? listStyleType, @@ -404,7 +405,7 @@ class Style { List? textShadow, VerticalAlign? verticalAlign, WhiteSpace? whiteSpace, - double? width, + Width? width, double? wordSpacing, String? before, String? after, From bb9145e3812c565b94295c6d1e1a004508c6252c Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Thu, 1 Sep 2022 22:17:34 -0600 Subject: [PATCH 06/14] WIP: New CSSBoxWidget bugfixes --- lib/custom_render.dart | 82 ++++++------ lib/flutter_html.dart | 82 ++++++------ lib/html_parser.dart | 203 ++++-------------------------- lib/src/css_box_widget.dart | 177 +++++++++++++++++++++----- lib/src/interactable_element.dart | 5 - lib/src/layout_element.dart | 39 ++---- lib/src/replaced_element.dart | 51 ++++---- lib/src/style/compute_style.dart | 202 ----------------------------- lib/src/styled_element.dart | 17 +-- test/html_parser_test.dart | 8 +- 10 files changed, 288 insertions(+), 578 deletions(-) delete mode 100644 lib/src/style/compute_style.dart diff --git a/lib/custom_render.dart b/lib/custom_render.dart index 5174c214ad..a9b7dd7740 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -6,6 +6,8 @@ import 'dart:convert'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_html/src/css_box_widget.dart'; +import 'package:flutter_html/src/html_elements.dart'; import 'package:flutter_html/src/utils.dart'; typedef CustomRenderMatcher = bool Function(RenderContext context); @@ -15,7 +17,8 @@ CustomRenderMatcher tagMatcher(String tag) => (context) { }; CustomRenderMatcher blockElementMatcher() => (context) { - return context.tree.style.display == Display.BLOCK && + return (context.tree.style.display == Display.BLOCK || + context.tree.style.display == Display.INLINE_BLOCK) && (context.tree.children.isNotEmpty || context.tree.element?.localName == "hr"); }; @@ -108,7 +111,7 @@ class SelectableCustomRender extends CustomRender { }) : super.inlineSpan(inlineSpan: null); } -CustomRender blockElementRender({Style? style, List? children, required Size containingBlockSize}) => +CustomRender blockElementRender({Style? style, List? children}) => CustomRender.inlineSpan(inlineSpan: (context, buildChildren) { if (context.parser.selectable) { return TextSpan( @@ -127,36 +130,36 @@ CustomRender blockElementRender({Style? style, List? children, requi ); } return WidgetSpan( - child: ContainerSpan( - key: context.key, - renderContext: context, - style: style ?? context.tree.style, - shrinkWrap: context.parser.shrinkWrap, - containingBlockSize: containingBlockSize, - children: children ?? - context.tree.children - .expandIndexed((i, childTree) => [ - 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(), - )); + alignment: PlaceholderAlignment.baseline, + baseline: TextBaseline.alphabetic, + child: CSSBoxWidget.withInlineSpanChildren( + key: context.key, + style: style ?? context.tree.style, + shrinkWrap: context.parser.shrinkWrap, + childIsReplaced: REPLACED_EXTERNAL_ELEMENTS.contains(context.tree.name), + children: children ?? + context.tree.children + .expandIndexed((i, childTree) => [ + context.parser.parseTree(context, childTree), + //TODO can this newline be added in a different step? + if (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, required Size containingBlockSize}) => + {Style? style, Widget? child, List? children}) => CustomRender.inlineSpan( inlineSpan: (context, buildChildren) => WidgetSpan( - child: ContainerSpan( + child: CSSBoxWidget( key: context.key, - renderContext: context, style: style ?? context.tree.style, shrinkWrap: context.parser.shrinkWrap, - containingBlockSize: containingBlockSize, child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -203,9 +206,8 @@ CustomRender listElementRender( ? 10.0 : 0.0) : EdgeInsets.zero, - child: StyledText( - textSpan: TextSpan( - children: _getListElementChildren( + child: CSSBoxWidget.withInlineSpanChildren( + children: _getListElementChildren( style?.listStylePosition ?? context.tree.style.listStylePosition, buildChildren) @@ -225,16 +227,15 @@ CustomRender listElementRender( height: 0, width: 0)) ] : []), - style: style?.generateTextStyle() ?? - context.style.generateTextStyle(), - ), style: style ?? context.style, - renderContext: context, - ))) + ), + ), + ), ], ), ), - )); + ), + ); CustomRender replacedElementRender( {PlaceholderAlignment? alignment, @@ -475,14 +476,9 @@ CustomRender verticalAlignRender( key: context.key, offset: Offset( 0, verticalOffset ?? _getVerticalOffset(context.tree)), - child: StyledText( - textSpan: TextSpan( - style: style?.generateTextStyle() ?? - context.style.generateTextStyle(), - children: children ?? buildChildren.call(), - ), + child: CSSBoxWidget.withInlineSpanChildren( + children: children ?? buildChildren.call(), style: context.style, - renderContext: context, ), ), )); @@ -505,10 +501,10 @@ CustomRender fallbackRender({Style? style, List? children}) => .toList(), )); -Map generateDefaultRenders(Size containingBlockSize) { +Map generateDefaultRenders() { return { - blockElementMatcher(): blockElementRender(containingBlockSize: containingBlockSize), - listElementMatcher(): listElementRender(containingBlockSize: containingBlockSize), + blockElementMatcher(): blockElementRender(), + listElementMatcher(): listElementRender(), textContentElementMatcher(): textContentElementRender(), dataUriMatcher(): base64ImageRender(), assetUriMatcher(): assetImageRender(), diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 8e3339dd82..63ff0a621e 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -177,29 +177,21 @@ class _HtmlState extends State { @override Widget build(BuildContext context) { - return Container( - width: widget.shrinkWrap ? null : MediaQuery.of(context).size.width, - child: LayoutBuilder( - builder: (context, constraints) { - return HtmlParser( - key: widget._anchorKey, - htmlData: documentElement, - onLinkTap: widget.onLinkTap, - onAnchorTap: widget.onAnchorTap, - onImageTap: widget.onImageTap, - onCssParseError: widget.onCssParseError, - onImageError: widget.onImageError, - shrinkWrap: widget.shrinkWrap, - selectable: false, - style: widget.style, - customRenders: {} - ..addAll(widget.customRenders) - ..addAll(generateDefaultRenders(MediaQuery.of(context).size)), - tagsList: widget.tagsList.isEmpty ? Html.tags : widget.tagsList, - constraints: constraints, - ); - } - ), + return HtmlParser( + key: widget._anchorKey, + htmlData: documentElement, + onLinkTap: widget.onLinkTap, + onAnchorTap: widget.onAnchorTap, + onImageTap: widget.onImageTap, + onCssParseError: widget.onCssParseError, + onImageError: widget.onImageError, + shrinkWrap: widget.shrinkWrap, + selectable: false, + style: widget.style, + customRenders: {} + ..addAll(widget.customRenders) + ..addAll(generateDefaultRenders()), + tagsList: widget.tagsList.isEmpty ? Html.tags : widget.tagsList, ); } } @@ -311,7 +303,9 @@ class SelectableHtml extends StatefulWidget { final OnCssParseError? onCssParseError; /// A parameter that should be set when the HTML widget is expected to be - /// flexible + /// have a flexible width, that doesn't always fill its maximum width + /// constraints. For example, auto horizontal margins are ignored, and + /// block-level elements only take up the width they need. final bool shrinkWrap; /// A list of HTML tags that are the only tags that are rendered. By default, this list is empty and all supported HTML tags are rendered. @@ -352,29 +346,23 @@ class _SelectableHtmlState extends State { Widget build(BuildContext context) { return Container( width: widget.shrinkWrap ? null : MediaQuery.of(context).size.width, - child: LayoutBuilder( - builder: (context, constraints) { - return HtmlParser( - key: widget._anchorKey, - htmlData: documentElement, - onLinkTap: widget.onLinkTap, - onAnchorTap: widget.onAnchorTap, - onImageTap: null, - onCssParseError: widget.onCssParseError, - onImageError: null, - shrinkWrap: widget.shrinkWrap, - selectable: true, - style: widget.style, - customRenders: {} - ..addAll(widget.customRenders) - ..addAll(generateDefaultRenders(MediaQuery.of(context).size)), - tagsList: - widget.tagsList.isEmpty ? SelectableHtml.tags : widget.tagsList, - selectionControls: widget.selectionControls, - scrollPhysics: widget.scrollPhysics, - constraints: constraints, - ); - } + child: HtmlParser( + key: widget._anchorKey, + htmlData: documentElement, + onLinkTap: widget.onLinkTap, + onAnchorTap: widget.onAnchorTap, + onImageTap: null, + onCssParseError: widget.onCssParseError, + onImageError: null, + shrinkWrap: widget.shrinkWrap, + selectable: true, + style: widget.style, + customRenders: {} + ..addAll(widget.customRenders) + ..addAll(generateDefaultRenders()), + tagsList: widget.tagsList.isEmpty ? SelectableHtml.tags : widget.tagsList, + selectionControls: widget.selectionControls, + scrollPhysics: widget.scrollPhysics, ), ); } diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 9d0e965f28..225a9690a5 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -6,9 +6,9 @@ import 'package:csslib/parser.dart' as cssparser; import 'package:csslib/visitor.dart' as css; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_html/src/css_box_widget.dart'; import 'package:flutter_html/src/css_parser.dart'; import 'package:flutter_html/src/html_elements.dart'; -import 'package:flutter_html/src/style/compute_style.dart'; import 'package:flutter_html/src/utils.dart'; import 'package:html/dom.dart' as dom; import 'package:html/parser.dart' as htmlparser; @@ -43,7 +43,6 @@ class HtmlParser extends StatelessWidget { final Html? root; final TextSelectionControls? selectionControls; final ScrollPhysics? scrollPhysics; - final BoxConstraints constraints; final Map cachedImageSizes = {}; @@ -60,7 +59,6 @@ class HtmlParser extends StatelessWidget { required this.style, required this.customRenders, required this.tagsList, - required this.constraints, this.root, this.selectionControls, this.scrollPhysics, @@ -84,7 +82,6 @@ class HtmlParser extends StatelessWidget { tagsList, context, this, - constraints, ); // Styling Step @@ -104,35 +101,13 @@ class HtmlParser extends StatelessWidget { processedTree, ); - // 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: processedTree.style, - textScaleFactor: MediaQuery.of(context).textScaleFactor, - renderContext: RenderContext( - buildContext: context, - parser: this, - tree: processedTree, - style: processedTree.style, - ), - selectionControls: selectionControls, - scrollPhysics: scrollPhysics, - ); - } - return StyledText( - textSpan: parsedTree, + return CSSBoxWidget.withInlineSpanChildren( style: processedTree.style, - textScaleFactor: MediaQuery.of(context).textScaleFactor, - renderContext: RenderContext( - buildContext: context, - parser: this, - tree: processedTree, - style: processedTree.style, - ), + children: [parsedTree], + selectable: selectable, + scrollPhysics: scrollPhysics, + selectionControls: selectionControls, + shrinkWrap: shrinkWrap, ); } @@ -153,7 +128,6 @@ class HtmlParser extends StatelessWidget { List tagsList, BuildContext context, HtmlParser parser, - BoxConstraints constraints, ) { StyledElement tree = StyledElement( name: "[Tree Root]", @@ -161,8 +135,6 @@ class HtmlParser extends StatelessWidget { node: html, //TODO(Sub6Resources): This seems difficult to customize style: Style.fromTextStyle(Theme.of(context).textTheme.bodyText2!), - //TODO(Sub6Resources): Okay, how about shrinkWrap? - containingBlockSize: Size(constraints.maxWidth, constraints.maxHeight), ); html.nodes.forEach((node) { @@ -172,7 +144,6 @@ class HtmlParser extends StatelessWidget { tagsList, context, parser, - constraints, )); }); @@ -189,7 +160,6 @@ class HtmlParser extends StatelessWidget { List tagsList, BuildContext context, HtmlParser parser, - BoxConstraints constraints, ) { List children = []; @@ -200,32 +170,28 @@ class HtmlParser extends StatelessWidget { tagsList, context, parser, - constraints, )); }); - //TODO(Sub6Resources): Okay, how about shrinkWrap? How to calculate this for children? - final maxSize = Size(constraints.maxWidth, constraints.maxHeight); - //TODO(Sub6Resources): There's probably a more efficient way to look this up. if (node is dom.Element) { if (!tagsList.contains(node.localName)) { return EmptyContentElement(); } if (STYLED_ELEMENTS.contains(node.localName)) { - return parseStyledElement(node, children, maxSize); + return parseStyledElement(node, children); } else if (INTERACTABLE_ELEMENTS.contains(node.localName)) { - return parseInteractableElement(node, children, maxSize); + return parseInteractableElement(node, children); } else if (REPLACED_ELEMENTS.contains(node.localName)) { - return parseReplacedElement(node, children, maxSize); + return parseReplacedElement(node, children); } else if (LAYOUT_ELEMENTS.contains(node.localName)) { - return parseLayoutElement(node, children, maxSize); + return parseLayoutElement(node, children); } else if (TABLE_CELL_ELEMENTS.contains(node.localName)) { - return parseTableCellElement(node, children, maxSize); + return parseTableCellElement(node, children); } else if (TABLE_DEFINITION_ELEMENTS.contains(node.localName)) { - return parseTableDefinitionElement(node, children, maxSize); + return parseTableDefinitionElement(node, children); } else { - final StyledElement tree = parseStyledElement(node, children, maxSize); + final StyledElement tree = parseStyledElement(node, children); for (final entry in customRenderMatchers) { if (entry.call( RenderContext( @@ -241,7 +207,12 @@ class HtmlParser extends StatelessWidget { return EmptyContentElement(); } } else if (node is dom.Text) { - return TextContentElement(text: node.text, style: Style(), element: node.parent, node: node, containingBlockSize: maxSize); + return TextContentElement( + text: node.text, + style: Style(), + element: node.parent, + node: node, + ); } else { return EmptyContentElement(); } @@ -370,12 +341,11 @@ class HtmlParser extends StatelessWidget { return customRenders[entry]!.inlineSpan!.call(newContext, buildChildren); } return WidgetSpan( - child: ContainerSpan( - renderContext: newContext, + child: CSSBoxWidget( style: tree.style, shrinkWrap: newContext.parser.shrinkWrap, child: customRenders[entry]!.widget!.call(newContext, buildChildren), - containingBlockSize: tree.containingBlockSize, + childIsReplaced: true, //TODO is this true? ), ); } @@ -663,7 +633,6 @@ class HtmlParser extends StatelessWidget { TextContentElement( text: tree.style.before, style: tree.style.copyWith(beforeAfterNull: true, display: Display.INLINE), - containingBlockSize: Size.infinite, //TODO(Sub6Resources): This can't be right... ), ); } @@ -671,7 +640,6 @@ class HtmlParser extends StatelessWidget { tree.children.add(TextContentElement( text: tree.style.after, style: tree.style.copyWith(beforeAfterNull: true, display: Display.INLINE), - containingBlockSize: Size.infinite, //TODO(Sub6Resources): This can't be right... )); } @@ -867,133 +835,6 @@ class RenderContext { }); } -/// A [ContainerSpan] is a widget with an [InlineSpan] child or children. -/// -/// A [ContainerSpan] can have a border, background color, height, width, padding, and margin -/// and can represent either an INLINE or BLOCK-level element. -class ContainerSpan extends StatelessWidget { - final AnchorKey? key; - final Widget? child; - final List? children; - final Style style; - final RenderContext renderContext; - final bool shrinkWrap; - final Size containingBlockSize; - - ContainerSpan({ - this.key, - this.child, - this.children, - required this.style, - required this.renderContext, - this.shrinkWrap = false, - required this.containingBlockSize, - }): super(key: key); - - @override - Widget build(BuildContext context) { - - final bool isReplaced = REPLACED_EXTERNAL_ELEMENTS.contains(renderContext.tree.name); - - //Calculate auto widths and margins: - final widthsAndMargins = WidthAndMargins.calculate(style, containingBlockSize, isReplaced, context); - - Widget container = Container( - decoration: BoxDecoration( - border: style.border, - color: style.backgroundColor, - ), - height: style.height?.value, //TODO - width: widthsAndMargins.width, - padding: style.padding?.nonNegative, - margin: widthsAndMargins.margins, - alignment: shrinkWrap ? null : style.alignment, - child: child ?? - StyledText( - textSpan: TextSpan( - style: renderContext.style.generateTextStyle(), - children: children, - ), - style: renderContext.style, - renderContext: renderContext, - ), - ); - - return LayoutBuilder(builder: (context, constraints) { - return container; - }); - } -} - -class StyledText extends StatelessWidget { - final InlineSpan textSpan; - final Style style; - final double textScaleFactor; - final RenderContext renderContext; - final AnchorKey? key; - final bool _selectable; - final TextSelectionControls? selectionControls; - final ScrollPhysics? scrollPhysics; - - const StyledText({ - required this.textSpan, - required this.style, - this.textScaleFactor = 1.0, - required this.renderContext, - this.key, - this.selectionControls, - this.scrollPhysics, - }) : _selectable = false, - super(key: key); - - const StyledText.selectable({ - required TextSpan textSpan, - required this.style, - this.textScaleFactor = 1.0, - required this.renderContext, - this.key, - this.selectionControls, - this.scrollPhysics, - }) : textSpan = textSpan, - _selectable = true, - super(key: key); - - @override - Widget build(BuildContext context) { - if (_selectable) { - return SelectableText.rich( - textSpan as TextSpan, - style: style.generateTextStyle(), - textAlign: style.textAlign, - textDirection: style.direction, - textScaleFactor: textScaleFactor, - maxLines: style.maxLines, - selectionControls: selectionControls, - scrollPhysics: scrollPhysics, - ); - } - return SizedBox( - width: consumeExpandedBlock(style.display, renderContext), - child: Text.rich( - textSpan, - style: style.generateTextStyle(), - textAlign: style.textAlign, - textDirection: style.direction, - textScaleFactor: textScaleFactor, - maxLines: style.maxLines, - overflow: style.textOverflow, - ), - ); - } - - double? consumeExpandedBlock(Display? display, RenderContext context) { - if ((display == Display.BLOCK || display == Display.LIST_ITEM) && !renderContext.parser.shrinkWrap) { - return double.infinity; - } - return null; - } -} - extension IterateLetters on String { String nextLetter() { String s = this.toLowerCase(); diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart index 5b404b6181..4b3758faa6 100644 --- a/lib/src/css_box_widget.dart +++ b/lib/src/css_box_widget.dart @@ -1,7 +1,7 @@ import 'dart:math' as math; +import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_html/flutter_html.dart'; class CSSBoxWidget extends StatelessWidget { @@ -13,6 +13,26 @@ class CSSBoxWidget extends StatelessWidget { this.shrinkWrap = false, }): super(key: key); + /// Generates a CSSBoxWidget that contains a list of InlineSpan children. + CSSBoxWidget.withInlineSpanChildren({ + this.key, + required List children, + required this.style, + this.childIsReplaced = false, + this.shrinkWrap = false, + bool selectable = false, + TextSelectionControls? selectionControls, + ScrollPhysics? scrollPhysics, + }) : this.child = selectable + ? _generateSelectableWidgetChild( + children, + style, + selectionControls, + scrollPhysics, + ) + : _generateWidgetChild(children, style), + super(key: key); + /// An optional anchor key to use in finding this box final AnchorKey? key; @@ -28,36 +48,87 @@ class CSSBoxWidget extends StatelessWidget { /// (e.g. img, video, iframe, audio, etc.) final bool childIsReplaced; - /// Whether or not the content should take its minimum possible width - /// TODO TODO TODO + /// Whether or not the content should ignore auto horizontal margins and not + /// necessarily take up the full available width unless necessary final bool shrinkWrap; @override Widget build(BuildContext context) { - return Container( - child: _CSSBoxRenderer( - width: style.width ?? Width.auto(), - height: style.height ?? Height.auto(), - paddingSize: style.padding?.collapsedSize ?? Size.zero, - borderSize: style.border?.dimensions.collapsedSize ?? Size.zero, - margins: style.margin ?? Margins.zero, - display: style.display ?? Display.BLOCK, - childIsReplaced: childIsReplaced, - emValue: _calculateEmValue(style, context), - child: Container( - decoration: BoxDecoration( - border: style.border, - color: style.backgroundColor, //Colors the padding and content boxes - ), - width: ((style.display == Display.BLOCK || style.display == Display.LIST_ITEM) && !childIsReplaced && !shrinkWrap) - ? double.infinity - : null, - padding: style.padding ?? EdgeInsets.zero, - child: child, + return _CSSBoxRenderer( + width: style.width ?? Width.auto(), + height: style.height ?? Height.auto(), + paddingSize: style.padding?.collapsedSize ?? Size.zero, + borderSize: style.border?.dimensions.collapsedSize ?? Size.zero, + margins: style.margin ?? Margins.zero, + display: style.display ?? Display.BLOCK, + childIsReplaced: childIsReplaced, + emValue: _calculateEmValue(style, context), + shrinkWrap: shrinkWrap, + child: Container( + decoration: BoxDecoration( + border: style.border, + color: style.backgroundColor, //Colors the padding and content boxes ), + width: _shouldExpandToFillBlock() ? double.infinity : null, + padding: style.padding ?? EdgeInsets.zero, + child: child, + ), + ); + } + + /// Takes a list of InlineSpan children and generates a Text.rich Widget + /// containing those children. + static Widget _generateWidgetChild(List children, Style style) { + if(children.isEmpty) { + return Container(); + } + + return Text.rich( + TextSpan( + style: style.generateTextStyle(), + children: children, + ), + style: style.generateTextStyle(), + textAlign: style.textAlign, + textDirection: style.direction, + maxLines: style.maxLines, + overflow: style.textOverflow, + ); + } + + static Widget _generateSelectableWidgetChild( + List children, + Style style, + TextSelectionControls? selectionControls, + ScrollPhysics? scrollPhysics, + ) { + if(children.isEmpty) { + return Container(); + } + + return SelectableText.rich( + TextSpan( + style: style.generateTextStyle(), + children: children, ), + style: style.generateTextStyle(), + textAlign: style.textAlign, + textDirection: style.direction, + maxLines: style.maxLines, + selectionControls: selectionControls, + scrollPhysics: scrollPhysics, ); } + + /// Whether or not the content-box should expand its width to fill the + /// width available to it or if it should just let its inner content + /// determine the content-box's width. + bool _shouldExpandToFillBlock() { + return (style.display == Display.BLOCK || + style.display == Display.LIST_ITEM) && + !childIsReplaced && + !shrinkWrap; + } } class _CSSBoxRenderer extends MultiChildRenderObjectWidget { @@ -72,6 +143,7 @@ class _CSSBoxRenderer extends MultiChildRenderObjectWidget { required this.paddingSize, required this.childIsReplaced, required this.emValue, + required this.shrinkWrap, }) : super(key: key, children: [child]); /// The Display type of the element @@ -99,16 +171,21 @@ class _CSSBoxRenderer extends MultiChildRenderObjectWidget { /// The calculated size of 1em in pixels final double emValue; + /// Whether or not this container should shrinkWrap its contents. + /// (see definition on [CSSBoxWidget]) + final bool shrinkWrap; + @override _RenderCSSBox createRenderObject(BuildContext context) { return _RenderCSSBox( display: display, width: width..normalize(emValue), height: height..normalize(emValue), - margins: _preProcessMargins(margins), + margins: _preProcessMargins(margins, shrinkWrap), borderSize: borderSize, paddingSize: paddingSize, childIsReplaced: childIsReplaced, + shrinkWrap: shrinkWrap, ); } @@ -118,13 +195,14 @@ class _CSSBoxRenderer extends MultiChildRenderObjectWidget { ..display = display ..width = (width..normalize(emValue)) ..height = (height..normalize(emValue)) - ..margins = _preProcessMargins(margins) + ..margins = _preProcessMargins(margins, shrinkWrap) ..borderSize = borderSize ..paddingSize = paddingSize - ..childIsReplaced = childIsReplaced; + ..childIsReplaced = childIsReplaced + ..shrinkWrap = shrinkWrap; } - Margins _preProcessMargins(Margins margins) { + Margins _preProcessMargins(Margins margins, bool shrinkWrap) { Margin leftMargin = margins.left ?? Margin.zero(); Margin rightMargin = margins.right ?? Margin.zero(); Margin topMargin = margins.top ?? Margin.zero(); @@ -149,6 +227,15 @@ class _CSSBoxRenderer extends MultiChildRenderObjectWidget { } } + //Shrink-wrap margins if applicable + if (shrinkWrap && leftMargin.unit == Unit.auto) { + leftMargin = Margin.zero(); + } + + if (shrinkWrap && rightMargin.unit == Unit.auto) { + rightMargin = Margin.zero(); + } + return Margins( top: topMargin, right: rightMargin, @@ -171,13 +258,15 @@ class _RenderCSSBox extends RenderBox required Size borderSize, required Size paddingSize, required bool childIsReplaced, + required bool shrinkWrap, }) : _display = display, _width = width, _height = height, _margins = margins, _borderSize = borderSize, _paddingSize = paddingSize, - _childIsReplaced = childIsReplaced; + _childIsReplaced = childIsReplaced, + _shrinkWrap = shrinkWrap; Display _display; @@ -242,6 +331,15 @@ class _RenderCSSBox extends RenderBox markNeedsLayout(); } + bool _shrinkWrap; + + bool get shrinkWrap => _shrinkWrap; + + set shrinkWrap(bool shrinkWrap) { + _shrinkWrap = shrinkWrap; + markNeedsLayout(); + } + @override void setupParentData(RenderBox child) { if (child.parentData is! CSSBoxParentData) @@ -288,7 +386,9 @@ class _RenderCSSBox extends RenderBox @override double? computeDistanceToActualBaseline(TextBaseline baseline) { + return firstChild?.getDistanceToActualBaseline(baseline); return defaultComputeDistanceToHighestActualBaseline(baseline); + //TODO TODO TODO } @override @@ -317,10 +417,10 @@ class _RenderCSSBox extends RenderBox final childConstraints = constraints.copyWith( maxWidth: (this.width.unit != Unit.auto) ? this.width.value - : constraints.maxWidth - + : containingBlockSize.width - (this.margins.left?.value ?? 0) - (this.margins.right?.value ?? 0), - maxHeight: constraints.maxHeight - + maxHeight: containingBlockSize.height - (this.margins.top?.value ?? 0) - (this.margins.bottom?.value ?? 0), minWidth: 0, @@ -339,7 +439,9 @@ class _RenderCSSBox extends RenderBox height = childSize.height; switch (display) { case Display.BLOCK: - width = containingBlockSize.width; + width = (shrinkWrap || childIsReplaced) + ? childSize.width + horizontalMargins + : containingBlockSize.width; height = childSize.height + verticalMargins; break; case Display.INLINE: @@ -351,7 +453,9 @@ class _RenderCSSBox extends RenderBox height = childSize.height + verticalMargins; break; case Display.LIST_ITEM: - width = containingBlockSize.width; + width = shrinkWrap + ? childSize.width + horizontalMargins + : containingBlockSize.width; height = childSize.height + verticalMargins; break; case Display.NONE: @@ -432,6 +536,10 @@ class _RenderCSSBox extends RenderBox widthIsAuto = false; } + if (shrinkWrap) { + widthIsAuto = false; + } + //If width is not auto and the width of the margin box is larger than the // width of the containing block, then consider left and right margins to // have a 0 value. @@ -447,8 +555,9 @@ class _RenderCSSBox extends RenderBox } // If all values are non-auto, the box is overconstrained. - // One of the margins will need to be ignored. - if (!widthIsAuto && !marginLeftIsAuto && !marginRightIsAuto) { + // One of the margins will need to be adjusted so that the + // entire width is taken + if (!widthIsAuto && !marginLeftIsAuto && !marginRightIsAuto && !shrinkWrap && !childIsReplaced) { //TODO ignore either left or right margin based on directionality of parent widgets. //For now, assume ltr, and just ignore the right margin. final difference = diff --git a/lib/src/interactable_element.dart b/lib/src/interactable_element.dart index 49b8baa0fb..43d29281b9 100644 --- a/lib/src/interactable_element.dart +++ b/lib/src/interactable_element.dart @@ -14,7 +14,6 @@ class InteractableElement extends StyledElement { required this.href, required dom.Node node, required super.elementId, - required super.containingBlockSize, }) : super(node: node as dom.Element?); } @@ -26,7 +25,6 @@ enum Gesture { StyledElement parseInteractableElement( dom.Element element, List children, - Size containingBlockSize, ) { switch (element.localName) { case "a": @@ -41,7 +39,6 @@ StyledElement parseInteractableElement( ), node: element, elementId: element.id, - containingBlockSize: containingBlockSize, ); } // When tag have no href, it must be non clickable and without decoration. @@ -51,7 +48,6 @@ StyledElement parseInteractableElement( style: Style(), node: element, elementId: element.id, - containingBlockSize: containingBlockSize, ); /// will never be called, just to suppress missing return warning default: @@ -62,7 +58,6 @@ StyledElement parseInteractableElement( href: '', style: Style(), elementId: "[[No ID]]", - containingBlockSize: containingBlockSize, ); } } diff --git a/lib/src/layout_element.dart b/lib/src/layout_element.dart index 32b881e683..8b3a49d22b 100644 --- a/lib/src/layout_element.dart +++ b/lib/src/layout_element.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/html_parser.dart'; import 'package:flutter_html/src/anchor.dart'; +import 'package:flutter_html/src/css_box_widget.dart'; import 'package:flutter_html/src/html_elements.dart'; import 'package:flutter_html/src/styled_element.dart'; import 'package:flutter_html/style.dart'; @@ -14,7 +15,6 @@ abstract class LayoutElement extends StyledElement { required super.children, String? elementId, super.node, - required super.containingBlockSize, }) : super(name: name, style: Style(), elementId: elementId ?? "[[No ID]]"); Widget? toWidget(RenderContext context); @@ -24,7 +24,6 @@ class TableSectionLayoutElement extends LayoutElement { TableSectionLayoutElement({ required String name, required List children, - required super.containingBlockSize, }) : super(name: name, children: children); @override @@ -39,7 +38,6 @@ class TableRowLayoutElement extends LayoutElement { required super.name, required super.children, required super.node, - required super.containingBlockSize, }); @override @@ -60,7 +58,6 @@ class TableCellElement extends StyledElement { required super.children, required super.style, required super.node, - required super.containingBlockSize, }) { colspan = _parseSpan(this, "colspan"); rowspan = _parseSpan(this, "rowspan"); @@ -75,7 +72,6 @@ class TableCellElement extends StyledElement { TableCellElement parseTableCellElement( dom.Element element, List children, - Size containingBlockSize, ) { final cell = TableCellElement( name: element.localName!, @@ -84,7 +80,6 @@ TableCellElement parseTableCellElement( children: children, node: element, style: Style(), - containingBlockSize: containingBlockSize, ); if (element.localName == "th") { cell.style = Style( @@ -100,14 +95,12 @@ class TableStyleElement extends StyledElement { required super.children, required super.style, required super.node, - required super.containingBlockSize, }); } TableStyleElement parseTableDefinitionElement( dom.Element element, List children, - Size containingBlockSize, ) { switch (element.localName) { case "colgroup": @@ -117,7 +110,6 @@ TableStyleElement parseTableDefinitionElement( children: children, node: element, style: Style(), - containingBlockSize: containingBlockSize, ); default: return TableStyleElement( @@ -125,7 +117,6 @@ TableStyleElement parseTableDefinitionElement( children: children, node: element, style: Style(), - containingBlockSize: containingBlockSize, ); } } @@ -138,7 +129,6 @@ class DetailsContentElement extends LayoutElement { required super.children, required dom.Element node, required this.elementList, - required super.containingBlockSize, }) : super(node: node, elementId: node.id); @override @@ -157,22 +147,15 @@ class DetailsContentElement extends LayoutElement { return ExpansionTile( key: AnchorKey.of(context.parser.key, this), expandedAlignment: Alignment.centerLeft, - title: elementList.isNotEmpty == true && elementList.first.localName == "summary" ? StyledText( - textSpan: TextSpan( - style: style.generateTextStyle(), - children: firstChild == null ? [] : [firstChild], - ), + title: elementList.isNotEmpty == true && elementList.first.localName == "summary" + ? CSSBoxWidget.withInlineSpanChildren( + children: firstChild == null ? [] : [firstChild], style: style, - renderContext: context, ) : Text("Details"), children: [ - StyledText( - textSpan: TextSpan( - style: style.generateTextStyle(), - children: getChildren(childrenList, context, elementList.isNotEmpty == true && elementList.first.localName == "summary" ? firstChild : null) - ), + CSSBoxWidget.withInlineSpanChildren( + children: getChildren(childrenList, context, elementList.isNotEmpty == true && elementList.first.localName == "summary" ? firstChild : null), style: style, - renderContext: context, ), ] ); @@ -185,7 +168,11 @@ class DetailsContentElement extends LayoutElement { } class EmptyLayoutElement extends LayoutElement { - EmptyLayoutElement({required String name}) : super(name: name, children: [], containingBlockSize: Size.zero); + EmptyLayoutElement({required String name}) + : super( + name: name, + children: [], + ); @override Widget? toWidget(_) => null; @@ -194,7 +181,6 @@ class EmptyLayoutElement extends LayoutElement { LayoutElement parseLayoutElement( dom.Element element, List children, - Size containingBlockSize, ) { switch (element.localName) { case "details": @@ -206,7 +192,6 @@ LayoutElement parseLayoutElement( name: element.localName!, children: children, elementList: element.children, - containingBlockSize: containingBlockSize, ); case "thead": case "tbody": @@ -214,14 +199,12 @@ LayoutElement parseLayoutElement( return TableSectionLayoutElement( name: element.localName!, children: children, - containingBlockSize: containingBlockSize, ); case "tr": return TableRowLayoutElement( name: element.localName!, children: children, node: element, - containingBlockSize: containingBlockSize, ); default: return EmptyLayoutElement(name: "[[No Name]]"); diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index c56831ab35..c11b209ecb 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_html/html_parser.dart'; import 'package:flutter_html/src/anchor.dart'; +import 'package:flutter_html/src/css_box_widget.dart'; import 'package:flutter_html/src/html_elements.dart'; import 'package:flutter_html/style.dart'; import 'package:html/dom.dart' as dom; @@ -20,7 +21,6 @@ abstract class ReplacedElement extends StyledElement { required super.name, required super.style, required super.elementId, - required super.containingBlockSize, List? children, super.node, this.alignment = PlaceholderAlignment.aboveBaseline, @@ -47,7 +47,6 @@ class TextContentElement extends ReplacedElement { required this.text, this.node, dom.Element? element, - required super.containingBlockSize, }) : super(name: "[text]", style: style, node: element, elementId: "[[No ID]]"); @override @@ -60,7 +59,7 @@ class TextContentElement extends ReplacedElement { } class EmptyContentElement extends ReplacedElement { - EmptyContentElement({String name = "empty"}) : super(name: name, style: Style(), elementId: "[[No ID]]", containingBlockSize: Size.zero); + EmptyContentElement({String name = "empty"}) : super(name: name, style: Style(), elementId: "[[No ID]]"); @override Widget? toWidget(_) => null; @@ -73,7 +72,6 @@ class RubyElement extends ReplacedElement { required this.element, required List children, String name = "ruby", - required super.containingBlockSize, }) : super(name: name, alignment: PlaceholderAlignment.middle, style: Style(), elementId: element.id, children: children); @override @@ -104,27 +102,29 @@ class RubyElement extends ReplacedElement { child: Transform( transform: Matrix4.translationValues(0, -(rubyYPos), 0), - child: ContainerSpan( - renderContext: RenderContext( - buildContext: context.buildContext, - parser: context.parser, - style: c.style, - tree: c, - ), + child: CSSBoxWidget( style: c.style, - containingBlockSize: containingBlockSize, - child: Text(c.element!.innerHtml, - style: c.style - .generateTextStyle() - .copyWith(fontSize: rubySize)), - )))), - ContainerSpan( - renderContext: context, - style: context.style, - containingBlockSize: containingBlockSize, - child: node is TextContentElement ? Text((node as TextContentElement).text?.trim() ?? "", - style: context.style.generateTextStyle()) : null, - children: node is TextContentElement ? null : [context.parser.parseTree(context, node!)]), + //TODO do any other attributes apply? + child: Text( + c.element!.innerHtml, + style: c.style + .generateTextStyle() + .copyWith(fontSize: rubySize), + ), + ), + ), + ), + ), + CSSBoxWidget( + //TODO do any other styles apply? Does ruby still work? + style: context.style, + child: node is TextContentElement + ? Text( + (node as TextContentElement).text?.trim() ?? "", + style: context.style.generateTextStyle(), + ) + : RichText(text: context.parser.parseTree(context, node!)), + ), ], ); widgets.add(widget); @@ -151,7 +151,6 @@ class RubyElement extends ReplacedElement { ReplacedElement parseReplacedElement( dom.Element element, List children, - Size containingBlockSize, ) { switch (element.localName) { case "br": @@ -160,13 +159,11 @@ ReplacedElement parseReplacedElement( style: Style(whiteSpace: WhiteSpace.PRE), element: element, node: element, - containingBlockSize: containingBlockSize, ); case "ruby": return RubyElement( element: element, children: children, - containingBlockSize: containingBlockSize, ); default: return EmptyContentElement(name: element.localName == null ? "[[No Name]]" : element.localName!); diff --git a/lib/src/style/compute_style.dart b/lib/src/style/compute_style.dart deleted file mode 100644 index ec550dae24..0000000000 --- a/lib/src/style/compute_style.dart +++ /dev/null @@ -1,202 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter_html/style.dart'; - -/// [computeDimensionUnit] takes a [Dimension] and some information about the -/// context where the Dimension is being used, and returns a "used" value to -/// use in a rendering. -double _computeDimensionValue(Dimension dimension, double emValue, double autoValue) { - switch (dimension.unit) { - case Unit.em: return emValue * dimension.value; - case Unit.px: return dimension.value; - case Unit.auto: return autoValue; - } -} - -double _calculateEmValue(Style style, BuildContext buildContext) { - //TODO is there a better value for this? - return (style.fontSize?.size ?? 16) * - MediaQuery.textScaleFactorOf(buildContext) * - MediaQuery.of(buildContext).devicePixelRatio; -} - -/// This class handles the calculation of widths and margins during parsing: -/// See [calculate] within. -class WidthAndMargins { - final double? width; - - final EdgeInsets margins; - - const WidthAndMargins({required this.width, required this.margins}); - - /// [WidthsAndMargins.calculate] calculates any auto values ans resolves any - /// overconstraint for various elements.. - /// See https://drafts.csswg.org/css2/#Computing_widths_and_margins - static WidthAndMargins calculate( - Style style, - Size containingBlockSize, - bool isReplaced, - BuildContext buildContext, - ) { - - final emValue = _calculateEmValue(style, buildContext); - - double? width = _computeDimensionValue(style.width ?? Width.auto(), emValue, 0); - double marginLeft = _computeDimensionValue(style.margin?.left ?? Margin.zero(), emValue, 0); - double marginRight = _computeDimensionValue(style.margin?.right ?? Margin.zero(), emValue, 0); - - bool autoWidth = style.width?.unit == Unit.auto || style.width == null; - bool autoMarginLeft = style.margin?.left?.unit == Unit.auto; - bool autoMarginRight = style.margin?.right?.unit == Unit.auto; - - switch(style.display ?? Display.BLOCK) { - case Display.BLOCK: - - if(isReplaced && autoWidth) { - //TODO calculate width as for inline replaced element - // See https://drafts.csswg.org/css2/#block-replaced-width - //For now, just let the element calculate its own width - width = null; - } - - double? overrideMarginLeft; - double? overrideMarginRight; - - double? autoLeftMarginValue; - double? autoRightMarginValue; - - final borderWidth = (style.border?.left.width ?? 0) + (style.border?.right.width ?? 0); - final paddingWidth = (style.padding?.left ?? 0) + (style.padding?.right ?? 0); - final nonAutoWidths = borderWidth + paddingWidth; - final nonAutoMarginWidth = marginLeft + marginRight; - - //If width is not auto, check the total width of the containing block: - if(!autoWidth) { - if(nonAutoWidths + (width ?? 0) + nonAutoMarginWidth > containingBlockSize.width) { - autoLeftMarginValue = 0; - autoRightMarginValue = 0; - autoMarginLeft = false; - autoMarginRight = false; - } - } - - //If all values are explicit, the box is over-constrained, and we must - //override one of the given margin values (left if the overconstrained - //element has a rtl directionality, and right if the overconstrained - //element has a ltr directionality). Margins must be non-negative in - //Flutter, so we set them to 0 if they go below that. - if(!autoWidth && !autoMarginLeft && !autoMarginRight && width != null) { - final difference = containingBlockSize.width - (nonAutoWidths + width + nonAutoMarginWidth); - switch(style.direction) { - case TextDirection.rtl: - overrideMarginLeft = max(marginLeft + difference, 0); - break; - case TextDirection.ltr: - overrideMarginRight = max(marginRight + difference, 0); - break; - case null: - final directionality = Directionality.maybeOf(buildContext); - if(directionality != null) { - switch(directionality) { - case TextDirection.rtl: - overrideMarginLeft = max(marginLeft + difference, 0); - break; - case TextDirection.ltr: - overrideMarginRight = max(marginRight + difference, 0); - break; - } - } else { - overrideMarginRight = max(marginRight + difference, 0); - } - } - } - - //If exactly one unit is auto, calculate it from the equality. - if(autoWidth && !autoMarginLeft && !autoMarginRight && width != null) { - width = containingBlockSize.width - (nonAutoWidths + nonAutoMarginWidth); - } else if((!autoWidth || width==null) && autoMarginLeft && !autoMarginRight) { - overrideMarginLeft = containingBlockSize.width - (nonAutoWidths + (width ?? 0) + marginRight); - } else if((!autoWidth || width == null) && !autoMarginLeft && autoMarginRight) { - overrideMarginRight = containingBlockSize.width - (nonAutoWidths + (width ?? 0) + marginLeft); - } - - //If width is auto, set all other auto values to 0, and the width is - //calculated from the equality - if(autoWidth && width != null) { - autoLeftMarginValue = 0; - autoRightMarginValue = 0; - autoMarginLeft = false; - autoMarginRight = false; - width = containingBlockSize.width - (nonAutoWidths + nonAutoMarginWidth); - } - - //If margin-left and margin-right are both auto, their values are equal, - // and the element is centered. - if(autoMarginLeft && autoMarginRight) { - final marginWidth = containingBlockSize.width - (nonAutoWidths + (width ?? 0)); - overrideMarginLeft = marginWidth / 2; - overrideMarginRight = marginWidth / 2; - } - - marginLeft = overrideMarginLeft ?? _computeDimensionValue(style.margin?.left ?? Margin.zero(), emValue, autoLeftMarginValue ?? 0); - marginRight = overrideMarginRight ?? _computeDimensionValue(style.margin?.right ?? Margin.zero(), emValue, autoRightMarginValue ?? 0); - break; - case Display.INLINE: - //All inline elements have a computed auto value for margin of 0. - if(autoMarginLeft) { - marginLeft = 0; - } - if(autoMarginRight) { - marginRight = 0; - } - if(isReplaced) { - //TODO calculate intrinsic width - //For now, we can just let the element calculate its own width! - width = null; - } - else { - width = null; - } - break; - case Display.INLINE_BLOCK: - //All inline elements have a computed auto value for margin of 0. - if(autoMarginLeft) { - marginLeft = 0; - } - if(autoMarginRight) { - marginRight = 0; - } - if(isReplaced) { - //TODO calculate intrinsic width - //For now, we can just let the element calculate its own width! - width = null; - } else { - //TODO calculate shrink-to-fit width for auto widths. - //For now, we can just let the element calculate its own width! - width = null; - } - - - // (See https://drafts.csswg.org/css2/#inline-replaced-width) - break; - case Display.LIST_ITEM: - // TODO: Any handling for this case? - break; - case Display.NONE: - // Do nothing - break; - } - - return WidthAndMargins( - width: width, - margins: EdgeInsets.only( - left: marginLeft, - right: marginRight, - top: _computeDimensionValue(style.margin?.top ?? Margin.zero(), emValue, 0), - bottom: _computeDimensionValue(style.margin?.bottom ?? Margin.zero(), emValue, 0), - ), - ); - } - -} diff --git a/lib/src/styled_element.dart b/lib/src/styled_element.dart index d02288a118..bd687efe67 100644 --- a/lib/src/styled_element.dart +++ b/lib/src/styled_element.dart @@ -14,7 +14,6 @@ class StyledElement { final List elementClasses; List children; Style style; - Size containingBlockSize; final dom.Element? _node; StyledElement({ @@ -24,7 +23,6 @@ class StyledElement { required this.children, required this.style, required dom.Element? node, - required this.containingBlockSize, }) : this._node = node; bool matchesSelector(String selector) => @@ -51,7 +49,9 @@ class StyledElement { } StyledElement parseStyledElement( - dom.Element element, List children, Size containingBlockSize) { + dom.Element element, + List children, + ) { StyledElement styledElement = StyledElement( name: element.localName!, elementId: element.id, @@ -59,7 +59,6 @@ StyledElement parseStyledElement( children: children, node: element, style: Style(), - containingBlockSize: containingBlockSize, ); switch (element.localName) { @@ -251,9 +250,13 @@ StyledElement parseStyledElement( break; case "hr": styledElement.style = Style( - margin: Margins.symmetric(vertical: 0.5, unit: Unit.em), - height: Height(1), - backgroundColor: Colors.black, + margin: Margins( + top: Margin(0.5, Unit.em), + bottom: Margin(0.5, Unit.em), + left: Margin.auto(), + right: Margin.auto(), + ), + border: Border.all(), display: Display.BLOCK, ); break; diff --git a/test/html_parser_test.dart b/test/html_parser_test.dart index 98b515aed1..0ded2f3bc7 100644 --- a/test/html_parser_test.dart +++ b/test/html_parser_test.dart @@ -50,7 +50,7 @@ void testNewParser(BuildContext context) { shrinkWrap: false, selectable: true, style: {}, - customRenders: defaultRenders, + customRenders: generateDefaultRenders(), tagsList: Html.tags, selectionControls: null, scrollPhysics: null, @@ -78,7 +78,7 @@ void testNewParser(BuildContext context) { shrinkWrap: false, selectable: true, style: {}, - customRenders: defaultRenders, + customRenders: generateDefaultRenders(), tagsList: Html.tags, selectionControls: null, scrollPhysics: null, @@ -104,7 +104,7 @@ void testNewParser(BuildContext context) { shrinkWrap: false, selectable: true, style: {}, - customRenders: defaultRenders, + customRenders: generateDefaultRenders(), tagsList: Html.tags, selectionControls: null, scrollPhysics: null, @@ -132,7 +132,7 @@ void testNewParser(BuildContext context) { shrinkWrap: false, selectable: true, style: {}, - customRenders: defaultRenders, + customRenders: generateDefaultRenders(), tagsList: Html.tags, selectionControls: null, scrollPhysics: null, From bcee9b342006b7a9effdaab0dc0e6731c8d19ed7 Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Thu, 1 Sep 2022 22:51:36 -0600 Subject: [PATCH 07/14] WIP: Fix directionality TODO --- lib/src/css_box_widget.dart | 62 ++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart index 4b3758faa6..726e407a89 100644 --- a/lib/src/css_box_widget.dart +++ b/lib/src/css_box_widget.dart @@ -9,6 +9,7 @@ class CSSBoxWidget extends StatelessWidget { this.key, required this.child, required this.style, + this.textDirection, this.childIsReplaced = false, this.shrinkWrap = false, }): super(key: key); @@ -18,6 +19,7 @@ class CSSBoxWidget extends StatelessWidget { this.key, required List children, required this.style, + this.textDirection, this.childIsReplaced = false, this.shrinkWrap = false, bool selectable = false, @@ -44,6 +46,11 @@ class CSSBoxWidget extends StatelessWidget { /// Note that this style will only apply to this box, and will not cascade to its child. final Style style; + /// Sets the direction the text of this widget should flow. If unset or null, + /// the nearest Directionality ancestor is used as a default. If that cannot + /// be found, this Widget's renderer will raise an assertion. + final TextDirection? textDirection; + /// Indicates whether this child is a replaced element that manages its own width /// (e.g. img, video, iframe, audio, etc.) final bool childIsReplaced; @@ -63,6 +70,7 @@ class CSSBoxWidget extends StatelessWidget { display: style.display ?? Display.BLOCK, childIsReplaced: childIsReplaced, emValue: _calculateEmValue(style, context), + textDirection: _checkTextDirection(context, textDirection), shrinkWrap: shrinkWrap, child: Container( decoration: BoxDecoration( @@ -129,6 +137,16 @@ class CSSBoxWidget extends StatelessWidget { !childIsReplaced && !shrinkWrap; } + + TextDirection _checkTextDirection(BuildContext context, TextDirection? direction) { + final textDirection = direction ?? Directionality.maybeOf(context); + + assert(textDirection != null, + "CSSBoxWidget needs either a Directionality ancestor or a provided textDirection", + ); + + return textDirection!; + } } class _CSSBoxRenderer extends MultiChildRenderObjectWidget { @@ -141,6 +159,7 @@ class _CSSBoxRenderer extends MultiChildRenderObjectWidget { required this.height, required this.borderSize, required this.paddingSize, + required this.textDirection, required this.childIsReplaced, required this.emValue, required this.shrinkWrap, @@ -164,6 +183,9 @@ class _CSSBoxRenderer extends MultiChildRenderObjectWidget { /// The collapsed size of the element's padding final Size paddingSize; + /// The direction for this widget's text to flow. + final TextDirection textDirection; + /// Whether or not the child being rendered is a replaced element /// (this changes the rules for rendering) final bool childIsReplaced; @@ -184,6 +206,7 @@ class _CSSBoxRenderer extends MultiChildRenderObjectWidget { margins: _preProcessMargins(margins, shrinkWrap), borderSize: borderSize, paddingSize: paddingSize, + textDirection: textDirection, childIsReplaced: childIsReplaced, shrinkWrap: shrinkWrap, ); @@ -198,6 +221,7 @@ class _CSSBoxRenderer extends MultiChildRenderObjectWidget { ..margins = _preProcessMargins(margins, shrinkWrap) ..borderSize = borderSize ..paddingSize = paddingSize + ..textDirection = textDirection ..childIsReplaced = childIsReplaced ..shrinkWrap = shrinkWrap; } @@ -257,6 +281,7 @@ class _RenderCSSBox extends RenderBox required Margins margins, required Size borderSize, required Size paddingSize, + required TextDirection textDirection, required bool childIsReplaced, required bool shrinkWrap, }) : _display = display, @@ -265,6 +290,7 @@ class _RenderCSSBox extends RenderBox _margins = margins, _borderSize = borderSize, _paddingSize = paddingSize, + _textDirection = textDirection, _childIsReplaced = childIsReplaced, _shrinkWrap = shrinkWrap; @@ -322,6 +348,15 @@ class _RenderCSSBox extends RenderBox markNeedsLayout(); } + TextDirection _textDirection; + + TextDirection get textDirection => _textDirection; + + set textDirection(TextDirection textDirection) { + _textDirection = textDirection; + markNeedsLayout(); + } + bool _childIsReplaced; bool get childIsReplaced => _childIsReplaced; @@ -387,8 +422,6 @@ class _RenderCSSBox extends RenderBox @override double? computeDistanceToActualBaseline(TextBaseline baseline) { return firstChild?.getDistanceToActualBaseline(baseline); - return defaultComputeDistanceToHighestActualBaseline(baseline); - //TODO TODO TODO } @override @@ -556,13 +589,26 @@ class _RenderCSSBox extends RenderBox // If all values are non-auto, the box is overconstrained. // One of the margins will need to be adjusted so that the - // entire width is taken + // entire width of the containing block is used. if (!widthIsAuto && !marginLeftIsAuto && !marginRightIsAuto && !shrinkWrap && !childIsReplaced) { - //TODO ignore either left or right margin based on directionality of parent widgets. - //For now, assume ltr, and just ignore the right margin. - final difference = - containingBlockSize.width - childSize.width - marginLeft.value; - marginRight = Margin(difference); + //Ignore either left or right margin based on textDirection. + + switch(textDirection) { + case TextDirection.rtl: + final difference = containingBlockSize.width + - childSize.width + - marginRight.value; + marginLeft = Margin(difference); + break; + case TextDirection.ltr: + final difference = containingBlockSize.width + - childSize.width + - marginLeft.value; + marginRight = Margin(difference); + break; + } + + } // If there is exactly one value specified as auto, compute it value from the equality (our widths are already set) From ed755505f7bd0bbde876ac11e3077db2cf7b4903 Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Thu, 1 Sep 2022 23:06:57 -0600 Subject: [PATCH 08/14] WIP: CSSBoxWidget - Render heights correctly --- lib/src/css_box_widget.dart | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart index 726e407a89..5a60422f6c 100644 --- a/lib/src/css_box_widget.dart +++ b/lib/src/css_box_widget.dart @@ -453,11 +453,17 @@ class _RenderCSSBox extends RenderBox : containingBlockSize.width - (this.margins.left?.value ?? 0) - (this.margins.right?.value ?? 0), - maxHeight: containingBlockSize.height - - (this.margins.top?.value ?? 0) - - (this.margins.bottom?.value ?? 0), - minWidth: 0, - minHeight: 0, + maxHeight: (this.height.unit != Unit.auto) + ? this.height.value + : containingBlockSize.height - + (this.margins.top?.value ?? 0) - + (this.margins.bottom?.value ?? 0), + minWidth: (this.width.unit != Unit.auto) + ? this.width.value + : 0, + minHeight: (this.height.unit != Unit.auto) + ? this.height.value + : 0, ); final Size childSize = layoutChild(child!, childConstraints); From 32bf16cd8670bb3d88c928174feb0e5533c79330 Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Mon, 5 Sep 2022 19:03:00 -0600 Subject: [PATCH 09/14] Update FontSize definition and fix bugs with CSSBoxWidget --- example/lib/main.dart | 2 +- lib/custom_render.dart | 5 +- lib/flutter_html.dart | 2 + lib/html_parser.dart | 75 ++++++-- lib/src/css_box_widget.dart | 5 +- lib/src/css_parser.dart | 12 +- lib/src/replaced_element.dart | 2 +- lib/src/style/fontsize.dart | 40 ++++ lib/src/style/length.dart | 39 ++-- lib/src/style/lineheight.dart | 25 +++ lib/src/styled_element.dart | 23 ++- lib/style.dart | 177 +++++++++--------- packages/flutter_html_audio/README.md | 2 +- .../lib/flutter_html_audio.dart | 7 +- .../lib/iframe_mobile.dart | 5 +- .../flutter_html_iframe/lib/iframe_web.dart | 11 +- .../lib/flutter_html_table.dart | 22 +-- packages/flutter_html_video/README.md | 2 +- test/html_parser_test.dart | 8 - 19 files changed, 281 insertions(+), 183 deletions(-) create mode 100644 lib/src/style/fontsize.dart create mode 100644 lib/src/style/lineheight.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 363f4279e3..eea3fc5cc1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -307,7 +307,7 @@ class _MyHomePageState extends State { ? FlutterLogoStyle.horizontal : FlutterLogoStyle.markOnly, textColor: context.style.color!, - size: context.style.fontSize!.size! * 5, + size: context.style.fontSize!.value * 5, )), tagMatcher("table"): CustomRender.widget(widget: (context, buildChildren) => SingleChildScrollView( scrollDirection: Axis.horizontal, diff --git a/lib/custom_render.dart b/lib/custom_render.dart index a9b7dd7740..1d4b6ee52e 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -6,7 +6,6 @@ import 'dart:convert'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; -import 'package:flutter_html/src/css_box_widget.dart'; import 'package:flutter_html/src/html_elements.dart'; import 'package:flutter_html/src/utils.dart'; @@ -576,9 +575,9 @@ final _dataUriFormat = RegExp( double _getVerticalOffset(StyledElement tree) { switch (tree.style.verticalAlign) { case VerticalAlign.SUB: - return tree.style.fontSize!.size! / 2.5; + return tree.style.fontSize!.value / 2.5; case VerticalAlign.SUPER: - return tree.style.fontSize!.size! / -2.5; + return tree.style.fontSize!.value / -2.5; default: return 0; } diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 63ff0a621e..c6e06ad7ab 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -18,6 +18,8 @@ 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 css_box_widget for use in custom render. +export 'package:flutter_html/src/css_box_widget.dart'; //export style api export 'package:flutter_html/style.dart'; diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 225a9690a5..e062b73575 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -6,7 +6,6 @@ import 'package:csslib/parser.dart' as cssparser; import 'package:csslib/visitor.dart' as css; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; -import 'package:flutter_html/src/css_box_widget.dart'; import 'package:flutter_html/src/css_parser.dart'; import 'package:flutter_html/src/html_elements.dart'; import 'package:flutter_html/src/utils.dart'; @@ -88,7 +87,7 @@ class HtmlParser extends StatelessWidget { StyledElement styledTree = styleTree(lexedTree, htmlData, style, onCssParseError); // Processing Step - StyledElement processedTree = processTree(styledTree); + StyledElement processedTree = processTree(styledTree, MediaQuery.of(context).devicePixelRatio); // Parsing Step InlineSpan parsedTree = parseTree( @@ -301,14 +300,15 @@ class HtmlParser extends StatelessWidget { /// [processTree] 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 processTree(StyledElement tree) { + static StyledElement processTree(StyledElement tree, double devicePixelRatio) { tree = _processInternalWhitespace(tree); tree = _processInlineWhitespace(tree); tree = _removeEmptyElements(tree); + + tree = _calculateRelativeValues(tree, devicePixelRatio); tree = _processListCharacters(tree); tree = _processBeforesAndAfters(tree); tree = _collapseMargins(tree); - tree = _processFontSize(tree); return tree; } @@ -648,7 +648,7 @@ class HtmlParser extends StatelessWidget { return tree; } - /// [collapseMargins] follows the specifications at https://www.w3.org/TR/CSS21/box.html#collapsing-margins + /// [collapseMargins] follows the specifications at https://www.w3.org/TR/CSS22/box.html#collapsing-margins /// for collapsing margins of block-level boxes. This prevents the doubling of margins between /// boxes, and makes for a more correct rendering of the html content. /// @@ -662,7 +662,7 @@ class HtmlParser extends StatelessWidget { //Short circuit if we've reached a leaf of the tree if (tree.children.isEmpty) { // Handle case (4) from above. - if ((tree.style.height ?? 0) == 0) { + if (tree.style.height?.value == 0 && tree.style.height?.unit != Unit.auto) { tree.style.margin = tree.style.margin?.collapse() ?? Margins.zero; } return tree; @@ -729,7 +729,7 @@ class HtmlParser extends StatelessWidget { final previousSiblingBottom = 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; + final newInternalMargin = max(previousSiblingBottom, thisTop); if (tree.children[i - 1].style.margin == null) { tree.children[i - 1].style.margin = @@ -798,21 +798,64 @@ class HtmlParser extends StatelessWidget { return tree; } - /// [_processFontSize] changes percent-based font sizes (negative numbers in this implementation) - /// to pixel-based font sizes. - static StyledElement _processFontSize(StyledElement tree) { - double? parentFontSize = tree.style.fontSize?.size ?? FontSize.medium.size; + /// [_calculateRelativeValues] converts rem values to px sizes and then + /// applies relative calculations + static StyledElement _calculateRelativeValues(StyledElement tree, double devicePixelRatio) { + double remSize = (tree.style.fontSize?.value ?? FontSize.medium.value); + + //If the root element has a rem-based fontSize, then give it the default + // font size times the set rem value. + if(tree.style.fontSize?.unit == Unit.rem) { + tree.style.fontSize = FontSize(FontSize.medium.value * remSize); + } + + _applyRelativeValuesRecursive(tree, remSize, devicePixelRatio); + tree.style.setRelativeValues(remSize, remSize / devicePixelRatio); + + return tree; + } + + /// This is the recursive worker function for [_calculateRelativeValues] + static void _applyRelativeValuesRecursive(StyledElement tree, double remFontSize, double devicePixelRatio) { + //When we get to this point, there should be a valid fontSize at every level. + assert(tree.style.fontSize != null); + + final parentFontSize = tree.style.fontSize!.value; tree.children.forEach((child) { - if ((child.style.fontSize?.size ?? parentFontSize)! < 0) { - child.style.fontSize = - FontSize(parentFontSize! * -child.style.fontSize!.size!); + + if(child.style.fontSize == null) { + child.style.fontSize = FontSize(parentFontSize); + } else { + switch(child.style.fontSize!.unit) { + case Unit.em: + child.style.fontSize = FontSize(parentFontSize * child.style.fontSize!.value); + break; + case Unit.percent: + child.style.fontSize = FontSize(parentFontSize * (child.style.fontSize!.value / 100.0)); + break; + case Unit.rem: + child.style.fontSize = FontSize(remFontSize * child.style.fontSize!.value); + break; + case Unit.px: + case Unit.auto: + //Ignore + break; + } } - _processFontSize(child); + // Note: it is necessary to scale down the emSize by the factor of + // devicePixelRatio since Flutter seems to calculates font sizes using + // physical pixels, but margins/padding using logical pixels. + final emSize = child.style.fontSize!.value / devicePixelRatio; + + tree.style.setRelativeValues(remFontSize, emSize); + + _applyRelativeValuesRecursive(child, remFontSize, devicePixelRatio); }); - return tree; } + + } /// The [RenderContext] is available when parsing the tree. It contains information diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart index 5a60422f6c..47c18f36d9 100644 --- a/lib/src/css_box_widget.dart +++ b/lib/src/css_box_widget.dart @@ -67,7 +67,7 @@ class CSSBoxWidget extends StatelessWidget { paddingSize: style.padding?.collapsedSize ?? Size.zero, borderSize: style.border?.dimensions.collapsedSize ?? Size.zero, margins: style.margin ?? Margins.zero, - display: style.display ?? Display.BLOCK, + display: style.display ?? Display.INLINE, childIsReplaced: childIsReplaced, emValue: _calculateEmValue(style, context), textDirection: _checkTextDirection(context, textDirection), @@ -691,6 +691,7 @@ extension Normalize on Dimension { return; case Unit.px: case Unit.auto: + case Unit.percent: return; } } @@ -698,7 +699,7 @@ extension Normalize on Dimension { double _calculateEmValue(Style style, BuildContext buildContext) { //TODO is there a better value for this? - return (style.fontSize?.size ?? 16) * + return (style.fontSize?.emValue ?? 16) * MediaQuery.textScaleFactorOf(buildContext) * MediaQuery.of(buildContext).devicePixelRatio; } diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index a82e2a9589..4f24ddd95d 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -620,15 +620,15 @@ class ExpressionMapping { static FontSize? expressionToFontSize(css.Expression value) { if (value is css.NumberTerm) { - return FontSize(double.tryParse(value.text)); + return FontSize(double.tryParse(value.text) ?? 16, Unit.px); } else if (value is css.PercentageTerm) { - return FontSize.percent(double.tryParse(value.text)!); + return FontSize(double.tryParse(value.text) ?? 100, Unit.percent); } else if (value is css.EmTerm) { - return FontSize.em(double.tryParse(value.text)); - } else if (value is css.RemTerm) { - return FontSize.rem(double.tryParse(value.text)!); + return FontSize(double.tryParse(value.text) ?? 1, Unit.em); + // } else if (value is css.RemTerm) { TODO + // return FontSize.rem(double.tryParse(value.text) ?? 1, Unit.em); } else if (value is css.LengthTerm) { - return FontSize(double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), ''))); + return FontSize(double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')) ?? 16); } else if (value is css.LiteralTerm) { switch (value.text) { case "xx-small": diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index c11b209ecb..ba1b559ea7 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -78,7 +78,7 @@ class RubyElement extends ReplacedElement { Widget toWidget(RenderContext context) { StyledElement? node; List widgets = []; - final rubySize = context.parser.style['rt']?.fontSize?.size ?? max(9.0, context.style.fontSize!.size! / 2); + final rubySize = context.parser.style['rt']?.fontSize?.value ?? max(9.0, context.style.fontSize!.value / 2); final rubyYPos = rubySize + rubySize / 2; List children = []; context.tree.children.forEachIndexed((index, element) { diff --git a/lib/src/style/fontsize.dart b/lib/src/style/fontsize.dart new file mode 100644 index 0000000000..950803ddb7 --- /dev/null +++ b/lib/src/style/fontsize.dart @@ -0,0 +1,40 @@ +//TODO implement dimensionality + +import 'length.dart'; + +class FontSize extends LengthOrPercent { + + FontSize(double size, [Unit unit = Unit.px]): super(size, unit); + + // These values are calculated based off of the default (`medium`) + // being 14px. + // + // TODO(Sub6Resources): This seems to override Flutter's accessibility text scaling. + // + // Negative values are computed during parsing to be a percentage of + // the parent style's font size. + static final xxSmall = FontSize(7.875); + static final xSmall = FontSize(8.75); + static final small = FontSize(11.375); + static final medium = FontSize(14.0); + static final large = FontSize(15.75); + static final xLarge = FontSize(21.0); + static final xxLarge = FontSize(28.0); + static final smaller = FontSize(83, Unit.percent); + static final larger = FontSize(120, Unit.percent); + + static FontSize? inherit(FontSize? parent, FontSize? child) { + if(child != null && parent != null) { + if(child.unit == Unit.em) { + return FontSize(child.value * parent.value); + } else if(child.unit == Unit.percent) { + return FontSize(child.value / 100.0 * parent.value); + } + return child; + } + + return parent; + } + + double get emValue => this.value; +} \ No newline at end of file diff --git a/lib/src/style/length.dart b/lib/src/style/length.dart index 4853201fd7..1984307323 100644 --- a/lib/src/style/length.dart +++ b/lib/src/style/length.dart @@ -1,17 +1,20 @@ +/// Increase new base unit types' values by a factor of 2 each time. const int _percent = 0x1; const int _length = 0x2; const int _auto = 0x4; + +/// These values are combinations of the base unit-types const int _lengthPercent = _length | _percent; const int _lengthPercentAuto = _lengthPercent | _auto; -//TODO there are more unit-types that need support +/// A Unit represents a CSS unit enum Unit { //ch, em(_length), //ex, - //percent(_percent), + percent(_percent), px(_length), - //rem, + rem(_length), //Q, //vh, //vw, @@ -25,29 +28,27 @@ abstract class Dimension { double value; Unit unit; - Dimension(this.value, this.unit) { - assert((unit.unitType | _unitType) == _unitType, "You used a unit for the property that was not allowed"); - } - - int get _unitType; + Dimension(this.value, this.unit, int _dimensionUnitType) + : assert(identical((unit.unitType | _dimensionUnitType), _dimensionUnitType), + "This dimension was given a Unit that isn't specified."); } +/// This dimension takes a value with a length unit such as px or em. Note that +/// these can be fixed or relative (but they must not be a percent) class Length extends Dimension { - Length(double value, [Unit unit = Unit.px]) : super(value, unit); - - @override - int get _unitType => _length; + Length(double value, [Unit unit = Unit.px]): + super(value, unit, _length); } +/// This dimension takes a value with a length-percent unit such as px or em +/// or %. Note that these can be fixed or relative (but they must not be a +/// percent) class LengthOrPercent extends Dimension { - LengthOrPercent(double value, [Unit unit = Unit.px]) : super(value, unit); - - @override - int get _unitType => _lengthPercent; + LengthOrPercent(double value, [Unit unit = Unit.px]): + super(value, unit, _lengthPercent); } class AutoOrLengthOrPercent extends Dimension { - AutoOrLengthOrPercent(double value, [Unit unit = Unit.px]): super(value, unit); - - int get _unitType => _lengthPercentAuto; + AutoOrLengthOrPercent(double value, [Unit unit = Unit.px]): + super(value, unit, _lengthPercentAuto); } \ No newline at end of file diff --git a/lib/src/style/lineheight.dart b/lib/src/style/lineheight.dart new file mode 100644 index 0000000000..9f96e7ee7a --- /dev/null +++ b/lib/src/style/lineheight.dart @@ -0,0 +1,25 @@ +//TODO implement dimensionality +class LineHeight { + final double? size; + final String units; + + const LineHeight(this.size, {this.units = ""}); + + factory LineHeight.percent(double percent) { + return LineHeight(percent / 100.0 * 1.2, units: "%"); + } + + factory LineHeight.em(double em) { + return LineHeight(em * 1.2, units: "em"); + } + + factory LineHeight.rem(double rem) { + return LineHeight(rem * 1.2, units: "rem"); + } + + factory LineHeight.number(double num) { + return LineHeight(num * 1.2, units: "number"); + } + + static const normal = LineHeight(1.2); +} \ No newline at end of file diff --git a/lib/src/styled_element.dart b/lib/src/styled_element.dart index bd687efe67..2b2cdd6d48 100644 --- a/lib/src/styled_element.dart +++ b/lib/src/styled_element.dart @@ -197,49 +197,48 @@ StyledElement parseStyledElement( break; case "h1": styledElement.style = Style( - fontSize: FontSize.xxLarge, + fontSize: FontSize(2, Unit.em), fontWeight: FontWeight.bold, - margin: Margins.symmetric(vertical: 18.67), + margin: Margins.symmetric(vertical: 0.67, unit: Unit.em), display: Display.BLOCK, ); break; case "h2": styledElement.style = Style( - fontSize: FontSize.xLarge, + fontSize: FontSize(1.5, Unit.em), fontWeight: FontWeight.bold, - margin: Margins.symmetric(vertical: 17.5), + margin: Margins.symmetric(vertical: 0.83, unit: Unit.em), display: Display.BLOCK, ); break; case "h3": styledElement.style = Style( - fontSize: FontSize(16.38), + fontSize: FontSize(1.17, Unit.em), fontWeight: FontWeight.bold, - margin: Margins.symmetric(vertical: 16.5), + margin: Margins.symmetric(vertical: 1, unit: Unit.em), display: Display.BLOCK, ); break; case "h4": styledElement.style = Style( - fontSize: FontSize.medium, fontWeight: FontWeight.bold, - margin: Margins.symmetric(vertical: 18.5), + margin: Margins.symmetric(vertical: 1.33, unit: Unit.em), display: Display.BLOCK, ); break; case "h5": styledElement.style = Style( - fontSize: FontSize(11.62), + fontSize: FontSize(0.83, Unit.em), fontWeight: FontWeight.bold, - margin: Margins.symmetric(vertical: 19.25), + margin: Margins.symmetric(vertical: 1.67, unit: Unit.em), display: Display.BLOCK, ); break; case "h6": styledElement.style = Style( - fontSize: FontSize(9.38), + fontSize: FontSize(0.67, Unit.em), fontWeight: FontWeight.bold, - margin: Margins.symmetric(vertical: 22), + margin: Margins.symmetric(vertical: 2.33, unit: Unit.em), display: Display.BLOCK, ); break; diff --git a/lib/style.dart b/lib/style.dart index fb1365b527..7caf9a3dba 100644 --- a/lib/style.dart +++ b/lib/style.dart @@ -8,6 +8,8 @@ import 'package:flutter_html/src/css_parser.dart'; export 'package:flutter_html/src/style/margin.dart'; export 'package:flutter_html/src/style/length.dart'; export 'package:flutter_html/src/style/size.dart'; +export 'package:flutter_html/src/style/fontsize.dart'; +export 'package:flutter_html/src/style/lineheight.dart'; ///This class represents all the available CSS attributes ///for this package. @@ -249,17 +251,19 @@ class Style { } } - static Map fromThemeData(ThemeData theme) => { - 'h1': Style.fromTextStyle(theme.textTheme.headline1!), - 'h2': Style.fromTextStyle(theme.textTheme.headline2!), - 'h3': Style.fromTextStyle(theme.textTheme.headline3!), - 'h4': Style.fromTextStyle(theme.textTheme.headline4!), - 'h5': Style.fromTextStyle(theme.textTheme.headline5!), - 'h6': Style.fromTextStyle(theme.textTheme.headline6!), - 'body': Style.fromTextStyle(theme.textTheme.bodyText2!), - }; - - static Map fromCss(String css, OnCssParseError? onCssParseError) { + static Map fromThemeData(ThemeData theme) => + { + 'h1': Style.fromTextStyle(theme.textTheme.headline1!), + 'h2': Style.fromTextStyle(theme.textTheme.headline2!), + 'h3': Style.fromTextStyle(theme.textTheme.headline3!), + 'h4': Style.fromTextStyle(theme.textTheme.headline4!), + 'h5': Style.fromTextStyle(theme.textTheme.headline5!), + 'h6': Style.fromTextStyle(theme.textTheme.headline6!), + 'body': Style.fromTextStyle(theme.textTheme.bodyText2!), + }; + + static Map fromCss(String css, + OnCssParseError? onCssParseError) { final declarations = parseExternalCss(css, onCssParseError); Map styleMap = {}; declarations.forEach((key, value) { @@ -279,7 +283,7 @@ class Style { fontFamily: fontFamily, fontFamilyFallback: fontFamilyFallback, fontFeatures: fontFeatureSettings, - fontSize: fontSize?.size, + fontSize: fontSize?.value, fontStyle: fontStyle, fontWeight: fontWeight, letterSpacing: letterSpacing, @@ -341,18 +345,19 @@ class Style { } Style copyOnlyInherited(Style child) { - FontSize? finalFontSize = child.fontSize != null ? - fontSize != null && child.fontSize?.units == "em" ? - FontSize(child.fontSize!.size! * fontSize!.size!) : child.fontSize - : fontSize != null && fontSize!.size! < 0 ? - FontSize.percent(100) : fontSize; - LineHeight? finalLineHeight = child.lineHeight != null ? - child.lineHeight?.units == "length" ? - LineHeight(child.lineHeight!.size! / (finalFontSize == null ? 14 : finalFontSize.size!) * 1.2) : child.lineHeight - : lineHeight; + + FontSize? finalFontSize = FontSize.inherit(fontSize, child.fontSize); + + LineHeight? finalLineHeight = child.lineHeight != null + ? child.lineHeight?.units == "length" + ? LineHeight(child.lineHeight!.size! / (finalFontSize == null ? 14 : finalFontSize.value) * 1.2) + : child.lineHeight + : lineHeight; + return child.copyWith( - backgroundColor: child.backgroundColor != Colors.transparent ? - child.backgroundColor : backgroundColor, + backgroundColor: child.backgroundColor != Colors.transparent + ? child.backgroundColor + : backgroundColor, color: child.color ?? color, direction: child.direction ?? direction, display: display == Display.NONE ? display : child.display, @@ -367,9 +372,10 @@ class Style { listStyleType: child.listStyleType ?? listStyleType, listStylePosition: child.listStylePosition ?? listStylePosition, textAlign: child.textAlign ?? textAlign, - textDecoration: TextDecoration.combine( - [child.textDecoration ?? TextDecoration.none, - textDecoration ?? TextDecoration.none]), + textDecoration: TextDecoration.combine([ + child.textDecoration ?? TextDecoration.none, + textDecoration ?? TextDecoration.none, + ]), textShadow: child.textShadow ?? textShadow, whiteSpace: child.whiteSpace ?? whiteSpace, wordSpacing: child.wordSpacing ?? wordSpacing, @@ -440,7 +446,7 @@ class Style { textDecorationColor: textDecorationColor ?? this.textDecorationColor, textDecorationStyle: textDecorationStyle ?? this.textDecorationStyle, textDecorationThickness: - textDecorationThickness ?? this.textDecorationThickness, + textDecorationThickness ?? this.textDecorationThickness, textShadow: textShadow ?? this.textShadow, verticalAlign: verticalAlign ?? this.verticalAlign, whiteSpace: whiteSpace ?? this.whiteSpace, @@ -467,7 +473,9 @@ class Style { this.fontFamily = textStyle.fontFamily; this.fontFamilyFallback = textStyle.fontFamilyFallback; this.fontFeatureSettings = textStyle.fontFeatures; - this.fontSize = FontSize(textStyle.fontSize); + this.fontSize = textStyle.fontSize != null + ? FontSize(textStyle.fontSize!) + : null; this.fontStyle = textStyle.fontStyle; this.fontWeight = textStyle.fontWeight; this.letterSpacing = textStyle.letterSpacing; @@ -476,76 +484,71 @@ class Style { this.lineHeight = LineHeight(textStyle.height ?? 1.2); this.textTransform = TextTransform.none; } -} - -enum Display { - BLOCK, - INLINE, - INLINE_BLOCK, - LIST_ITEM, - NONE, -} - -//TODO implement dimensionality -class FontSize { - final double? size; - final String units; - - const FontSize(this.size, {this.units = ""}); - /// A percentage of the parent style's font size. - factory FontSize.percent(double percent) { - return FontSize(percent / -100.0, units: "%"); - } + /// Sets any dimensions set to rem or em to the computed size + void setRelativeValues(double remValue, double emValue) { + if(width?.unit == Unit.rem) { + width = Width(width!.value * remValue); + } else if(width?.unit == Unit.em) { + width = Width(width!.value * emValue); + } - factory FontSize.em(double? em) { - return FontSize(em, units: "em"); - } + if(height?.unit == Unit.rem) { + height = Height(height!.value * remValue); + } else if(height?.unit == Unit.em) { + height = Height(height!.value * emValue); + } - factory FontSize.rem(double rem) { - return FontSize(rem * 16 - 2, units: "rem"); - } - // These values are calculated based off of the default (`medium`) - // being 14px. - // - // TODO(Sub6Resources): This seems to override Flutter's accessibility text scaling. - // - // Negative values are computed during parsing to be a percentage of - // the parent style's font size. - static const xxSmall = FontSize(7.875); - static const xSmall = FontSize(8.75); - static const small = FontSize(11.375); - static const medium = FontSize(14.0); - static const large = FontSize(15.75); - static const xLarge = FontSize(21.0); - static const xxLarge = FontSize(28.0); - static const smaller = FontSize(-0.83); - static const larger = FontSize(-1.2); -} + if(fontSize?.unit == Unit.rem) { + fontSize = FontSize(fontSize!.value * remValue); + } else if(fontSize?.unit == Unit.em) { + fontSize = FontSize(fontSize!.value * emValue); + } -class LineHeight { - final double? size; - final String units; + Margin? marginLeft; + Margin? marginTop; + Margin? marginRight; + Margin? marginBottom; - const LineHeight(this.size, {this.units = ""}); + if(margin?.left?.unit == Unit.rem) { + marginLeft = Margin(margin!.left!.value * remValue); + } else if(margin?.left?.unit == Unit.em) { + marginLeft = Margin(margin!.left!.value * emValue); + } - factory LineHeight.percent(double percent) { - return LineHeight(percent / 100.0 * 1.2, units: "%"); - } + if(margin?.top?.unit == Unit.rem) { + marginTop = Margin(margin!.top!.value * remValue); + } else if(margin?.top?.unit == Unit.em) { + marginTop = Margin(margin!.top!.value * emValue); + } - factory LineHeight.em(double em) { - return LineHeight(em * 1.2, units: "em"); - } + if(margin?.right?.unit == Unit.rem) { + marginRight = Margin(margin!.right!.value * remValue); + } else if(margin?.right?.unit == Unit.em) { + marginRight = Margin(margin!.right!.value * emValue); + } - factory LineHeight.rem(double rem) { - return LineHeight(rem * 1.2, units: "rem"); - } + if(margin?.bottom?.unit == Unit.rem) { + marginBottom = Margin(margin!.bottom!.value * remValue); + } else if(margin?.bottom?.unit == Unit.em) { + marginBottom = Margin(margin!.bottom!.value * emValue); + } - factory LineHeight.number(double num) { - return LineHeight(num * 1.2, units: "number"); + margin = margin?.copyWith( + left: marginLeft, + top: marginTop, + right: marginRight, + bottom: marginBottom, + ); } +} - static const normal = LineHeight(1.2); +enum Display { + BLOCK, + INLINE, + INLINE_BLOCK, + LIST_ITEM, + NONE, } class ListStyleType { diff --git a/packages/flutter_html_audio/README.md b/packages/flutter_html_audio/README.md index 8c63614c36..81ac741006 100644 --- a/packages/flutter_html_audio/README.md +++ b/packages/flutter_html_audio/README.md @@ -1,6 +1,6 @@ # flutter_html_audio -Audio widget for flutter_html. +Audio extension for flutter_html. This package renders audio elements using the [`chewie_audio`](https://pub.dev/packages/chewie_audio) and the [`video_player`](https://pub.dev/packages/video_player) plugin. diff --git a/packages/flutter_html_audio/lib/flutter_html_audio.dart b/packages/flutter_html_audio/lib/flutter_html_audio.dart index 2484b8e45d..d9576f0270 100644 --- a/packages/flutter_html_audio/lib/flutter_html_audio.dart +++ b/packages/flutter_html_audio/lib/flutter_html_audio.dart @@ -80,13 +80,14 @@ class _AudioWidgetState extends State { if (sources.isEmpty || sources.first == null) { return Container(height: 0, width: 0); } - return Container( + + return CSSBoxWidget( key: widget.context.key, - width: widget.context.style.width ?? 300, - height: Theme.of(bContext).platform == TargetPlatform.android ? 48 : 75, + style: widget.context.style, child: ChewieAudio( controller: chewieAudioController!, ), + childIsReplaced: true, ); } } diff --git a/packages/flutter_html_iframe/lib/iframe_mobile.dart b/packages/flutter_html_iframe/lib/iframe_mobile.dart index 2ae61de59e..80b20cd09c 100644 --- a/packages/flutter_html_iframe/lib/iframe_mobile.dart +++ b/packages/flutter_html_iframe/lib/iframe_mobile.dart @@ -15,10 +15,9 @@ CustomRender iframeRender({NavigationDelegate? navigationDelegate}) => return Container( width: givenWidth ?? (givenHeight ?? 150) * 2, height: givenHeight ?? (givenWidth ?? 300) / 2, - child: ContainerSpan( + child: CSSBoxWidget( style: context.style, - renderContext: context, - containingBlockSize: Size.zero, //TODO this is incorrect + childIsReplaced: true, child: WebView( initialUrl: context.tree.element?.attributes['src'], key: key, diff --git a/packages/flutter_html_iframe/lib/iframe_web.dart b/packages/flutter_html_iframe/lib/iframe_web.dart index fee8a95fac..e5252c7d0f 100644 --- a/packages/flutter_html_iframe/lib/iframe_web.dart +++ b/packages/flutter_html_iframe/lib/iframe_web.dart @@ -36,16 +36,17 @@ CustomRender iframeRender({NavigationDelegate? navigationDelegate}) => context.tree.element?.attributes['width'] ?? "") ?? 300) / 2, - child: ContainerSpan( + child: CSSBoxWidget( style: context.style, - renderContext: context, - containingBlockSize: Size.zero, //TODO this is incorrect + childIsReplaced: true, child: Directionality( textDirection: TextDirection.ltr, child: HtmlElementView( viewType: createdViewId, - )), - )); + ), + ), + ), + ); }); String getRandString(int len) { diff --git a/packages/flutter_html_table/lib/flutter_html_table.dart b/packages/flutter_html_table/lib/flutter_html_table.dart index ec34ae68a9..d6dbce29a8 100644 --- a/packages/flutter_html_table/lib/flutter_html_table.dart +++ b/packages/flutter_html_table/lib/flutter_html_table.dart @@ -24,8 +24,8 @@ CustomRender tableRender() => color: context.style.backgroundColor, border: context.style.border, ), - width: context.style.width, - height: context.style.height, + width: context.style.width?.value, //TODO calculate actual value + height: context.style.height?.value, //TODO calculate actual value child: LayoutBuilder( builder: (_, constraints) => _layoutCells(context, constraints)), ); @@ -116,24 +116,16 @@ Widget _layoutCells(RenderContext context, BoxConstraints constraints) { columnColspanOffset[columni].clamp(1, columnMax - columni - 1); } cells.add(GridPlacement( - child: Container( - width: child.style.width ?? double.infinity, - height: child.style.height, - padding: child.style.padding?.nonNegative ?? - row.style.padding?.nonNegative, - decoration: BoxDecoration( - color: child.style.backgroundColor ?? row.style.backgroundColor, - border: child.style.border ?? row.style.border, - ), + child: CSSBoxWidget( + style: child.style.merge(row.style), //TODO padding/decoration(color/border) child: SizedBox.expand( child: Container( alignment: child.style.alignment ?? context.style.alignment ?? Alignment.centerLeft, - child: StyledText( - textSpan: context.parser.parseTree(context, child), - style: child.style, - renderContext: context, + child: CSSBoxWidget.withInlineSpanChildren( + children: [context.parser.parseTree(context, child)], + style: child.style, //TODO updated this. Does it work? ), ), ), diff --git a/packages/flutter_html_video/README.md b/packages/flutter_html_video/README.md index 62b7551e4b..a4d0644a4c 100644 --- a/packages/flutter_html_video/README.md +++ b/packages/flutter_html_video/README.md @@ -1,6 +1,6 @@ # flutter_html_video -Video widget for flutter_html. +Video extension for flutter_html. This package renders video elements using the [`chewie`](https://pub.dev/packages/chewie) and the [`video_player`](https://pub.dev/packages/video_player) plugin. diff --git a/test/html_parser_test.dart b/test/html_parser_test.dart index 0ded2f3bc7..3937c4a296 100644 --- a/test/html_parser_test.dart +++ b/test/html_parser_test.dart @@ -54,9 +54,7 @@ void testNewParser(BuildContext context) { tagsList: Html.tags, selectionControls: null, scrollPhysics: null, - constraints: BoxConstraints(), ), - BoxConstraints(), ); print(tree.toString()); @@ -82,9 +80,7 @@ void testNewParser(BuildContext context) { tagsList: Html.tags, selectionControls: null, scrollPhysics: null, - constraints: BoxConstraints(), ), - BoxConstraints() ); print(tree.toString()); @@ -108,9 +104,7 @@ void testNewParser(BuildContext context) { tagsList: Html.tags, selectionControls: null, scrollPhysics: null, - constraints: BoxConstraints(), ), - BoxConstraints() ); print(tree.toString()); @@ -136,9 +130,7 @@ void testNewParser(BuildContext context) { tagsList: Html.tags, selectionControls: null, scrollPhysics: null, - constraints: BoxConstraints(), ), - BoxConstraints(), ); print(tree.toString()); From aa800e29ef68952b0e88018a06bf879427757d10 Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Tue, 6 Sep 2022 09:30:23 -0600 Subject: [PATCH 10/14] Ran flutter format --- lib/custom_render.dart | 185 +++-- lib/flutter_html.dart | 3 +- lib/html_parser.dart | 289 +++++--- lib/src/anchor.dart | 5 +- lib/src/css_box_widget.dart | 48 +- lib/src/css_parser.dart | 665 ++++++++++++------ lib/src/html_elements.dart | 10 +- lib/src/interactable_element.dart | 25 +- lib/src/layout_element.dart | 47 +- lib/src/replaced_element.dart | 77 +- lib/src/style/fontsize.dart | 11 +- lib/src/style/length.dart | 17 +- lib/src/style/lineheight.dart | 2 +- lib/src/style/margin.dart | 79 ++- lib/src/style/size.dart | 14 +- lib/src/styled_element.dart | 23 +- lib/src/utils.dart | 5 +- lib/style.dart | 57 +- .../flutter_html_iframe/lib/iframe_web.dart | 39 +- ...svg_image_matcher_source_matcher_test.dart | 24 +- .../lib/flutter_html_table.dart | 3 +- 21 files changed, 999 insertions(+), 629 deletions(-) diff --git a/lib/custom_render.dart b/lib/custom_render.dart index 1d4b6ee52e..15ba16aeb9 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -17,7 +17,7 @@ CustomRenderMatcher tagMatcher(String tag) => (context) { CustomRenderMatcher blockElementMatcher() => (context) { return (context.tree.style.display == Display.BLOCK || - context.tree.style.display == Display.INLINE_BLOCK) && + context.tree.style.display == Display.INLINE_BLOCK) && (context.tree.children.isNotEmpty || context.tree.element?.localName == "hr"); }; @@ -132,21 +132,23 @@ CustomRender blockElementRender({Style? style, List? children}) => alignment: PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic, child: CSSBoxWidget.withInlineSpanChildren( - key: context.key, - style: style ?? context.tree.style, - shrinkWrap: context.parser.shrinkWrap, - childIsReplaced: REPLACED_EXTERNAL_ELEMENTS.contains(context.tree.name), - children: children ?? - context.tree.children - .expandIndexed((i, childTree) => [ - context.parser.parseTree(context, childTree), - //TODO can this newline be added in a different step? - if (i != context.tree.children.length - 1 && - childTree.style.display == Display.BLOCK && - childTree.element?.localName != "html" && - childTree.element?.localName != "body") - TextSpan(text: "\n"), - ]).toList(), + key: context.key, + style: style ?? context.tree.style, + shrinkWrap: context.parser.shrinkWrap, + childIsReplaced: + REPLACED_EXTERNAL_ELEMENTS.contains(context.tree.name), + children: children ?? + context.tree.children + .expandIndexed((i, childTree) => [ + context.parser.parseTree(context, childTree), + //TODO can this newline be added in a different step? + if (i != context.tree.children.length - 1 && + childTree.style.display == Display.BLOCK && + childTree.element?.localName != "html" && + childTree.element?.localName != "body") + TextSpan(text: "\n"), + ]) + .toList(), ), ); }); @@ -154,86 +156,81 @@ CustomRender blockElementRender({Style? style, List? children}) => CustomRender listElementRender( {Style? style, Widget? child, List? children}) => CustomRender.inlineSpan( - inlineSpan: (context, buildChildren) => WidgetSpan( - child: CSSBoxWidget( - key: context.key, - 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: CSSBoxWidget.withInlineSpanChildren( - 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 ?? context.style, - ), - ), - ), - ], + inlineSpan: (context, buildChildren) => WidgetSpan( + child: CSSBoxWidget( + key: context.key, + 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: CSSBoxWidget.withInlineSpanChildren( + 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 ?? context.style, + ), ), ), - ), + ], + ), + ), + ), ); CustomRender replacedElementRender( diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index c6e06ad7ab..ec60476178 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -362,7 +362,8 @@ class _SelectableHtmlState extends State { customRenders: {} ..addAll(widget.customRenders) ..addAll(generateDefaultRenders()), - 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/lib/html_parser.dart b/lib/html_parser.dart index e062b73575..dfbd996fae 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -14,10 +14,10 @@ import 'package:html/parser.dart' as htmlparser; import 'package:numerus/numerus.dart'; typedef OnTap = void Function( - String? url, - RenderContext context, - Map attributes, - dom.Element? element, + String? url, + RenderContext context, + Map attributes, + dom.Element? element, ); typedef OnCssParseError = String? Function( String css, @@ -62,10 +62,10 @@ class HtmlParser extends StatelessWidget { this.selectionControls, this.scrollPhysics, }) : this.internalOnAnchorTap = onAnchorTap != null - ? onAnchorTap - : key != null - ? _handleAnchorTap(key, onLinkTap) - : onLinkTap, + ? onAnchorTap + : key != null + ? _handleAnchorTap(key, onLinkTap) + : onLinkTap, super(key: key); /// As the widget [build]s, the HTML data is processed into a tree of [StyledElement]s, @@ -73,7 +73,6 @@ class HtmlParser extends StatelessWidget { //TODO Lazy processing of data. We don't need the processing steps done every build phase unless the data has changed. @override Widget build(BuildContext context) { - // Lexing Step StyledElement lexedTree = lexDomTree( htmlData, @@ -84,10 +83,12 @@ class HtmlParser extends StatelessWidget { ); // Styling Step - StyledElement styledTree = styleTree(lexedTree, htmlData, style, onCssParseError); + StyledElement styledTree = + styleTree(lexedTree, htmlData, style, onCssParseError); // Processing Step - StyledElement processedTree = processTree(styledTree, MediaQuery.of(context).devicePixelRatio); + StyledElement processedTree = + processTree(styledTree, MediaQuery.of(context).devicePixelRatio); // Parsing Step InlineSpan parsedTree = parseTree( @@ -193,13 +194,14 @@ class HtmlParser extends StatelessWidget { final StyledElement tree = parseStyledElement(node, children); for (final entry in customRenderMatchers) { if (entry.call( - RenderContext( - buildContext: context, - parser: parser, - tree: tree, - style: Style.fromTextStyle(Theme.of(context).textTheme.bodyText2!), - ), - )) { + RenderContext( + buildContext: context, + parser: parser, + tree: tree, + style: + Style.fromTextStyle(Theme.of(context).textTheme.bodyText2!), + ), + )) { return tree; } } @@ -217,7 +219,9 @@ class HtmlParser extends StatelessWidget { } } - static Map>> _getExternalCssDeclarations(List styles, OnCssParseError? errorHandler) { + static Map>> + _getExternalCssDeclarations( + List styles, OnCssParseError? errorHandler) { String fullCss = ""; for (final e in styles) { fullCss = fullCss + e.innerHtml; @@ -230,7 +234,9 @@ class HtmlParser extends StatelessWidget { } } - static StyledElement _applyExternalCss(Map>> declarations, StyledElement tree) { + static StyledElement _applyExternalCss( + Map>> declarations, + StyledElement tree) { declarations.forEach((key, style) { try { if (tree.matchesSelector(key)) { @@ -244,7 +250,8 @@ class HtmlParser extends StatelessWidget { return tree; } - static StyledElement _applyInlineStyles(StyledElement tree, OnCssParseError? errorHandler) { + static StyledElement _applyInlineStyles( + StyledElement tree, OnCssParseError? errorHandler) { if (tree.attributes.containsKey("style")) { final newStyle = inlineCssToStyle(tree.attributes['style'], errorHandler); if (newStyle != null) { @@ -258,7 +265,8 @@ class HtmlParser extends StatelessWidget { /// [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( + Map style, StyledElement tree) { style.forEach((key, style) { try { if (tree.matchesSelector(key)) { @@ -273,7 +281,8 @@ class HtmlParser extends StatelessWidget { /// [_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( + Map style, StyledElement tree) { tree.children.forEach((child) { child.style = tree.style.copyOnlyInherited(child.style); _cascadeStyles(style, child); @@ -284,8 +293,11 @@ class HtmlParser extends StatelessWidget { /// [styleTree] takes the lexed [StyleElement] tree and applies external, /// inline, and custom CSS/Flutter styles, and then cascades the styles down the tree. - static StyledElement styleTree(StyledElement tree, dom.Element htmlData, Map style, OnCssParseError? onCssParseError) { - Map>> declarations = _getExternalCssDeclarations(htmlData.getElementsByTagName("style"), onCssParseError); + static StyledElement styleTree(StyledElement tree, dom.Element htmlData, + Map style, OnCssParseError? onCssParseError) { + Map>> declarations = + _getExternalCssDeclarations( + htmlData.getElementsByTagName("style"), onCssParseError); StyledElement? externalCssStyledTree; if (declarations.isNotEmpty) { @@ -300,7 +312,8 @@ class HtmlParser extends StatelessWidget { /// [processTree] 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 processTree(StyledElement tree, double devicePixelRatio) { + static StyledElement processTree( + StyledElement tree, double devicePixelRatio) { tree = _processInternalWhitespace(tree); tree = _processInlineWhitespace(tree); tree = _removeEmptyElements(tree); @@ -329,22 +342,33 @@ class HtmlParser extends StatelessWidget { 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); + 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; + return customRenders[entry]! + .inlineSpan! + .call(newContext, buildChildren) as TextSpan; } if (customRenders[entry]?.inlineSpan != null) { - return customRenders[entry]!.inlineSpan!.call(newContext, buildChildren); + return customRenders[entry]! + .inlineSpan! + .call(newContext, buildChildren); } return WidgetSpan( child: CSSBoxWidget( style: tree.style, shrinkWrap: newContext.parser.shrinkWrap, - child: customRenders[entry]!.widget!.call(newContext, buildChildren), + child: + customRenders[entry]!.widget!.call(newContext, buildChildren), childIsReplaced: true, //TODO is this true? ), ); @@ -353,10 +377,13 @@ class HtmlParser extends StatelessWidget { return WidgetSpan(child: Container(height: 0, width: 0)); } - static OnTap _handleAnchorTap(Key key, OnTap? onLinkTap) => - (String? url, RenderContext context, Map attributes, dom.Element? element) { + static OnTap _handleAnchorTap(Key key, OnTap? onLinkTap) => (String? url, + RenderContext context, + Map attributes, + dom.Element? element) { if (url?.startsWith("#") == true) { - final anchorContext = AnchorKey.forId(key, url!.substring(1))?.currentContext; + final anchorContext = + AnchorKey.forId(key, url!.substring(1))?.currentContext; if (anchorContext != null) { Scrollable.ensureVisible(anchorContext); } @@ -400,24 +427,33 @@ class HtmlParser extends StatelessWidget { /// initialize indices to negative numbers to make conditionals a little easier int textIndex = -1; int elementIndex = -1; + /// initialize parent after to a whitespace to account for elements that are /// the last child in the list of elements String parentAfterText = " "; + /// find the index of the text in the current tree if ((tree.element?.nodes.length ?? 0) >= 1) { - textIndex = tree.element?.nodes.indexWhere((element) => element == tree.node) ?? -1; + textIndex = + tree.element?.nodes.indexWhere((element) => element == tree.node) ?? + -1; } + /// get the parent nodes dom.NodeList? parentNodes = tree.element?.parent?.nodes; + /// find the index of the tree itself in the parent nodes if ((parentNodes?.length ?? 0) >= 1) { - elementIndex = parentNodes?.indexWhere((element) => element == tree.element) ?? -1; + elementIndex = + parentNodes?.indexWhere((element) => element == tree.element) ?? -1; } + /// if the tree is any node except the last node in the node list and the /// next node in the node list is a text node, then get its text. Otherwise /// the next node will be a [dom.Element], so keep unwrapping that until /// we get the underlying text node, and finally get its text. - if (elementIndex < (parentNodes?.length ?? 1) - 1 && parentNodes?[elementIndex + 1] is dom.Text) { + if (elementIndex < (parentNodes?.length ?? 1) - 1 && + parentNodes?[elementIndex + 1] is dom.Text) { parentAfterText = parentNodes?[elementIndex + 1].text ?? " "; } else if (elementIndex < (parentNodes?.length ?? 1) - 1) { var parentAfter = parentNodes?[elementIndex + 1]; @@ -430,6 +466,7 @@ class HtmlParser extends StatelessWidget { } parentAfterText = parentAfter?.text ?? " "; } + /// If the text is the first element in the current tree node list, it /// starts with a whitespace, it isn't a line break, either the /// whitespace is unnecessary or it is a block element, and either it is @@ -439,38 +476,37 @@ class HtmlParser extends StatelessWidget { /// We should also delete the whitespace at any point in the node list /// if the previous element is a
because that tag makes the element /// act like a block element. - if (textIndex < 1 - && tree.text!.startsWith(' ') - && tree.element?.localName != "br" - && (!keepLeadingSpace.data - || tree.style.display == Display.BLOCK) - && (elementIndex < 1 - || (elementIndex >= 1 - && parentNodes?[elementIndex - 1] is dom.Text - && parentNodes![elementIndex - 1].text!.endsWith(" "))) - ) { + if (textIndex < 1 && + tree.text!.startsWith(' ') && + tree.element?.localName != "br" && + (!keepLeadingSpace.data || tree.style.display == Display.BLOCK) && + (elementIndex < 1 || + (elementIndex >= 1 && + parentNodes?[elementIndex - 1] is dom.Text && + parentNodes![elementIndex - 1].text!.endsWith(" ")))) { tree.text = tree.text!.replaceFirst(' ', ''); - } else if (textIndex >= 1 - && tree.text!.startsWith(' ') - && tree.element?.nodes[textIndex - 1] is dom.Element - && (tree.element?.nodes[textIndex - 1] as dom.Element).localName == "br" - ) { + } else if (textIndex >= 1 && + tree.text!.startsWith(' ') && + tree.element?.nodes[textIndex - 1] is dom.Element && + (tree.element?.nodes[textIndex - 1] as dom.Element).localName == + "br") { tree.text = tree.text!.replaceFirst(' ', ''); } + /// If the text is the last element in the current tree node list, it isn't /// a line break, and the next text node starts with a whitespace, /// update the [Context] to signify to that next text node whether it should /// keep its whitespace. This is based on whether the current text ends with a /// whitespace. - if (textIndex == (tree.element?.nodes.length ?? 1) - 1 - && tree.element?.localName != "br" - && parentAfterText.startsWith(' ') - ) { + if (textIndex == (tree.element?.nodes.length ?? 1) - 1 && + tree.element?.localName != "br" && + parentAfterText.startsWith(' ')) { keepLeadingSpace.data = !tree.text!.endsWith(' '); } } - tree.children.forEach((e) => _processInlineWhitespaceRecursive(e, keepLeadingSpace)); + tree.children + .forEach((e) => _processInlineWhitespaceRecursive(e, keepLeadingSpace)); return tree; } @@ -507,14 +543,19 @@ class HtmlParser extends StatelessWidget { if (tree.style.listStylePosition == null) { tree.style.listStylePosition = ListStylePosition.OUTSIDE; } - if (tree.name == 'ol' && tree.style.listStyleType != null && tree.style.listStyleType!.type == "marker") { + if (tree.name == 'ol' && + tree.style.listStyleType != null && + tree.style.listStyleType!.type == "marker") { switch (tree.style.listStyleType!) { case ListStyleType.LOWER_LATIN: case ListStyleType.LOWER_ALPHA: case ListStyleType.UPPER_LATIN: case ListStyleType.UPPER_ALPHA: olStack.add(Context('a')); - if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) { + if ((tree.attributes['start'] != null + ? int.tryParse(tree.attributes['start']!) + : null) != + null) { var start = int.tryParse(tree.attributes['start']!) ?? 1; var x = 1; while (x < start) { @@ -524,14 +565,22 @@ class HtmlParser extends StatelessWidget { } break; default: - olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1)); + olStack.add(Context((tree.attributes['start'] != null + ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 + : 1) - + 1)); break; } - } else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null && tree.style.listStyleType!.type == "widget") { + } else if (tree.style.display == Display.LIST_ITEM && + tree.style.listStyleType != null && + tree.style.listStyleType!.type == "widget") { tree.style.markerContent = tree.style.listStyleType!.widget!; - } else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null && tree.style.listStyleType!.type == "image") { + } else if (tree.style.display == Display.LIST_ITEM && + tree.style.listStyleType != null && + tree.style.listStyleType!.type == "image") { tree.style.markerContent = Image.network(tree.style.listStyleType!.text); - } else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null) { + } else if (tree.style.display == Display.LIST_ITEM && + tree.style.listStyleType != null) { String marker = ""; switch (tree.style.listStyleType!) { case ListStyleType.NONE: @@ -547,7 +596,10 @@ class HtmlParser extends StatelessWidget { break; case ListStyleType.DECIMAL: if (olStack.isEmpty) { - olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1)); + olStack.add(Context((tree.attributes['start'] != null + ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 + : 1) - + 1)); } olStack.last.data += 1; marker = '${olStack.last.data}.'; @@ -556,7 +608,10 @@ class HtmlParser extends StatelessWidget { case ListStyleType.LOWER_ALPHA: if (olStack.isEmpty) { olStack.add(Context('a')); - if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) { + if ((tree.attributes['start'] != null + ? int.tryParse(tree.attributes['start']!) + : null) != + null) { var start = int.tryParse(tree.attributes['start']!) ?? 1; var x = 1; while (x < start) { @@ -572,7 +627,10 @@ class HtmlParser extends StatelessWidget { case ListStyleType.UPPER_ALPHA: if (olStack.isEmpty) { olStack.add(Context('a')); - if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) { + if ((tree.attributes['start'] != null + ? int.tryParse(tree.attributes['start']!) + : null) != + null) { var start = int.tryParse(tree.attributes['start']!) ?? 1; var x = 1; while (x < start) { @@ -586,18 +644,27 @@ class HtmlParser extends StatelessWidget { break; case ListStyleType.LOWER_ROMAN: if (olStack.isEmpty) { - olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1)); + olStack.add(Context((tree.attributes['start'] != null + ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 + : 1) - + 1)); } olStack.last.data += 1; if (olStack.last.data <= 0) { marker = '${olStack.last.data}.'; } else { - marker = (olStack.last.data as int).toRomanNumeralString()!.toLowerCase() + "."; + marker = (olStack.last.data as int) + .toRomanNumeralString()! + .toLowerCase() + + "."; } break; case ListStyleType.UPPER_ROMAN: if (olStack.isEmpty) { - olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1)); + olStack.add(Context((tree.attributes['start'] != null + ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 + : 1) - + 1)); } olStack.last.data += 1; if (olStack.last.data <= 0) { @@ -608,9 +675,9 @@ class HtmlParser extends StatelessWidget { break; } tree.style.markerContent = Text( - marker, - textAlign: TextAlign.right, - style: tree.style.generateTextStyle(), + marker, + textAlign: TextAlign.right, + style: tree.style.generateTextStyle(), ); } @@ -632,14 +699,16 @@ class HtmlParser extends StatelessWidget { 0, TextContentElement( text: tree.style.before, - style: tree.style.copyWith(beforeAfterNull: true, display: Display.INLINE), + style: tree.style + .copyWith(beforeAfterNull: true, display: Display.INLINE), ), ); } if (tree.style.after != null) { tree.children.add(TextContentElement( - text: tree.style.after, - style: tree.style.copyWith(beforeAfterNull: true, display: Display.INLINE), + text: tree.style.after, + style: + tree.style.copyWith(beforeAfterNull: true, display: Display.INLINE), )); } @@ -662,7 +731,8 @@ class HtmlParser extends StatelessWidget { //Short circuit if we've reached a leaf of the tree if (tree.children.isEmpty) { // Handle case (4) from above. - if (tree.style.height?.value == 0 && tree.style.height?.unit != Unit.auto) { + if (tree.style.height?.value == 0 && + tree.style.height?.unit != Unit.auto) { tree.style.margin = tree.style.margin?.collapse() ?? Margins.zero; } return tree; @@ -687,7 +757,8 @@ class HtmlParser extends StatelessWidget { if (tree.style.margin == null) { tree.style.margin = Margins.only(top: newOuterMarginTop); } else { - tree.style.margin = tree.style.margin!.copyWithEdge(top: newOuterMarginTop); + tree.style.margin = + tree.style.margin!.copyWithEdge(top: newOuterMarginTop); } // And remove the child's margin @@ -703,7 +774,8 @@ class HtmlParser extends StatelessWidget { // Bottom margins cannot collapse if the element has padding if ((tree.style.padding?.bottom ?? 0) == 0) { final parentBottom = tree.style.margin?.bottom?.value ?? 0; - final lastChildBottom = tree.children.last.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 @@ -740,11 +812,10 @@ class HtmlParser extends StatelessWidget { } if (tree.children[i].style.margin == null) { - tree.children[i].style.margin = - Margins.only(top: newInternalMargin); + tree.children[i].style.margin = Margins.only(top: newInternalMargin); } else { - tree.children[i].style.margin = - tree.children[i].style.margin!.copyWithEdge(top: newInternalMargin); + tree.children[i].style.margin = tree.children[i].style.margin! + .copyWithEdge(top: newInternalMargin); } } } @@ -763,18 +834,19 @@ class HtmlParser extends StatelessWidget { tree.children.forEachIndexed((index, child) { if (child is EmptyContentElement || child is EmptyLayoutElement) { toRemove.add(child); - } else if (child is TextContentElement - && ((tree.name == "body" - && (index == 0 - || index + 1 == tree.children.length - || tree.children[index - 1].style.display == Display.BLOCK - || tree.children[index + 1].style.display == Display.BLOCK)) - || tree.name == "ul") - && child.text!.replaceAll(' ', '').isEmpty) { + } else if (child is TextContentElement && + ((tree.name == "body" && + (index == 0 || + index + 1 == tree.children.length || + tree.children[index - 1].style.display == Display.BLOCK || + tree.children[index + 1].style.display == + Display.BLOCK)) || + tree.name == "ul") && + child.text!.replaceAll(' ', '').isEmpty) { toRemove.add(child); - } else if (child is TextContentElement - && child.text!.isEmpty - && child.style.whiteSpace != WhiteSpace.PRE) { + } else if (child is TextContentElement && + child.text!.isEmpty && + child.style.whiteSpace != WhiteSpace.PRE) { toRemove.add(child); } else if (child is TextContentElement && child.style.whiteSpace != WhiteSpace.PRE && @@ -800,12 +872,13 @@ class HtmlParser extends StatelessWidget { /// [_calculateRelativeValues] converts rem values to px sizes and then /// applies relative calculations - static StyledElement _calculateRelativeValues(StyledElement tree, double devicePixelRatio) { + static StyledElement _calculateRelativeValues( + StyledElement tree, double devicePixelRatio) { double remSize = (tree.style.fontSize?.value ?? FontSize.medium.value); //If the root element has a rem-based fontSize, then give it the default // font size times the set rem value. - if(tree.style.fontSize?.unit == Unit.rem) { + if (tree.style.fontSize?.unit == Unit.rem) { tree.style.fontSize = FontSize(FontSize.medium.value * remSize); } @@ -816,26 +889,29 @@ class HtmlParser extends StatelessWidget { } /// This is the recursive worker function for [_calculateRelativeValues] - static void _applyRelativeValuesRecursive(StyledElement tree, double remFontSize, double devicePixelRatio) { + static void _applyRelativeValuesRecursive( + StyledElement tree, double remFontSize, double devicePixelRatio) { //When we get to this point, there should be a valid fontSize at every level. assert(tree.style.fontSize != null); final parentFontSize = tree.style.fontSize!.value; tree.children.forEach((child) { - - if(child.style.fontSize == null) { + if (child.style.fontSize == null) { child.style.fontSize = FontSize(parentFontSize); } else { - switch(child.style.fontSize!.unit) { + switch (child.style.fontSize!.unit) { case Unit.em: - child.style.fontSize = FontSize(parentFontSize * child.style.fontSize!.value); + child.style.fontSize = + FontSize(parentFontSize * child.style.fontSize!.value); break; case Unit.percent: - child.style.fontSize = FontSize(parentFontSize * (child.style.fontSize!.value / 100.0)); + child.style.fontSize = FontSize( + parentFontSize * (child.style.fontSize!.value / 100.0)); break; case Unit.rem: - child.style.fontSize = FontSize(remFontSize * child.style.fontSize!.value); + child.style.fontSize = + FontSize(remFontSize * child.style.fontSize!.value); break; case Unit.px: case Unit.auto: @@ -854,8 +930,6 @@ class HtmlParser extends StatelessWidget { _applyRelativeValuesRecursive(child, remFontSize, devicePixelRatio); }); } - - } /// The [RenderContext] is available when parsing the tree. It contains information @@ -882,7 +956,8 @@ extension IterateLetters on String { String nextLetter() { String s = this.toLowerCase(); if (s == "z") { - return String.fromCharCode(s.codeUnitAt(0) - 25) + String.fromCharCode(s.codeUnitAt(0) - 25); // AA or aa + return String.fromCharCode(s.codeUnitAt(0) - 25) + + String.fromCharCode(s.codeUnitAt(0) - 25); // AA or aa } else { var lastChar = s.substring(s.length - 1); var sub = s.substring(0, s.length - 1); diff --git a/lib/src/anchor.dart b/lib/src/anchor.dart index bdba172c97..2dbdd7a1bb 100644 --- a/lib/src/anchor.dart +++ b/lib/src/anchor.dart @@ -30,7 +30,10 @@ class AnchorKey extends GlobalKey { @override bool operator ==(Object other) => identical(this, other) || - other is AnchorKey && runtimeType == other.runtimeType && parentKey == other.parentKey && id == other.id; + other is AnchorKey && + runtimeType == other.runtimeType && + parentKey == other.parentKey && + id == other.id; @override int get hashCode => parentKey.hashCode ^ id.hashCode; diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart index 47c18f36d9..785b2b46c5 100644 --- a/lib/src/css_box_widget.dart +++ b/lib/src/css_box_widget.dart @@ -12,7 +12,7 @@ class CSSBoxWidget extends StatelessWidget { this.textDirection, this.childIsReplaced = false, this.shrinkWrap = false, - }): super(key: key); + }) : super(key: key); /// Generates a CSSBoxWidget that contains a list of InlineSpan children. CSSBoxWidget.withInlineSpanChildren({ @@ -87,7 +87,7 @@ class CSSBoxWidget extends StatelessWidget { /// Takes a list of InlineSpan children and generates a Text.rich Widget /// containing those children. static Widget _generateWidgetChild(List children, Style style) { - if(children.isEmpty) { + if (children.isEmpty) { return Container(); } @@ -110,7 +110,7 @@ class CSSBoxWidget extends StatelessWidget { TextSelectionControls? selectionControls, ScrollPhysics? scrollPhysics, ) { - if(children.isEmpty) { + if (children.isEmpty) { return Container(); } @@ -138,11 +138,13 @@ class CSSBoxWidget extends StatelessWidget { !shrinkWrap; } - TextDirection _checkTextDirection(BuildContext context, TextDirection? direction) { + TextDirection _checkTextDirection( + BuildContext context, TextDirection? direction) { final textDirection = direction ?? Directionality.maybeOf(context); - assert(textDirection != null, - "CSSBoxWidget needs either a Directionality ancestor or a provided textDirection", + assert( + textDirection != null, + "CSSBoxWidget needs either a Directionality ancestor or a provided textDirection", ); return textDirection!; @@ -454,16 +456,12 @@ class _RenderCSSBox extends RenderBox (this.margins.left?.value ?? 0) - (this.margins.right?.value ?? 0), maxHeight: (this.height.unit != Unit.auto) - ? this.height.value - : containingBlockSize.height - - (this.margins.top?.value ?? 0) - - (this.margins.bottom?.value ?? 0), - minWidth: (this.width.unit != Unit.auto) - ? this.width.value - : 0, - minHeight: (this.height.unit != Unit.auto) ? this.height.value - : 0, + : containingBlockSize.height - + (this.margins.top?.value ?? 0) - + (this.margins.bottom?.value ?? 0), + minWidth: (this.width.unit != Unit.auto) ? this.width.value : 0, + minHeight: (this.height.unit != Unit.auto) ? this.height.value : 0, ); final Size childSize = layoutChild(child!, childConstraints); @@ -596,25 +594,25 @@ class _RenderCSSBox extends RenderBox // If all values are non-auto, the box is overconstrained. // One of the margins will need to be adjusted so that the // entire width of the containing block is used. - if (!widthIsAuto && !marginLeftIsAuto && !marginRightIsAuto && !shrinkWrap && !childIsReplaced) { + if (!widthIsAuto && + !marginLeftIsAuto && + !marginRightIsAuto && + !shrinkWrap && + !childIsReplaced) { //Ignore either left or right margin based on textDirection. - switch(textDirection) { + switch (textDirection) { case TextDirection.rtl: - final difference = containingBlockSize.width - - childSize.width - - marginRight.value; + final difference = + containingBlockSize.width - childSize.width - marginRight.value; marginLeft = Margin(difference); break; case TextDirection.ltr: - final difference = containingBlockSize.width - - childSize.width - - marginLeft.value; + final difference = + containingBlockSize.width - childSize.width - marginLeft.value; marginRight = Margin(difference); break; } - - } // If there is exactly one value specified as auto, compute it value from the equality (our widths are already set) diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index 4f24ddd95d..2d709187e9 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -13,53 +13,105 @@ Style declarationsToStyle(Map> declarations) { if (value.isNotEmpty) { switch (property) { case 'background-color': - style.backgroundColor = ExpressionMapping.expressionToColor(value.first) ?? style.backgroundColor; + style.backgroundColor = + ExpressionMapping.expressionToColor(value.first) ?? + style.backgroundColor; break; case 'border': - List? borderWidths = value.whereType().toList(); + List? borderWidths = + value.whereType().toList(); + /// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping] - borderWidths.removeWhere((element) => element == null || (element.text != "thin" - && element.text != "medium" && element.text != "thick" - && !(element is css.LengthTerm) && !(element is css.PercentageTerm) - && !(element is css.EmTerm) && !(element is css.RemTerm) - && !(element is css.NumberTerm)) - ); - List? borderColors = value.where((element) => ExpressionMapping.expressionToColor(element) != null).toList(); - List? potentialStyles = value.whereType().toList(); + borderWidths.removeWhere((element) => + element == null || + (element.text != "thin" && + element.text != "medium" && + element.text != "thick" && + !(element is css.LengthTerm) && + !(element is css.PercentageTerm) && + !(element is css.EmTerm) && + !(element is css.RemTerm) && + !(element is css.NumberTerm))); + List? borderColors = value + .where((element) => + ExpressionMapping.expressionToColor(element) != null) + .toList(); + List? potentialStyles = + value.whereType().toList(); + /// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future. - List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"]; + List possibleBorderValues = [ + "dotted", + "dashed", + "solid", + "double", + "groove", + "ridge", + "inset", + "outset", + "none", + "hidden" + ]; + /// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping] - potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text)); + potentialStyles.removeWhere((element) => + element == null || !possibleBorderValues.contains(element.text)); List? borderStyles = potentialStyles; - style.border = ExpressionMapping.expressionToBorder(borderWidths, borderStyles, borderColors); + style.border = ExpressionMapping.expressionToBorder( + borderWidths, borderStyles, borderColors); break; case 'border-left': - List? borderWidths = value.whereType().toList(); + List? borderWidths = + value.whereType().toList(); + /// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping] - borderWidths.removeWhere((element) => element == null || (element.text != "thin" - && element.text != "medium" && element.text != "thick" - && !(element is css.LengthTerm) && !(element is css.PercentageTerm) - && !(element is css.EmTerm) && !(element is css.RemTerm) - && !(element is css.NumberTerm)) - ); - css.LiteralTerm? borderWidth = borderWidths.firstWhereOrNull((element) => element != null); - css.Expression? borderColor = value.firstWhereOrNull((element) => ExpressionMapping.expressionToColor(element) != null); - List? potentialStyles = value.whereType().toList(); + borderWidths.removeWhere((element) => + element == null || + (element.text != "thin" && + element.text != "medium" && + element.text != "thick" && + !(element is css.LengthTerm) && + !(element is css.PercentageTerm) && + !(element is css.EmTerm) && + !(element is css.RemTerm) && + !(element is css.NumberTerm))); + css.LiteralTerm? borderWidth = + borderWidths.firstWhereOrNull((element) => element != null); + css.Expression? borderColor = value.firstWhereOrNull((element) => + ExpressionMapping.expressionToColor(element) != null); + List? potentialStyles = + value.whereType().toList(); + /// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future. - List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"]; + List possibleBorderValues = [ + "dotted", + "dashed", + "solid", + "double", + "groove", + "ridge", + "inset", + "outset", + "none", + "hidden" + ]; + /// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping] - potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text)); + potentialStyles.removeWhere((element) => + element == null || !possibleBorderValues.contains(element.text)); css.LiteralTerm? borderStyle = potentialStyles.firstOrNull; Border newBorder = Border( left: style.border?.left.copyWith( - width: ExpressionMapping.expressionToBorderWidth(borderWidth), - style: ExpressionMapping.expressionToBorderStyle(borderStyle), - color: ExpressionMapping.expressionToColor(borderColor), - ) ?? BorderSide( - width: ExpressionMapping.expressionToBorderWidth(borderWidth), - style: ExpressionMapping.expressionToBorderStyle(borderStyle), - color: ExpressionMapping.expressionToColor(borderColor) ?? Colors.black, - ), + width: ExpressionMapping.expressionToBorderWidth(borderWidth), + style: ExpressionMapping.expressionToBorderStyle(borderStyle), + color: ExpressionMapping.expressionToColor(borderColor), + ) ?? + BorderSide( + width: ExpressionMapping.expressionToBorderWidth(borderWidth), + style: ExpressionMapping.expressionToBorderStyle(borderStyle), + color: ExpressionMapping.expressionToColor(borderColor) ?? + Colors.black, + ), right: style.border?.right ?? BorderSide.none, top: style.border?.top ?? BorderSide.none, bottom: style.border?.bottom ?? BorderSide.none, @@ -67,135 +119,226 @@ Style declarationsToStyle(Map> declarations) { style.border = newBorder; break; case 'border-right': - List? borderWidths = value.whereType().toList(); + List? borderWidths = + value.whereType().toList(); + /// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping] - borderWidths.removeWhere((element) => element == null || (element.text != "thin" - && element.text != "medium" && element.text != "thick" - && !(element is css.LengthTerm) && !(element is css.PercentageTerm) - && !(element is css.EmTerm) && !(element is css.RemTerm) - && !(element is css.NumberTerm)) - ); - css.LiteralTerm? borderWidth = borderWidths.firstWhereOrNull((element) => element != null); - css.Expression? borderColor = value.firstWhereOrNull((element) => ExpressionMapping.expressionToColor(element) != null); - List? potentialStyles = value.whereType().toList(); + borderWidths.removeWhere((element) => + element == null || + (element.text != "thin" && + element.text != "medium" && + element.text != "thick" && + !(element is css.LengthTerm) && + !(element is css.PercentageTerm) && + !(element is css.EmTerm) && + !(element is css.RemTerm) && + !(element is css.NumberTerm))); + css.LiteralTerm? borderWidth = + borderWidths.firstWhereOrNull((element) => element != null); + css.Expression? borderColor = value.firstWhereOrNull((element) => + ExpressionMapping.expressionToColor(element) != null); + List? potentialStyles = + value.whereType().toList(); + /// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future. - List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"]; + List possibleBorderValues = [ + "dotted", + "dashed", + "solid", + "double", + "groove", + "ridge", + "inset", + "outset", + "none", + "hidden" + ]; + /// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping] - potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text)); + potentialStyles.removeWhere((element) => + element == null || !possibleBorderValues.contains(element.text)); css.LiteralTerm? borderStyle = potentialStyles.firstOrNull; Border newBorder = Border( left: style.border?.left ?? BorderSide.none, right: style.border?.right.copyWith( - width: ExpressionMapping.expressionToBorderWidth(borderWidth), - style: ExpressionMapping.expressionToBorderStyle(borderStyle), - color: ExpressionMapping.expressionToColor(borderColor), - ) ?? BorderSide( - width: ExpressionMapping.expressionToBorderWidth(borderWidth), - style: ExpressionMapping.expressionToBorderStyle(borderStyle), - color: ExpressionMapping.expressionToColor(borderColor) ?? Colors.black, - ), + width: ExpressionMapping.expressionToBorderWidth(borderWidth), + style: ExpressionMapping.expressionToBorderStyle(borderStyle), + color: ExpressionMapping.expressionToColor(borderColor), + ) ?? + BorderSide( + width: ExpressionMapping.expressionToBorderWidth(borderWidth), + style: ExpressionMapping.expressionToBorderStyle(borderStyle), + color: ExpressionMapping.expressionToColor(borderColor) ?? + Colors.black, + ), top: style.border?.top ?? BorderSide.none, bottom: style.border?.bottom ?? BorderSide.none, ); style.border = newBorder; break; case 'border-top': - List? borderWidths = value.whereType().toList(); + List? borderWidths = + value.whereType().toList(); + /// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping] - borderWidths.removeWhere((element) => element == null || (element.text != "thin" - && element.text != "medium" && element.text != "thick" - && !(element is css.LengthTerm) && !(element is css.PercentageTerm) - && !(element is css.EmTerm) && !(element is css.RemTerm) - && !(element is css.NumberTerm)) - ); - css.LiteralTerm? borderWidth = borderWidths.firstWhereOrNull((element) => element != null); - css.Expression? borderColor = value.firstWhereOrNull((element) => ExpressionMapping.expressionToColor(element) != null); - List? potentialStyles = value.whereType().toList(); + borderWidths.removeWhere((element) => + element == null || + (element.text != "thin" && + element.text != "medium" && + element.text != "thick" && + !(element is css.LengthTerm) && + !(element is css.PercentageTerm) && + !(element is css.EmTerm) && + !(element is css.RemTerm) && + !(element is css.NumberTerm))); + css.LiteralTerm? borderWidth = + borderWidths.firstWhereOrNull((element) => element != null); + css.Expression? borderColor = value.firstWhereOrNull((element) => + ExpressionMapping.expressionToColor(element) != null); + List? potentialStyles = + value.whereType().toList(); + /// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future. - List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"]; + List possibleBorderValues = [ + "dotted", + "dashed", + "solid", + "double", + "groove", + "ridge", + "inset", + "outset", + "none", + "hidden" + ]; + /// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping] - potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text)); + potentialStyles.removeWhere((element) => + element == null || !possibleBorderValues.contains(element.text)); css.LiteralTerm? borderStyle = potentialStyles.firstOrNull; Border newBorder = Border( left: style.border?.left ?? BorderSide.none, right: style.border?.right ?? BorderSide.none, top: style.border?.top.copyWith( - width: ExpressionMapping.expressionToBorderWidth(borderWidth), - style: ExpressionMapping.expressionToBorderStyle(borderStyle), - color: ExpressionMapping.expressionToColor(borderColor), - ) ?? BorderSide( - width: ExpressionMapping.expressionToBorderWidth(borderWidth), - style: ExpressionMapping.expressionToBorderStyle(borderStyle), - color: ExpressionMapping.expressionToColor(borderColor) ?? Colors.black, - ), + width: ExpressionMapping.expressionToBorderWidth(borderWidth), + style: ExpressionMapping.expressionToBorderStyle(borderStyle), + color: ExpressionMapping.expressionToColor(borderColor), + ) ?? + BorderSide( + width: ExpressionMapping.expressionToBorderWidth(borderWidth), + style: ExpressionMapping.expressionToBorderStyle(borderStyle), + color: ExpressionMapping.expressionToColor(borderColor) ?? + Colors.black, + ), bottom: style.border?.bottom ?? BorderSide.none, ); style.border = newBorder; break; case 'border-bottom': - List? borderWidths = value.whereType().toList(); + List? borderWidths = + value.whereType().toList(); + /// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping] - borderWidths.removeWhere((element) => element == null || (element.text != "thin" - && element.text != "medium" && element.text != "thick" - && !(element is css.LengthTerm) && !(element is css.PercentageTerm) - && !(element is css.EmTerm) && !(element is css.RemTerm) - && !(element is css.NumberTerm)) - ); - css.LiteralTerm? borderWidth = borderWidths.firstWhereOrNull((element) => element != null); - css.Expression? borderColor = value.firstWhereOrNull((element) => ExpressionMapping.expressionToColor(element) != null); - List? potentialStyles = value.whereType().toList(); + borderWidths.removeWhere((element) => + element == null || + (element.text != "thin" && + element.text != "medium" && + element.text != "thick" && + !(element is css.LengthTerm) && + !(element is css.PercentageTerm) && + !(element is css.EmTerm) && + !(element is css.RemTerm) && + !(element is css.NumberTerm))); + css.LiteralTerm? borderWidth = + borderWidths.firstWhereOrNull((element) => element != null); + css.Expression? borderColor = value.firstWhereOrNull((element) => + ExpressionMapping.expressionToColor(element) != null); + List? potentialStyles = + value.whereType().toList(); + /// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future. - List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"]; + List possibleBorderValues = [ + "dotted", + "dashed", + "solid", + "double", + "groove", + "ridge", + "inset", + "outset", + "none", + "hidden" + ]; + /// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping] - potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text)); + potentialStyles.removeWhere((element) => + element == null || !possibleBorderValues.contains(element.text)); css.LiteralTerm? borderStyle = potentialStyles.firstOrNull; Border newBorder = Border( left: style.border?.left ?? BorderSide.none, right: style.border?.right ?? BorderSide.none, top: style.border?.top ?? BorderSide.none, bottom: style.border?.bottom.copyWith( - width: ExpressionMapping.expressionToBorderWidth(borderWidth), - style: ExpressionMapping.expressionToBorderStyle(borderStyle), - color: ExpressionMapping.expressionToColor(borderColor), - ) ?? BorderSide( - width: ExpressionMapping.expressionToBorderWidth(borderWidth), - style: ExpressionMapping.expressionToBorderStyle(borderStyle), - color: ExpressionMapping.expressionToColor(borderColor) ?? Colors.black, - ), + width: ExpressionMapping.expressionToBorderWidth(borderWidth), + style: ExpressionMapping.expressionToBorderStyle(borderStyle), + color: ExpressionMapping.expressionToColor(borderColor), + ) ?? + BorderSide( + width: ExpressionMapping.expressionToBorderWidth(borderWidth), + style: ExpressionMapping.expressionToBorderStyle(borderStyle), + color: ExpressionMapping.expressionToColor(borderColor) ?? + Colors.black, + ), ); style.border = newBorder; break; case 'color': - style.color = ExpressionMapping.expressionToColor(value.first) ?? style.color; + style.color = + ExpressionMapping.expressionToColor(value.first) ?? style.color; break; case 'direction': - style.direction = ExpressionMapping.expressionToDirection(value.first); + style.direction = + ExpressionMapping.expressionToDirection(value.first); break; case 'display': style.display = ExpressionMapping.expressionToDisplay(value.first); break; case 'line-height': - style.lineHeight = ExpressionMapping.expressionToLineHeight(value.first); + style.lineHeight = + ExpressionMapping.expressionToLineHeight(value.first); break; case 'font-family': - style.fontFamily = ExpressionMapping.expressionToFontFamily(value.first) ?? style.fontFamily; + style.fontFamily = + ExpressionMapping.expressionToFontFamily(value.first) ?? + style.fontFamily; break; case 'font-feature-settings': - style.fontFeatureSettings = ExpressionMapping.expressionToFontFeatureSettings(value); + style.fontFeatureSettings = + ExpressionMapping.expressionToFontFeatureSettings(value); break; case 'font-size': - style.fontSize = ExpressionMapping.expressionToFontSize(value.first) ?? style.fontSize; + style.fontSize = + ExpressionMapping.expressionToFontSize(value.first) ?? + style.fontSize; break; case 'font-style': - style.fontStyle = ExpressionMapping.expressionToFontStyle(value.first); + style.fontStyle = + ExpressionMapping.expressionToFontStyle(value.first); break; case 'font-weight': - style.fontWeight = ExpressionMapping.expressionToFontWeight(value.first); + style.fontWeight = + ExpressionMapping.expressionToFontWeight(value.first); 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?; - css.LiteralTerm? type = value.firstWhereOrNull((e) => e is css.LiteralTerm && e.text != "outside" && e.text != "inside") as css.LiteralTerm?; + 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?; + css.LiteralTerm? type = value.firstWhereOrNull((e) => + e is css.LiteralTerm && + e.text != "outside" && + e.text != "inside") as css.LiteralTerm?; if (position != null) { switch (position.text) { case 'outside': @@ -207,14 +350,20 @@ Style declarationsToStyle(Map> declarations) { } } if (image != null) { - style.listStyleType = ExpressionMapping.expressionToListStyleType(image) ?? style.listStyleType; + style.listStyleType = + ExpressionMapping.expressionToListStyleType(image) ?? + style.listStyleType; } else if (type != null) { - style.listStyleType = ExpressionMapping.expressionToListStyleType(type) ?? style.listStyleType; + style.listStyleType = + ExpressionMapping.expressionToListStyleType(type) ?? + style.listStyleType; } break; case 'list-style-image': if (value.first is css.UriTerm) { - style.listStyleType = ExpressionMapping.expressionToListStyleType(value.first as css.UriTerm) ?? style.listStyleType; + style.listStyleType = ExpressionMapping.expressionToListStyleType( + value.first as css.UriTerm) ?? + style.listStyleType; } break; case 'list-style-position': @@ -230,22 +379,27 @@ Style declarationsToStyle(Map> declarations) { } break; case 'height': - style.height = ExpressionMapping.expressionToHeight(value.first) ?? style.height; + style.height = + ExpressionMapping.expressionToHeight(value.first) ?? style.height; break; case 'list-style-type': if (value.first is css.LiteralTerm) { - style.listStyleType = ExpressionMapping.expressionToListStyleType(value.first as css.LiteralTerm) ?? style.listStyleType; + style.listStyleType = ExpressionMapping.expressionToListStyleType( + value.first as css.LiteralTerm) ?? + style.listStyleType; } break; case 'margin': - List? marginLengths = value.whereType().toList(); + List? marginLengths = + value.whereType().toList(); + /// List might include other values than the ones we want for margin length, so make sure to remove those before passing it to [ExpressionMapping] - marginLengths.removeWhere((element) => !(element is css.LengthTerm) - && !(element is css.EmTerm) - && !(element is css.RemTerm) - && !(element is css.NumberTerm) - && !(element.text == 'auto') - ); + marginLengths.removeWhere((element) => + !(element is css.LengthTerm) && + !(element is css.EmTerm) && + !(element is css.RemTerm) && + !(element is css.NumberTerm) && + !(element.text == 'auto')); Margins margin = ExpressionMapping.expressionToMargins(marginLengths); style.margin = (style.margin ?? Margins.all(0)).copyWith( left: margin.left, @@ -263,22 +417,25 @@ Style declarationsToStyle(Map> declarations) { right: ExpressionMapping.expressionToMargin(value.first)); break; case 'margin-top': - style.margin = (style.margin ?? Margins.zero).copyWith( - top: ExpressionMapping.expressionToMargin(value.first)); + style.margin = (style.margin ?? Margins.zero) + .copyWith(top: ExpressionMapping.expressionToMargin(value.first)); break; case 'margin-bottom': style.margin = (style.margin ?? Margins.zero).copyWith( bottom: ExpressionMapping.expressionToMargin(value.first)); break; case 'padding': - List? paddingLengths = value.whereType().toList(); + List? paddingLengths = + value.whereType().toList(); + /// List might include other values than the ones we want for padding length, so make sure to remove those before passing it to [ExpressionMapping] - paddingLengths.removeWhere((element) => !(element is css.LengthTerm) - && !(element is css.EmTerm) - && !(element is css.RemTerm) - && !(element is css.NumberTerm) - ); - List padding = ExpressionMapping.expressionToPadding(paddingLengths); + paddingLengths.removeWhere((element) => + !(element is css.LengthTerm) && + !(element is css.EmTerm) && + !(element is css.RemTerm) && + !(element is css.NumberTerm)); + List padding = + ExpressionMapping.expressionToPadding(paddingLengths); style.padding = (style.padding ?? EdgeInsets.zero).copyWith( left: padding[0], right: padding[1], @@ -303,36 +460,65 @@ Style declarationsToStyle(Map> declarations) { bottom: ExpressionMapping.expressionToPaddingLength(value.first)); break; case 'text-align': - style.textAlign = ExpressionMapping.expressionToTextAlign(value.first); + style.textAlign = + ExpressionMapping.expressionToTextAlign(value.first); break; case 'text-decoration': - List? textDecorationList = value.whereType().toList(); + List? textDecorationList = + value.whereType().toList(); + /// List might include other values than the ones we want for [textDecorationList], so make sure to remove those before passing it to [ExpressionMapping] - textDecorationList.removeWhere((element) => element == null || (element.text != "none" - && element.text != "overline" && element.text != "underline" && element.text != "line-through")); + textDecorationList.removeWhere((element) => + element == null || + (element.text != "none" && + element.text != "overline" && + element.text != "underline" && + element.text != "line-through")); List? nullableList = value; css.Expression? textDecorationColor; - textDecorationColor = nullableList.firstWhereOrNull( - (element) => element is css.HexColorTerm || element is css.FunctionTerm); - List? potentialStyles = value.whereType().toList(); + textDecorationColor = nullableList.firstWhereOrNull((element) => + element is css.HexColorTerm || element is css.FunctionTerm); + List? potentialStyles = + value.whereType().toList(); + /// List might include other values than the ones we want for [textDecorationStyle], so make sure to remove those before passing it to [ExpressionMapping] - potentialStyles.removeWhere((element) => element == null || (element.text != "solid" - && element.text != "double" && element.text != "dashed" && element.text != "dotted" && element.text != "wavy")); - css.LiteralTerm? textDecorationStyle = potentialStyles.isNotEmpty ? potentialStyles.last : null; - style.textDecoration = ExpressionMapping.expressionToTextDecorationLine(textDecorationList); - if (textDecorationColor != null) style.textDecorationColor = ExpressionMapping.expressionToColor(textDecorationColor) - ?? style.textDecorationColor; - if (textDecorationStyle != null) style.textDecorationStyle = ExpressionMapping.expressionToTextDecorationStyle(textDecorationStyle); + potentialStyles.removeWhere((element) => + element == null || + (element.text != "solid" && + element.text != "double" && + element.text != "dashed" && + element.text != "dotted" && + element.text != "wavy")); + css.LiteralTerm? textDecorationStyle = + potentialStyles.isNotEmpty ? potentialStyles.last : null; + style.textDecoration = + ExpressionMapping.expressionToTextDecorationLine( + textDecorationList); + if (textDecorationColor != null) + style.textDecorationColor = + ExpressionMapping.expressionToColor(textDecorationColor) ?? + style.textDecorationColor; + if (textDecorationStyle != null) + style.textDecorationStyle = + ExpressionMapping.expressionToTextDecorationStyle( + textDecorationStyle); break; case 'text-decoration-color': - style.textDecorationColor = ExpressionMapping.expressionToColor(value.first) ?? style.textDecorationColor; + style.textDecorationColor = + ExpressionMapping.expressionToColor(value.first) ?? + style.textDecorationColor; break; case 'text-decoration-line': - List? textDecorationList = value.whereType().toList(); - style.textDecoration = ExpressionMapping.expressionToTextDecorationLine(textDecorationList); + List? textDecorationList = + value.whereType().toList(); + style.textDecoration = + ExpressionMapping.expressionToTextDecorationLine( + textDecorationList); break; case 'text-decoration-style': - style.textDecorationStyle = ExpressionMapping.expressionToTextDecorationStyle(value.first as css.LiteralTerm); + style.textDecorationStyle = + ExpressionMapping.expressionToTextDecorationStyle( + value.first as css.LiteralTerm); break; case 'text-shadow': style.textShadow = ExpressionMapping.expressionToTextShadow(value); @@ -350,7 +536,8 @@ Style declarationsToStyle(Map> declarations) { } break; case 'width': - style.width = ExpressionMapping.expressionToWidth(value.first) ?? style.width; + style.width = + ExpressionMapping.expressionToWidth(value.first) ?? style.width; break; } } @@ -373,7 +560,8 @@ Style? inlineCssToStyle(String? inlineStyle, OnCssParseError? errorHandler) { return null; } -Map>> parseExternalCss(String css, OnCssParseError? errorHandler) { +Map>> parseExternalCss( + String css, OnCssParseError? errorHandler) { var errors = []; final sheet = cssparser.parse(css, errors: errors); if (errors.isEmpty) { @@ -393,7 +581,8 @@ class DeclarationVisitor extends css.Visitor { late String _selector; late String _currentProperty; - Map>> getDeclarations(css.StyleSheet sheet) { + Map>> getDeclarations( + css.StyleSheet sheet) { sheet.topLevels.forEach((element) { if (element.span != null) { _selector = element.span!.text; @@ -401,13 +590,15 @@ class DeclarationVisitor extends css.Visitor { if (_result[_selector] != null) { _properties.forEach((key, value) { if (_result[_selector]![key] != null) { - _result[_selector]![key]!.addAll(new List.from(value)); + _result[_selector]![key]! + .addAll(new List.from(value)); } else { _result[_selector]![key] = new List.from(value); } }); } else { - _result[_selector] = new Map>.from(_properties); + _result[_selector] = + new Map>.from(_properties); } _properties.clear(); } @@ -434,8 +625,10 @@ class DeclarationVisitor extends css.Visitor { //Mapping functions class ExpressionMapping { - - static Border expressionToBorder(List? borderWidths, List? borderStyles, List? borderColors) { + static Border expressionToBorder( + List? borderWidths, + List? borderStyles, + List? borderColors) { CustomBorderSide left = CustomBorderSide(); CustomBorderSide top = CustomBorderSide(); CustomBorderSide right = CustomBorderSide(); @@ -510,11 +703,22 @@ class ExpressionMapping { } } return Border( - top: BorderSide(width: top.width, color: top.color ?? Colors.black, style: top.style), - right: BorderSide(width: right.width, color: right.color ?? Colors.black, style: right.style), - bottom: BorderSide(width: bottom.width, color: bottom.color ?? Colors.black, style: bottom.style), - left: BorderSide(width: left.width, color: left.color ?? Colors.black, style: left.style) - ); + top: BorderSide( + width: top.width, + color: top.color ?? Colors.black, + style: top.style), + right: BorderSide( + width: right.width, + color: right.color ?? Colors.black, + style: right.style), + bottom: BorderSide( + width: bottom.width, + color: bottom.color ?? Colors.black, + style: bottom.style), + left: BorderSide( + width: left.width, + color: left.color ?? Colors.black, + style: left.style)); } static double expressionToBorderWidth(css.Expression? value) { @@ -527,7 +731,9 @@ class ExpressionMapping { } else if (value is css.RemTerm) { return double.tryParse(value.text) ?? 1.0; } else if (value is css.LengthTerm) { - return double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')) ?? 1.0; + return double.tryParse( + value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')) ?? + 1.0; } else if (value is css.LiteralTerm) { switch (value.text) { case "thin": @@ -567,7 +773,7 @@ class ExpressionMapping { static TextDirection expressionToDirection(css.Expression value) { if (value is css.LiteralTerm) { - switch(value.text) { + switch (value.text) { case "ltr": return TextDirection.ltr; case "rtl": @@ -579,7 +785,7 @@ class ExpressionMapping { static Display expressionToDisplay(css.Expression value) { if (value is css.LiteralTerm) { - switch(value.text) { + switch (value.text) { case 'block': return Display.BLOCK; case 'inline-block': @@ -595,16 +801,25 @@ class ExpressionMapping { return Display.INLINE; } - static List expressionToFontFeatureSettings(List value) { + static List expressionToFontFeatureSettings( + List value) { List fontFeatures = []; for (int i = 0; i < value.length; i++) { css.Expression exp = value[i]; if (exp is css.LiteralTerm) { - if (exp.text != "on" && exp.text != "off" && exp.text != "1" && exp.text != "0") { + if (exp.text != "on" && + exp.text != "off" && + exp.text != "1" && + exp.text != "0") { if (i < value.length - 1) { - css.Expression nextExp = value[i+1]; - if (nextExp is css.LiteralTerm && (nextExp.text == "on" || nextExp.text == "off" || nextExp.text == "1" || nextExp.text == "0")) { - fontFeatures.add(FontFeature(exp.text, nextExp.text == "on" || nextExp.text == "1" ? 1 : 0)); + css.Expression nextExp = value[i + 1]; + if (nextExp is css.LiteralTerm && + (nextExp.text == "on" || + nextExp.text == "off" || + nextExp.text == "1" || + nextExp.text == "0")) { + fontFeatures.add(FontFeature(exp.text, + nextExp.text == "on" || nextExp.text == "1" ? 1 : 0)); } else { fontFeatures.add(FontFeature.enable(exp.text)); } @@ -625,10 +840,12 @@ class ExpressionMapping { return FontSize(double.tryParse(value.text) ?? 100, Unit.percent); } else if (value is css.EmTerm) { return FontSize(double.tryParse(value.text) ?? 1, Unit.em); - // } else if (value is css.RemTerm) { TODO - // return FontSize.rem(double.tryParse(value.text) ?? 1, Unit.em); + // } else if (value is css.RemTerm) { TODO + // return FontSize.rem(double.tryParse(value.text) ?? 1, Unit.em); } else if (value is css.LengthTerm) { - return FontSize(double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')) ?? 16); + return FontSize(double.tryParse( + value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')) ?? + 16); } else if (value is css.LiteralTerm) { switch (value.text) { case "xx-small": @@ -652,7 +869,7 @@ class ExpressionMapping { static FontStyle expressionToFontStyle(css.Expression value) { if (value is css.LiteralTerm) { - switch(value.text) { + switch (value.text) { case "italic": case "oblique": return FontStyle.italic; @@ -685,7 +902,7 @@ class ExpressionMapping { return FontWeight.w900; } } else if (value is css.LiteralTerm) { - switch(value.text) { + switch (value.text) { case "bold": return FontWeight.bold; case "bolder": @@ -713,7 +930,10 @@ class ExpressionMapping { } else if (value is css.RemTerm) { return LineHeight.rem(double.tryParse(value.text)!); } else if (value is css.LengthTerm) { - return LineHeight(double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')), units: "length"); + return LineHeight( + double.tryParse( + value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')), + units: "length"); } return LineHeight.normal; } @@ -846,7 +1066,8 @@ class ExpressionMapping { } else if (value is css.RemTerm) { return double.tryParse(value.text); } else if (value is css.LengthTerm) { - return double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')); + return double.tryParse( + value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')); } return null; } @@ -856,11 +1077,12 @@ class ExpressionMapping { return LengthOrPercent(double.parse(value.text)); } else if (value is css.EmTerm) { return LengthOrPercent(double.parse(value.text), Unit.em); - // } else if (value is css.RemTerm) { - // return LengthOrPercent(double.parse(value.text), Unit.rem); - // TODO there are several other available terms processed by the CSS parser + // } else if (value is css.RemTerm) { + // return LengthOrPercent(double.parse(value.text), Unit.rem); + // TODO there are several other available terms processed by the CSS parser } else if (value is css.LengthTerm) { - double number = double.parse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')); + double number = double.parse( + value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')); Unit unit = _unitMap(value.unit); return LengthOrPercent(number, unit); } @@ -870,15 +1092,15 @@ class ExpressionMapping { } static Unit _unitMap(int cssParserUnitToken) { - switch(cssParserUnitToken) { - - default: return Unit.px; + switch (cssParserUnitToken) { + default: + return Unit.px; } } static TextAlign expressionToTextAlign(css.Expression value) { if (value is css.LiteralTerm) { - switch(value.text) { + switch (value.text) { case "center": return TextAlign.center; case "left": @@ -896,11 +1118,12 @@ class ExpressionMapping { return TextAlign.start; } - static TextDecoration expressionToTextDecorationLine(List value) { + static TextDecoration expressionToTextDecorationLine( + List value) { List decorationList = []; for (css.LiteralTerm? term in value) { if (term != null) { - switch(term.text) { + switch (term.text) { case "overline": decorationList.add(TextDecoration.overline); break; @@ -916,12 +1139,14 @@ class ExpressionMapping { } } } - if (decorationList.contains(TextDecoration.none)) decorationList = [TextDecoration.none]; + if (decorationList.contains(TextDecoration.none)) + decorationList = [TextDecoration.none]; return TextDecoration.combine(decorationList); } - static TextDecorationStyle expressionToTextDecorationStyle(css.LiteralTerm value) { - switch(value.text) { + static TextDecorationStyle expressionToTextDecorationStyle( + css.LiteralTerm value) { + switch (value.text) { case "wavy": return TextDecorationStyle.wavy; case "dotted": @@ -971,20 +1196,37 @@ class ExpressionMapping { }); RegExp nonNumberRegex = RegExp(r'\s+(\d+\.\d+)\s+'); if (offsetX is css.LiteralTerm && offsetY is css.LiteralTerm) { - if (color != null && ExpressionMapping.expressionToColor(color) != null) { + if (color != null && + ExpressionMapping.expressionToColor(color) != null) { shadow.add(Shadow( - color: expressionToColor(color)!, - offset: Offset( - double.tryParse((offsetX as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!, - double.tryParse((offsetY as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!), - blurRadius: (blurRadius is css.LiteralTerm) ? double.tryParse((blurRadius as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))! : 0.0, + color: expressionToColor(color)!, + offset: Offset( + double.tryParse((offsetX as css.LiteralTerm) + .text + .replaceAll(nonNumberRegex, ''))!, + double.tryParse((offsetY as css.LiteralTerm) + .text + .replaceAll(nonNumberRegex, ''))!), + blurRadius: (blurRadius is css.LiteralTerm) + ? double.tryParse((blurRadius as css.LiteralTerm) + .text + .replaceAll(nonNumberRegex, ''))! + : 0.0, )); } else { shadow.add(Shadow( - offset: Offset( - double.tryParse((offsetX as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!, - double.tryParse((offsetY as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!), - blurRadius: (blurRadius is css.LiteralTerm) ? double.tryParse((blurRadius as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))! : 0.0, + offset: Offset( + double.tryParse((offsetX as css.LiteralTerm) + .text + .replaceAll(nonNumberRegex, ''))!, + double.tryParse((offsetY as css.LiteralTerm) + .text + .replaceAll(nonNumberRegex, ''))!), + blurRadius: (blurRadius is css.LiteralTerm) + ? double.tryParse((blurRadius as css.LiteralTerm) + .text + .replaceAll(nonNumberRegex, ''))! + : 0.0, )); } } @@ -996,10 +1238,8 @@ class ExpressionMapping { static Color stringToColor(String _text) { var text = _text.replaceFirst('#', ''); if (text.length == 3) - text = text.replaceAllMapped( - RegExp(r"[a-f]|\d", caseSensitive: false), - (match) => '${match.group(0)}${match.group(0)}' - ); + text = text.replaceAllMapped(RegExp(r"[a-f]|\d", caseSensitive: false), + (match) => '${match.group(0)}${match.group(0)}'); if (text.length > 6) { text = "0x" + text; } else { @@ -1012,7 +1252,7 @@ class ExpressionMapping { final rgbaText = text.replaceAll(')', '').replaceAll(' ', ''); try { final rgbaValues = - rgbaText.split(',').map((value) => double.parse(value)).toList(); + rgbaText.split(',').map((value) => double.parse(value)).toList(); if (rgbaValues.length == 4) { return Color.fromRGBO( rgbaValues[0].toInt(), @@ -1039,10 +1279,13 @@ class ExpressionMapping { final hslValues = hslText.split(',').toList(); List parsedHsl = []; hslValues.forEach((element) { - if (element.contains("%") && double.tryParse(element.replaceAll("%", "")) != null) { + if (element.contains("%") && + double.tryParse(element.replaceAll("%", "")) != null) { parsedHsl.add(double.tryParse(element.replaceAll("%", ""))! * 0.01); } else { - if (element != hslValues.first && (double.tryParse(element) == null || double.tryParse(element)! > 1)) { + if (element != hslValues.first && + (double.tryParse(element) == null || + double.tryParse(element)! > 1)) { parsedHsl.add(null); } else { parsedHsl.add(double.tryParse(element)); @@ -1050,16 +1293,24 @@ class ExpressionMapping { } }); if (parsedHsl.length == 4 && !parsedHsl.contains(null)) { - return HSLColor.fromAHSL(parsedHsl.last!, parsedHsl.first!, parsedHsl[1]!, parsedHsl[2]!).toColor(); + return HSLColor.fromAHSL( + parsedHsl.last!, parsedHsl.first!, parsedHsl[1]!, parsedHsl[2]!) + .toColor(); } else if (parsedHsl.length == 3 && !parsedHsl.contains(null)) { - return HSLColor.fromAHSL(1.0, parsedHsl.first!, parsedHsl[1]!, parsedHsl.last!).toColor(); - } else return Colors.black; + return HSLColor.fromAHSL( + 1.0, parsedHsl.first!, parsedHsl[1]!, parsedHsl.last!) + .toColor(); + } else + return Colors.black; } static Color? namedColorToColor(String text) { - String namedColor = namedColors.keys.firstWhere((element) => element.toLowerCase() == text.toLowerCase(), orElse: () => ""); - if (namedColor != "") { - return stringToColor(namedColors[namedColor]!); - } else return null; + String namedColor = namedColors.keys.firstWhere( + (element) => element.toLowerCase() == text.toLowerCase(), + orElse: () => ""); + if (namedColor != "") { + return stringToColor(namedColors[namedColor]!); + } else + return null; } } diff --git a/lib/src/html_elements.dart b/lib/src/html_elements.dart index 3899d54a06..bf4e363024 100644 --- a/lib/src/html_elements.dart +++ b/lib/src/html_elements.dart @@ -129,7 +129,15 @@ const TABLE_CELL_ELEMENTS = ["th", "td"]; const TABLE_DEFINITION_ELEMENTS = ["col", "colgroup"]; -const EXTERNAL_ELEMENTS = ["audio", "iframe", "img", "math", "svg", "table", "video"]; +const EXTERNAL_ELEMENTS = [ + "audio", + "iframe", + "img", + "math", + "svg", + "table", + "video" +]; const REPLACED_EXTERNAL_ELEMENTS = ["iframe", "img", "video", "audio"]; diff --git a/lib/src/interactable_element.dart b/lib/src/interactable_element.dart index 43d29281b9..57a918fdbe 100644 --- a/lib/src/interactable_element.dart +++ b/lib/src/interactable_element.dart @@ -23,22 +23,22 @@ enum Gesture { } StyledElement parseInteractableElement( - dom.Element element, - List children, - ) { + dom.Element element, + List children, +) { switch (element.localName) { case "a": if (element.attributes.containsKey('href')) { return InteractableElement( - name: element.localName!, - children: children, - href: element.attributes['href'], - style: Style( - color: Colors.blue, - textDecoration: TextDecoration.underline, - ), - node: element, - elementId: element.id, + name: element.localName!, + children: children, + href: element.attributes['href'], + style: Style( + color: Colors.blue, + textDecoration: TextDecoration.underline, + ), + node: element, + elementId: element.id, ); } // When
tag have no href, it must be non clickable and without decoration. @@ -49,6 +49,7 @@ StyledElement parseInteractableElement( node: element, elementId: element.id, ); + /// will never be called, just to suppress missing return warning default: return InteractableElement( diff --git a/lib/src/layout_element.dart b/lib/src/layout_element.dart index 8b3a49d22b..778cb93516 100644 --- a/lib/src/layout_element.dart +++ b/lib/src/layout_element.dart @@ -133,35 +133,48 @@ class DetailsContentElement extends LayoutElement { @override Widget toWidget(RenderContext context) { - List? childrenList = children.map((tree) => context.parser.parseTree(context, tree)).toList(); + List? childrenList = children + .map((tree) => context.parser.parseTree(context, tree)) + .toList(); List toRemove = []; for (InlineSpan child in childrenList) { - if (child is TextSpan && child.text != null && child.text!.trim().isEmpty) { + if (child is TextSpan && + child.text != null && + child.text!.trim().isEmpty) { toRemove.add(child); } } for (InlineSpan child in toRemove) { childrenList.remove(child); } - InlineSpan? firstChild = childrenList.isNotEmpty == true ? childrenList.first : null; + InlineSpan? firstChild = + childrenList.isNotEmpty == true ? childrenList.first : null; return ExpansionTile( key: AnchorKey.of(context.parser.key, this), expandedAlignment: Alignment.centerLeft, - title: elementList.isNotEmpty == true && elementList.first.localName == "summary" + title: elementList.isNotEmpty == true && + elementList.first.localName == "summary" ? CSSBoxWidget.withInlineSpanChildren( - children: firstChild == null ? [] : [firstChild], - style: style, - ) : Text("Details"), + children: firstChild == null ? [] : [firstChild], + style: style, + ) + : Text("Details"), children: [ CSSBoxWidget.withInlineSpanChildren( - children: getChildren(childrenList, context, elementList.isNotEmpty == true && elementList.first.localName == "summary" ? firstChild : null), + children: getChildren( + childrenList, + context, + elementList.isNotEmpty == true && + elementList.first.localName == "summary" + ? firstChild + : null), style: style, ), - ] - ); + ]); } - List getChildren(List children, RenderContext context, InlineSpan? firstChild) { + List getChildren(List children, RenderContext context, + InlineSpan? firstChild) { if (firstChild != null) children.removeAt(0); return children; } @@ -179,8 +192,8 @@ class EmptyLayoutElement extends LayoutElement { } LayoutElement parseLayoutElement( - dom.Element element, - List children, + dom.Element element, + List children, ) { switch (element.localName) { case "details": @@ -188,10 +201,10 @@ LayoutElement parseLayoutElement( return EmptyLayoutElement(name: "empty"); } return DetailsContentElement( - node: element, - name: element.localName!, - children: children, - elementList: element.children, + node: element, + name: element.localName!, + children: children, + elementList: element.children, ); case "thead": case "tbody": diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index ba1b559ea7..c33f9ff3cb 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -47,7 +47,11 @@ class TextContentElement extends ReplacedElement { required this.text, this.node, dom.Element? element, - }) : super(name: "[text]", style: style, node: element, elementId: "[[No ID]]"); + }) : super( + name: "[text]", + style: style, + node: element, + elementId: "[[No ID]]"); @override String toString() { @@ -59,7 +63,8 @@ class TextContentElement extends ReplacedElement { } class EmptyContentElement extends ReplacedElement { - EmptyContentElement({String name = "empty"}) : super(name: name, style: Style(), elementId: "[[No ID]]"); + EmptyContentElement({String name = "empty"}) + : super(name: name, style: Style(), elementId: "[[No ID]]"); @override Widget? toWidget(_) => null; @@ -72,22 +77,28 @@ class RubyElement extends ReplacedElement { required this.element, required List children, String name = "ruby", - }) : super(name: name, alignment: PlaceholderAlignment.middle, style: Style(), elementId: element.id, children: children); + }) : super( + name: name, + alignment: PlaceholderAlignment.middle, + style: Style(), + elementId: element.id, + children: children); @override Widget toWidget(RenderContext context) { StyledElement? node; List widgets = []; - final rubySize = context.parser.style['rt']?.fontSize?.value ?? max(9.0, context.style.fontSize!.value / 2); + final rubySize = context.parser.style['rt']?.fontSize?.value ?? + max(9.0, context.style.fontSize!.value / 2); final rubyYPos = rubySize + rubySize / 2; List children = []; context.tree.children.forEachIndexed((index, element) { - if (!((element is TextContentElement) - && (element.text ?? "").trim().isEmpty - && index > 0 - && index + 1 < context.tree.children.length - && !(context.tree.children[index - 1] is TextContentElement) - && !(context.tree.children[index + 1] is TextContentElement))) { + if (!((element is TextContentElement) && + (element.text ?? "").trim().isEmpty && + index > 0 && + index + 1 < context.tree.children.length && + !(context.tree.children[index - 1] is TextContentElement) && + !(context.tree.children[index + 1] is TextContentElement))) { children.add(element); } }); @@ -97,23 +108,22 @@ class RubyElement extends ReplacedElement { alignment: Alignment.center, children: [ Container( - alignment: Alignment.bottomCenter, - child: Center( - child: Transform( - transform: - Matrix4.translationValues(0, -(rubyYPos), 0), - child: CSSBoxWidget( - style: c.style, - //TODO do any other attributes apply? - child: Text( - c.element!.innerHtml, - style: c.style - .generateTextStyle() - .copyWith(fontSize: rubySize), - ), - ), + alignment: Alignment.bottomCenter, + child: Center( + child: Transform( + transform: Matrix4.translationValues(0, -(rubyYPos), 0), + child: CSSBoxWidget( + style: c.style, + //TODO do any other attributes apply? + child: Text( + c.element!.innerHtml, + style: c.style + .generateTextStyle() + .copyWith(fontSize: rubySize), ), + ), ), + ), ), CSSBoxWidget( //TODO do any other styles apply? Does ruby still work? @@ -137,12 +147,14 @@ class RubyElement extends ReplacedElement { child: Wrap( key: AnchorKey.of(context.parser.key, this), runSpacing: rubySize, - children: widgets.map((e) => Row( - crossAxisAlignment: CrossAxisAlignment.end, - textBaseline: TextBaseline.alphabetic, - mainAxisSize: MainAxisSize.min, - children: [e], - )).toList(), + children: widgets + .map((e) => Row( + crossAxisAlignment: CrossAxisAlignment.end, + textBaseline: TextBaseline.alphabetic, + mainAxisSize: MainAxisSize.min, + children: [e], + )) + .toList(), ), ); } @@ -166,6 +178,7 @@ ReplacedElement parseReplacedElement( children: children, ); default: - return EmptyContentElement(name: element.localName == null ? "[[No Name]]" : element.localName!); + return EmptyContentElement( + name: element.localName == null ? "[[No Name]]" : element.localName!); } } diff --git a/lib/src/style/fontsize.dart b/lib/src/style/fontsize.dart index 950803ddb7..5d766c630a 100644 --- a/lib/src/style/fontsize.dart +++ b/lib/src/style/fontsize.dart @@ -3,8 +3,7 @@ import 'length.dart'; class FontSize extends LengthOrPercent { - - FontSize(double size, [Unit unit = Unit.px]): super(size, unit); + FontSize(double size, [Unit unit = Unit.px]) : super(size, unit); // These values are calculated based off of the default (`medium`) // being 14px. @@ -24,10 +23,10 @@ class FontSize extends LengthOrPercent { static final larger = FontSize(120, Unit.percent); static FontSize? inherit(FontSize? parent, FontSize? child) { - if(child != null && parent != null) { - if(child.unit == Unit.em) { + if (child != null && parent != null) { + if (child.unit == Unit.em) { return FontSize(child.value * parent.value); - } else if(child.unit == Unit.percent) { + } else if (child.unit == Unit.percent) { return FontSize(child.value / 100.0 * parent.value); } return child; @@ -37,4 +36,4 @@ class FontSize extends LengthOrPercent { } double get emValue => this.value; -} \ No newline at end of file +} diff --git a/lib/src/style/length.dart b/lib/src/style/length.dart index 1984307323..dbc47f71d4 100644 --- a/lib/src/style/length.dart +++ b/lib/src/style/length.dart @@ -19,6 +19,7 @@ enum Unit { //vh, //vw, auto(_auto); + const Unit(this.unitType); final int unitType; } @@ -29,26 +30,26 @@ abstract class Dimension { Unit unit; Dimension(this.value, this.unit, int _dimensionUnitType) - : assert(identical((unit.unitType | _dimensionUnitType), _dimensionUnitType), + : assert( + identical((unit.unitType | _dimensionUnitType), _dimensionUnitType), "This dimension was given a Unit that isn't specified."); } /// This dimension takes a value with a length unit such as px or em. Note that /// these can be fixed or relative (but they must not be a percent) class Length extends Dimension { - Length(double value, [Unit unit = Unit.px]): - super(value, unit, _length); + Length(double value, [Unit unit = Unit.px]) : super(value, unit, _length); } /// This dimension takes a value with a length-percent unit such as px or em /// or %. Note that these can be fixed or relative (but they must not be a /// percent) class LengthOrPercent extends Dimension { - LengthOrPercent(double value, [Unit unit = Unit.px]): - super(value, unit, _lengthPercent); + LengthOrPercent(double value, [Unit unit = Unit.px]) + : super(value, unit, _lengthPercent); } class AutoOrLengthOrPercent extends Dimension { - AutoOrLengthOrPercent(double value, [Unit unit = Unit.px]): - super(value, unit, _lengthPercentAuto); -} \ No newline at end of file + AutoOrLengthOrPercent(double value, [Unit unit = Unit.px]) + : super(value, unit, _lengthPercentAuto); +} diff --git a/lib/src/style/lineheight.dart b/lib/src/style/lineheight.dart index 9f96e7ee7a..0550ee1a7a 100644 --- a/lib/src/style/lineheight.dart +++ b/lib/src/style/lineheight.dart @@ -22,4 +22,4 @@ class LineHeight { } static const normal = LineHeight(1.2); -} \ No newline at end of file +} diff --git a/lib/src/style/margin.dart b/lib/src/style/margin.dart index 91c87f0a86..4df4e7b890 100644 --- a/lib/src/style/margin.dart +++ b/lib/src/style/margin.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/src/style/length.dart'; class Margin extends AutoOrLengthOrPercent { - Margin(double value, [Unit? unit = Unit.px]): super(value, unit ?? Unit.px); + Margin(double value, [Unit? unit = Unit.px]) : super(value, unit ?? Unit.px); - Margin.auto(): super(0, Unit.auto); + Margin.auto() : super(0, Unit.auto); - Margin.zero(): super(0, Unit.px); + Margin.zero() : super(0, Unit.px); } class Margins { @@ -15,29 +15,34 @@ class Margins { final Margin? top; final Margin? bottom; - const Margins({ this.left, this.right, this.top, this.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?.unit == Unit.auto ? left : Margin(0, Unit.px), - right: right?.unit == Unit.auto ? right : Margin(0, Unit.px), - top: top?.unit == Unit.auto ? top : Margin(0, Unit.px), - bottom: bottom?.unit == Unit.auto ? bottom : Margin(0, Unit.px), - ); + left: left?.unit == Unit.auto ? left : Margin(0, Unit.px), + right: right?.unit == Unit.auto ? right : Margin(0, Unit.px), + top: top?.unit == Unit.auto ? top : Margin(0, Unit.px), + bottom: bottom?.unit == Unit.auto ? bottom : Margin(0, Unit.px), + ); - 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 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 ? Margin(left, this.left?.unit) : this.left, - right: right != null ? Margin(right, this.right?.unit) : this.right, - top: top != null ? Margin(top, this.top?.unit) : this.top, - bottom: bottom != null ? Margin(bottom, this.bottom?.unit) : this.bottom, - ); + Margins copyWithEdge( + {double? left, double? right, double? top, double? bottom}) => + Margins( + left: left != null ? Margin(left, this.left?.unit) : this.left, + right: right != null ? Margin(right, this.right?.unit) : this.right, + top: top != null ? Margin(top, this.top?.unit) : this.top, + bottom: + bottom != null ? Margin(bottom, this.bottom?.unit) : this.bottom, + ); // bool get isAutoHorizontal => (left is MarginAuto) || (right is MarginAuto); @@ -45,24 +50,24 @@ class Margins { static Margins get zero => Margins.all(0); /// Analogous to [EdgeInsets.all] - Margins.all(double value, {Unit? unit}): - left = Margin(value, unit), - right = Margin(value, unit), - top = Margin(value, unit), - bottom = Margin(value, unit); + Margins.all(double value, {Unit? unit}) + : left = Margin(value, unit), + right = Margin(value, unit), + top = Margin(value, unit), + bottom = Margin(value, unit); /// Analogous to [EdgeInsets.only] - Margins.only({ double? left, double? right, double? top, double? bottom, Unit? unit}): - left = Margin(left ?? 0, unit), - right = Margin(right ?? 0, unit), - top = Margin(top ?? 0, unit), - bottom = Margin(bottom ?? 0, unit); - + Margins.only( + {double? left, double? right, double? top, double? bottom, Unit? unit}) + : left = Margin(left ?? 0, unit), + right = Margin(right ?? 0, unit), + top = Margin(top ?? 0, unit), + bottom = Margin(bottom ?? 0, unit); /// Analogous to [EdgeInsets.symmetric] - Margins.symmetric({double? horizontal, double? vertical, Unit? unit}): - left = Margin(horizontal ?? 0, unit), - right = Margin(horizontal ?? 0, unit), - top = Margin(vertical ?? 0, unit), - bottom = Margin(vertical ?? 0, unit); + Margins.symmetric({double? horizontal, double? vertical, Unit? unit}) + : left = Margin(horizontal ?? 0, unit), + right = Margin(horizontal ?? 0, unit), + top = Margin(vertical ?? 0, unit), + bottom = Margin(vertical ?? 0, unit); } diff --git a/lib/src/style/size.dart b/lib/src/style/size.dart index 9891131e38..1b73663793 100644 --- a/lib/src/style/size.dart +++ b/lib/src/style/size.dart @@ -4,15 +4,15 @@ import 'package:flutter_html/flutter_html.dart'; /// units are provided. A helper constructor, [Width.auto] constructor is /// provided for convenience. class Width extends AutoOrLengthOrPercent { - Width(super.value, [super.unit = Unit.px]): - assert(value >= 0, 'Width value must be non-negative'); + Width(super.value, [super.unit = Unit.px]) + : assert(value >= 0, 'Width value must be non-negative'); - Width.auto(): super(0, Unit.auto); + Width.auto() : super(0, Unit.auto); } class Height extends AutoOrLengthOrPercent { - Height(super.value, [super.unit = Unit.px]): - assert(value >= 0, 'Height value must be non-negative'); + Height(super.value, [super.unit = Unit.px]) + : assert(value >= 0, 'Height value must be non-negative'); - Height.auto(): super(0, Unit.auto); -} \ No newline at end of file + Height.auto() : super(0, Unit.auto); +} diff --git a/lib/src/styled_element.dart b/lib/src/styled_element.dart index 2b2cdd6d48..8434544c50 100644 --- a/lib/src/styled_element.dart +++ b/lib/src/styled_element.dart @@ -49,9 +49,9 @@ class StyledElement { } StyledElement parseStyledElement( - dom.Element element, - List children, - ) { + dom.Element element, + List children, +) { StyledElement styledElement = StyledElement( name: element.localName!, elementId: element.id, @@ -186,13 +186,16 @@ StyledElement parseStyledElement( break; case "font": styledElement.style = Style( - color: element.attributes['color'] != null ? - element.attributes['color']!.startsWith("#") ? - ExpressionMapping.stringToColor(element.attributes['color']!) : - ExpressionMapping.namedColorToColor(element.attributes['color']!) : - null, + color: element.attributes['color'] != null + ? element.attributes['color']!.startsWith("#") + ? ExpressionMapping.stringToColor(element.attributes['color']!) + : ExpressionMapping.namedColorToColor( + element.attributes['color']!) + : null, fontFamily: element.attributes['face']?.split(",").first, - fontSize: element.attributes['size'] != null ? numberToFontSize(element.attributes['size']!) : null, + fontSize: element.attributes['size'] != null + ? numberToFontSize(element.attributes['size']!) + : null, ); break; case "h1": @@ -414,4 +417,4 @@ FontSize numberToFontSize(String num) { return numberToFontSize((3 - relativeNum).toString()); } return FontSize.medium; -} \ No newline at end of file +} diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 389cb4fc6b..e79bba1008 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -38,7 +38,8 @@ class MultipleTapGestureDetector extends InheritedWidget { }) : super(key: key, child: child); static MultipleTapGestureDetector? of(BuildContext context) { - return context.dependOnInheritedWidgetOfExactType(); + return context + .dependOnInheritedWidgetOfExactType(); } @override @@ -85,4 +86,4 @@ extension TextTransformUtil on String? { return this; } } -} \ No newline at end of file +} diff --git a/lib/style.dart b/lib/style.dart index 7caf9a3dba..38d12ca136 100644 --- a/lib/style.dart +++ b/lib/style.dart @@ -44,14 +44,12 @@ 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, @@ -251,8 +249,7 @@ class Style { } } - static Map fromThemeData(ThemeData theme) => - { + static Map fromThemeData(ThemeData theme) => { 'h1': Style.fromTextStyle(theme.textTheme.headline1!), 'h2': Style.fromTextStyle(theme.textTheme.headline2!), 'h3': Style.fromTextStyle(theme.textTheme.headline3!), @@ -262,8 +259,8 @@ class Style { 'body': Style.fromTextStyle(theme.textTheme.bodyText2!), }; - static Map fromCss(String css, - OnCssParseError? onCssParseError) { + static Map fromCss( + String css, OnCssParseError? onCssParseError) { final declarations = parseExternalCss(css, onCssParseError); Map styleMap = {}; declarations.forEach((key, value) { @@ -345,13 +342,14 @@ class Style { } Style copyOnlyInherited(Style child) { - FontSize? finalFontSize = FontSize.inherit(fontSize, child.fontSize); LineHeight? finalLineHeight = child.lineHeight != null ? child.lineHeight?.units == "length" - ? LineHeight(child.lineHeight!.size! / (finalFontSize == null ? 14 : finalFontSize.value) * 1.2) - : child.lineHeight + ? LineHeight(child.lineHeight!.size! / + (finalFontSize == null ? 14 : finalFontSize.value) * + 1.2) + : child.lineHeight : lineHeight; return child.copyWith( @@ -446,7 +444,7 @@ class Style { textDecorationColor: textDecorationColor ?? this.textDecorationColor, textDecorationStyle: textDecorationStyle ?? this.textDecorationStyle, textDecorationThickness: - textDecorationThickness ?? this.textDecorationThickness, + textDecorationThickness ?? this.textDecorationThickness, textShadow: textShadow ?? this.textShadow, verticalAlign: verticalAlign ?? this.verticalAlign, whiteSpace: whiteSpace ?? this.whiteSpace, @@ -473,9 +471,8 @@ class Style { this.fontFamily = textStyle.fontFamily; this.fontFamilyFallback = textStyle.fontFamilyFallback; this.fontFeatureSettings = textStyle.fontFeatures; - this.fontSize = textStyle.fontSize != null - ? FontSize(textStyle.fontSize!) - : null; + this.fontSize = + textStyle.fontSize != null ? FontSize(textStyle.fontSize!) : null; this.fontStyle = textStyle.fontStyle; this.fontWeight = textStyle.fontWeight; this.letterSpacing = textStyle.letterSpacing; @@ -487,21 +484,21 @@ class Style { /// Sets any dimensions set to rem or em to the computed size void setRelativeValues(double remValue, double emValue) { - if(width?.unit == Unit.rem) { + if (width?.unit == Unit.rem) { width = Width(width!.value * remValue); - } else if(width?.unit == Unit.em) { + } else if (width?.unit == Unit.em) { width = Width(width!.value * emValue); } - if(height?.unit == Unit.rem) { + if (height?.unit == Unit.rem) { height = Height(height!.value * remValue); - } else if(height?.unit == Unit.em) { + } else if (height?.unit == Unit.em) { height = Height(height!.value * emValue); } - if(fontSize?.unit == Unit.rem) { + if (fontSize?.unit == Unit.rem) { fontSize = FontSize(fontSize!.value * remValue); - } else if(fontSize?.unit == Unit.em) { + } else if (fontSize?.unit == Unit.em) { fontSize = FontSize(fontSize!.value * emValue); } @@ -510,27 +507,27 @@ class Style { Margin? marginRight; Margin? marginBottom; - if(margin?.left?.unit == Unit.rem) { + if (margin?.left?.unit == Unit.rem) { marginLeft = Margin(margin!.left!.value * remValue); - } else if(margin?.left?.unit == Unit.em) { + } else if (margin?.left?.unit == Unit.em) { marginLeft = Margin(margin!.left!.value * emValue); } - if(margin?.top?.unit == Unit.rem) { + if (margin?.top?.unit == Unit.rem) { marginTop = Margin(margin!.top!.value * remValue); - } else if(margin?.top?.unit == Unit.em) { + } else if (margin?.top?.unit == Unit.em) { marginTop = Margin(margin!.top!.value * emValue); } - if(margin?.right?.unit == Unit.rem) { + if (margin?.right?.unit == Unit.rem) { marginRight = Margin(margin!.right!.value * remValue); - } else if(margin?.right?.unit == Unit.em) { + } else if (margin?.right?.unit == Unit.em) { marginRight = Margin(margin!.right!.value * emValue); } - if(margin?.bottom?.unit == Unit.rem) { + if (margin?.bottom?.unit == Unit.rem) { marginBottom = Margin(margin!.bottom!.value * remValue); - } else if(margin?.bottom?.unit == Unit.em) { + } else if (margin?.bottom?.unit == Unit.em) { marginBottom = Margin(margin!.bottom!.value * emValue); } @@ -558,9 +555,11 @@ class ListStyleType { const ListStyleType(this.text, {this.type = "marker", this.widget}); - factory ListStyleType.fromImage(String url) => ListStyleType(url, type: "image"); + factory ListStyleType.fromImage(String url) => + ListStyleType(url, type: "image"); - factory ListStyleType.fromWidget(Widget widget) => ListStyleType("", widget: widget, type: "widget"); + factory ListStyleType.fromWidget(Widget widget) => + ListStyleType("", widget: widget, type: "widget"); static const LOWER_ALPHA = ListStyleType("LOWER_ALPHA"); static const UPPER_ALPHA = ListStyleType("UPPER_ALPHA"); diff --git a/packages/flutter_html_iframe/lib/iframe_web.dart b/packages/flutter_html_iframe/lib/iframe_web.dart index e5252c7d0f..12476c3edd 100644 --- a/packages/flutter_html_iframe/lib/iframe_web.dart +++ b/packages/flutter_html_iframe/lib/iframe_web.dart @@ -24,28 +24,27 @@ CustomRender iframeRender({NavigationDelegate? navigationDelegate}) => ui.platformViewRegistry .registerViewFactory(createdViewId, (int viewId) => iframe); 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, - child: CSSBoxWidget( - style: context.style, - childIsReplaced: true, - child: Directionality( - textDirection: TextDirection.ltr, - child: HtmlElementView( - viewType: createdViewId, - ), + 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, + child: CSSBoxWidget( + style: context.style, + childIsReplaced: true, + child: Directionality( + textDirection: TextDirection.ltr, + child: HtmlElementView( + viewType: createdViewId, ), ), + ), ); }); 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 index ed136f0e18..eb31307d7a 100644 --- 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 @@ -6,19 +6,20 @@ import 'package:meta/meta.dart'; void main() { group("custom image data uri matcher", () { - CustomRenderMatcher matcher = svgDataUriMatcher(encoding: null, mime: 'image/svg+xml'); + 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=', + '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', + '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( @@ -31,7 +32,7 @@ void main() { "doesn't match non-base64 image data uri", matcher, imgSrc: - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==', shouldMatch: false, ); testImgSrcMatcher( @@ -69,11 +70,11 @@ String _fakeElement(String? src) { @isTest void testImgSrcMatcher( - String name, - CustomRenderMatcher matcher, { - required String? imgSrc, - required bool shouldMatch, - }) { + String name, + CustomRenderMatcher matcher, { + required String? imgSrc, + required bool shouldMatch, +}) { testWidgets(name, (WidgetTester tester) async { await tester.pumpWidget( TestApp( @@ -89,7 +90,8 @@ void testImgSrcMatcher( ), ), ); - await expectLater(find.text("Success"), shouldMatch ? findsOneWidget : findsNothing); + await expectLater( + find.text("Success"), shouldMatch ? findsOneWidget : findsNothing); }); } @@ -107,4 +109,4 @@ class TestApp extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/packages/flutter_html_table/lib/flutter_html_table.dart b/packages/flutter_html_table/lib/flutter_html_table.dart index d6dbce29a8..353f3117c0 100644 --- a/packages/flutter_html_table/lib/flutter_html_table.dart +++ b/packages/flutter_html_table/lib/flutter_html_table.dart @@ -117,7 +117,8 @@ Widget _layoutCells(RenderContext context, BoxConstraints constraints) { } cells.add(GridPlacement( child: CSSBoxWidget( - style: child.style.merge(row.style), //TODO padding/decoration(color/border) + style: child.style + .merge(row.style), //TODO padding/decoration(color/border) child: SizedBox.expand( child: Container( alignment: child.style.alignment ?? From 0f3e3a4bdcb1c33599f244d0daf6fc2fc9f0313c Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Tue, 6 Sep 2022 14:31:13 -0600 Subject: [PATCH 11/14] test: Add a few more unit tests --- test/flutter_html_test.dart | 105 +++++++++++++++++++++++++++++++++ test/style/dimension_test.dart | 26 ++++---- test/style/fontsize_test.dart | 59 ++++++++++++++++++ test/utils_test.dart | 42 +++++++++++++ 4 files changed, 219 insertions(+), 13 deletions(-) create mode 100644 test/flutter_html_test.dart create mode 100644 test/style/fontsize_test.dart create mode 100644 test/utils_test.dart diff --git a/test/flutter_html_test.dart b/test/flutter_html_test.dart new file mode 100644 index 0000000000..9a5a752c70 --- /dev/null +++ b/test/flutter_html_test.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets( + "Check that widget does not fail on empty data", + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Html( + data: "", + ), + ), + ); + expect(find.text('', findRichText: true), findsOneWidget); + }, + ); + + testWidgets( + "Check that selectable widget does not fail on empty data", + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: SelectableHtml( + data: '', + ), + ), + ); + expect(find.text('', findRichText: true), findsOneWidget); + }, + ); + + testWidgets( + "Check that widget displays given text", + (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Html( + data: "Text", + ), + ), + ); + expect(find.text('Text', findRichText: true), findsOneWidget); + }, + ); + + testWidgets('Check that a simple element is displayed', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Html( + data: "

Text

", + ), + ), + ); + expect(find.text('Text', findRichText: true), findsOneWidget); + }); + + testWidgets('Check that a simple element is hidden when tagsList does not contain it', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Html( + data: "

Text

", + tagsList: ['div'], //Anything but `p` + ), + ), + ); + expect(find.text('Text', findRichText: true), findsNothing); + }); + + testWidgets('Check that a simple element is displayed when it is included in tagsList', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Html( + data: "

Text

", + tagsList: ['html', 'body', 'p'], + ), + ), + ); + expect(find.text('Text', findRichText: true), findsOneWidget); + }); + + testWidgets('Check that a custom element is not displayed', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Html( + data: "Text", + ), + ), + ); + expect(find.text('Text', findRichText: true), findsNothing); + }); + + testWidgets('Check that a custom element is not displayed', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Html( + data: "Text", + tagsList: Html.tags..add('custom'), + ), + ), + ); + expect(find.text('Text', findRichText: true), findsOneWidget); + }); +} diff --git a/test/style/dimension_test.dart b/test/style/dimension_test.dart index b36fa475bc..cf239249f2 100644 --- a/test/style/dimension_test.dart +++ b/test/style/dimension_test.dart @@ -28,17 +28,17 @@ void main() { expect(lengthPercent.unit, equals(Unit.px)); }); - // test("Pass in invalid unit", () { - // expect(() => Length(nonZeroNumber, Unit.percent), throwsAssertionError); - // }); - - // test("Pass in invalid unit with zero", () { - // expect(() => Length(0, Unit.percent), throwsAssertionError); - // }); - - // test("Pass in a valid unit", () { - // final lengthPercent = LengthOrPercent(nonZeroNumber, Unit.percent); - // expect(lengthPercent.value, equals(nonZeroNumber)); - // expect(lengthPercent.unit, equals(Unit.percent)); - // }); + test("Pass in invalid unit", () { + expect(() => Length(nonZeroNumber, Unit.percent), throwsAssertionError); + }); + + test("Pass in invalid unit with zero", () { + expect(() => Length(0, Unit.percent), throwsAssertionError); + }); + + test("Pass in a valid unit", () { + final lengthPercent = LengthOrPercent(nonZeroNumber, Unit.percent); + expect(lengthPercent.value, equals(nonZeroNumber)); + expect(lengthPercent.unit, equals(Unit.percent)); + }); } \ No newline at end of file diff --git a/test/style/fontsize_test.dart b/test/style/fontsize_test.dart new file mode 100644 index 0000000000..e985d73706 --- /dev/null +++ b/test/style/fontsize_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter_html/src/style/fontsize.dart'; +import 'package:flutter_html/src/style/length.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Check basic FontSize inheritance', () { + final FontSize parent = FontSize(16); + final FontSize? child = null; + + final result = FontSize.inherit(parent, child); + + expect(result?.value, equals(16)); + }); + + test('Check double null FontSize inheritance', () { + final FontSize? parent = null; + final FontSize? child = null; + + final result = FontSize.inherit(parent, child); + + expect(result?.value, equals(null)); + }); + + test('Check basic em inheritance', () { + final FontSize? parent = FontSize(16); + final FontSize? child = FontSize(1, Unit.em); + + final result = FontSize.inherit(parent, child); + + expect(result?.value, equals(16)); + }); + + test('Check factor em inheritance', () { + final FontSize? parent = FontSize(16); + final FontSize? child = FontSize(0.5, Unit.em); + + final result = FontSize.inherit(parent, child); + + expect(result?.value, equals(8)); + }); + + test('Check basic % inheritance', () { + final FontSize? parent = FontSize(16); + final FontSize? child = FontSize(100, Unit.percent); + + final result = FontSize.inherit(parent, child); + + expect(result?.value, equals(16)); + }); + + test('Check scaled % inheritance', () { + final FontSize? parent = FontSize(16); + final FontSize? child = FontSize(50, Unit.percent); + + final result = FontSize.inherit(parent, child); + + expect(result?.value, equals(8)); + }); +} \ No newline at end of file diff --git a/test/utils_test.dart b/test/utils_test.dart new file mode 100644 index 0000000000..c4cec61f93 --- /dev/null +++ b/test/utils_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_html/src/css_parser.dart'; +import 'package:flutter_html/src/utils.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Tests the file lib/src/utils.dart + +void main() { + test('Tests that namedColors returns a valid color', () { + expect(ExpressionMapping.namedColorToColor('red'), equals(ExpressionMapping.stringToColor(namedColors['Red']!))); + expect(namedColors['Red'], equals('#FF0000')); + }); + + test('CustomBorderSide does not allow negative width', () { + expect(() => CustomBorderSide(width: -5), throwsAssertionError); + expect(CustomBorderSide(width: 0), TypeMatcher()); + expect(CustomBorderSide(width: 5), TypeMatcher()); + }); + + const originalString = 'Hello'; + const uppercaseString = 'HELLO'; + const lowercaseString = 'hello'; + + test('TextTransformUtil returns self if transform is null', () { + expect(originalString.transformed(null), equals(originalString)); + }); + + test('TextTransformUtil uppercases correctly', () { + expect(originalString.transformed(TextTransform.uppercase), equals(uppercaseString)); + }); + + test('TextTransformUtil lowercases correctly', () { + expect(originalString.transformed(TextTransform.lowercase), equals(lowercaseString)); + }); + + const originalLongString = 'Hello, world! pub.dev'; + const capitalizedLongString = 'Hello, World! Pub.Dev'; + + test('TextTransformUtil capitalizs correctly', () { + expect(originalLongString.transformed(TextTransform.capitalize), equals(capitalizedLongString)); + }); +} \ No newline at end of file From a62449a77c18701a0faf8ffd650f9c535b2d006c Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Sat, 17 Sep 2022 06:55:46 -0600 Subject: [PATCH 12/14] fix: Change CSSBoxWidget to CssBoxWidget --- lib/custom_render.dart | 8 ++++---- lib/html_parser.dart | 4 ++-- lib/src/css_box_widget.dart | 7 +++---- lib/src/layout_element.dart | 4 ++-- lib/src/replaced_element.dart | 6 ++---- packages/flutter_html_audio/lib/flutter_html_audio.dart | 2 +- packages/flutter_html_iframe/lib/iframe_mobile.dart | 2 +- packages/flutter_html_iframe/lib/iframe_web.dart | 2 +- packages/flutter_html_table/lib/flutter_html_table.dart | 4 ++-- 9 files changed, 18 insertions(+), 21 deletions(-) diff --git a/lib/custom_render.dart b/lib/custom_render.dart index 15ba16aeb9..27d15f13ff 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -131,7 +131,7 @@ CustomRender blockElementRender({Style? style, List? children}) => return WidgetSpan( alignment: PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic, - child: CSSBoxWidget.withInlineSpanChildren( + child: CssBoxWidget.withInlineSpanChildren( key: context.key, style: style ?? context.tree.style, shrinkWrap: context.parser.shrinkWrap, @@ -157,7 +157,7 @@ CustomRender listElementRender( {Style? style, Widget? child, List? children}) => CustomRender.inlineSpan( inlineSpan: (context, buildChildren) => WidgetSpan( - child: CSSBoxWidget( + child: CssBoxWidget( key: context.key, style: style ?? context.tree.style, shrinkWrap: context.parser.shrinkWrap, @@ -206,7 +206,7 @@ CustomRender listElementRender( ? 10.0 : 0.0) : EdgeInsets.zero, - child: CSSBoxWidget.withInlineSpanChildren( + child: CssBoxWidget.withInlineSpanChildren( children: _getListElementChildren( style?.listStylePosition ?? context.tree.style.listStylePosition, @@ -472,7 +472,7 @@ CustomRender verticalAlignRender( key: context.key, offset: Offset( 0, verticalOffset ?? _getVerticalOffset(context.tree)), - child: CSSBoxWidget.withInlineSpanChildren( + child: CssBoxWidget.withInlineSpanChildren( children: children ?? buildChildren.call(), style: context.style, ), diff --git a/lib/html_parser.dart b/lib/html_parser.dart index dfbd996fae..7ed4e959af 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -101,7 +101,7 @@ class HtmlParser extends StatelessWidget { processedTree, ); - return CSSBoxWidget.withInlineSpanChildren( + return CssBoxWidget.withInlineSpanChildren( style: processedTree.style, children: [parsedTree], selectable: selectable, @@ -364,7 +364,7 @@ class HtmlParser extends StatelessWidget { .call(newContext, buildChildren); } return WidgetSpan( - child: CSSBoxWidget( + child: CssBoxWidget( style: tree.style, shrinkWrap: newContext.parser.shrinkWrap, child: diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart index 785b2b46c5..5bfffd82da 100644 --- a/lib/src/css_box_widget.dart +++ b/lib/src/css_box_widget.dart @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_html/flutter_html.dart'; -class CSSBoxWidget extends StatelessWidget { - CSSBoxWidget({ +class CssBoxWidget extends StatelessWidget { + CssBoxWidget({ this.key, required this.child, required this.style, @@ -15,7 +15,7 @@ class CSSBoxWidget extends StatelessWidget { }) : super(key: key); /// Generates a CSSBoxWidget that contains a list of InlineSpan children. - CSSBoxWidget.withInlineSpanChildren({ + CssBoxWidget.withInlineSpanChildren({ this.key, required List children, required this.style, @@ -696,7 +696,6 @@ extension Normalize on Dimension { } double _calculateEmValue(Style style, BuildContext buildContext) { - //TODO is there a better value for this? return (style.fontSize?.emValue ?? 16) * MediaQuery.textScaleFactorOf(buildContext) * MediaQuery.of(buildContext).devicePixelRatio; diff --git a/lib/src/layout_element.dart b/lib/src/layout_element.dart index 778cb93516..d366ce1fc3 100644 --- a/lib/src/layout_element.dart +++ b/lib/src/layout_element.dart @@ -154,13 +154,13 @@ class DetailsContentElement extends LayoutElement { expandedAlignment: Alignment.centerLeft, title: elementList.isNotEmpty == true && elementList.first.localName == "summary" - ? CSSBoxWidget.withInlineSpanChildren( + ? CssBoxWidget.withInlineSpanChildren( children: firstChild == null ? [] : [firstChild], style: style, ) : Text("Details"), children: [ - CSSBoxWidget.withInlineSpanChildren( + CssBoxWidget.withInlineSpanChildren( children: getChildren( childrenList, context, diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index c33f9ff3cb..3234da0f1d 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -112,9 +112,8 @@ class RubyElement extends ReplacedElement { child: Center( child: Transform( transform: Matrix4.translationValues(0, -(rubyYPos), 0), - child: CSSBoxWidget( + child: CssBoxWidget( style: c.style, - //TODO do any other attributes apply? child: Text( c.element!.innerHtml, style: c.style @@ -125,8 +124,7 @@ class RubyElement extends ReplacedElement { ), ), ), - CSSBoxWidget( - //TODO do any other styles apply? Does ruby still work? + CssBoxWidget( style: context.style, child: node is TextContentElement ? Text( diff --git a/packages/flutter_html_audio/lib/flutter_html_audio.dart b/packages/flutter_html_audio/lib/flutter_html_audio.dart index d9576f0270..c542f12242 100644 --- a/packages/flutter_html_audio/lib/flutter_html_audio.dart +++ b/packages/flutter_html_audio/lib/flutter_html_audio.dart @@ -81,7 +81,7 @@ class _AudioWidgetState extends State { return Container(height: 0, width: 0); } - return CSSBoxWidget( + return CssBoxWidget( key: widget.context.key, style: widget.context.style, child: ChewieAudio( diff --git a/packages/flutter_html_iframe/lib/iframe_mobile.dart b/packages/flutter_html_iframe/lib/iframe_mobile.dart index 80b20cd09c..b35f7c7e3b 100644 --- a/packages/flutter_html_iframe/lib/iframe_mobile.dart +++ b/packages/flutter_html_iframe/lib/iframe_mobile.dart @@ -15,7 +15,7 @@ CustomRender iframeRender({NavigationDelegate? navigationDelegate}) => return Container( width: givenWidth ?? (givenHeight ?? 150) * 2, height: givenHeight ?? (givenWidth ?? 300) / 2, - child: CSSBoxWidget( + child: CssBoxWidget( style: context.style, childIsReplaced: true, child: WebView( diff --git a/packages/flutter_html_iframe/lib/iframe_web.dart b/packages/flutter_html_iframe/lib/iframe_web.dart index 12476c3edd..aaf81c23ff 100644 --- a/packages/flutter_html_iframe/lib/iframe_web.dart +++ b/packages/flutter_html_iframe/lib/iframe_web.dart @@ -35,7 +35,7 @@ CustomRender iframeRender({NavigationDelegate? navigationDelegate}) => (double.tryParse(context.tree.element?.attributes['width'] ?? "") ?? 300) / 2, - child: CSSBoxWidget( + child: CssBoxWidget( style: context.style, childIsReplaced: true, child: Directionality( diff --git a/packages/flutter_html_table/lib/flutter_html_table.dart b/packages/flutter_html_table/lib/flutter_html_table.dart index 353f3117c0..32c6a66f8b 100644 --- a/packages/flutter_html_table/lib/flutter_html_table.dart +++ b/packages/flutter_html_table/lib/flutter_html_table.dart @@ -116,7 +116,7 @@ Widget _layoutCells(RenderContext context, BoxConstraints constraints) { columnColspanOffset[columni].clamp(1, columnMax - columni - 1); } cells.add(GridPlacement( - child: CSSBoxWidget( + child: CssBoxWidget( style: child.style .merge(row.style), //TODO padding/decoration(color/border) child: SizedBox.expand( @@ -124,7 +124,7 @@ Widget _layoutCells(RenderContext context, BoxConstraints constraints) { alignment: child.style.alignment ?? context.style.alignment ?? Alignment.centerLeft, - child: CSSBoxWidget.withInlineSpanChildren( + child: CssBoxWidget.withInlineSpanChildren( children: [context.parser.parseTree(context, child)], style: child.style, //TODO updated this. Does it work? ), From 9dc7f08ca238ff6a93314be5de716ad4e3baebb8 Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Sat, 17 Sep 2022 07:13:57 -0600 Subject: [PATCH 13/14] fix: Use enum instead of const int internally in length.dart --- lib/src/style/length.dart | 47 ++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/lib/src/style/length.dart b/lib/src/style/length.dart index dbc47f71d4..3fab69263d 100644 --- a/lib/src/style/length.dart +++ b/lib/src/style/length.dart @@ -1,27 +1,35 @@ -/// Increase new base unit types' values by a factor of 2 each time. -const int _percent = 0x1; -const int _length = 0x2; -const int _auto = 0x4; +/// These are the base unit types +enum _UnitType { + percent, + length, + auto, + lengthPercent(children: [_UnitType.length, _UnitType.percent]), + lengthPercentAuto(children: [_UnitType.length, _UnitType.percent, _UnitType.auto]); -/// These values are combinations of the base unit-types -const int _lengthPercent = _length | _percent; -const int _lengthPercentAuto = _lengthPercent | _auto; + final List<_UnitType> children; + + const _UnitType({this.children = const []}); + + bool matches(_UnitType other) { + return this == other || children.contains(other); + } +} /// A Unit represents a CSS unit enum Unit { //ch, - em(_length), + em(_UnitType.length), //ex, - percent(_percent), - px(_length), - rem(_length), + percent(_UnitType.percent), + px(_UnitType.length), + rem(_UnitType.length), //Q, //vh, //vw, - auto(_auto); + auto(_UnitType.auto); const Unit(this.unitType); - final int unitType; + final _UnitType unitType; } /// Represents a CSS dimension https://drafts.csswg.org/css-values/#dimensions @@ -29,16 +37,15 @@ abstract class Dimension { double value; Unit unit; - Dimension(this.value, this.unit, int _dimensionUnitType) - : assert( - identical((unit.unitType | _dimensionUnitType), _dimensionUnitType), - "This dimension was given a Unit that isn't specified."); + Dimension(this.value, this.unit, _UnitType _dimensionUnitType) + : assert(_dimensionUnitType.matches(unit.unitType), + "This Dimension was given a Unit that isn't specified."); } /// This dimension takes a value with a length unit such as px or em. Note that /// these can be fixed or relative (but they must not be a percent) class Length extends Dimension { - Length(double value, [Unit unit = Unit.px]) : super(value, unit, _length); + Length(double value, [Unit unit = Unit.px]) : super(value, unit, _UnitType.length); } /// This dimension takes a value with a length-percent unit such as px or em @@ -46,10 +53,10 @@ class Length extends Dimension { /// percent) class LengthOrPercent extends Dimension { LengthOrPercent(double value, [Unit unit = Unit.px]) - : super(value, unit, _lengthPercent); + : super(value, unit, _UnitType.lengthPercent); } class AutoOrLengthOrPercent extends Dimension { AutoOrLengthOrPercent(double value, [Unit unit = Unit.px]) - : super(value, unit, _lengthPercentAuto); + : super(value, unit, _UnitType.lengthPercentAuto); } From 7581ea798744b2830affaaf75bbdff016b03f7af Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Sat, 17 Sep 2022 08:08:00 -0600 Subject: [PATCH 14/14] fix: Apply margins to properly --- .../lib/flutter_html_table.dart | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/packages/flutter_html_table/lib/flutter_html_table.dart b/packages/flutter_html_table/lib/flutter_html_table.dart index 32c6a66f8b..8a207abbf3 100644 --- a/packages/flutter_html_table/lib/flutter_html_table.dart +++ b/packages/flutter_html_table/lib/flutter_html_table.dart @@ -9,25 +9,12 @@ import 'package:flutter_html/flutter_html.dart'; /// The CustomRender function that will render the
HTML tag CustomRender tableRender() => CustomRender.widget(widget: (context, buildChildren) { - return Container( + return CssBoxWidget( key: context.key, - //TODO(Sub6Resources): This needs to be computed with Units!! - margin: EdgeInsets.only( - left: context.style.margin?.left?.value.abs() ?? 0, - right: context.style.margin?.right?.value.abs() ?? 0, - bottom: context.style.margin?.bottom?.value.abs() ?? 0, - top: context.style.margin?.bottom?.value.abs() ?? 0, - ), - padding: context.style.padding?.nonNegative, - alignment: context.style.alignment, - decoration: BoxDecoration( - color: context.style.backgroundColor, - border: context.style.border, - ), - width: context.style.width?.value, //TODO calculate actual value - height: context.style.height?.value, //TODO calculate actual value + style: context.style, child: LayoutBuilder( - builder: (_, constraints) => _layoutCells(context, constraints)), + builder: (_, constraints) => _layoutCells(context, constraints), + ), ); });