From a17a8006035ae8d9fecfe07d06c635f2781b3279 Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 30 May 2025 17:41:27 +0800 Subject: [PATCH 1/7] feat: border-radius --- lib/src/css_box_widget.dart | 29 ++++++++++++++++------------- lib/src/css_parser.dart | 36 ++++++++++++++++++++++++++++++++++++ lib/src/style.dart | 5 +++++ 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart index 77d545d022..0fd24b93d5 100644 --- a/lib/src/css_box_widget.dart +++ b/lib/src/css_box_widget.dart @@ -71,19 +71,22 @@ class CssBoxWidget extends StatelessWidget { textDirection: direction, shrinkWrap: shrinkWrap, children: [ - Container( - decoration: BoxDecoration( - border: style.border, - color: style.backgroundColor, //Colors the padding and content boxes - ), - padding: padding, - child: top - ? child - : MediaQuery( - data: MediaQuery.of(context) - .copyWith(textScaler: TextScaler.linear(1.0)), - child: child, - ), + ClipRRect( + borderRadius: style.borderRadius ?? BorderRadius.zero, + child: Container( + decoration: BoxDecoration( + border: style.border, + color: style.backgroundColor, //Colors the padding and content boxes + ), + padding: padding, + child: top + ? child + : MediaQuery( + data: MediaQuery.of(context) + .copyWith(textScaler: TextScaler.linear(1.0)), + child: child, + ), + ) ), if (markerBox != null) Text.rich(markerBox), ], diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index 02cbe8a3ae..8c2c625352 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -59,6 +59,25 @@ Style declarationsToStyle(Map> declarations) { style.border = ExpressionMapping.expressionToBorder( borderWidths, borderStyles, borderColors); break; + case 'border-radius': + List? borderRadiuses = + 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] + borderRadiuses.removeWhere((element) => + element == null || + (element is! css.LengthTerm && + element is! css.PercentageTerm && + element is! css.EmTerm && + element is! css.RemTerm && + element is! css.NumberTerm)); + css.LiteralTerm? borderRadius = + borderRadiuses.firstWhereOrNull((element) => element != null); + BorderRadius newBorderRadius = BorderRadius.all(Radius.circular( + ExpressionMapping.expressionToBorderRadius(borderRadius)), + ); + style.borderRadius = newBorderRadius; + break; case 'border-left': List? borderWidths = value.whereType().toList(); @@ -890,6 +909,23 @@ class ExpressionMapping { return null; } + static double expressionToBorderRadius(css.Expression? value) { + if (value is css.NumberTerm) { + return double.tryParse(value.text) ?? 1.0; + } else if (value is css.PercentageTerm) { + return (double.tryParse(value.text) ?? 400) / 100; + } else if (value is css.EmTerm) { + return double.tryParse(value.text) ?? 1.0; + } 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(RegExp(r'\s+(\d+\.\d+)\s+'), '')) ?? + 1.0; + } + return 4.0; + } + static TextDirection expressionToDirection(css.Expression value) { if (value is css.LiteralTerm) { switch (value.text) { diff --git a/lib/src/style.dart b/lib/src/style.dart index 014f7ff91f..28b004305a 100644 --- a/lib/src/style.dart +++ b/lib/src/style.dart @@ -213,6 +213,7 @@ class Style { String? after; Border? border; Alignment? alignment; + BorderRadius? borderRadius; /// MaxLine /// @@ -271,6 +272,7 @@ class Style { this.before, this.after, this.border, + this.borderRadius, this.alignment, this.maxLines, this.textOverflow, @@ -363,6 +365,7 @@ class Style { before: other.before, after: other.after, border: border?.merge(other.border) ?? other.border, + borderRadius: borderRadius ?? other.borderRadius, alignment: other.alignment, maxLines: other.maxLines, textOverflow: other.textOverflow, @@ -452,6 +455,7 @@ class Style { String? before, String? after, Border? border, + BorderRadius? borderRadius, Alignment? alignment, Widget? markerContent, int? maxLines, @@ -495,6 +499,7 @@ class Style { before: beforeAfterNull == true ? null : before ?? this.before, after: beforeAfterNull == true ? null : after ?? this.after, border: border ?? this.border, + borderRadius: borderRadius ?? this.borderRadius, alignment: alignment ?? this.alignment, maxLines: maxLines ?? this.maxLines, textOverflow: textOverflow ?? this.textOverflow, From 76dbd63e1970000a7081592524f491a6ad03e63a Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 30 May 2025 18:49:38 +0800 Subject: [PATCH 2/7] fix: radius support em/rem --- lib/src/css_box_widget.dart | 2 +- lib/src/css_parser.dart | 29 +++++------- lib/src/style.dart | 23 +++++++++- lib/src/style/radius.dart | 90 +++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 lib/src/style/radius.dart diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart index 0fd24b93d5..1c6bac75bd 100644 --- a/lib/src/css_box_widget.dart +++ b/lib/src/css_box_widget.dart @@ -72,7 +72,7 @@ class CssBoxWidget extends StatelessWidget { shrinkWrap: shrinkWrap, children: [ ClipRRect( - borderRadius: style.borderRadius ?? BorderRadius.zero, + borderRadius: style.borderRadius?.toBorderRadius() ?? BorderRadius.zero, child: Container( decoration: BoxDecoration( border: style.border, diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index 8c2c625352..15f78a69bc 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -3,6 +3,7 @@ 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/radius.dart'; import 'package:flutter_html/src/utils.dart'; //TODO refactor @@ -73,9 +74,7 @@ Style declarationsToStyle(Map> declarations) { element is! css.NumberTerm)); css.LiteralTerm? borderRadius = borderRadiuses.firstWhereOrNull((element) => element != null); - BorderRadius newBorderRadius = BorderRadius.all(Radius.circular( - ExpressionMapping.expressionToBorderRadius(borderRadius)), - ); + HtmlRadii newBorderRadius = ExpressionMapping.expressionToBorderRadius(borderRadius); style.borderRadius = newBorderRadius; break; case 'border-left': @@ -909,21 +908,17 @@ class ExpressionMapping { return null; } - static double expressionToBorderRadius(css.Expression? value) { - if (value is css.NumberTerm) { - return double.tryParse(value.text) ?? 1.0; - } else if (value is css.PercentageTerm) { - return (double.tryParse(value.text) ?? 400) / 100; - } else if (value is css.EmTerm) { - return double.tryParse(value.text) ?? 1.0; - } 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(RegExp(r'\s+(\d+\.\d+)\s+'), '')) ?? - 1.0; + static HtmlRadii expressionToBorderRadius(css.Expression? value) { + if (value == null) { + return HtmlRadii.zero; } - return 4.0; + final computedValue = expressionToLengthOrPercent(value); + return HtmlRadii( + topLeft: HtmlRadius(computedValue.value, computedValue.unit), + topRight: HtmlRadius(computedValue.value, computedValue.unit), + bottomLeft: HtmlRadius(computedValue.value, computedValue.unit), + bottomRight: HtmlRadius(computedValue.value, computedValue.unit), + ); } static TextDirection expressionToDirection(css.Expression value) { diff --git a/lib/src/style.dart b/lib/src/style.dart index 28b004305a..8154f09bea 100644 --- a/lib/src/style.dart +++ b/lib/src/style.dart @@ -1,6 +1,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/style/radius.dart'; //Export Style value-unit APIs export 'package:flutter_html/src/style/display.dart'; @@ -213,7 +214,7 @@ class Style { String? after; Border? border; Alignment? alignment; - BorderRadius? borderRadius; + HtmlRadii? borderRadius; /// MaxLine /// @@ -455,7 +456,7 @@ class Style { String? before, String? after, Border? border, - BorderRadius? borderRadius, + HtmlRadii? borderRadius, Alignment? alignment, Widget? markerContent, int? maxLines, @@ -568,6 +569,13 @@ class Style { blockStart: padding?.blockStart?.getRelativeValue(remValue, emValue), blockEnd: padding?.blockEnd?.getRelativeValue(remValue, emValue), ); + + borderRadius = borderRadius?.copyWith( + topLeft: borderRadius?.topLeft?.getRelativeValue(remValue, emValue), + topRight: borderRadius?.topRight?.getRelativeValue(remValue, emValue), + bottomLeft: borderRadius?.bottomLeft?.getRelativeValue(remValue, emValue), + bottomRight: borderRadius?.bottomRight?.getRelativeValue(remValue, emValue), + ); } } @@ -604,6 +612,17 @@ extension MergeBorders on Border? { } } +extension _RadiusRelativeValues on HtmlRadius { + HtmlRadius? getRelativeValue(double remValue, double emValue) { + double? calculatedValue = calculateRelativeValue(remValue, emValue); + if (calculatedValue != null) { + return HtmlRadius(calculatedValue); + } + + return null; + } +} + enum ListStyleType { arabicIndic('arabic-indic'), armenian('armenian'), diff --git a/lib/src/style/radius.dart b/lib/src/style/radius.dart new file mode 100644 index 0000000000..f64034beed --- /dev/null +++ b/lib/src/style/radius.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/src/style/length.dart'; + +class HtmlRadius extends LengthOrPercent { + HtmlRadius(double value, [Unit? unit = Unit.px]) + : assert(value >= 0, "Radius must be non-negative"), + super(value, unit ?? Unit.px); + + HtmlRadius.zero() : super(0, Unit.px); + + @override + int get hashCode => Object.hash(value, unit); + + @override + bool operator ==(Object other) { + return other is HtmlRadius && other.value == value && other.unit == unit; + } +} + +class HtmlRadii { + final HtmlRadius? topLeft; + final HtmlRadius? topRight; + final HtmlRadius? bottomLeft; + final HtmlRadius? bottomRight; + + const HtmlRadii({ + this.topLeft, + this.topRight, + this.bottomLeft, + this.bottomRight, + }); + + /// 类似 EdgeInsets.zero + static HtmlRadii get zero => HtmlRadii.all(0); + + /// 类似 EdgeInsets.all + HtmlRadii.all(double value, [Unit? unit]) + : topLeft = HtmlRadius(value, unit), + topRight = HtmlRadius(value, unit), + bottomLeft = HtmlRadius(value, unit), + bottomRight = HtmlRadius(value, unit); + + /// 类似 EdgeInsets.only + HtmlRadii.only({ + double? topLeft, + double? topRight, + double? bottomLeft, + double? bottomRight, + Unit? unit, + }) : topLeft = topLeft != null ? HtmlRadius(topLeft, unit) : null, + topRight = topRight != null ? HtmlRadius(topRight, unit) : null, + bottomLeft = bottomLeft != null ? HtmlRadius(bottomLeft, unit) : null, + bottomRight = bottomRight != null ? HtmlRadius(bottomRight, unit) : null; + + HtmlRadii copyWith({ + HtmlRadius? topLeft, + HtmlRadius? topRight, + HtmlRadius? bottomLeft, + HtmlRadius? bottomRight, + }) { + return HtmlRadii( + topLeft: topLeft ?? this.topLeft, + topRight: topRight ?? this.topRight, + bottomLeft: bottomLeft ?? this.bottomLeft, + bottomRight: bottomRight ?? this.bottomRight, + ); + } + + @override + int get hashCode => + Object.hash(topLeft, topRight, bottomLeft, bottomRight); + + @override + bool operator ==(Object other) { + return other is HtmlRadii && + topLeft == other.topLeft && + topRight == other.topRight && + bottomLeft == other.bottomLeft && + bottomRight == other.bottomRight; + } + + BorderRadius toBorderRadius() { + return BorderRadius.only( + topLeft: Radius.circular(topLeft?.value ?? 0), + topRight: Radius.circular(topRight?.value ?? 0), + bottomLeft: Radius.circular(bottomLeft?.value ?? 0), + bottomRight: Radius.circular(bottomRight?.value ?? 0), + ); + } +} From 736ac878656cb61213b14587eca1ac52a322f19a Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 30 May 2025 19:08:20 +0800 Subject: [PATCH 3/7] refine: border-radius support 4 corners --- lib/src/css_parser.dart | 71 ++++++++++++++++++++++++++--------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index 15f78a69bc..5a7398b11b 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -60,23 +60,6 @@ Style declarationsToStyle(Map> declarations) { style.border = ExpressionMapping.expressionToBorder( borderWidths, borderStyles, borderColors); break; - case 'border-radius': - List? borderRadiuses = - 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] - borderRadiuses.removeWhere((element) => - element == null || - (element is! css.LengthTerm && - element is! css.PercentageTerm && - element is! css.EmTerm && - element is! css.RemTerm && - element is! css.NumberTerm)); - css.LiteralTerm? borderRadius = - borderRadiuses.firstWhereOrNull((element) => element != null); - HtmlRadii newBorderRadius = ExpressionMapping.expressionToBorderRadius(borderRadius); - style.borderRadius = newBorderRadius; - break; case 'border-left': List? borderWidths = value.whereType().toList(); @@ -309,6 +292,49 @@ Style declarationsToStyle(Map> declarations) { ); style.border = newBorder; break; + case 'border-radius': + List? borderRadiuses = + 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] + borderRadiuses.removeWhere((element) => + element == null || + (element is! css.LengthTerm && + element is! css.PercentageTerm && + element is! css.EmTerm && + element is! css.RemTerm && + element is! css.NumberTerm)); + css.LiteralTerm? borderRadius = + borderRadiuses.firstWhereOrNull((element) => element != null); + final newBorderRadius = ExpressionMapping.expressionToBorderRadius(borderRadius); + HtmlRadii newBorderRadii = HtmlRadii.all(newBorderRadius.value, newBorderRadius.unit); + style.borderRadius = newBorderRadii; + break; + case 'border-top-left-radius': + case 'border-top-right-radius': + case 'border-bottom-left-radius': + case 'border-bottom-right-radius': + List? borderRadiuses = + 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] + borderRadiuses.removeWhere((element) => + element == null || + (element is! css.LengthTerm && + element is! css.PercentageTerm && + element is! css.EmTerm && + element is! css.RemTerm && + element is! css.NumberTerm)); + css.LiteralTerm? borderRadius = + borderRadiuses.firstWhereOrNull((element) => element != null); + HtmlRadii newBorder = HtmlRadii( + topLeft: property == 'border-top-left-radius' ? ExpressionMapping.expressionToBorderRadius(borderRadius) : (style.borderRadius?.topLeft ?? HtmlRadius.zero()), + topRight: property == 'border-top-right-radius' ? ExpressionMapping.expressionToBorderRadius(borderRadius) : (style.borderRadius?.topRight ?? HtmlRadius.zero()), + bottomLeft: property == 'border-bottom-left-radius' ? ExpressionMapping.expressionToBorderRadius(borderRadius) : (style.borderRadius?.bottomLeft ?? HtmlRadius.zero()), + bottomRight: property == 'border-bottom-right-radius' ? ExpressionMapping.expressionToBorderRadius(borderRadius) : (style.borderRadius?.bottomRight ?? HtmlRadius.zero()), + ); + style.borderRadius = newBorder; + break; case 'color': style.color = style.textDecorationColor = ExpressionMapping.expressionToColor(value.first) ?? style.color; @@ -908,17 +934,12 @@ class ExpressionMapping { return null; } - static HtmlRadii expressionToBorderRadius(css.Expression? value) { + static HtmlRadius expressionToBorderRadius(css.Expression? value) { if (value == null) { - return HtmlRadii.zero; + return HtmlRadius.zero(); } final computedValue = expressionToLengthOrPercent(value); - return HtmlRadii( - topLeft: HtmlRadius(computedValue.value, computedValue.unit), - topRight: HtmlRadius(computedValue.value, computedValue.unit), - bottomLeft: HtmlRadius(computedValue.value, computedValue.unit), - bottomRight: HtmlRadius(computedValue.value, computedValue.unit), - ); + return HtmlRadius(computedValue.value, computedValue.unit); } static TextDirection expressionToDirection(css.Expression value) { From adccbec2f0bd8921a9a9c4ce43afc71f7ad01e42 Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 30 May 2025 19:46:34 +0800 Subject: [PATCH 4/7] refine: use container borderRadius instead of ClipRRect --- lib/src/css_box_widget.dart | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart index 1c6bac75bd..2537a4edc2 100644 --- a/lib/src/css_box_widget.dart +++ b/lib/src/css_box_widget.dart @@ -71,22 +71,20 @@ class CssBoxWidget extends StatelessWidget { textDirection: direction, shrinkWrap: shrinkWrap, children: [ - ClipRRect( - borderRadius: style.borderRadius?.toBorderRadius() ?? BorderRadius.zero, - child: Container( - decoration: BoxDecoration( - border: style.border, - color: style.backgroundColor, //Colors the padding and content boxes - ), - padding: padding, - child: top - ? child - : MediaQuery( - data: MediaQuery.of(context) - .copyWith(textScaler: TextScaler.linear(1.0)), - child: child, - ), - ) + Container( + decoration: BoxDecoration( + borderRadius: style.borderRadius?.toBorderRadius() ?? BorderRadius.zero, + border: style.border, + color: style.backgroundColor, //Colors the padding and content boxes + ), + padding: padding, + child: top + ? child + : MediaQuery( + data: MediaQuery.of(context) + .copyWith(textScaler: TextScaler.linear(1.0)), + child: child, + ), ), if (markerBox != null) Text.rich(markerBox), ], From 69b03f6fd15ecec8ff80da22745b6d0927dc3ed6 Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 30 May 2025 20:45:45 +0800 Subject: [PATCH 5/7] fix: null check for text shadow tag --- lib/src/css_parser.dart | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index 5a7398b11b..da58961897 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -1397,23 +1397,21 @@ class ExpressionMapping { shadow.add(Shadow( color: expressionToColor(color)!, offset: Offset( - double.tryParse((offsetX).text.replaceAll(nonNumberRegex, ''))!, - double.tryParse( - (offsetY).text.replaceAll(nonNumberRegex, ''))!), + double.tryParse((offsetX).text.replaceAll(nonNumberRegex, '')) ?? 0.0, + double.tryParse((offsetY).text.replaceAll(nonNumberRegex, '')) ?? 0.0 + ), blurRadius: (blurRadius is css.LiteralTerm) - ? double.tryParse( - (blurRadius).text.replaceAll(nonNumberRegex, ''))! + ? double.tryParse((blurRadius).text.replaceAll(nonNumberRegex, '')) ?? 0.0 : 0.0, )); } else { shadow.add(Shadow( offset: Offset( - double.tryParse((offsetX).text.replaceAll(nonNumberRegex, ''))!, - double.tryParse( - (offsetY).text.replaceAll(nonNumberRegex, ''))!), + double.tryParse((offsetX).text.replaceAll(nonNumberRegex, '')) ?? 0.0, + double.tryParse((offsetY).text.replaceAll(nonNumberRegex, '')) ?? 0.0 + ), blurRadius: (blurRadius is css.LiteralTerm) - ? double.tryParse( - (blurRadius).text.replaceAll(nonNumberRegex, ''))! + ? double.tryParse((blurRadius).text.replaceAll(nonNumberRegex, '')) ?? 0.0 : 0.0, )); } From f40e636ef6060681b64270b400836f9dc9d61a82 Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 30 May 2025 23:17:32 +0800 Subject: [PATCH 6/7] fix: export radius --- lib/src/style.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/style.dart b/lib/src/style.dart index 8154f09bea..d677a61bd7 100644 --- a/lib/src/style.dart +++ b/lib/src/style.dart @@ -1,7 +1,6 @@ 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/radius.dart'; //Export Style value-unit APIs export 'package:flutter_html/src/style/display.dart'; @@ -12,6 +11,7 @@ export 'package:flutter_html/src/style/size.dart'; export 'package:flutter_html/src/style/fontsize.dart'; export 'package:flutter_html/src/style/lineheight.dart'; export 'package:flutter_html/src/style/marker.dart'; +export 'package:flutter_html/src/style/radius.dart'; ///This class represents all the available CSS attributes ///for this package. From c913ab743a272331f2078c94ddbae8d3ced360ab Mon Sep 17 00:00:00 2001 From: Colin Date: Thu, 5 Jun 2025 16:10:38 +0800 Subject: [PATCH 7/7] fix: no background color copy when build TextSpan in inlineBlock element --- lib/src/style.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/style.dart b/lib/src/style.dart index d677a61bd7..d22106f7c0 100644 --- a/lib/src/style.dart +++ b/lib/src/style.dart @@ -306,7 +306,7 @@ class Style { TextStyle generateTextStyle() { return TextStyle( - backgroundColor: (display?.isBlock ?? false) ? null : backgroundColor, + backgroundColor: ((display?.isBlock ?? false) || display == Display.inlineBlock) ? null : backgroundColor, color: color, decoration: textDecoration, decorationColor: textDecorationColor,