From c5f396dd29b9ede943d1e908b48d4b80ac2d2542 Mon Sep 17 00:00:00 2001 From: Zak Barbuto Date: Mon, 25 Oct 2021 17:43:30 +1030 Subject: [PATCH] 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(