Skip to content

Commit 53d8c17

Browse files
committed
Merge branch 'master' of https://github.com/Sub6Resources/flutter_html into feature/modularization
� Conflicts: � lib/flutter_html.dart � lib/html_parser.dart � lib/src/html_elements.dart
2 parents 0a201ce + 5de96d7 commit 53d8c17

File tree

8 files changed

+223
-28
lines changed

8 files changed

+223
-28
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
name: Bug report
3+
about: Something isn't working as intended
4+
title: "[BUG]"
5+
labels: bug
6+
assignees: ''
7+
8+
---
9+
10+
<!---
11+
12+
Please do not delete this issue template as it helps us organize and easily work on issues!!
13+
14+
NOTE: Before posting, please make sure you have
15+
1. Searched the README
16+
2. Searched the Issues tab for similar bugs
17+
3. Please provide the required information in the template - HTML code and Html widget configuration
18+
--->
19+
20+
**Describe the bug:**
21+
<!--- Please provide a clear and concise description of the bug --->
22+
23+
**HTML to reproduce the issue:**
24+
<!--- Please provide your HTML code below. If it contains sensitive information please post a minimal reproducible HTML snippet. --->
25+
26+
**`Html` widget configuration:**
27+
<!--- Please provide your HTML widget configuration below --->
28+
29+
**Expected behavior:**
30+
<!--- Expected behavior, if applicable, otherwise please delete --->
31+
32+
**Screenshots:**
33+
<!--- Screenshots can be helpful to analyze your issue. Please delete this section if you don't provide any. --->
34+
35+
**Device details and Flutter/Dart/`flutter_html` versions:**
36+
<!--- These details can be helpful to analyze your issue. Please delete this section if you don't provide any. --->
37+
38+
**Stacktrace/Logcat**
39+
<!--- The error stacktrace if applicable, otherwise please delete --->
40+
41+
**Additional info:**
42+
<!--- Any other info relevant to the bug, otherwise please delete --->
43+
44+
**A picture of a cute animal (not mandatory but encouraged)**
45+
<!--- A picture of a cute animal that would nicely complement this bug report.
46+
If you don't have one, please delete, just know we will be a little disappointed ;) --->
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
name: Feature request
3+
about: Suggest an idea for flutter_html
4+
title: "[FEATURE]"
5+
labels: enhancement
6+
assignees: ''
7+
8+
---
9+
10+
<!---
11+
12+
Please do not delete this issue template as it helps us organize and easily work on issues!!
13+
14+
NOTE: Before posting, please make sure you have
15+
1. Searched the README
16+
2. Searched the Issues tab for similar feature requests
17+
--->
18+
19+
**Describe your feature request**
20+
<!--- Please provide a clear and concise description of the feature request --->
21+
22+
**Additional context**
23+
<!--- Any other info relevant to the feature request, otherwise please delete --->
24+
25+
**A picture of a cute animal (not mandatory but encouraged)**
26+
<!--- A picture of a cute animal that would nicely complement this feature request.
27+
If you don't have one, please delete, just know we will be a little disappointed ;) --->

.github/ISSUE_TEMPLATE/question.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
name: Question
3+
about: Ask a question about flutter_html
4+
title: "[QUESTION]"
5+
labels: question
6+
assignees: ''
7+
8+
---
9+
10+
<!---
11+
NOTE: Before posting, please make sure you have
12+
1. Searched the README
13+
2. Searched the Issues tab for similar questions
14+
--->
15+
16+
Type question here.
17+
18+
**A picture of a cute animal (not mandatory but encouraged)**
19+
<!--- A picture of a cute animal that would nicely complement this question.
20+
If you don't have one, please delete, just know we will be a little disappointed ;) --->

example/lib/main.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,13 @@ class _MyHomePageState extends State<MyHomePage> {
325325
onImageError: (exception, stackTrace) {
326326
print(exception);
327327
},
328+
onCssParseError: (css, messages) {
329+
print("css that errored: $css");
330+
print("error messages:");
331+
messages.forEach((element) {
332+
print(element);
333+
});
334+
},
328335
),
329336
),
330337
);

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
required this.data,
5454
this.onLinkTap,
5555
this.customRenders = const {},
56+
this.onCssParseError,
5657
this.onImageError,
5758
this.shrinkWrap = false,
5859
this.onImageTap,
@@ -68,6 +69,7 @@ class Html extends StatelessWidget {
6869
@required this.document,
6970
this.onLinkTap,
7071
this.customRenders = const {},
72+
this.onCssParseError,
7173
this.onImageError,
7274
this.shrinkWrap = false,
7375
this.onImageTap,
@@ -90,6 +92,9 @@ class Html extends StatelessWidget {
9092
/// A function that defines what to do when a link is tapped
9193
final OnTap? onLinkTap;
9294

95+
/// A function that defines what to do when CSS fails to parse
96+
final OnCssParseError? onCssParseError;
97+
9398
/// A function that defines what to do when an image errors
9499
final ImageErrorListener? onImageError;
95100

@@ -130,6 +135,7 @@ class Html extends StatelessWidget {
130135
htmlData: doc,
131136
onLinkTap: onLinkTap,
132137
onImageTap: onImageTap,
138+
onCssParseError: onCssParseError,
133139
onImageError: onImageError,
134140
shrinkWrap: shrinkWrap,
135141
style: style,

lib/html_parser.dart

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@ typedef OnTap = void Function(
2020
Map<String, String> attributes,
2121
dom.Element? element,
2222
);
23+
typedef OnCssParseError = String? Function(
24+
String css,
25+
List<cssparser.Message> errors,
26+
);
2327

2428
class HtmlParser extends StatelessWidget {
2529
final Key? key;
2630
final dom.Document htmlData;
2731
final OnTap? onLinkTap;
2832
final OnTap? onImageTap;
33+
final OnCssParseError? onCssParseError;
2934
final ImageErrorListener? onImageError;
3035
final bool shrinkWrap;
3136

@@ -39,6 +44,7 @@ class HtmlParser extends StatelessWidget {
3944
required this.htmlData,
4045
required this.onLinkTap,
4146
required this.onImageTap,
47+
required this.onCssParseError,
4248
required this.onImageError,
4349
required this.shrinkWrap,
4450
required this.style,
@@ -48,16 +54,21 @@ class HtmlParser extends StatelessWidget {
4854

4955
@override
5056
Widget build(BuildContext context) {
57+
Map<String, Map<String, List<css.Expression>>> declarations = _getExternalCssDeclarations(htmlData.getElementsByTagName("style"), onCssParseError);
5158
StyledElement lexedTree = lexDomTree(
5259
htmlData,
5360
customRenders.keys.toList(),
5461
tagsList,
5562
context,
5663
this,
5764
);
58-
StyledElement inlineStyledTree = applyInlineStyles(lexedTree);
59-
StyledElement customStyledTree = _applyCustomStyles(inlineStyledTree);
60-
StyledElement cascadedStyledTree = _cascadeStyles(customStyledTree);
65+
StyledElement? externalCssStyledTree;
66+
if (declarations.isNotEmpty) {
67+
externalCssStyledTree = _applyExternalCss(declarations, lexedTree);
68+
}
69+
StyledElement inlineStyledTree = _applyInlineStyles(externalCssStyledTree ?? lexedTree, onCssParseError);
70+
StyledElement customStyledTree = _applyCustomStyles(style, inlineStyledTree);
71+
StyledElement cascadedStyledTree = _cascadeStyles(style, customStyledTree);
6172
StyledElement cleanedTree = cleanTree(cascadedStyledTree);
6273
InlineSpan parsedTree = parseTree(
6374
RenderContext(
@@ -91,8 +102,8 @@ class HtmlParser extends StatelessWidget {
91102
return htmlparser.parse(data);
92103
}
93104

94-
/// [parseCSS] converts a string of CSS to a CSS stylesheet using the dart `csslib` library.
95-
static css.StyleSheet parseCSS(String data) {
105+
/// [parseCss] converts a string of CSS to a CSS stylesheet using the dart `csslib` library.
106+
static css.StyleSheet parseCss(String data) {
96107
return cssparser.parse(data);
97108
}
98109

@@ -187,34 +198,62 @@ class HtmlParser extends StatelessWidget {
187198
}
188199
}
189200

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

195-
tree.children.forEach(applyInlineStyles);
234+
tree.children.forEach((e) => _applyInlineStyles(e, errorHandler));
196235
return tree;
197236
}
198237

199238
/// [applyCustomStyles] applies the [Style] objects passed into the [Html]
200239
/// widget onto the [StyledElement] tree, no cascading of styles is done at this point.
201-
StyledElement _applyCustomStyles(StyledElement tree) {
240+
static StyledElement _applyCustomStyles(Map<String, Style> style, StyledElement tree) {
202241
style.forEach((key, style) {
203242
if (tree.matchesSelector(key)) {
204243
tree.style = tree.style.merge(style);
205244
}
206245
});
207-
tree.children.forEach(_applyCustomStyles);
246+
tree.children.forEach((e) => _applyCustomStyles(style, e));
208247

209248
return tree;
210249
}
211250

212251
/// [_cascadeStyles] cascades all of the inherited styles down the tree, applying them to each
213252
/// child that doesn't specify a different style.
214-
StyledElement _cascadeStyles(StyledElement tree) {
253+
static StyledElement _cascadeStyles(Map<String, Style> style, StyledElement tree) {
215254
tree.children.forEach((child) {
216255
child.style = tree.style.copyOnlyInherited(child.style);
217-
_cascadeStyles(child);
256+
_cascadeStyles(style, child);
218257
});
219258

220259
return tree;
@@ -526,7 +565,7 @@ class HtmlParser extends StatelessWidget {
526565
tree.children.forEach((child) {
527566
if (child is EmptyContentElement || child is EmptyLayoutElement) {
528567
toRemove.add(child);
529-
} else if (child is TextContentElement && (child.text!.isEmpty)) {
568+
} else if (child is TextContentElement && (child.text!.trim().isEmpty)) {
530569
toRemove.add(child);
531570
} else if (child is TextContentElement &&
532571
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

0 commit comments

Comments
 (0)