From 1a1f6f77d8d3e636a3c11b0a658dae974081f593 Mon Sep 17 00:00:00 2001 From: tanay Date: Sat, 1 May 2021 17:33:10 -0400 Subject: [PATCH] Add support for more list style types & detect list style type from inline style --- example/lib/main.dart | 6 ++- lib/html_parser.dart | 110 ++++++++++++++++++++++++++++++++++++++-- lib/src/css_parser.dart | 31 +++++++++++ lib/style.dart | 8 +++ pubspec.yaml | 3 ++ 5 files changed, 151 insertions(+), 7 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 665fc768f7..e70f330d9b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -100,9 +100,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 8a83e24996..978660fa60 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( @@ -536,7 +537,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; } @@ -544,21 +545,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; } } @@ -847,3 +926,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 d731edfe61..1a4abd70c1 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -61,6 +61,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 'text-align': style.textAlign = ExpressionMapping.expressionToTextAlign(value.first); break; @@ -419,6 +424,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 TextAlign expressionToTextAlign(css.Expression value) { if (value is css.LiteralTerm) { switch(value.text) { diff --git a/lib/style.dart b/lib/style.dart index 651efb91eb..bead95c276 100644 --- a/lib/style.dart +++ b/lib/style.dart @@ -474,8 +474,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 65c066f0fa..cc887c36db 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