diff --git a/example/lib/main.dart b/example/lib/main.dart index 8a50a99279..e303346363 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -102,9 +102,11 @@ const htmlData = r"""
  • a
  • nested
  • unordered -
      +
      1. With a nested
      2. -
      3. ordered list.
      4. +
      5. ordered list
      6. +
      7. with a lower alpha list style
      8. +
      9. starting at letter e
    1. list
    2. diff --git a/lib/html_parser.dart b/lib/html_parser.dart index c984be592d..336a6c70f3 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -15,6 +15,7 @@ import 'package:flutter_html/src/utils.dart'; import 'package:flutter_html/style.dart'; import 'package:html/dom.dart' as dom; import 'package:html/parser.dart' as htmlparser; +import 'package:numerus/numerus.dart'; import 'package:webview_flutter/webview_flutter.dart'; typedef OnTap = void Function( @@ -575,7 +576,7 @@ class HtmlParser extends StatelessWidget { /// /// The function uses the [_processListCharactersRecursive] function to do most of its work. static StyledElement _processListCharacters(StyledElement tree) { - final olStack = ListQueue>(); + final olStack = ListQueue(); tree = _processListCharactersRecursive(tree, olStack); return tree; } @@ -583,21 +584,99 @@ class HtmlParser extends StatelessWidget { /// [_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) { - if (tree.name == 'ol') { - olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1)); + StyledElement tree, ListQueue olStack) { + if (tree.name == 'ol' && tree.style.listStyleType != null) { + switch (tree.style.listStyleType!) { + case ListStyleType.LOWER_LATIN: + case ListStyleType.LOWER_ALPHA: + case ListStyleType.UPPER_LATIN: + case ListStyleType.UPPER_ALPHA: + 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; + } } else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null) { switch (tree.style.listStyleType!) { + case ListStyleType.CIRCLE: + tree.style.markerContent = '○'; + break; + case ListStyleType.SQUARE: + tree.style.markerContent = '■'; + break; case ListStyleType.DISC: tree.style.markerContent = '•'; break; case ListStyleType.DECIMAL: if (olStack.isEmpty) { - olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1)); + olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1)); } olStack.last.data += 1; tree.style.markerContent = '${olStack.last.data}.'; break; + case ListStyleType.LOWER_LATIN: + case ListStyleType.LOWER_ALPHA: + 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++; + } + } + } + tree.style.markerContent = olStack.last.data.toString() + "."; + olStack.last.data = olStack.last.data.toString().nextLetter(); + break; + case ListStyleType.UPPER_LATIN: + case ListStyleType.UPPER_ALPHA: + 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++; + } + } + } + tree.style.markerContent = olStack.last.data.toString().toUpperCase() + "."; + olStack.last.data = olStack.last.data.toString().nextLetter(); + break; + case ListStyleType.LOWER_ROMAN: + 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) { + tree.style.markerContent = '${olStack.last.data}.'; + } else { + tree.style.markerContent = (olStack.last.data as int).toRomanNumeralString()!.toLowerCase() + "."; + } + break; + case ListStyleType.UPPER_ROMAN: + 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) { + tree.style.markerContent = '${olStack.last.data}.'; + } else { + tree.style.markerContent = (olStack.last.data as int).toRomanNumeralString()! + "."; + } + break; } } @@ -889,3 +968,24 @@ class StyledText extends StatelessWidget { return null; } } + +extension IterateLetters on String { + String nextLetter() { + String s = this.toLowerCase(); + if (s == "z") { + return String.fromCharCode(s.codeUnitAt(0) - 25) + String.fromCharCode(s.codeUnitAt(0) - 25); // AA or aa + } else { + var lastChar = s.substring(s.length - 1); + var sub = s.substring(0, s.length - 1); + if (lastChar == "z") { + // If a string of length > 1 ends in Z/z, + // increment the string (excluding the last Z/z) recursively, + // and append A/a (depending on casing) to it + return sub.nextLetter() + 'a'; + } else { + // (take till last char) append with (increment last char) + return sub + String.fromCharCode(lastChar.codeUnitAt(0) + 1); + } + } + } +} diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart index aea2140c4a..b945b8b2cf 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -194,6 +194,11 @@ Style declarationsToStyle(Map> declarations) { case 'font-weight': style.fontWeight = ExpressionMapping.expressionToFontWeight(value.first); break; + case 'list-style-type': + if (value.first is css.LiteralTerm) { + style.listStyleType = ExpressionMapping.expressionToListStyleType(value.first as css.LiteralTerm) ?? style.listStyleType; + } + break; case 'margin': List? marginLengths = value.whereType().toList(); /// List might include other values than the ones we want for margin length, so make sure to remove those before passing it to [ExpressionMapping] @@ -655,6 +660,32 @@ class ExpressionMapping { return LineHeight.normal; } + static ListStyleType? expressionToListStyleType(css.LiteralTerm value) { + switch (value.text) { + case 'disc': + return ListStyleType.DISC; + case 'circle': + return ListStyleType.CIRCLE; + case 'decimal': + return ListStyleType.DECIMAL; + case 'lower-alpha': + return ListStyleType.LOWER_ALPHA; + case 'lower-latin': + return ListStyleType.LOWER_LATIN; + case 'lower-roman': + return ListStyleType.LOWER_ROMAN; + case 'square': + return ListStyleType.SQUARE; + case 'upper-alpha': + return ListStyleType.UPPER_ALPHA; + case 'upper-latin': + return ListStyleType.UPPER_LATIN; + case 'upper-roman': + return ListStyleType.UPPER_ROMAN; + } + return null; + } + static List expressionToPadding(List? lengths) { double? left; double? right; diff --git a/lib/style.dart b/lib/style.dart index ac72d7d676..41a5b6cd14 100644 --- a/lib/style.dart +++ b/lib/style.dart @@ -520,8 +520,16 @@ class LineHeight { } enum ListStyleType { + LOWER_ALPHA, + UPPER_ALPHA, + LOWER_LATIN, + UPPER_LATIN, + CIRCLE, DISC, DECIMAL, + LOWER_ROMAN, + UPPER_ROMAN, + SQUARE, } enum ListStylePosition { diff --git a/pubspec.yaml b/pubspec.yaml index 04b7d5a03b..1c3dccc966 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,6 +36,9 @@ dependencies: # plugin for firstWhereOrNull extension on lists collection: '>=1.15.0 <2.0.0' + # plugin to convert integers to numerals + numerus: '>=1.1.1 <2.0.0' + flutter: sdk: flutter