diff --git a/lib/custom_render.dart b/lib/custom_render.dart index 35e678beec..abb1931418 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -154,84 +154,20 @@ CustomRender blockElementRender({Style? style, List? children}) => }); CustomRender listElementRender( - {Style? style, Widget? child, List? children}) => - CustomRender.inlineSpan( - inlineSpan: (context, buildChildren) => WidgetSpan( - child: CssBoxWidget( + {Style? style, Widget? child, List? children}) { + return CustomRender.inlineSpan( + inlineSpan: (context, buildChildren) { + return WidgetSpan( + child: CssBoxWidget.withInlineSpanChildren( key: context.key, - style: style ?? context.tree.style, + style: style ?? context.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, - ), - ), - ), - ], - ), + children: buildChildren(), ), - ), - ); + ); + }, + ); +} CustomRender replacedElementRender( {PlaceholderAlignment? alignment, @@ -517,20 +453,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 8d73cc5d18..a739b2b987 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,9 @@ class HtmlParser extends StatelessWidget { tree = _removeEmptyElements(tree); tree = _calculateRelativeValues(tree, devicePixelRatio); - tree = _processListCharacters(tree); + tree = _preprocessListMarkers(tree); + tree = _processCounters(tree); + tree = _processListMarkers(tree); tree = _processBeforesAndAfters(tree); tree = _collapseMargins(tree); return tree; @@ -530,162 +533,112 @@ 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) { + /// [preprocessListMarkers] adds marker pseudo elements to the front of all list + /// items. + static StyledElement _preprocessListMarkers(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, + ); + + // 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 ??= {}; + 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 child in tree.children) { + _preprocessListMarkers(child); + } + + 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) { - _processListCharactersRecursive(element, olStack); + _processCounters(element, tree.counters); } - if (tree.name == 'ol') { - olStack.removeLast(); + 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; diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart index 727095c0f4..1926a43cd5 100644 --- a/lib/src/css_box_widget.dart +++ b/lib/src/css_box_widget.dart @@ -57,6 +57,10 @@ class CssBoxWidget extends StatelessWidget { @override Widget build(BuildContext context) { + final markerBox = style.listStylePosition == ListStylePosition.outside + ? _generateMarkerBoxSpan(style) + : null; + return _CSSBoxRenderer( width: style.width ?? Width.auto(), height: style.height ?? Height.auto(), @@ -68,15 +72,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) Text.rich(markerBox), + ], ); } @@ -87,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(), @@ -124,6 +140,43 @@ class CssBoxWidget extends StatelessWidget { ); } + 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; + } + /// 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 +203,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 +214,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 +495,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 +516,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 +570,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/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..86af32f48f --- /dev/null +++ b/lib/src/style/marker.dart @@ -0,0 +1,35 @@ +import 'package:flutter_html/flutter_html.dart'; + +class Marker { + final Content content; + + Style? style; + + 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; +} diff --git a/lib/src/styled_element.dart b/lib/src/styled_element.dart index 87625b2d98..0a1d949397 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,13 @@ 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, + padding: const EdgeInsets.only(left: 40), + ); break; case "p": styledElement.style = Style( @@ -417,3 +410,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..8b4797835f 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, @@ -301,6 +330,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 +343,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, @@ -367,6 +400,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 +420,8 @@ class Style { Style copyWith({ Color? backgroundColor, Color? color, + Map? counterIncrement, + Map? counterReset, TextDirection? direction, Display? display, String? fontFamily, @@ -397,10 +433,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 +462,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,10 +475,12 @@ 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, @@ -548,30 +590,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