Skip to content

Commit cab3533

Browse files
authored
Merge pull request Sub6Resources#544 from tneotia/feature/more-inline-styles
Add support for more inline styles
2 parents e421c47 + 022313a commit cab3533

File tree

3 files changed

+233
-18
lines changed

3 files changed

+233
-18
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets.
2727

2828
- [Currently Supported CSS Attributes](#currently-supported-css-attributes)
2929

30+
- [Currently Supported Inline CSS Attributes](#currently-supported-inline-css-attributes)
31+
3032
- [Why flutter_html?](#why-this-package)
3133

3234
- [API Reference](#api-reference)
@@ -106,6 +108,13 @@ Add the following to your `pubspec.yaml` file:
106108
|`padding` | `margin`| `text-align`| `text-decoration`| `text-decoration-color`| `text-decoration-style`| `text-decoration-thickness`|
107109
|`text-shadow` | `vertical-align`| `white-space`| `width` | `word-spacing`| | |
108110

111+
## Currently Supported Inline CSS Attributes:
112+
| | | | | | | |
113+
|------------------|--------|------------|----------|--------------|------------------------|------------|
114+
|`background-color`| `border` | `color`| `direction`| `display`| `font-family`| `font-feature-settings` |
115+
| `font-size`|`font-style` | `font-weight`| `line-height` | `list-style-type` | `list-style-position`|`padding` |
116+
| `margin`| `text-align`| `text-decoration`| `text-decoration-color`| `text-decoration-style`| `text-shadow` | |
117+
109118
Don't see a tag or attribute you need? File a feature request or contribute to the project!
110119

111120
## Why this package?

lib/src/css_parser.dart

Lines changed: 190 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import 'dart:ui';
33
import 'package:csslib/visitor.dart' as css;
44
import 'package:csslib/parser.dart' as cssparser;
55
import 'package:flutter/cupertino.dart';
6+
import 'package:flutter/material.dart';
7+
import 'package:flutter_html/src/utils.dart';
68
import 'package:flutter_html/style.dart';
79

810
Style declarationsToStyle(Map<String?, List<css.Expression>> declarations) {
@@ -13,6 +15,24 @@ Style declarationsToStyle(Map<String?, List<css.Expression>> declarations) {
1315
case 'background-color':
1416
style.backgroundColor = ExpressionMapping.expressionToColor(value.first) ?? style.backgroundColor;
1517
break;
18+
case 'border':
19+
List<css.LiteralTerm?>? borderWidths = value.whereType<css.LiteralTerm>().toList();
20+
/// List<css.LiteralTerm> might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping]
21+
borderWidths.removeWhere((element) => element != null && element.text != "thin"
22+
&& element.text != "medium" && element.text != "thick"
23+
&& !(element is css.LengthTerm) && !(element is css.PercentageTerm)
24+
&& !(element is css.EmTerm) && !(element is css.RemTerm)
25+
&& !(element is css.NumberTerm)
26+
);
27+
List<css.Expression?>? borderColors = value.where((element) => ExpressionMapping.expressionToColor(element) != null).toList();
28+
List<css.LiteralTerm?>? potentialStyles = value.whereType<css.LiteralTerm>().toList();
29+
/// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future.
30+
List<String> possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"];
31+
/// List<css.LiteralTerm> might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping]
32+
potentialStyles.removeWhere((element) => element != null && !possibleBorderValues.contains(element.text));
33+
List<css.LiteralTerm?>? borderStyles = potentialStyles;
34+
style.border = ExpressionMapping.expressionToBorder(borderWidths, borderStyles, borderColors);
35+
break;
1636
case 'color':
1737
style.color = ExpressionMapping.expressionToColor(value.first) ?? style.color;
1838
break;
@@ -49,8 +69,15 @@ Style declarationsToStyle(Map<String?, List<css.Expression>> declarations) {
4969
textDecorationList.removeWhere((element) => element != null && element.text != "none"
5070
&& element.text != "overline" && element.text != "underline" && element.text != "line-through");
5171
List<css.Expression?>? nullableList = value;
52-
css.Expression? textDecorationColor = nullableList.firstWhere(
53-
(css.Expression? element) => element is css.HexColorTerm || element is css.FunctionTerm, orElse: () => null);
72+
css.Expression? textDecorationColor;
73+
/// orElse: will not allow me to return null (even if the compiler says its okay, it errors on runtime).
74+
/// try/catch is a workaround for this.
75+
try {
76+
textDecorationColor = nullableList.firstWhere(
77+
(css.Expression? element) => element is css.HexColorTerm || element is css.FunctionTerm);
78+
} catch (e) {
79+
textDecorationColor = null;
80+
}
5481
List<css.LiteralTerm?>? potentialStyles = value.whereType<css.LiteralTerm>().toList();
5582
/// List<css.LiteralTerm> might include other values than the ones we want for [textDecorationStyle], so make sure to remove those before passing it to [ExpressionMapping]
5683
potentialStyles.removeWhere((element) => element != null && element.text != "solid"
@@ -112,14 +139,132 @@ class DeclarationVisitor extends css.Visitor {
112139

113140
//Mapping functions
114141
class ExpressionMapping {
115-
static Color? expressionToColor(css.Expression value) {
116-
if (value is css.HexColorTerm) {
117-
return stringToColor(value.text);
118-
} else if (value is css.FunctionTerm) {
119-
if (value.text == 'rgba') {
120-
return rgbOrRgbaToColor(value.span!.text);
121-
} else if (value.text == 'rgb') {
122-
return rgbOrRgbaToColor(value.span!.text);
142+
143+
static Border expressionToBorder(List<css.Expression?>? borderWidths, List<css.LiteralTerm?>? borderStyles, List<css.Expression?>? borderColors) {
144+
CustomBorderSide left = CustomBorderSide();
145+
CustomBorderSide top = CustomBorderSide();
146+
CustomBorderSide right = CustomBorderSide();
147+
CustomBorderSide bottom = CustomBorderSide();
148+
if (borderWidths != null && borderWidths.isNotEmpty) {
149+
top.width = expressionToBorderWidth(borderWidths.first);
150+
if (borderWidths.length == 4) {
151+
right.width = expressionToBorderWidth(borderWidths[1]);
152+
bottom.width = expressionToBorderWidth(borderWidths[2]);
153+
left.width = expressionToBorderWidth(borderWidths.last);
154+
}
155+
if (borderWidths.length == 3) {
156+
left.width = expressionToBorderWidth(borderWidths[1]);
157+
right.width = expressionToBorderWidth(borderWidths[1]);
158+
bottom.width = expressionToBorderWidth(borderWidths.last);
159+
}
160+
if (borderWidths.length == 2) {
161+
bottom.width = expressionToBorderWidth(borderWidths.first);
162+
left.width = expressionToBorderWidth(borderWidths.last);
163+
right.width = expressionToBorderWidth(borderWidths.last);
164+
}
165+
if (borderWidths.length == 1) {
166+
bottom.width = expressionToBorderWidth(borderWidths.first);
167+
left.width = expressionToBorderWidth(borderWidths.first);
168+
right.width = expressionToBorderWidth(borderWidths.first);
169+
}
170+
}
171+
if (borderStyles != null && borderStyles.isNotEmpty) {
172+
top.style = expressionToBorderStyle(borderStyles.first);
173+
if (borderStyles.length == 4) {
174+
right.style = expressionToBorderStyle(borderStyles[1]);
175+
bottom.style = expressionToBorderStyle(borderStyles[2]);
176+
left.style = expressionToBorderStyle(borderStyles.last);
177+
}
178+
if (borderStyles.length == 3) {
179+
left.style = expressionToBorderStyle(borderStyles[1]);
180+
right.style = expressionToBorderStyle(borderStyles[1]);
181+
bottom.style = expressionToBorderStyle(borderStyles.last);
182+
}
183+
if (borderStyles.length == 2) {
184+
bottom.style = expressionToBorderStyle(borderStyles.first);
185+
left.style = expressionToBorderStyle(borderStyles.last);
186+
right.style = expressionToBorderStyle(borderStyles.last);
187+
}
188+
if (borderStyles.length == 1) {
189+
bottom.style = expressionToBorderStyle(borderStyles.first);
190+
left.style = expressionToBorderStyle(borderStyles.first);
191+
right.style = expressionToBorderStyle(borderStyles.first);
192+
}
193+
}
194+
if (borderColors != null && borderColors.isNotEmpty) {
195+
top.color = expressionToColor(borderColors.first);
196+
if (borderColors.length == 4) {
197+
right.color = expressionToColor(borderColors[1]);
198+
bottom.color = expressionToColor(borderColors[2]);
199+
left.color = expressionToColor(borderColors.last);
200+
}
201+
if (borderColors.length == 3) {
202+
left.color = expressionToColor(borderColors[1]);
203+
right.color = expressionToColor(borderColors[1]);
204+
bottom.color = expressionToColor(borderColors.last);
205+
}
206+
if (borderColors.length == 2) {
207+
bottom.color = expressionToColor(borderColors.first);
208+
left.color = expressionToColor(borderColors.last);
209+
right.color = expressionToColor(borderColors.last);
210+
}
211+
if (borderColors.length == 1) {
212+
bottom.color = expressionToColor(borderColors.first);
213+
left.color = expressionToColor(borderColors.first);
214+
right.color = expressionToColor(borderColors.first);
215+
}
216+
}
217+
return Border(
218+
top: BorderSide(width: top.width, color: top.color ?? Colors.black, style: top.style),
219+
right: BorderSide(width: right.width, color: right.color ?? Colors.black, style: right.style),
220+
bottom: BorderSide(width: bottom.width, color: bottom.color ?? Colors.black, style: bottom.style),
221+
left: BorderSide(width: left.width, color: left.color ?? Colors.black, style: left.style)
222+
);
223+
}
224+
225+
static double expressionToBorderWidth(css.Expression? value) {
226+
if (value is css.NumberTerm) {
227+
return double.tryParse(value.text) ?? 1.0;
228+
} else if (value is css.PercentageTerm) {
229+
return (double.tryParse(value.text) ?? 400) / 100;
230+
} else if (value is css.EmTerm) {
231+
return double.tryParse(value.text) ?? 1.0;
232+
} else if (value is css.RemTerm) {
233+
return double.tryParse(value.text) ?? 1.0;
234+
} else if (value is css.LengthTerm) {
235+
return double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')) ?? 1.0;
236+
} else if (value is css.LiteralTerm) {
237+
switch (value.text) {
238+
case "thin":
239+
return 2.0;
240+
case "medium":
241+
return 4.0;
242+
case "thick":
243+
return 6.0;
244+
}
245+
}
246+
return 4.0;
247+
}
248+
249+
static BorderStyle expressionToBorderStyle(css.LiteralTerm? value) {
250+
if (value != null && value.text != "none" && value.text != "hidden") {
251+
return BorderStyle.solid;
252+
}
253+
return BorderStyle.none;
254+
}
255+
256+
static Color? expressionToColor(css.Expression? value) {
257+
if (value != null) {
258+
if (value is css.HexColorTerm) {
259+
return stringToColor(value.text);
260+
} else if (value is css.FunctionTerm) {
261+
if (value.text == 'rgba' || value.text == 'rgb') {
262+
return rgbOrRgbaToColor(value.span!.text);
263+
} else if (value.text == 'hsla' || value.text == 'hsl') {
264+
return hslToRgbToColor(value.span!.text);
265+
}
266+
} else if (value is css.LiteralTerm) {
267+
return namedColorToColor(value.text);
123268
}
124269
}
125270
return null;
@@ -359,21 +504,21 @@ class ExpressionMapping {
359504
css.LiteralTerm? exp4 = list.length > 3 ? list[3] as css.LiteralTerm? : null;
360505
RegExp nonNumberRegex = RegExp(r'\s+(\d+\.\d+)\s+');
361506
if (exp is css.LiteralTerm && exp2 is css.LiteralTerm) {
362-
if (exp3 != null && (exp3 is css.HexColorTerm || exp3 is css.FunctionTerm)) {
507+
if (exp3 != null && ExpressionMapping.expressionToColor(exp3) != null) {
363508
shadow.add(Shadow(
364-
color: expressionToColor(exp3)!,
509+
color: expressionToColor(exp3)!,
365510
offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, ''))!, double.tryParse(exp2.text.replaceAll(nonNumberRegex, ''))!)
366511
));
367512
} else if (exp3 != null && exp3 is css.LiteralTerm) {
368-
if (exp4 != null && (exp4 is css.HexColorTerm || exp4 is css.FunctionTerm)) {
513+
if (exp4 != null && ExpressionMapping.expressionToColor(exp4) != null) {
369514
shadow.add(Shadow(
370-
color: expressionToColor(exp4)!,
371-
offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, ''))!, double.tryParse(exp2.text.replaceAll(nonNumberRegex, ''))!),
515+
color: expressionToColor(exp4)!,
516+
offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, ''))!, double.tryParse(exp2.text.replaceAll(nonNumberRegex, ''))!),
372517
blurRadius: double.tryParse(exp3.text.replaceAll(nonNumberRegex, ''))!
373518
));
374519
} else {
375520
shadow.add(Shadow(
376-
offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, ''))!, double.tryParse(exp2.text.replaceAll(nonNumberRegex, ''))!),
521+
offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, ''))!, double.tryParse(exp2.text.replaceAll(nonNumberRegex, ''))!),
377522
blurRadius: double.tryParse(exp3.text.replaceAll(nonNumberRegex, ''))!
378523
));
379524
}
@@ -427,4 +572,33 @@ class ExpressionMapping {
427572
return null;
428573
}
429574
}
575+
576+
static Color hslToRgbToColor(String text) {
577+
final hslText = text.replaceAll(')', '').replaceAll(' ', '');
578+
final hslValues = hslText.split(',').toList();
579+
List<double?> parsedHsl = [];
580+
hslValues.forEach((element) {
581+
if (element.contains("%") && double.tryParse(element.replaceAll("%", "")) != null) {
582+
parsedHsl.add(double.tryParse(element.replaceAll("%", ""))! * 0.01);
583+
} else {
584+
if (element != hslValues.first && (double.tryParse(element) == null || double.tryParse(element)! > 1)) {
585+
parsedHsl.add(null);
586+
} else {
587+
parsedHsl.add(double.tryParse(element));
588+
}
589+
}
590+
});
591+
if (parsedHsl.length == 4 && !parsedHsl.contains(null)) {
592+
return HSLColor.fromAHSL(parsedHsl.last!, parsedHsl.first!, parsedHsl[1]!, parsedHsl[2]!).toColor();
593+
} else if (parsedHsl.length == 3 && !parsedHsl.contains(null)) {
594+
return HSLColor.fromAHSL(1.0, parsedHsl.first!, parsedHsl[1]!, parsedHsl.last!).toColor();
595+
} else return Colors.black;
596+
}
597+
598+
static Color? namedColorToColor(String text) {
599+
String namedColor = namedColors.keys.firstWhere((element) => element.toLowerCase() == text.toLowerCase(), orElse: () => "");
600+
if (namedColor != "") {
601+
return stringToColor(namedColors[namedColor]!);
602+
} else return null;
603+
}
430604
}

lib/src/utils.dart

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
1+
import 'dart:convert';
2+
import 'dart:math';
3+
14
import 'package:flutter/gestures.dart';
5+
import 'package:flutter/material.dart';
26

3-
import 'dart:math';
4-
import 'dart:convert';
7+
Map<String, String> namedColors = {
8+
"White": "#FFFFFF",
9+
"Silver": "#C0C0C0",
10+
"Gray": "#808080",
11+
"Black": "#000000",
12+
"Red": "#FF0000",
13+
"Maroon": "#800000",
14+
"Yellow": "#FFFF00",
15+
"Olive": "#808000",
16+
"Lime": "#00FF00",
17+
"Green": "#008000",
18+
"Aqua": "#00FFFF",
19+
"Teal": "#008080",
20+
"Blue": "#0000FF",
21+
"Navy": "#000080",
22+
"Fuchsia": "#FF00FF",
23+
"Purple": "#800080",
24+
};
525

626
class Context<T> {
727
T data;
@@ -47,6 +67,18 @@ class MultipleTapGestureRecognizer extends TapGestureRecognizer {
4767
}
4868
}
4969

70+
class CustomBorderSide {
71+
CustomBorderSide({
72+
this.color = const Color(0xFF000000),
73+
this.width = 1.0,
74+
this.style = BorderStyle.none,
75+
}) : assert(width >= 0.0);
76+
77+
Color? color;
78+
double width;
79+
BorderStyle style;
80+
}
81+
5082
String getRandString(int len) {
5183
var random = Random.secure();
5284
var values = List<int>.generate(len, (i) => random.nextInt(255));

0 commit comments

Comments
 (0)