From 1b10a5b3a1eba78f6b1a17ef489b9d24dbd54fe7 Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Thu, 6 Oct 2022 21:27:09 -0600 Subject: [PATCH 1/3] feat: Upgrade list-style-type to CSS3 --- lib/custom_render.dart | 173 +++++++++++++++++------------ lib/html_parser.dart | 215 +++++++++++------------------------- lib/src/css_parser.dart | 43 ++------ lib/src/style/marker.dart | 31 ++++++ lib/src/styled_element.dart | 36 +++--- lib/style.dart | 160 +++++++++++++++++++++------ pubspec.yaml | 4 +- 7 files changed, 352 insertions(+), 310 deletions(-) create mode 100644 lib/src/style/marker.dart diff --git a/lib/custom_render.dart b/lib/custom_render.dart index 35e678beec..87b10c5356 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/src/html_elements.dart'; import 'package:flutter_html/src/utils.dart'; +import 'package:list_counter/list_counter.dart'; typedef CustomRenderMatcher = bool Function(RenderContext context); @@ -156,81 +157,109 @@ CustomRender blockElementRender({Style? style, List? children}) => CustomRender listElementRender( {Style? style, Widget? child, List? children}) => CustomRender.inlineSpan( - inlineSpan: (context, buildChildren) => WidgetSpan( - child: CssBoxWidget( - key: context.key, - style: style ?? context.tree.style, - shrinkWrap: context.parser.shrinkWrap, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - textDirection: style?.direction ?? context.tree.style.direction, - children: [ - (style?.listStylePosition ?? - context.tree.style.listStylePosition) == - ListStylePosition.outside - ? Padding( - padding: style?.padding?.nonNegative ?? - context.tree.style.padding?.nonNegative ?? - EdgeInsets.only( - left: (style?.direction ?? - context.tree.style.direction) != - TextDirection.rtl - ? 10.0 - : 0.0, - right: (style?.direction ?? - context.tree.style.direction) == - TextDirection.rtl - ? 10.0 - : 0.0), - child: - style?.markerContent ?? context.style.markerContent) - : const SizedBox(height: 0, width: 0), - const Text("\u0020", - textAlign: TextAlign.right, - style: TextStyle(fontWeight: FontWeight.w400)), - Expanded( - child: Padding( - padding: (style?.listStylePosition ?? - context.tree.style.listStylePosition) == - ListStylePosition.inside - ? EdgeInsets.only( - left: (style?.direction ?? - context.tree.style.direction) != - TextDirection.rtl - ? 10.0 - : 0.0, - right: (style?.direction ?? - context.tree.style.direction) == - TextDirection.rtl - ? 10.0 - : 0.0) - : EdgeInsets.zero, - child: CssBoxWidget.withInlineSpanChildren( - children: _getListElementChildren( - style?.listStylePosition ?? - context.tree.style.listStylePosition, - buildChildren) - ..insertAll( - 0, - context.tree.style.listStylePosition == - ListStylePosition.inside - ? [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: style?.markerContent ?? - context.style.markerContent ?? - const SizedBox(height: 0, width: 0)) - ] - : []), - style: style ?? context.style, + inlineSpan: (context, buildChildren) { + final listStyleType = style?.listStyleType ?? + context.style.listStyleType ?? + ListStyleType.decimal; + final counterStyle = + CounterStyleRegistry.lookup(listStyleType.counterStyle); + String counterContent; + if (style?.marker?.content.isNormal ?? + context.style.marker?.content.isNormal ?? + true) { + counterContent = counterStyle.generateMarkerContent( + context.tree.counters.lastOrNull?.value ?? 0); + } else if (!(style?.marker?.content.display ?? + context.style.marker?.content.display ?? + true)) { + counterContent = ''; + } else { + counterContent = style?.marker?.content.replacementContent ?? + context.style.marker?.content.replacementContent ?? + counterStyle.generateMarkerContent( + context.tree.counters.lastOrNull?.value ?? 0); + } + final markerWidget = counterContent.isNotEmpty + ? Text.rich(TextSpan( + text: counterContent, + style: context.tree.style.marker?.style?.generateTextStyle(), + )) + : const SizedBox(width: 0, height: 0); //TODO this is hardcoded + + return WidgetSpan( + child: CssBoxWidget( + key: context.key, + style: style ?? context.tree.style, + shrinkWrap: context.parser.shrinkWrap, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + textDirection: style?.direction ?? context.tree.style.direction, + children: [ + (style?.listStylePosition ?? + context.tree.style.listStylePosition) == + ListStylePosition.outside + ? Padding( + padding: style?.padding?.nonNegative ?? + context.tree.style.padding?.nonNegative ?? + EdgeInsets.only( + left: (style?.direction ?? + context.tree.style.direction) != + TextDirection.rtl + ? 10.0 + : 0.0, + right: (style?.direction ?? + context.tree.style.direction) == + TextDirection.rtl + ? 10.0 + : 0.0), + child: markerWidget, + ) + : const SizedBox(height: 0, width: 0), + const Text("\u0020", + textAlign: TextAlign.right, + style: TextStyle(fontWeight: FontWeight.w400)), + Expanded( + child: Padding( + padding: (style?.listStylePosition ?? + context.tree.style.listStylePosition) == + ListStylePosition.inside + ? EdgeInsets.only( + left: (style?.direction ?? + context.tree.style.direction) != + TextDirection.rtl + ? 10.0 + : 0.0, + right: (style?.direction ?? + context.tree.style.direction) == + TextDirection.rtl + ? 10.0 + : 0.0) + : EdgeInsets.zero, + child: CssBoxWidget.withInlineSpanChildren( + children: _getListElementChildren( + style?.listStylePosition ?? + context.tree.style.listStylePosition, + buildChildren) + ..insertAll( + 0, + context.tree.style.listStylePosition == + ListStylePosition.inside + ? [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: markerWidget) + ] + : []), + style: style ?? context.style, + ), ), ), - ), - ], + ], + ), ), - ), - ), + ); + }, ); CustomRender replacedElementRender( diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 8d73cc5d18..58420dbb0d 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -8,10 +8,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/src/css_parser.dart'; import 'package:flutter_html/src/html_elements.dart'; +import 'package:flutter_html/src/style/marker.dart'; import 'package:flutter_html/src/utils.dart'; import 'package:html/dom.dart' as dom; import 'package:html/parser.dart' as htmlparser; -import 'package:numerus/numerus.dart'; +import 'package:list_counter/list_counter.dart'; typedef OnTap = void Function( String? url, @@ -321,7 +322,8 @@ class HtmlParser extends StatelessWidget { tree = _removeEmptyElements(tree); tree = _calculateRelativeValues(tree, devicePixelRatio); - tree = _processListCharacters(tree); + tree = _processListMarkers(tree); + tree = _processCounters(tree); tree = _processBeforesAndAfters(tree); tree = _collapseMargins(tree); return tree; @@ -530,162 +532,73 @@ class HtmlParser extends StatelessWidget { .replaceAll(RegExp(" {2,}"), " "); } - /// [processListCharacters] adds list characters to the front of all list items. - /// - /// The function uses the [_processListCharactersRecursive] function to do most of its work. - static StyledElement _processListCharacters(StyledElement tree) { - final olStack = ListQueue(); - tree = _processListCharactersRecursive(tree, olStack); - return tree; - } - - /// [_processListCharactersRecursive] uses a Stack of integers to properly number and - /// bullet all list items according to the [ListStyleType] they have been given. - static StyledElement _processListCharactersRecursive( - StyledElement tree, ListQueue olStack) { + /// [processListMarkers] adds marker pseudo elements to the front of all list + /// items. + static StyledElement _processListMarkers(StyledElement tree) { tree.style.listStylePosition ??= ListStylePosition.outside; - if (tree.name == 'ol' && - tree.style.listStyleType != null && - tree.style.listStyleType!.type == "marker") { - switch (tree.style.listStyleType!) { - case ListStyleType.lowerLatin: - case ListStyleType.lowerAlpha: - case ListStyleType.upperLatin: - case ListStyleType.upperAlpha: - olStack.add(Context('a')); - if ((tree.attributes['start'] != null - ? int.tryParse(tree.attributes['start']!) - : null) != - null) { - var start = int.tryParse(tree.attributes['start']!) ?? 1; - var x = 1; - while (x < start) { - olStack.last.data = olStack.last.data.toString().nextLetter(); - x++; - } - } - break; - default: - olStack.add(Context((tree.attributes['start'] != null - ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 - : 1) - - 1)); - break; + + if (tree.style.display == Display.listItem) { + // Add the marker pseudo-element if it doesn't exist + tree.style.marker ??= Marker( + content: Content.normal, + style: tree.style, + ); + + // Add the implicit counter-increment on `list-item` if it isn't set + // explicitly already + tree.style.counterIncrement ??= {}; + if (!tree.style.counterIncrement!.containsKey('list-item')) { + tree.style.counterIncrement!['list-item'] = 1; } - } else if (tree.style.display == Display.listItem && - tree.style.listStyleType != null && - tree.style.listStyleType!.type == "widget") { - tree.style.markerContent = tree.style.listStyleType!.widget!; - } else if (tree.style.display == Display.listItem && - tree.style.listStyleType != null && - tree.style.listStyleType!.type == "image") { - tree.style.markerContent = Image.network(tree.style.listStyleType!.text); - } else if (tree.style.display == Display.listItem && - tree.style.listStyleType != null) { - String marker = ""; - switch (tree.style.listStyleType!) { - case ListStyleType.none: - break; - case ListStyleType.circle: - marker = '○'; - break; - case ListStyleType.square: - marker = '■'; - break; - case ListStyleType.disc: - marker = '•'; - break; - case ListStyleType.decimal: - if (olStack.isEmpty) { - olStack.add(Context((tree.attributes['start'] != null - ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 - : 1) - - 1)); - } - olStack.last.data += 1; - marker = '${olStack.last.data}.'; - break; - case ListStyleType.lowerLatin: - case ListStyleType.lowerAlpha: - if (olStack.isEmpty) { - olStack.add(Context('a')); - if ((tree.attributes['start'] != null - ? int.tryParse(tree.attributes['start']!) - : null) != - null) { - var start = int.tryParse(tree.attributes['start']!) ?? 1; - var x = 1; - while (x < start) { - olStack.last.data = olStack.last.data.toString().nextLetter(); - x++; - } - } - } - marker = "${olStack.last.data}."; - olStack.last.data = olStack.last.data.toString().nextLetter(); - break; - case ListStyleType.upperLatin: - case ListStyleType.upperAlpha: - if (olStack.isEmpty) { - olStack.add(Context('a')); - if ((tree.attributes['start'] != null - ? int.tryParse(tree.attributes['start']!) - : null) != - null) { - var start = int.tryParse(tree.attributes['start']!) ?? 1; - var x = 1; - while (x < start) { - olStack.last.data = olStack.last.data.toString().nextLetter(); - x++; - } - } - } - marker = "${olStack.last.data.toString().toUpperCase()}."; - olStack.last.data = olStack.last.data.toString().nextLetter(); - break; - case ListStyleType.lowerRoman: - if (olStack.isEmpty) { - olStack.add(Context((tree.attributes['start'] != null - ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 - : 1) - - 1)); - } - olStack.last.data += 1; - if (olStack.last.data <= 0) { - marker = '${olStack.last.data}.'; - } else { - marker = - "${(olStack.last.data as int).toRomanNumeralString()!.toLowerCase()}."; - } - break; - case ListStyleType.upperRoman: - if (olStack.isEmpty) { - olStack.add(Context((tree.attributes['start'] != null - ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 - : 1) - - 1)); - } - olStack.last.data += 1; - if (olStack.last.data <= 0) { - marker = '${olStack.last.data}.'; - } else { - marker = "${(olStack.last.data as int).toRomanNumeralString()!}."; - } - break; + } + + // Add the counters to ol and ul types. + if (tree.name == 'ol' || tree.name == 'ul') { + tree.style.counterReset ??= {}; + if (!tree.style.counterReset!.containsKey('list-item')) { + tree.style.counterReset!['list-item'] = 0; } - tree.style.markerContent = Text( - marker, - textAlign: TextAlign.right, - style: tree.style.generateTextStyle(), - ); } - for (var element in tree.children) { - _processListCharactersRecursive(element, olStack); + for (var child in tree.children) { + _processListMarkers(child); } - if (tree.name == 'ol') { - olStack.removeLast(); + return tree; + } + + /// [_processListCounters] adds the appropriate counter values to each + /// StyledElement on the tree. + static StyledElement _processCounters(StyledElement tree, + [ListQueue? counters]) { + // Add the counters for the current scope. + tree.counters.addAll(counters?.deepCopy() ?? []); + + // Create any new counters + if (tree.style.counterReset != null) { + tree.style.counterReset!.forEach((counterName, initialValue) { + tree.counters.add(Counter(counterName, initialValue ?? 0)); + }); + } + + // Increment any counters that are to be incremented + if (tree.style.counterIncrement != null) { + tree.style.counterIncrement!.forEach((counterName, increment) { + tree.counters.lastWhereOrNull( + (counter) => counter.name == counterName, + )?.increment(increment ?? 1); + + // If we didn't newly create the counter, increment the counter in the old copy as well. + if(tree.style.counterReset == null || !tree.style.counterReset!.containsKey(counterName)) { + counters?.lastWhereOrNull( + (counter) => counter.name == counterName, + )?.increment(increment ?? 1); + } + }); + } + + for (var element in tree.children) { + _processCounters(element, tree.counters); } return tree; diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index c191a2907d..28ea9b9fe4 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -350,9 +350,9 @@ Style declarationsToStyle(Map> declarations) { } } if (image != null) { - style.listStyleType = - ExpressionMapping.expressionToListStyleType(image) ?? - style.listStyleType; + style.listStyleImage = + ExpressionMapping.expressionToListStyleImage(image) ?? + style.listStyleImage; } else if (type != null) { style.listStyleType = ExpressionMapping.expressionToListStyleType(type) ?? @@ -361,9 +361,9 @@ Style declarationsToStyle(Map> declarations) { break; case 'list-style-image': if (value.first is css.UriTerm) { - style.listStyleType = ExpressionMapping.expressionToListStyleType( + style.listStyleImage = ExpressionMapping.expressionToListStyleImage( value.first as css.UriTerm) ?? - style.listStyleType; + style.listStyleImage; } break; case 'list-style-position': @@ -940,35 +940,12 @@ class ExpressionMapping { return LineHeight.normal; } + static ListStyleImage? expressionToListStyleImage(css.UriTerm value) { + return ListStyleImage(value.text); + } + static ListStyleType? expressionToListStyleType(css.LiteralTerm value) { - if (value is css.UriTerm) { - return ListStyleType.fromImage(value.text); - } - switch (value.text) { - case 'disc': - return ListStyleType.disc; - case 'circle': - return ListStyleType.circle; - case 'decimal': - return ListStyleType.decimal; - case 'lower-alpha': - return ListStyleType.lowerAlpha; - case 'lower-latin': - return ListStyleType.lowerLatin; - case 'lower-roman': - return ListStyleType.lowerRoman; - case 'square': - return ListStyleType.square; - case 'upper-alpha': - return ListStyleType.upperAlpha; - case 'upper-latin': - return ListStyleType.upperLatin; - case 'upper-roman': - return ListStyleType.upperRoman; - case 'none': - return ListStyleType.none; - } - return null; + return ListStyleType.fromName(value.text); } static Width? expressionToWidth(css.Expression value) { diff --git a/lib/src/style/marker.dart b/lib/src/style/marker.dart new file mode 100644 index 0000000000..828ca0f328 --- /dev/null +++ b/lib/src/style/marker.dart @@ -0,0 +1,31 @@ + +import 'package:flutter_html/flutter_html.dart'; + +class Marker { + + + final Content content; + + final Style? style; + + + const Marker({ + this.content = Content.normal, + this.style, + }); +} + +class Content { + final String? replacementContent; + final bool _normal; + final bool display; + + const Content(this.replacementContent): _normal = false, display = true; + const Content._normal(): _normal = true, display = true, replacementContent = null; + const Content._none(): _normal = false, display = false, replacementContent = null; + + static const Content none = Content._none(); + static const Content normal = Content._normal(); + + bool get isNormal => _normal; +} \ No newline at end of file diff --git a/lib/src/styled_element.dart b/lib/src/styled_element.dart index 87625b2d98..4f474c5545 100644 --- a/lib/src/styled_element.dart +++ b/lib/src/styled_element.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:flutter/material.dart'; import 'package:flutter_html/src/css_parser.dart'; import 'package:flutter_html/style.dart'; @@ -5,6 +7,7 @@ import 'package:html/dom.dart' as dom; //TODO(Sub6Resources): don't use the internal code of the html package as it may change unexpectedly. //ignore: implementation_imports import 'package:html/src/query_selector.dart'; +import 'package:list_counter/list_counter.dart'; /// A [StyledElement] applies a style to all of its children. class StyledElement { @@ -14,6 +17,7 @@ class StyledElement { List children; Style style; final dom.Element? _node; + final ListQueue counters = ListQueue(); StyledElement({ this.name = "[[No name]]", @@ -304,24 +308,12 @@ StyledElement parseStyledElement( break; case "ol": case "ul": - //TODO(Sub6Resources): This is a workaround for collapsed margins. Remove. - if (element.parent!.localName == "li") { - styledElement.style = Style( -// margin: EdgeInsets.only(left: 30.0), - display: Display.block, - listStyleType: element.localName == "ol" - ? ListStyleType.decimal - : ListStyleType.disc, - ); - } else { - styledElement.style = Style( -// margin: EdgeInsets.only(left: 30.0, top: 14.0, bottom: 14.0), - display: Display.block, - listStyleType: element.localName == "ol" - ? ListStyleType.decimal - : ListStyleType.disc, - ); - } + styledElement.style = Style( + display: Display.block, + listStyleType: element.localName == "ol" + ? ListStyleType.decimal + : ListStyleType.disc, + ); break; case "p": styledElement.style = Style( @@ -417,3 +409,11 @@ FontSize numberToFontSize(String num) { } return FontSize.medium; } + +extension DeepCopy on ListQueue { + ListQueue deepCopy() { + return ListQueue.from(map((counter) { + return Counter(counter.name, counter.value); + })); + } +} diff --git a/lib/style.dart b/lib/style.dart index d9993d1ba5..e2b399e6b7 100644 --- a/lib/style.dart +++ b/lib/style.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/src/css_parser.dart'; +import 'package:flutter_html/src/style/marker.dart'; //Export Style value-unit APIs export 'package:flutter_html/src/style/margin.dart'; @@ -26,6 +27,18 @@ class Style { /// Default: unspecified, Color? color; + /// CSS attribute "`counter-increment`" + /// + /// Inherited: no + /// Initial: none + Map? counterIncrement; + + /// CSS attribute "`counter-reset`" + /// + /// Inherited: no + /// Initial: none + Map? counterReset; + /// CSS attribute "`direction`" /// /// Inherited: yes, @@ -86,10 +99,16 @@ class Style { /// Default: normal (0), double? letterSpacing; + /// CSS attribute "`list-style-image`" + /// + /// Inherited: yes, + /// Default: TODO + ListStyleImage? listStyleImage; + /// CSS attribute "`list-style-type`" /// /// Inherited: yes, - /// Default: ListStyleType.DISC + /// Default: ListStyleType.disc ListStyleType? listStyleType; /// CSS attribute "`list-style-position`" @@ -104,6 +123,12 @@ class Style { /// Default: EdgeInsets.zero EdgeInsets? padding; + /// CSS pseudo-element "`::marker`" + /// + /// Inherited: no, + /// Default: null + Marker? marker; + /// CSS attribute "`margin`" /// /// Inherited: no, @@ -209,6 +234,8 @@ class Style { Style({ this.backgroundColor = Colors.transparent, this.color, + this.counterIncrement, + this.counterReset, this.direction, this.display, this.fontFamily, @@ -220,9 +247,11 @@ class Style { this.height, this.lineHeight, this.letterSpacing, + this.listStyleImage, this.listStyleType, this.listStylePosition, this.padding, + this.marker, this.margin, this.textAlign, this.textDecoration, @@ -249,7 +278,8 @@ class Style { } } - static Map fromThemeData(ThemeData theme) => { + static Map fromThemeData(ThemeData theme) => + { 'h1': Style.fromTextStyle(theme.textTheme.headline1!), 'h2': Style.fromTextStyle(theme.textTheme.headline2!), 'h3': Style.fromTextStyle(theme.textTheme.headline3!), @@ -259,8 +289,8 @@ class Style { 'body': Style.fromTextStyle(theme.textTheme.bodyText2!), }; - static Map fromCss( - String css, OnCssParseError? onCssParseError) { + static Map fromCss(String css, + OnCssParseError? onCssParseError) { final declarations = parseExternalCss(css, onCssParseError); Map styleMap = {}; declarations.forEach((key, value) { @@ -301,6 +331,8 @@ class Style { return copyWith( backgroundColor: other.backgroundColor, color: other.color, + counterIncrement: other.counterIncrement, + counterReset: other.counterReset, direction: other.direction, display: other.display, fontFamily: other.fontFamily, @@ -312,12 +344,14 @@ class Style { height: other.height, lineHeight: other.lineHeight, letterSpacing: other.letterSpacing, + listStyleImage: other.listStyleImage, listStyleType: other.listStyleType, listStylePosition: other.listStylePosition, padding: other.padding, //TODO merge EdgeInsets margin: other.margin, //TODO merge Margins + marker: other.marker, textAlign: other.textAlign, textDecoration: other.textDecoration, textDecorationColor: other.textDecorationColor, @@ -346,10 +380,10 @@ class Style { LineHeight? finalLineHeight = child.lineHeight != null ? child.lineHeight?.units == "length" - ? LineHeight(child.lineHeight!.size! / - (finalFontSize == null ? 14 : finalFontSize.value) * - 1.2) - : child.lineHeight + ? LineHeight(child.lineHeight!.size! / + (finalFontSize == null ? 14 : finalFontSize.value) * + 1.2) + : child.lineHeight : lineHeight; return child.copyWith( @@ -367,6 +401,7 @@ class Style { fontWeight: child.fontWeight ?? fontWeight, lineHeight: finalLineHeight, letterSpacing: child.letterSpacing ?? letterSpacing, + listStyleImage: child.listStyleImage ?? listStyleImage, listStyleType: child.listStyleType ?? listStyleType, listStylePosition: child.listStylePosition ?? listStylePosition, textAlign: child.textAlign ?? textAlign, @@ -386,6 +421,8 @@ class Style { Style copyWith({ Color? backgroundColor, Color? color, + Map? counterIncrement, + Map? counterReset, TextDirection? direction, Display? display, String? fontFamily, @@ -397,10 +434,12 @@ class Style { Height? height, LineHeight? lineHeight, double? letterSpacing, + ListStyleImage? listStyleImage, ListStyleType? listStyleType, ListStylePosition? listStylePosition, EdgeInsets? padding, Margins? margin, + Marker? marker, TextAlign? textAlign, TextDecoration? textDecoration, Color? textDecorationColor, @@ -424,6 +463,8 @@ class Style { return Style( backgroundColor: backgroundColor ?? this.backgroundColor, color: color ?? this.color, + counterIncrement: counterIncrement ?? this.counterIncrement, + counterReset: counterReset ?? this.counterReset, direction: direction ?? this.direction, display: display ?? this.display, fontFamily: fontFamily ?? this.fontFamily, @@ -435,16 +476,18 @@ class Style { height: height ?? this.height, lineHeight: lineHeight ?? this.lineHeight, letterSpacing: letterSpacing ?? this.letterSpacing, + listStyleImage: listStyleImage ?? this.listStyleImage, listStyleType: listStyleType ?? this.listStyleType, listStylePosition: listStylePosition ?? this.listStylePosition, padding: padding ?? this.padding, margin: margin ?? this.margin, + marker: marker ?? this.marker, textAlign: textAlign ?? this.textAlign, textDecoration: textDecoration ?? this.textDecoration, textDecorationColor: textDecorationColor ?? this.textDecorationColor, textDecorationStyle: textDecorationStyle ?? this.textDecorationStyle, textDecorationThickness: - textDecorationThickness ?? this.textDecorationThickness, + textDecorationThickness ?? this.textDecorationThickness, textShadow: textShadow ?? this.textShadow, verticalAlign: verticalAlign ?? this.verticalAlign, whiteSpace: whiteSpace ?? this.whiteSpace, @@ -472,7 +515,7 @@ class Style { fontFamilyFallback = textStyle.fontFamilyFallback; fontFeatureSettings = textStyle.fontFeatures; fontSize = - textStyle.fontSize != null ? FontSize(textStyle.fontSize!) : null; + textStyle.fontSize != null ? FontSize(textStyle.fontSize!) : null; fontStyle = textStyle.fontStyle; fontWeight = textStyle.fontWeight; letterSpacing = textStyle.letterSpacing; @@ -548,30 +591,79 @@ enum Display { none, } -class ListStyleType { - final String text; - final String type; - final Widget? widget; - - const ListStyleType(this.text, {this.type = "marker", this.widget}); - - factory ListStyleType.fromImage(String url) => - ListStyleType(url, type: "image"); - - factory ListStyleType.fromWidget(Widget widget) => - ListStyleType("", widget: widget, type: "widget"); - - static const lowerAlpha = ListStyleType("LOWER_ALPHA"); - static const upperAlpha = ListStyleType("UPPER_ALPHA"); - static const lowerLatin = ListStyleType("LOWER_LATIN"); - static const upperLatin = ListStyleType("UPPER_LATIN"); - static const circle = ListStyleType("CIRCLE"); - static const disc = ListStyleType("DISC"); - static const decimal = ListStyleType("DECIMAL"); - static const lowerRoman = ListStyleType("LOWER_ROMAN"); - static const upperRoman = ListStyleType("UPPER_ROMAN"); - static const square = ListStyleType("SQUARE"); - static const none = ListStyleType("NONE"); +enum ListStyleType { + arabicIndic('arabic-indic'), + armenian('armenian'), + lowerArmenian('lower-armenian'), + upperArmenian('upper-armenian'), + bengali('bengali'), + cambodian('cambodian'), + khmer('khmer'), + circle('circle'), + cjkDecimal('cjk-decimal'), + cjkEarthlyBranch('cjk-earthly-branch'), + cjkHeavenlyStem('cjk-heavenly-stem'), + cjkIdeographic('cjk-ideographic'), + decimal('decimal'), + decimalLeadingZero('decimal-leading-zero'), + devanagari('devanagari'), + disc('disc'), + disclosureClosed('disclosure-closed'), + disclosureOpen('disclosure-open'), + ethiopicNumeric('ethiopic-numeric'), + georgian('georgian'), + gujarati('gujarati'), + gurmukhi('gurmukhi'), + hebrew('hebrew'), + hiragana('hiragana'), + hiraganaIroha('hiragana-iroha'), + japaneseFormal('japanese-formal'), + japaneseInformal('japanese-informal'), + kannada('kannada'), + katakana('katakana'), + katakanaIroha('katakana-iroha'), + koreanHangulFormal('korean-hangul-formal'), + koreanHanjaInformal('korean-hanja-informal'), + koreanHanjaFormal('korean-hanja-formal'), + lao('lao'), + lowerAlpha('lower-alpha'), + lowerGreek('lower-greek'), + lowerLatin('lower-latin'), + lowerRoman('lower-roman'), + malayalam('malayalam'), + mongolian('mongolian'), + myanmar('myanmar'), + none('none'), + oriya('oriya'), + persian('persian'), + simpChineseFormal('simp-chinese-formal'), + simpChineseInformal('simp-chinese-informal'), + square('square'), + tamil('tamil'), + telugu('telugu'), + thai('thai'), + tibetan('tibetan'), + tradChineseFormal('trad-chinese-formal'), + tradChineseInformal('trad-chinese-informal'), + upperAlpha('upper-alpha'), + upperLatin('upper-latin'), + upperRoman('upper-roman'); + + final String counterStyle; + + const ListStyleType(this.counterStyle); + + factory ListStyleType.fromName(String name) { + return ListStyleType.values.firstWhere((value) { + return name == value.counterStyle; + }); + } +} + +class ListStyleImage { + final String uriText; + + const ListStyleImage(this.uriText); } enum ListStylePosition { diff --git a/pubspec.yaml b/pubspec.yaml index 6e4ea4795e..5f01092554 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,8 +17,8 @@ dependencies: # Plugin for firstWhereOrNull extension on lists collection: '>=1.15.0 <2.0.0' - # plugin to convert integers to numerals - numerus: '>=2.0.0 <3.0.0' + # plugin to manage lists and counting in a variety of styles + list_counter: '>=1.0.2 <2.0.0' flutter: sdk: flutter From e3d9831c21c640d4b2fbf2f6f785da7dcc336262 Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Sat, 8 Oct 2022 14:38:05 -0600 Subject: [PATCH 2/3] fix: Fix various issues with list rendering --- lib/custom_render.dart | 159 ++++++++++-------------------------- lib/html_parser.dart | 60 +++++++++++--- lib/src/css_box_widget.dart | 135 +++++++++++++++++++----------- lib/src/style/marker.dart | 24 +++--- lib/src/styled_element.dart | 1 + lib/style.dart | 19 ++--- 6 files changed, 202 insertions(+), 196 deletions(-) diff --git a/lib/custom_render.dart b/lib/custom_render.dart index 87b10c5356..60e3dfb634 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -155,112 +155,49 @@ CustomRender blockElementRender({Style? style, List? children}) => }); CustomRender listElementRender( - {Style? style, Widget? child, List? children}) => - CustomRender.inlineSpan( - inlineSpan: (context, buildChildren) { - final listStyleType = style?.listStyleType ?? - context.style.listStyleType ?? - ListStyleType.decimal; - final counterStyle = - CounterStyleRegistry.lookup(listStyleType.counterStyle); - String counterContent; - if (style?.marker?.content.isNormal ?? - context.style.marker?.content.isNormal ?? - true) { - counterContent = counterStyle.generateMarkerContent( - context.tree.counters.lastOrNull?.value ?? 0); - } else if (!(style?.marker?.content.display ?? - context.style.marker?.content.display ?? - true)) { - counterContent = ''; - } else { - counterContent = style?.marker?.content.replacementContent ?? - context.style.marker?.content.replacementContent ?? - counterStyle.generateMarkerContent( - context.tree.counters.lastOrNull?.value ?? 0); - } - final markerWidget = counterContent.isNotEmpty - ? Text.rich(TextSpan( + {Style? style, Widget? child, List? children}) { + return CustomRender.inlineSpan( + inlineSpan: (context, buildChildren) { + final usedStyle = style ?? context.style; + final listStyleType = usedStyle.listStyleType ?? ListStyleType.decimal; + final counterStyle = + CounterStyleRegistry.lookup(listStyleType.counterStyle); + String counterContent; + if (usedStyle.marker?.content.isNormal ?? true) { + counterContent = counterStyle.generateMarkerContent( + context.tree.counters.lastOrNull?.value ?? 0, + ); + } else if (!(usedStyle.marker?.content.display ?? true)) { + counterContent = ''; + } else { + counterContent = usedStyle.marker?.content.replacementContent ?? + counterStyle.generateMarkerContent( + context.tree.counters.lastOrNull?.value ?? 0, + ); + } + final listChildren = buildChildren() + ..insertAll( + 0, + [ + if (usedStyle.listStylePosition == ListStylePosition.inside) + TextSpan( text: counterContent, - style: context.tree.style.marker?.style?.generateTextStyle(), - )) - : const SizedBox(width: 0, height: 0); //TODO this is hardcoded - - return WidgetSpan( - child: CssBoxWidget( - key: context.key, - style: style ?? context.tree.style, - shrinkWrap: context.parser.shrinkWrap, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - textDirection: style?.direction ?? context.tree.style.direction, - children: [ - (style?.listStylePosition ?? - context.tree.style.listStylePosition) == - ListStylePosition.outside - ? Padding( - padding: style?.padding?.nonNegative ?? - context.tree.style.padding?.nonNegative ?? - EdgeInsets.only( - left: (style?.direction ?? - context.tree.style.direction) != - TextDirection.rtl - ? 10.0 - : 0.0, - right: (style?.direction ?? - context.tree.style.direction) == - TextDirection.rtl - ? 10.0 - : 0.0), - child: markerWidget, - ) - : const SizedBox(height: 0, width: 0), - const Text("\u0020", - textAlign: TextAlign.right, - style: TextStyle(fontWeight: FontWeight.w400)), - Expanded( - child: Padding( - padding: (style?.listStylePosition ?? - context.tree.style.listStylePosition) == - ListStylePosition.inside - ? EdgeInsets.only( - left: (style?.direction ?? - context.tree.style.direction) != - TextDirection.rtl - ? 10.0 - : 0.0, - right: (style?.direction ?? - context.tree.style.direction) == - TextDirection.rtl - ? 10.0 - : 0.0) - : EdgeInsets.zero, - child: CssBoxWidget.withInlineSpanChildren( - children: _getListElementChildren( - style?.listStylePosition ?? - context.tree.style.listStylePosition, - buildChildren) - ..insertAll( - 0, - context.tree.style.listStylePosition == - ListStylePosition.inside - ? [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: markerWidget) - ] - : []), - style: style ?? context.style, - ), - ), - ), - ], - ), - ), + style: usedStyle.marker?.style?.generateTextStyle(), + ), + ], ); - }, - ); + + return WidgetSpan( + child: CssBoxWidget.withInlineSpanChildren( + key: context.key, + style: usedStyle, + shrinkWrap: context.parser.shrinkWrap, + children: listChildren, + ), + ); + }, + ); +} CustomRender replacedElementRender( {PlaceholderAlignment? alignment, @@ -546,20 +483,6 @@ Map generateDefaultRenders() { }; } -List _getListElementChildren( - ListStylePosition? position, Function() buildChildren) { - List children = buildChildren.call(); - if (position == ListStylePosition.inside) { - const tabSpan = WidgetSpan( - child: Text("\t", - textAlign: TextAlign.right, - style: TextStyle(fontWeight: FontWeight.w400)), - ); - children.insert(0, tabSpan); - } - return children; -} - InlineSpan _getInteractableChildren(RenderContext context, InteractableElement tree, InlineSpan childSpan, TextStyle childStyle) { if (childSpan is TextSpan) { diff --git a/lib/html_parser.dart b/lib/html_parser.dart index 58420dbb0d..a739b2b987 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -322,8 +322,9 @@ class HtmlParser extends StatelessWidget { tree = _removeEmptyElements(tree); tree = _calculateRelativeValues(tree, devicePixelRatio); - tree = _processListMarkers(tree); + tree = _preprocessListMarkers(tree); tree = _processCounters(tree); + tree = _processListMarkers(tree); tree = _processBeforesAndAfters(tree); tree = _collapseMargins(tree); return tree; @@ -532,9 +533,9 @@ class HtmlParser extends StatelessWidget { .replaceAll(RegExp(" {2,}"), " "); } - /// [processListMarkers] adds marker pseudo elements to the front of all list + /// [preprocessListMarkers] adds marker pseudo elements to the front of all list /// items. - static StyledElement _processListMarkers(StyledElement tree) { + static StyledElement _preprocessListMarkers(StyledElement tree) { tree.style.listStylePosition ??= ListStylePosition.outside; if (tree.style.display == Display.listItem) { @@ -544,6 +545,10 @@ class HtmlParser extends StatelessWidget { style: tree.style, ); + // Inherit styles from originating widget + tree.style.marker!.style = + tree.style.copyOnlyInherited(tree.style.marker!.style ?? Style()); + // Add the implicit counter-increment on `list-item` if it isn't set // explicitly already tree.style.counterIncrement ??= {}; @@ -561,7 +566,7 @@ class HtmlParser extends StatelessWidget { } for (var child in tree.children) { - _processListMarkers(child); + _preprocessListMarkers(child); } return tree; @@ -584,15 +589,20 @@ class HtmlParser extends StatelessWidget { // Increment any counters that are to be incremented if (tree.style.counterIncrement != null) { tree.style.counterIncrement!.forEach((counterName, increment) { - tree.counters.lastWhereOrNull( - (counter) => counter.name == counterName, - )?.increment(increment ?? 1); + tree.counters + .lastWhereOrNull( + (counter) => counter.name == counterName, + ) + ?.increment(increment ?? 1); // If we didn't newly create the counter, increment the counter in the old copy as well. - if(tree.style.counterReset == null || !tree.style.counterReset!.containsKey(counterName)) { - counters?.lastWhereOrNull( + if (tree.style.counterReset == null || + !tree.style.counterReset!.containsKey(counterName)) { + counters + ?.lastWhereOrNull( (counter) => counter.name == counterName, - )?.increment(increment ?? 1); + ) + ?.increment(increment ?? 1); } }); } @@ -604,6 +614,36 @@ class HtmlParser extends StatelessWidget { return tree; } + static StyledElement _processListMarkers(StyledElement tree) { + if (tree.style.display == Display.listItem) { + final listStyleType = tree.style.listStyleType ?? ListStyleType.decimal; + final counterStyle = CounterStyleRegistry.lookup( + listStyleType.counterStyle, + ); + String counterContent; + if (tree.style.marker?.content.isNormal ?? true) { + counterContent = counterStyle.generateMarkerContent( + tree.counters.lastOrNull?.value ?? 0, + ); + } else if (!(tree.style.marker?.content.display ?? true)) { + counterContent = ''; + } else { + counterContent = tree.style.marker?.content.replacementContent ?? + counterStyle.generateMarkerContent( + tree.counters.lastOrNull?.value ?? 0, + ); + } + tree.style.marker = Marker( + content: Content(counterContent), style: tree.style.marker?.style); + } + + for (var child in tree.children) { + _processListMarkers(child); + } + + return tree; + } + /// [_processBeforesAndAfters] adds text content to the beginning and end of /// the list of the trees children according to the `before` and `after` Style /// properties. diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart index 727095c0f4..127b010435 100644 --- a/lib/src/css_box_widget.dart +++ b/lib/src/css_box_widget.dart @@ -57,6 +57,8 @@ class CssBoxWidget extends StatelessWidget { @override Widget build(BuildContext context) { + final markerBox = _generateMarkerBox(style); + return _CSSBoxRenderer( width: style.width ?? Width.auto(), height: style.height ?? Height.auto(), @@ -68,15 +70,18 @@ class CssBoxWidget extends StatelessWidget { emValue: _calculateEmValue(style, context), textDirection: _checkTextDirection(context, textDirection), shrinkWrap: shrinkWrap, - child: Container( - decoration: BoxDecoration( - border: style.border, - color: style.backgroundColor, //Colors the padding and content boxes + children: [ + Container( + decoration: BoxDecoration( + border: style.border, + color: style.backgroundColor, //Colors the padding and content boxes + ), + width: _shouldExpandToFillBlock() ? double.infinity : null, + padding: style.padding ?? EdgeInsets.zero, + child: child, ), - width: _shouldExpandToFillBlock() ? double.infinity : null, - padding: style.padding ?? EdgeInsets.zero, - child: child, - ), + if (markerBox != null) markerBox, + ], ); } @@ -124,6 +129,22 @@ class CssBoxWidget extends StatelessWidget { ); } + static Widget? _generateMarkerBox(Style style) { + if (style.display == Display.listItem && + style.listStylePosition == ListStylePosition.outside) { + if (style.marker?.content.replacementContent?.isNotEmpty ?? false) { + return Text.rich( + TextSpan( + text: style.marker!.content.replacementContent!, + style: style.marker!.style?.generateTextStyle(), + ), + ); + } + } + + return null; + } + /// Whether or not the content-box should expand its width to fill the /// width available to it or if it should just let its inner content /// determine the content-box's width. @@ -150,7 +171,7 @@ class CssBoxWidget extends StatelessWidget { class _CSSBoxRenderer extends MultiChildRenderObjectWidget { _CSSBoxRenderer({ Key? key, - required Widget child, + required super.children, required this.display, required this.margins, required this.width, @@ -161,7 +182,7 @@ class _CSSBoxRenderer extends MultiChildRenderObjectWidget { required this.childIsReplaced, required this.emValue, required this.shrinkWrap, - }) : super(key: key, children: [child]); + }) : super(key: key); /// The Display type of the element final Display display; @@ -442,8 +463,11 @@ class _RenderCSSBox extends RenderBox double width = containingBlockSize.width; double height = containingBlockSize.height; - RenderBox? child = firstChild; - assert(child != null); + assert(firstChild != null); + RenderBox child = firstChild!; + + final CSSBoxParentData parentData = child.parentData! as CSSBoxParentData; + RenderBox? markerBoxChild = parentData.nextSibling; // Calculate child size final childConstraints = constraints.copyWith( @@ -460,7 +484,10 @@ class _RenderCSSBox extends RenderBox minWidth: (this.width.unit != Unit.auto) ? this.width.value : 0, minHeight: (this.height.unit != Unit.auto) ? this.height.value : 0, ); - final Size childSize = layoutChild(child!, childConstraints); + final Size childSize = layoutChild(child, childConstraints); + if (markerBoxChild != null) { + layoutChild(markerBoxChild, childConstraints); + } // Calculate used values of margins based on rules final usedMargins = _calculateUsedMargins(childSize, containingBlockSize); @@ -511,43 +538,55 @@ class _RenderCSSBox extends RenderBox ); size = sizes.parentSize; - RenderBox? child = firstChild; - while (child != null) { - final CSSBoxParentData childParentData = - child.parentData! as CSSBoxParentData; + assert(firstChild != null); + RenderBox child = firstChild!; - // Calculate used margins based on constraints and child size - final usedMargins = - _calculateUsedMargins(sizes.childSize, constraints.biggest); - final leftMargin = usedMargins.left?.value ?? 0; - final topMargin = usedMargins.top?.value ?? 0; - - double leftOffset = 0; - double topOffset = 0; - switch (display) { - case Display.block: - leftOffset = leftMargin; - topOffset = topMargin; - break; - case Display.inline: - leftOffset = leftMargin; - break; - case Display.inlineBlock: - leftOffset = leftMargin; - topOffset = topMargin; - break; - case Display.listItem: - leftOffset = leftMargin; - topOffset = topMargin; - break; - case Display.none: - //No offset - break; - } - childParentData.offset = Offset(leftOffset, topOffset); + final CSSBoxParentData childParentData = + child.parentData! as CSSBoxParentData; - assert(child.parentData == childParentData); - child = childParentData.nextSibling; + // Calculate used margins based on constraints and child size + final usedMargins = + _calculateUsedMargins(sizes.childSize, constraints.biggest); + final leftMargin = usedMargins.left?.value ?? 0; + final topMargin = usedMargins.top?.value ?? 0; + + double leftOffset = 0; + double topOffset = 0; + switch (display) { + case Display.block: + leftOffset = leftMargin; + topOffset = topMargin; + break; + case Display.inline: + leftOffset = leftMargin; + break; + case Display.inlineBlock: + leftOffset = leftMargin; + topOffset = topMargin; + break; + case Display.listItem: + leftOffset = leftMargin; + topOffset = topMargin; + break; + case Display.none: + //No offset + break; + } + childParentData.offset = Offset(leftOffset, topOffset); + assert(child.parentData == childParentData); + + // Now, layout the marker box if it exists: + RenderBox? markerBox = childParentData.nextSibling; + if (markerBox != null) { + final markerBoxParentData = markerBox.parentData! as CSSBoxParentData; + final distance = (child.getDistanceToBaseline(TextBaseline.alphabetic, + onlyReal: true) ?? + 0) + + topOffset; + final offsetHeight = distance - + (markerBox.getDistanceToBaseline(TextBaseline.alphabetic) ?? + markerBox.size.height); + markerBoxParentData.offset = Offset(-markerBox.size.width, offsetHeight); } } diff --git a/lib/src/style/marker.dart b/lib/src/style/marker.dart index 828ca0f328..86af32f48f 100644 --- a/lib/src/style/marker.dart +++ b/lib/src/style/marker.dart @@ -1,15 +1,11 @@ - import 'package:flutter_html/flutter_html.dart'; class Marker { - - final Content content; - final Style? style; + Style? style; - - const Marker({ + Marker({ this.content = Content.normal, this.style, }); @@ -20,12 +16,20 @@ class Content { final bool _normal; final bool display; - const Content(this.replacementContent): _normal = false, display = true; - const Content._normal(): _normal = true, display = true, replacementContent = null; - const Content._none(): _normal = false, display = false, replacementContent = null; + const Content(this.replacementContent) + : _normal = false, + display = true; + const Content._normal() + : _normal = true, + display = true, + replacementContent = null; + const Content._none() + : _normal = false, + display = false, + replacementContent = null; static const Content none = Content._none(); static const Content normal = Content._normal(); bool get isNormal => _normal; -} \ No newline at end of file +} diff --git a/lib/src/styled_element.dart b/lib/src/styled_element.dart index 4f474c5545..0a1d949397 100644 --- a/lib/src/styled_element.dart +++ b/lib/src/styled_element.dart @@ -313,6 +313,7 @@ StyledElement parseStyledElement( listStyleType: element.localName == "ol" ? ListStyleType.decimal : ListStyleType.disc, + padding: const EdgeInsets.only(left: 40), ); break; case "p": diff --git a/lib/style.dart b/lib/style.dart index e2b399e6b7..8b4797835f 100644 --- a/lib/style.dart +++ b/lib/style.dart @@ -278,8 +278,7 @@ class Style { } } - static Map fromThemeData(ThemeData theme) => - { + static Map fromThemeData(ThemeData theme) => { 'h1': Style.fromTextStyle(theme.textTheme.headline1!), 'h2': Style.fromTextStyle(theme.textTheme.headline2!), 'h3': Style.fromTextStyle(theme.textTheme.headline3!), @@ -289,8 +288,8 @@ class Style { 'body': Style.fromTextStyle(theme.textTheme.bodyText2!), }; - static Map fromCss(String css, - OnCssParseError? onCssParseError) { + static Map fromCss( + String css, OnCssParseError? onCssParseError) { final declarations = parseExternalCss(css, onCssParseError); Map styleMap = {}; declarations.forEach((key, value) { @@ -380,10 +379,10 @@ class Style { LineHeight? finalLineHeight = child.lineHeight != null ? child.lineHeight?.units == "length" - ? LineHeight(child.lineHeight!.size! / - (finalFontSize == null ? 14 : finalFontSize.value) * - 1.2) - : child.lineHeight + ? LineHeight(child.lineHeight!.size! / + (finalFontSize == null ? 14 : finalFontSize.value) * + 1.2) + : child.lineHeight : lineHeight; return child.copyWith( @@ -487,7 +486,7 @@ class Style { textDecorationColor: textDecorationColor ?? this.textDecorationColor, textDecorationStyle: textDecorationStyle ?? this.textDecorationStyle, textDecorationThickness: - textDecorationThickness ?? this.textDecorationThickness, + textDecorationThickness ?? this.textDecorationThickness, textShadow: textShadow ?? this.textShadow, verticalAlign: verticalAlign ?? this.verticalAlign, whiteSpace: whiteSpace ?? this.whiteSpace, @@ -515,7 +514,7 @@ class Style { fontFamilyFallback = textStyle.fontFamilyFallback; fontFeatureSettings = textStyle.fontFeatures; fontSize = - textStyle.fontSize != null ? FontSize(textStyle.fontSize!) : null; + textStyle.fontSize != null ? FontSize(textStyle.fontSize!) : null; fontStyle = textStyle.fontStyle; fontWeight = textStyle.fontWeight; letterSpacing = textStyle.letterSpacing; From 91a7e702d3cfcead0425cd3bb332ea0406d2d04e Mon Sep 17 00:00:00 2001 From: Matthew Whitaker Date: Thu, 20 Oct 2022 15:14:18 -0600 Subject: [PATCH 3/3] Re-add support for list-style-image and do some final cleanup --- lib/custom_render.dart | 34 ++---------------------- lib/src/css_box_widget.dart | 52 ++++++++++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/lib/custom_render.dart b/lib/custom_render.dart index 60e3dfb634..abb1931418 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -8,7 +8,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/src/html_elements.dart'; import 'package:flutter_html/src/utils.dart'; -import 'package:list_counter/list_counter.dart'; typedef CustomRenderMatcher = bool Function(RenderContext context); @@ -158,41 +157,12 @@ CustomRender listElementRender( {Style? style, Widget? child, List? children}) { return CustomRender.inlineSpan( inlineSpan: (context, buildChildren) { - final usedStyle = style ?? context.style; - final listStyleType = usedStyle.listStyleType ?? ListStyleType.decimal; - final counterStyle = - CounterStyleRegistry.lookup(listStyleType.counterStyle); - String counterContent; - if (usedStyle.marker?.content.isNormal ?? true) { - counterContent = counterStyle.generateMarkerContent( - context.tree.counters.lastOrNull?.value ?? 0, - ); - } else if (!(usedStyle.marker?.content.display ?? true)) { - counterContent = ''; - } else { - counterContent = usedStyle.marker?.content.replacementContent ?? - counterStyle.generateMarkerContent( - context.tree.counters.lastOrNull?.value ?? 0, - ); - } - final listChildren = buildChildren() - ..insertAll( - 0, - [ - if (usedStyle.listStylePosition == ListStylePosition.inside) - TextSpan( - text: counterContent, - style: usedStyle.marker?.style?.generateTextStyle(), - ), - ], - ); - return WidgetSpan( child: CssBoxWidget.withInlineSpanChildren( key: context.key, - style: usedStyle, + style: style ?? context.style, shrinkWrap: context.parser.shrinkWrap, - children: listChildren, + children: buildChildren(), ), ); }, diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart index 127b010435..1926a43cd5 100644 --- a/lib/src/css_box_widget.dart +++ b/lib/src/css_box_widget.dart @@ -57,7 +57,9 @@ class CssBoxWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final markerBox = _generateMarkerBox(style); + final markerBox = style.listStylePosition == ListStylePosition.outside + ? _generateMarkerBoxSpan(style) + : null; return _CSSBoxRenderer( width: style.width ?? Width.auto(), @@ -80,7 +82,7 @@ class CssBoxWidget extends StatelessWidget { padding: style.padding ?? EdgeInsets.zero, child: child, ), - if (markerBox != null) markerBox, + if (markerBox != null) Text.rich(markerBox), ], ); } @@ -92,6 +94,15 @@ class CssBoxWidget extends StatelessWidget { return Container(); } + // Generate an inline marker box if the list-style-position is set to + // inside. Otherwise the marker box will be added elsewhere. + if (style.listStylePosition == ListStylePosition.inside) { + final inlineMarkerBox = _generateMarkerBoxSpan(style); + if (inlineMarkerBox != null) { + children.insert(0, inlineMarkerBox); + } + } + return Text.rich( TextSpan( style: style.generateTextStyle(), @@ -129,17 +140,38 @@ class CssBoxWidget extends StatelessWidget { ); } - static Widget? _generateMarkerBox(Style style) { - if (style.display == Display.listItem && - style.listStylePosition == ListStylePosition.outside) { - if (style.marker?.content.replacementContent?.isNotEmpty ?? false) { - return Text.rich( - TextSpan( - text: style.marker!.content.replacementContent!, - style: style.marker!.style?.generateTextStyle(), + static InlineSpan? _generateMarkerBoxSpan(Style style) { + if (style.display == Display.listItem) { + // First handle listStyleImage + if (style.listStyleImage != null) { + return WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Image.network( + style.listStyleImage!.uriText, + errorBuilder: (_, __, ___) { + if (style.marker?.content.replacementContent?.isNotEmpty ?? + false) { + return Text.rich( + TextSpan( + text: style.marker!.content.replacementContent!, + style: style.marker!.style?.generateTextStyle(), + ), + ); + } + + return Container(); + }, ), ); } + + // Display list marker with given style + if (style.marker?.content.replacementContent?.isNotEmpty ?? false) { + return TextSpan( + text: style.marker!.content.replacementContent!, + style: style.marker!.style?.generateTextStyle(), + ); + } } return null;