Skip to content

Commit 5de96d7

Browse files
authored
Merge pull request Sub6Resources#655 from tneotia/feature/style-tag-support
Add support for the style tag
2 parents 758867f + 6a9fb1a commit 5de96d7

File tree

6 files changed

+130
-29
lines changed

6 files changed

+130
-29
lines changed

example/lib/main.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,13 @@ class _MyHomePageState extends State<MyHomePage> {
315315
onImageError: (exception, stackTrace) {
316316
print(exception);
317317
},
318+
onCssParseError: (css, messages) {
319+
print("css that errored: $css");
320+
print("error messages:");
321+
messages.forEach((element) {
322+
print(element);
323+
});
324+
},
318325
),
319326
),
320327
);

lib/flutter_html.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class Html extends StatelessWidget {
5353
this.onLinkTap,
5454
this.customRender = const {},
5555
this.customImageRenders = const {},
56+
this.onCssParseError,
5657
this.onImageError,
5758
this.onMathError,
5859
this.shrinkWrap = false,
@@ -71,6 +72,7 @@ class Html extends StatelessWidget {
7172
this.onLinkTap,
7273
this.customRender = const {},
7374
this.customImageRenders = const {},
75+
this.onCssParseError,
7476
this.onImageError,
7577
this.onMathError,
7678
this.shrinkWrap = false,
@@ -99,6 +101,9 @@ class Html extends StatelessWidget {
99101
/// See the README for more details.
100102
final Map<ImageSourceMatcher, ImageRender> customImageRenders;
101103

104+
/// A function that defines what to do when CSS fails to parse
105+
final OnCssParseError? onCssParseError;
106+
102107
/// A function that defines what to do when an image errors
103108
final ImageErrorListener? onImageError;
104109

@@ -148,6 +153,7 @@ class Html extends StatelessWidget {
148153
htmlData: doc,
149154
onLinkTap: onLinkTap,
150155
onImageTap: onImageTap,
156+
onCssParseError: onCssParseError,
151157
onImageError: onImageError,
152158
onMathError: onMathError,
153159
shrinkWrap: shrinkWrap,

lib/html_parser.dart

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ typedef OnMathError = Widget Function(
2828
String exception,
2929
String exceptionWithType,
3030
);
31+
typedef OnCssParseError = String? Function(
32+
String css,
33+
List<cssparser.Message> errors,
34+
);
3135
typedef CustomRender = dynamic Function(
3236
RenderContext context,
3337
Widget parsedChild,
@@ -38,6 +42,7 @@ class HtmlParser extends StatelessWidget {
3842
final dom.Document htmlData;
3943
final OnTap? onLinkTap;
4044
final OnTap? onImageTap;
45+
final OnCssParseError? onCssParseError;
4146
final ImageErrorListener? onImageError;
4247
final OnMathError? onMathError;
4348
final bool shrinkWrap;
@@ -54,6 +59,7 @@ class HtmlParser extends StatelessWidget {
5459
required this.htmlData,
5560
required this.onLinkTap,
5661
required this.onImageTap,
62+
required this.onCssParseError,
5763
required this.onImageError,
5864
required this.onMathError,
5965
required this.shrinkWrap,
@@ -66,15 +72,20 @@ class HtmlParser extends StatelessWidget {
6672

6773
@override
6874
Widget build(BuildContext context) {
75+
Map<String, Map<String, List<css.Expression>>> declarations = _getExternalCssDeclarations(htmlData.getElementsByTagName("style"), onCssParseError);
6976
StyledElement lexedTree = lexDomTree(
7077
htmlData,
7178
customRender.keys.toList(),
7279
tagsList,
7380
navigationDelegateForIframe,
7481
);
75-
StyledElement inlineStyledTree = applyInlineStyles(lexedTree);
76-
StyledElement customStyledTree = _applyCustomStyles(inlineStyledTree);
77-
StyledElement cascadedStyledTree = _cascadeStyles(customStyledTree);
82+
StyledElement? externalCssStyledTree;
83+
if (declarations.isNotEmpty) {
84+
externalCssStyledTree = _applyExternalCss(declarations, lexedTree);
85+
}
86+
StyledElement inlineStyledTree = _applyInlineStyles(externalCssStyledTree ?? lexedTree, onCssParseError);
87+
StyledElement customStyledTree = _applyCustomStyles(style, inlineStyledTree);
88+
StyledElement cascadedStyledTree = _cascadeStyles(style, customStyledTree);
7889
StyledElement cleanedTree = cleanTree(cascadedStyledTree);
7990
InlineSpan parsedTree = parseTree(
8091
RenderContext(
@@ -108,8 +119,8 @@ class HtmlParser extends StatelessWidget {
108119
return htmlparser.parse(data);
109120
}
110121

111-
/// [parseCSS] converts a string of CSS to a CSS stylesheet using the dart `csslib` library.
112-
static css.StyleSheet parseCSS(String data) {
122+
/// [parseCss] converts a string of CSS to a CSS stylesheet using the dart `csslib` library.
123+
static css.StyleSheet parseCss(String data) {
113124
return cssparser.parse(data);
114125
}
115126

@@ -189,34 +200,62 @@ class HtmlParser extends StatelessWidget {
189200
}
190201
}
191202

192-
static StyledElement applyInlineStyles(StyledElement tree) {
203+
static Map<String, Map<String, List<css.Expression>>> _getExternalCssDeclarations(List<dom.Element> styles, OnCssParseError? errorHandler) {
204+
String fullCss = "";
205+
for (final e in styles) {
206+
fullCss = fullCss + e.innerHtml;
207+
}
208+
if (fullCss.isNotEmpty) {
209+
final declarations = parseExternalCss(fullCss, errorHandler);
210+
return declarations;
211+
} else {
212+
return {};
213+
}
214+
}
215+
216+
static StyledElement _applyExternalCss(Map<String, Map<String, List<css.Expression>>> declarations, StyledElement tree) {
217+
declarations.forEach((key, style) {
218+
if (tree.matchesSelector(key)) {
219+
tree.style = tree.style.merge(declarationsToStyle(style));
220+
}
221+
});
222+
223+
tree.children.forEach((e) => _applyExternalCss(declarations, e));
224+
225+
return tree;
226+
}
227+
228+
static StyledElement _applyInlineStyles(StyledElement tree, OnCssParseError? errorHandler) {
193229
if (tree.attributes.containsKey("style")) {
194-
tree.style = tree.style.merge(inlineCSSToStyle(tree.attributes['style']));
230+
final newStyle = inlineCssToStyle(tree.attributes['style'], errorHandler);
231+
if (newStyle != null) {
232+
tree.style = tree.style.merge(newStyle);
233+
}
195234
}
196235

197-
tree.children.forEach(applyInlineStyles);
236+
tree.children.forEach((e) => _applyInlineStyles(e, errorHandler));
198237
return tree;
199238
}
200239

201240
/// [applyCustomStyles] applies the [Style] objects passed into the [Html]
202241
/// widget onto the [StyledElement] tree, no cascading of styles is done at this point.
203-
StyledElement _applyCustomStyles(StyledElement tree) {
242+
static StyledElement _applyCustomStyles(Map<String, Style> style, StyledElement tree) {
204243
style.forEach((key, style) {
205244
if (tree.matchesSelector(key)) {
206245
tree.style = tree.style.merge(style);
207246
}
208247
});
209-
tree.children.forEach(_applyCustomStyles);
248+
tree.children.forEach((e) => _applyCustomStyles(style, e));
210249

211250
return tree;
212251
}
213252

214253
/// [_cascadeStyles] cascades all of the inherited styles down the tree, applying them to each
215254
/// child that doesn't specify a different style.
216-
StyledElement _cascadeStyles(StyledElement tree) {
255+
static StyledElement _cascadeStyles(Map<String, Style> style, StyledElement tree) {
217256
tree.children.forEach((child) {
218257
child.style = tree.style.copyOnlyInherited(child.style);
219-
_cascadeStyles(child);
258+
_cascadeStyles(style, child);
220259
});
221260

222261
return tree;
@@ -704,7 +743,7 @@ class HtmlParser extends StatelessWidget {
704743
tree.children.forEach((child) {
705744
if (child is EmptyContentElement || child is EmptyLayoutElement) {
706745
toRemove.add(child);
707-
} else if (child is TextContentElement && (child.text!.isEmpty)) {
746+
} else if (child is TextContentElement && (child.text!.trim().isEmpty)) {
708747
toRemove.add(child);
709748
} else if (child is TextContentElement &&
710749
child.style.whiteSpace != WhiteSpace.PRE &&

lib/src/css_parser.dart

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import 'package:csslib/visitor.dart' as css;
55
import 'package:csslib/parser.dart' as cssparser;
66
import 'package:flutter/cupertino.dart';
77
import 'package:flutter/material.dart';
8+
import 'package:flutter_html/flutter_html.dart';
89
import 'package:flutter_html/src/utils.dart';
910
import 'package:flutter_html/style.dart';
1011

11-
Style declarationsToStyle(Map<String?, List<css.Expression>> declarations) {
12+
Style declarationsToStyle(Map<String, List<css.Expression>> declarations) {
1213
Style style = new Style();
1314
declarations.forEach((property, value) {
1415
if (value.isNotEmpty) {
@@ -298,34 +299,73 @@ Style declarationsToStyle(Map<String?, List<css.Expression>> declarations) {
298299
return style;
299300
}
300301

301-
Style inlineCSSToStyle(String? inlineStyle) {
302-
final sheet = cssparser.parse("*{$inlineStyle}");
303-
final declarations = DeclarationVisitor().getDeclarations(sheet)!;
304-
return declarationsToStyle(declarations);
302+
Style? inlineCssToStyle(String? inlineStyle, OnCssParseError? errorHandler) {
303+
var errors = <cssparser.Message>[];
304+
final sheet = cssparser.parse("*{$inlineStyle}", errors: errors);
305+
if (errors.isEmpty) {
306+
final declarations = DeclarationVisitor().getDeclarations(sheet);
307+
return declarationsToStyle(declarations["*"]!);
308+
} else if (errorHandler != null) {
309+
String? newCss = errorHandler.call(inlineStyle ?? "", errors);
310+
if (newCss != null) {
311+
return inlineCssToStyle(newCss, errorHandler);
312+
}
313+
}
314+
return null;
315+
}
316+
317+
Map<String, Map<String, List<css.Expression>>> parseExternalCss(String css, OnCssParseError? errorHandler) {
318+
var errors = <cssparser.Message>[];
319+
final sheet = cssparser.parse(css, errors: errors);
320+
if (errors.isEmpty) {
321+
return DeclarationVisitor().getDeclarations(sheet);
322+
} else if (errorHandler != null) {
323+
String? newCss = errorHandler.call(css, errors);
324+
if (newCss != null) {
325+
return parseExternalCss(newCss, errorHandler);
326+
}
327+
}
328+
return {};
305329
}
306330

307331
class DeclarationVisitor extends css.Visitor {
308-
Map<String?, List<css.Expression>>? _result;
309-
String? _currentProperty;
332+
Map<String, Map<String, List<css.Expression>>> _result = {};
333+
Map<String, List<css.Expression>> _properties = {};
334+
late String _selector;
335+
late String _currentProperty;
310336

311-
Map<String?, List<css.Expression>>? getDeclarations(css.StyleSheet sheet) {
312-
_result = new Map<String?, List<css.Expression>>();
313-
sheet.visit(this);
337+
Map<String, Map<String, List<css.Expression>>> getDeclarations(css.StyleSheet sheet) {
338+
sheet.topLevels.forEach((element) {
339+
if (element.span != null) {
340+
_selector = element.span!.text;
341+
element.visit(this);
342+
if (_result[_selector] != null) {
343+
_properties.forEach((key, value) {
344+
if (_result[_selector]![key] != null) {
345+
_result[_selector]![key]!.addAll(new List<css.Expression>.from(value));
346+
} else {
347+
_result[_selector]![key] = new List<css.Expression>.from(value);
348+
}
349+
});
350+
} else {
351+
_result[_selector] = new Map<String, List<css.Expression>>.from(_properties);
352+
}
353+
_properties.clear();
354+
}
355+
});
314356
return _result;
315357
}
316358

317359
@override
318360
void visitDeclaration(css.Declaration node) {
319361
_currentProperty = node.property;
320-
_result![_currentProperty] = <css.Expression>[];
362+
_properties[_currentProperty] = <css.Expression>[];
321363
node.expression!.visit(this);
322364
}
323365

324366
@override
325367
void visitExpressions(css.Expressions node) {
326-
node.expressions.forEach((expression) {
327-
_result![_currentProperty]!.add(expression);
328-
});
368+
_properties[_currentProperty]!.addAll(node.expressions);
329369
}
330370
}
331371

lib/src/html_elements.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ const INTERACTABLE_ELEMENTS = [
7777
const REPLACED_ELEMENTS = [
7878
"audio",
7979
"br",
80-
"head",
8180
"iframe",
8281
"img",
8382
"svg",

lib/style.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'dart:ui';
22

33
import 'package:flutter/material.dart';
4+
import 'package:flutter_html/flutter_html.dart';
5+
import 'package:flutter_html/src/css_parser.dart';
46

57
///This class represents all the available CSS attributes
68
///for this package.
@@ -240,6 +242,15 @@ class Style {
240242
'body': Style.fromTextStyle(theme.textTheme.bodyText2!),
241243
};
242244

245+
static Map<String, Style> fromCss(String css, OnCssParseError? onCssParseError) {
246+
final declarations = parseExternalCss(css, onCssParseError);
247+
Map<String, Style> styleMap = {};
248+
declarations.forEach((key, value) {
249+
styleMap[key] = declarationsToStyle(value);
250+
});
251+
return styleMap;
252+
}
253+
243254
TextStyle generateTextStyle() {
244255
return TextStyle(
245256
backgroundColor: backgroundColor,
@@ -304,7 +315,6 @@ class Style {
304315
//TODO merge border
305316
alignment: other.alignment,
306317
markerContent: other.markerContent,
307-
308318
maxLines: other.maxLines,
309319
textOverflow: other.textOverflow,
310320
);

0 commit comments

Comments
 (0)