Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,13 @@ class _MyHomePageState extends State<MyHomePage> {
onImageError: (exception, stackTrace) {
print(exception);
},
onCssParseError: (css, messages) {
print("css that errored: $css");
print("error messages:");
messages.forEach((element) {
print(element);
});
},
),
),
);
Expand Down
6 changes: 6 additions & 0 deletions lib/flutter_html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class Html extends StatelessWidget {
this.onLinkTap,
this.customRender = const {},
this.customImageRenders = const {},
this.onCssParseError,
this.onImageError,
this.onMathError,
this.shrinkWrap = false,
Expand All @@ -71,6 +72,7 @@ class Html extends StatelessWidget {
this.onLinkTap,
this.customRender = const {},
this.customImageRenders = const {},
this.onCssParseError,
this.onImageError,
this.onMathError,
this.shrinkWrap = false,
Expand Down Expand Up @@ -99,6 +101,9 @@ class Html extends StatelessWidget {
/// See the README for more details.
final Map<ImageSourceMatcher, ImageRender> customImageRenders;

/// A function that defines what to do when CSS fails to parse
final OnCssParseError? onCssParseError;

/// A function that defines what to do when an image errors
final ImageErrorListener? onImageError;

Expand Down Expand Up @@ -148,6 +153,7 @@ class Html extends StatelessWidget {
htmlData: doc,
onLinkTap: onLinkTap,
onImageTap: onImageTap,
onCssParseError: onCssParseError,
onImageError: onImageError,
onMathError: onMathError,
shrinkWrap: shrinkWrap,
Expand Down
65 changes: 52 additions & 13 deletions lib/html_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ typedef OnMathError = Widget Function(
String exception,
String exceptionWithType,
);
typedef OnCssParseError = String? Function(
String css,
List<cssparser.Message> errors,
);
typedef CustomRender = dynamic Function(
RenderContext context,
Widget parsedChild,
Expand All @@ -38,6 +42,7 @@ class HtmlParser extends StatelessWidget {
final dom.Document htmlData;
final OnTap? onLinkTap;
final OnTap? onImageTap;
final OnCssParseError? onCssParseError;
final ImageErrorListener? onImageError;
final OnMathError? onMathError;
final bool shrinkWrap;
Expand All @@ -54,6 +59,7 @@ class HtmlParser extends StatelessWidget {
required this.htmlData,
required this.onLinkTap,
required this.onImageTap,
required this.onCssParseError,
required this.onImageError,
required this.onMathError,
required this.shrinkWrap,
Expand All @@ -66,15 +72,20 @@ class HtmlParser extends StatelessWidget {

@override
Widget build(BuildContext context) {
Map<String, Map<String, List<css.Expression>>> declarations = _getExternalCssDeclarations(htmlData.getElementsByTagName("style"), onCssParseError);
StyledElement lexedTree = lexDomTree(
htmlData,
customRender.keys.toList(),
tagsList,
navigationDelegateForIframe,
);
StyledElement inlineStyledTree = applyInlineStyles(lexedTree);
StyledElement customStyledTree = _applyCustomStyles(inlineStyledTree);
StyledElement cascadedStyledTree = _cascadeStyles(customStyledTree);
StyledElement? externalCssStyledTree;
if (declarations.isNotEmpty) {
externalCssStyledTree = _applyExternalCss(declarations, lexedTree);
}
StyledElement inlineStyledTree = _applyInlineStyles(externalCssStyledTree ?? lexedTree, onCssParseError);
StyledElement customStyledTree = _applyCustomStyles(style, inlineStyledTree);
StyledElement cascadedStyledTree = _cascadeStyles(style, customStyledTree);
StyledElement cleanedTree = cleanTree(cascadedStyledTree);
InlineSpan parsedTree = parseTree(
RenderContext(
Expand Down Expand Up @@ -108,8 +119,8 @@ class HtmlParser extends StatelessWidget {
return htmlparser.parse(data);
}

/// [parseCSS] converts a string of CSS to a CSS stylesheet using the dart `csslib` library.
static css.StyleSheet parseCSS(String data) {
/// [parseCss] converts a string of CSS to a CSS stylesheet using the dart `csslib` library.
static css.StyleSheet parseCss(String data) {
return cssparser.parse(data);
}

Expand Down Expand Up @@ -189,34 +200,62 @@ class HtmlParser extends StatelessWidget {
}
}

static StyledElement applyInlineStyles(StyledElement tree) {
static Map<String, Map<String, List<css.Expression>>> _getExternalCssDeclarations(List<dom.Element> styles, OnCssParseError? errorHandler) {
String fullCss = "";
for (final e in styles) {
fullCss = fullCss + e.innerHtml;
}
if (fullCss.isNotEmpty) {
final declarations = parseExternalCss(fullCss, errorHandler);
return declarations;
} else {
return {};
}
}

static StyledElement _applyExternalCss(Map<String, Map<String, List<css.Expression>>> declarations, StyledElement tree) {
declarations.forEach((key, style) {
if (tree.matchesSelector(key)) {
tree.style = tree.style.merge(declarationsToStyle(style));
}
});

tree.children.forEach((e) => _applyExternalCss(declarations, e));

return tree;
}

static StyledElement _applyInlineStyles(StyledElement tree, OnCssParseError? errorHandler) {
if (tree.attributes.containsKey("style")) {
tree.style = tree.style.merge(inlineCSSToStyle(tree.attributes['style']));
final newStyle = inlineCssToStyle(tree.attributes['style'], errorHandler);
if (newStyle != null) {
tree.style = tree.style.merge(newStyle);
}
}

tree.children.forEach(applyInlineStyles);
tree.children.forEach((e) => _applyInlineStyles(e, errorHandler));
return tree;
}

/// [applyCustomStyles] applies the [Style] objects passed into the [Html]
/// widget onto the [StyledElement] tree, no cascading of styles is done at this point.
StyledElement _applyCustomStyles(StyledElement tree) {
static StyledElement _applyCustomStyles(Map<String, Style> style, StyledElement tree) {
style.forEach((key, style) {
if (tree.matchesSelector(key)) {
tree.style = tree.style.merge(style);
}
});
tree.children.forEach(_applyCustomStyles);
tree.children.forEach((e) => _applyCustomStyles(style, e));

return tree;
}

/// [_cascadeStyles] cascades all of the inherited styles down the tree, applying them to each
/// child that doesn't specify a different style.
StyledElement _cascadeStyles(StyledElement tree) {
static StyledElement _cascadeStyles(Map<String, Style> style, StyledElement tree) {
tree.children.forEach((child) {
child.style = tree.style.copyOnlyInherited(child.style);
_cascadeStyles(child);
_cascadeStyles(style, child);
});

return tree;
Expand Down Expand Up @@ -704,7 +743,7 @@ class HtmlParser extends StatelessWidget {
tree.children.forEach((child) {
if (child is EmptyContentElement || child is EmptyLayoutElement) {
toRemove.add(child);
} else if (child is TextContentElement && (child.text!.isEmpty)) {
} else if (child is TextContentElement && (child.text!.trim().isEmpty)) {
toRemove.add(child);
} else if (child is TextContentElement &&
child.style.whiteSpace != WhiteSpace.PRE &&
Expand Down
68 changes: 54 additions & 14 deletions lib/src/css_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import 'package:csslib/visitor.dart' as css;
import 'package:csslib/parser.dart' as cssparser;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_html/src/utils.dart';
import 'package:flutter_html/style.dart';

Style declarationsToStyle(Map<String?, List<css.Expression>> declarations) {
Style declarationsToStyle(Map<String, List<css.Expression>> declarations) {
Style style = new Style();
declarations.forEach((property, value) {
if (value.isNotEmpty) {
Expand Down Expand Up @@ -102,34 +103,73 @@ Style declarationsToStyle(Map<String?, List<css.Expression>> declarations) {
return style;
}

Style inlineCSSToStyle(String? inlineStyle) {
final sheet = cssparser.parse("*{$inlineStyle}");
final declarations = DeclarationVisitor().getDeclarations(sheet)!;
return declarationsToStyle(declarations);
Style? inlineCssToStyle(String? inlineStyle, OnCssParseError? errorHandler) {
var errors = <cssparser.Message>[];
final sheet = cssparser.parse("*{$inlineStyle}", errors: errors);
if (errors.isEmpty) {
final declarations = DeclarationVisitor().getDeclarations(sheet);
return declarationsToStyle(declarations["*"]!);
} else if (errorHandler != null) {
String? newCss = errorHandler.call(inlineStyle ?? "", errors);
if (newCss != null) {
return inlineCssToStyle(newCss, errorHandler);
}
}
return null;
}

Map<String, Map<String, List<css.Expression>>> parseExternalCss(String css, OnCssParseError? errorHandler) {
var errors = <cssparser.Message>[];
final sheet = cssparser.parse(css, errors: errors);
if (errors.isEmpty) {
return DeclarationVisitor().getDeclarations(sheet);
} else if (errorHandler != null) {
String? newCss = errorHandler.call(css, errors);
if (newCss != null) {
return parseExternalCss(newCss, errorHandler);
}
}
return {};
}

class DeclarationVisitor extends css.Visitor {
Map<String?, List<css.Expression>>? _result;
String? _currentProperty;
Map<String, Map<String, List<css.Expression>>> _result = {};
Map<String, List<css.Expression>> _properties = {};
late String _selector;
late String _currentProperty;

Map<String?, List<css.Expression>>? getDeclarations(css.StyleSheet sheet) {
_result = new Map<String?, List<css.Expression>>();
sheet.visit(this);
Map<String, Map<String, List<css.Expression>>> getDeclarations(css.StyleSheet sheet) {
sheet.topLevels.forEach((element) {
if (element.span != null) {
_selector = element.span!.text;
element.visit(this);
if (_result[_selector] != null) {
_properties.forEach((key, value) {
if (_result[_selector]![key] != null) {
_result[_selector]![key]!.addAll(new List<css.Expression>.from(value));
} else {
_result[_selector]![key] = new List<css.Expression>.from(value);
}
});
} else {
_result[_selector] = new Map<String, List<css.Expression>>.from(_properties);
}
_properties.clear();
}
});
return _result;
}

@override
void visitDeclaration(css.Declaration node) {
_currentProperty = node.property;
_result![_currentProperty] = <css.Expression>[];
_properties[_currentProperty] = <css.Expression>[];
node.expression!.visit(this);
}

@override
void visitExpressions(css.Expressions node) {
node.expressions.forEach((expression) {
_result![_currentProperty]!.add(expression);
});
_properties[_currentProperty]!.addAll(node.expressions);
}
}

Expand Down
1 change: 0 additions & 1 deletion lib/src/html_elements.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ const INTERACTABLE_ELEMENTS = [
const REPLACED_ELEMENTS = [
"audio",
"br",
"head",
"iframe",
"img",
"svg",
Expand Down
12 changes: 11 additions & 1 deletion lib/style.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_html/src/css_parser.dart';

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

static Map<String, Style> fromCss(String css, OnCssParseError? onCssParseError) {
final declarations = parseExternalCss(css, onCssParseError);
Map<String, Style> styleMap = {};
declarations.forEach((key, value) {
styleMap[key] = declarationsToStyle(value);
});
return styleMap;
}

TextStyle generateTextStyle() {
return TextStyle(
backgroundColor: backgroundColor,
Expand Down Expand Up @@ -304,7 +315,6 @@ class Style {
//TODO merge border
alignment: other.alignment,
markerContent: other.markerContent,

maxLines: other.maxLines,
textOverflow: other.textOverflow,
);
Expand Down