Skip to content

Commit 99c7df4

Browse files
authored
Merge pull request Sub6Resources#654 from tneotia/feature/list-style-type
Add support for more list style types & detect list style type from inline style
2 parents 2a21971 + 0a4df74 commit 99c7df4

File tree

5 files changed

+151
-7
lines changed

5 files changed

+151
-7
lines changed

example/lib/main.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,11 @@ const htmlData = r"""
102102
<li>a</li>
103103
<li>nested</li>
104104
<li>unordered
105-
<ol>
105+
<ol style="list-style-type: lower-alpha;" start="5">
106106
<li>With a nested</li>
107-
<li>ordered list.</li>
107+
<li>ordered list</li>
108+
<li>with a lower alpha list style</li>
109+
<li>starting at letter e</li>
108110
</ol>
109111
</li>
110112
<li>list</li>

lib/html_parser.dart

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'package:flutter_html/src/utils.dart';
1616
import 'package:flutter_html/style.dart';
1717
import 'package:html/dom.dart' as dom;
1818
import 'package:html/parser.dart' as htmlparser;
19+
import 'package:numerus/numerus.dart';
1920
import 'package:webview_flutter/webview_flutter.dart';
2021

2122
typedef OnTap = void Function(
@@ -591,29 +592,107 @@ class HtmlParser extends StatelessWidget {
591592
///
592593
/// The function uses the [_processListCharactersRecursive] function to do most of its work.
593594
static StyledElement _processListCharacters(StyledElement tree) {
594-
final olStack = ListQueue<Context<int>>();
595+
final olStack = ListQueue<Context>();
595596
tree = _processListCharactersRecursive(tree, olStack);
596597
return tree;
597598
}
598599

599600
/// [_processListCharactersRecursive] uses a Stack of integers to properly number and
600601
/// bullet all list items according to the [ListStyleType] they have been given.
601602
static StyledElement _processListCharactersRecursive(
602-
StyledElement tree, ListQueue<Context<int>> olStack) {
603-
if (tree.name == 'ol') {
604-
olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
603+
StyledElement tree, ListQueue<Context> olStack) {
604+
if (tree.name == 'ol' && tree.style.listStyleType != null) {
605+
switch (tree.style.listStyleType!) {
606+
case ListStyleType.LOWER_LATIN:
607+
case ListStyleType.LOWER_ALPHA:
608+
case ListStyleType.UPPER_LATIN:
609+
case ListStyleType.UPPER_ALPHA:
610+
olStack.add(Context<String>('a'));
611+
if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) {
612+
var start = int.tryParse(tree.attributes['start']!) ?? 1;
613+
var x = 1;
614+
while (x < start) {
615+
olStack.last.data = olStack.last.data.toString().nextLetter();
616+
x++;
617+
}
618+
}
619+
break;
620+
default:
621+
olStack.add(Context<int>((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
622+
break;
623+
}
605624
} else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null) {
606625
switch (tree.style.listStyleType!) {
626+
case ListStyleType.CIRCLE:
627+
tree.style.markerContent = '○';
628+
break;
629+
case ListStyleType.SQUARE:
630+
tree.style.markerContent = '■';
631+
break;
607632
case ListStyleType.DISC:
608633
tree.style.markerContent = '•';
609634
break;
610635
case ListStyleType.DECIMAL:
611636
if (olStack.isEmpty) {
612-
olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
637+
olStack.add(Context<int>((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
613638
}
614639
olStack.last.data += 1;
615640
tree.style.markerContent = '${olStack.last.data}.';
616641
break;
642+
case ListStyleType.LOWER_LATIN:
643+
case ListStyleType.LOWER_ALPHA:
644+
if (olStack.isEmpty) {
645+
olStack.add(Context<String>('a'));
646+
if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) {
647+
var start = int.tryParse(tree.attributes['start']!) ?? 1;
648+
var x = 1;
649+
while (x < start) {
650+
olStack.last.data = olStack.last.data.toString().nextLetter();
651+
x++;
652+
}
653+
}
654+
}
655+
tree.style.markerContent = olStack.last.data.toString() + ".";
656+
olStack.last.data = olStack.last.data.toString().nextLetter();
657+
break;
658+
case ListStyleType.UPPER_LATIN:
659+
case ListStyleType.UPPER_ALPHA:
660+
if (olStack.isEmpty) {
661+
olStack.add(Context<String>('a'));
662+
if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) {
663+
var start = int.tryParse(tree.attributes['start']!) ?? 1;
664+
var x = 1;
665+
while (x < start) {
666+
olStack.last.data = olStack.last.data.toString().nextLetter();
667+
x++;
668+
}
669+
}
670+
}
671+
tree.style.markerContent = olStack.last.data.toString().toUpperCase() + ".";
672+
olStack.last.data = olStack.last.data.toString().nextLetter();
673+
break;
674+
case ListStyleType.LOWER_ROMAN:
675+
if (olStack.isEmpty) {
676+
olStack.add(Context<int>((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
677+
}
678+
olStack.last.data += 1;
679+
if (olStack.last.data <= 0) {
680+
tree.style.markerContent = '${olStack.last.data}.';
681+
} else {
682+
tree.style.markerContent = (olStack.last.data as int).toRomanNumeralString()!.toLowerCase() + ".";
683+
}
684+
break;
685+
case ListStyleType.UPPER_ROMAN:
686+
if (olStack.isEmpty) {
687+
olStack.add(Context<int>((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
688+
}
689+
olStack.last.data += 1;
690+
if (olStack.last.data <= 0) {
691+
tree.style.markerContent = '${olStack.last.data}.';
692+
} else {
693+
tree.style.markerContent = (olStack.last.data as int).toRomanNumeralString()! + ".";
694+
}
695+
break;
617696
}
618697
}
619698

@@ -902,3 +981,24 @@ class StyledText extends StatelessWidget {
902981
return null;
903982
}
904983
}
984+
985+
extension IterateLetters on String {
986+
String nextLetter() {
987+
String s = this.toLowerCase();
988+
if (s == "z") {
989+
return String.fromCharCode(s.codeUnitAt(0) - 25) + String.fromCharCode(s.codeUnitAt(0) - 25); // AA or aa
990+
} else {
991+
var lastChar = s.substring(s.length - 1);
992+
var sub = s.substring(0, s.length - 1);
993+
if (lastChar == "z") {
994+
// If a string of length > 1 ends in Z/z,
995+
// increment the string (excluding the last Z/z) recursively,
996+
// and append A/a (depending on casing) to it
997+
return sub.nextLetter() + 'a';
998+
} else {
999+
// (take till last char) append with (increment last char)
1000+
return sub + String.fromCharCode(lastChar.codeUnitAt(0) + 1);
1001+
}
1002+
}
1003+
}
1004+
}

lib/src/css_parser.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ Style declarationsToStyle(Map<String, List<css.Expression>> declarations) {
194194
case 'font-weight':
195195
style.fontWeight = ExpressionMapping.expressionToFontWeight(value.first);
196196
break;
197+
case 'list-style-type':
198+
if (value.first is css.LiteralTerm) {
199+
style.listStyleType = ExpressionMapping.expressionToListStyleType(value.first as css.LiteralTerm) ?? style.listStyleType;
200+
}
201+
break;
197202
case 'margin':
198203
List<css.LiteralTerm>? marginLengths = value.whereType<css.LiteralTerm>().toList();
199204
/// List<css.LiteralTerm> 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 {
655660
return LineHeight.normal;
656661
}
657662

663+
static ListStyleType? expressionToListStyleType(css.LiteralTerm value) {
664+
switch (value.text) {
665+
case 'disc':
666+
return ListStyleType.DISC;
667+
case 'circle':
668+
return ListStyleType.CIRCLE;
669+
case 'decimal':
670+
return ListStyleType.DECIMAL;
671+
case 'lower-alpha':
672+
return ListStyleType.LOWER_ALPHA;
673+
case 'lower-latin':
674+
return ListStyleType.LOWER_LATIN;
675+
case 'lower-roman':
676+
return ListStyleType.LOWER_ROMAN;
677+
case 'square':
678+
return ListStyleType.SQUARE;
679+
case 'upper-alpha':
680+
return ListStyleType.UPPER_ALPHA;
681+
case 'upper-latin':
682+
return ListStyleType.UPPER_LATIN;
683+
case 'upper-roman':
684+
return ListStyleType.UPPER_ROMAN;
685+
}
686+
return null;
687+
}
688+
658689
static List<double?> expressionToPadding(List<css.Expression>? lengths) {
659690
double? left;
660691
double? right;

lib/style.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,8 +520,16 @@ class LineHeight {
520520
}
521521

522522
enum ListStyleType {
523+
LOWER_ALPHA,
524+
UPPER_ALPHA,
525+
LOWER_LATIN,
526+
UPPER_LATIN,
527+
CIRCLE,
523528
DISC,
524529
DECIMAL,
530+
LOWER_ROMAN,
531+
UPPER_ROMAN,
532+
SQUARE,
525533
}
526534

527535
enum ListStylePosition {

pubspec.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ dependencies:
3636
# plugin for firstWhereOrNull extension on lists
3737
collection: '>=1.15.0 <2.0.0'
3838

39+
# plugin to convert integers to numerals
40+
numerus: '>=1.1.1 <2.0.0'
41+
3942
flutter:
4043
sdk: flutter
4144

0 commit comments

Comments
 (0)