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
-
+
- With a nested
- - ordered list.
+ - ordered list
+ - with a lower alpha list style
+ - starting at letter e
list
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