diff --git a/example/lib/main.dart b/example/lib/main.dart index 47fafc19f2..57763ce562 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -303,6 +303,13 @@ class _MyHomePageState extends State { onImageError: (exception, stackTrace) { print(exception); }, + onCssParseError: (css, messages) { + print("css that errored: $css"); + print("error messages:"); + messages.forEach((element) { + print(element); + }); + }, ), ), ); diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart index 2397fc0410..893b6d2bc3 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -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, @@ -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, @@ -99,6 +101,9 @@ class Html extends StatelessWidget { /// See the README for more details. final Map 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; @@ -148,6 +153,7 @@ class Html extends StatelessWidget { htmlData: doc, onLinkTap: onLinkTap, onImageTap: onImageTap, + onCssParseError: onCssParseError, onImageError: onImageError, onMathError: onMathError, shrinkWrap: shrinkWrap, diff --git a/lib/html_parser.dart b/lib/html_parser.dart index a3ed4cc88f..c984be592d 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -28,6 +28,10 @@ typedef OnMathError = Widget Function( String exception, String exceptionWithType, ); +typedef OnCssParseError = String? Function( + String css, + List errors, +); typedef CustomRender = dynamic Function( RenderContext context, Widget parsedChild, @@ -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; @@ -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, @@ -66,15 +72,20 @@ class HtmlParser extends StatelessWidget { @override Widget build(BuildContext context) { + Map>> 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( @@ -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); } @@ -189,34 +200,62 @@ class HtmlParser extends StatelessWidget { } } - static StyledElement applyInlineStyles(StyledElement tree) { + static Map>> _getExternalCssDeclarations(List 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>> 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 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 style, StyledElement tree) { tree.children.forEach((child) { child.style = tree.style.copyOnlyInherited(child.style); - _cascadeStyles(child); + _cascadeStyles(style, child); }); return tree; @@ -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 && diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index d731edfe61..d3c217862e 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -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> declarations) { +Style declarationsToStyle(Map> declarations) { Style style = new Style(); declarations.forEach((property, value) { if (value.isNotEmpty) { @@ -102,34 +103,73 @@ Style declarationsToStyle(Map> 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 = []; + 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>> parseExternalCss(String css, OnCssParseError? errorHandler) { + var errors = []; + 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>? _result; - String? _currentProperty; + Map>> _result = {}; + Map> _properties = {}; + late String _selector; + late String _currentProperty; - Map>? getDeclarations(css.StyleSheet sheet) { - _result = new Map>(); - sheet.visit(this); + Map>> 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.from(value)); + } else { + _result[_selector]![key] = new List.from(value); + } + }); + } else { + _result[_selector] = new Map>.from(_properties); + } + _properties.clear(); + } + }); return _result; } @override void visitDeclaration(css.Declaration node) { _currentProperty = node.property; - _result![_currentProperty] = []; + _properties[_currentProperty] = []; node.expression!.visit(this); } @override void visitExpressions(css.Expressions node) { - node.expressions.forEach((expression) { - _result![_currentProperty]!.add(expression); - }); + _properties[_currentProperty]!.addAll(node.expressions); } } diff --git a/lib/src/html_elements.dart b/lib/src/html_elements.dart index 020f0ae345..2711c83c67 100644 --- a/lib/src/html_elements.dart +++ b/lib/src/html_elements.dart @@ -77,7 +77,6 @@ const INTERACTABLE_ELEMENTS = [ const REPLACED_ELEMENTS = [ "audio", "br", - "head", "iframe", "img", "svg", diff --git a/lib/style.dart b/lib/style.dart index 0072ee3423..ac72d7d676 100644 --- a/lib/style.dart +++ b/lib/style.dart @@ -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. @@ -240,6 +242,15 @@ class Style { 'body': Style.fromTextStyle(theme.textTheme.bodyText2!), }; + static Map fromCss(String css, OnCssParseError? onCssParseError) { + final declarations = parseExternalCss(css, onCssParseError); + Map styleMap = {}; + declarations.forEach((key, value) { + styleMap[key] = declarationsToStyle(value); + }); + return styleMap; + } + TextStyle generateTextStyle() { return TextStyle( backgroundColor: backgroundColor, @@ -304,7 +315,6 @@ class Style { //TODO merge border alignment: other.alignment, markerContent: other.markerContent, - maxLines: other.maxLines, textOverflow: other.textOverflow, );