Skip to content

Commit 1a1f6f7

Browse files
committed
Add support for more list style types & detect list style type from inline style
1 parent 89a89ba commit 1a1f6f7

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
@@ -100,9 +100,11 @@ const htmlData = r"""
100100
<li>a</li>
101101
<li>nested</li>
102102
<li>unordered
103-
<ol>
103+
<ol style="list-style-type: lower-alpha;" start="5">
104104
<li>With a nested</li>
105-
<li>ordered list.</li>
105+
<li>ordered list</li>
106+
<li>with a lower alpha list style</li>
107+
<li>starting at letter e</li>
106108
</ol>
107109
</li>
108110
<li>list</li>

lib/html_parser.dart

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

2021
typedef OnTap = void Function(
@@ -536,29 +537,107 @@ class HtmlParser extends StatelessWidget {
536537
///
537538
/// The function uses the [_processListCharactersRecursive] function to do most of its work.
538539
static StyledElement _processListCharacters(StyledElement tree) {
539-
final olStack = ListQueue<Context<int>>();
540+
final olStack = ListQueue<Context>();
540541
tree = _processListCharactersRecursive(tree, olStack);
541542
return tree;
542543
}
543544

544545
/// [_processListCharactersRecursive] uses a Stack of integers to properly number and
545546
/// bullet all list items according to the [ListStyleType] they have been given.
546547
static StyledElement _processListCharactersRecursive(
547-
StyledElement tree, ListQueue<Context<int>> olStack) {
548-
if (tree.name == 'ol') {
549-
olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
548+
StyledElement tree, ListQueue<Context> olStack) {
549+
if (tree.name == 'ol' && tree.style.listStyleType != null) {
550+
switch (tree.style.listStyleType!) {
551+
case ListStyleType.LOWER_LATIN:
552+
case ListStyleType.LOWER_ALPHA:
553+
case ListStyleType.UPPER_LATIN:
554+
case ListStyleType.UPPER_ALPHA:
555+
olStack.add(Context<String>('a'));
556+
if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) {
557+
var start = int.tryParse(tree.attributes['start']!) ?? 1;
558+
var x = 1;
559+
while (x < start) {
560+
olStack.last.data = olStack.last.data.toString().nextLetter();
561+
x++;
562+
}
563+
}
564+
break;
565+
default:
566+
olStack.add(Context<int>((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
567+
break;
568+
}
550569
} else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null) {
551570
switch (tree.style.listStyleType!) {
571+
case ListStyleType.CIRCLE:
572+
tree.style.markerContent = '○';
573+
break;
574+
case ListStyleType.SQUARE:
575+
tree.style.markerContent = '■';
576+
break;
552577
case ListStyleType.DISC:
553578
tree.style.markerContent = '•';
554579
break;
555580
case ListStyleType.DECIMAL:
556581
if (olStack.isEmpty) {
557-
olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
582+
olStack.add(Context<int>((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
558583
}
559584
olStack.last.data += 1;
560585
tree.style.markerContent = '${olStack.last.data}.';
561586
break;
587+
case ListStyleType.LOWER_LATIN:
588+
case ListStyleType.LOWER_ALPHA:
589+
if (olStack.isEmpty) {
590+
olStack.add(Context<String>('a'));
591+
if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) {
592+
var start = int.tryParse(tree.attributes['start']!) ?? 1;
593+
var x = 1;
594+
while (x < start) {
595+
olStack.last.data = olStack.last.data.toString().nextLetter();
596+
x++;
597+
}
598+
}
599+
}
600+
tree.style.markerContent = olStack.last.data.toString() + ".";
601+
olStack.last.data = olStack.last.data.toString().nextLetter();
602+
break;
603+
case ListStyleType.UPPER_LATIN:
604+
case ListStyleType.UPPER_ALPHA:
605+
if (olStack.isEmpty) {
606+
olStack.add(Context<String>('a'));
607+
if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) {
608+
var start = int.tryParse(tree.attributes['start']!) ?? 1;
609+
var x = 1;
610+
while (x < start) {
611+
olStack.last.data = olStack.last.data.toString().nextLetter();
612+
x++;
613+
}
614+
}
615+
}
616+
tree.style.markerContent = olStack.last.data.toString().toUpperCase() + ".";
617+
olStack.last.data = olStack.last.data.toString().nextLetter();
618+
break;
619+
case ListStyleType.LOWER_ROMAN:
620+
if (olStack.isEmpty) {
621+
olStack.add(Context<int>((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
622+
}
623+
olStack.last.data += 1;
624+
if (olStack.last.data <= 0) {
625+
tree.style.markerContent = '${olStack.last.data}.';
626+
} else {
627+
tree.style.markerContent = (olStack.last.data as int).toRomanNumeralString()!.toLowerCase() + ".";
628+
}
629+
break;
630+
case ListStyleType.UPPER_ROMAN:
631+
if (olStack.isEmpty) {
632+
olStack.add(Context<int>((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
633+
}
634+
olStack.last.data += 1;
635+
if (olStack.last.data <= 0) {
636+
tree.style.markerContent = '${olStack.last.data}.';
637+
} else {
638+
tree.style.markerContent = (olStack.last.data as int).toRomanNumeralString()! + ".";
639+
}
640+
break;
562641
}
563642
}
564643

@@ -847,3 +926,24 @@ class StyledText extends StatelessWidget {
847926
return null;
848927
}
849928
}
929+
930+
extension IterateLetters on String {
931+
String nextLetter() {
932+
String s = this.toLowerCase();
933+
if (s == "z") {
934+
return String.fromCharCode(s.codeUnitAt(0) - 25) + String.fromCharCode(s.codeUnitAt(0) - 25); // AA or aa
935+
} else {
936+
var lastChar = s.substring(s.length - 1);
937+
var sub = s.substring(0, s.length - 1);
938+
if (lastChar == "z") {
939+
// If a string of length > 1 ends in Z/z,
940+
// increment the string (excluding the last Z/z) recursively,
941+
// and append A/a (depending on casing) to it
942+
return sub.nextLetter() + 'a';
943+
} else {
944+
// (take till last char) append with (increment last char)
945+
return sub + String.fromCharCode(lastChar.codeUnitAt(0) + 1);
946+
}
947+
}
948+
}
949+
}

lib/src/css_parser.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ Style declarationsToStyle(Map<String?, List<css.Expression>> declarations) {
6161
case 'font-weight':
6262
style.fontWeight = ExpressionMapping.expressionToFontWeight(value.first);
6363
break;
64+
case 'list-style-type':
65+
if (value.first is css.LiteralTerm) {
66+
style.listStyleType = ExpressionMapping.expressionToListStyleType(value.first as css.LiteralTerm) ?? style.listStyleType;
67+
}
68+
break;
6469
case 'text-align':
6570
style.textAlign = ExpressionMapping.expressionToTextAlign(value.first);
6671
break;
@@ -419,6 +424,32 @@ class ExpressionMapping {
419424
return LineHeight.normal;
420425
}
421426

427+
static ListStyleType? expressionToListStyleType(css.LiteralTerm value) {
428+
switch (value.text) {
429+
case 'disc':
430+
return ListStyleType.DISC;
431+
case 'circle':
432+
return ListStyleType.CIRCLE;
433+
case 'decimal':
434+
return ListStyleType.DECIMAL;
435+
case 'lower-alpha':
436+
return ListStyleType.LOWER_ALPHA;
437+
case 'lower-latin':
438+
return ListStyleType.LOWER_LATIN;
439+
case 'lower-roman':
440+
return ListStyleType.LOWER_ROMAN;
441+
case 'square':
442+
return ListStyleType.SQUARE;
443+
case 'upper-alpha':
444+
return ListStyleType.UPPER_ALPHA;
445+
case 'upper-latin':
446+
return ListStyleType.UPPER_LATIN;
447+
case 'upper-roman':
448+
return ListStyleType.UPPER_ROMAN;
449+
}
450+
return null;
451+
}
452+
422453
static TextAlign expressionToTextAlign(css.Expression value) {
423454
if (value is css.LiteralTerm) {
424455
switch(value.text) {

lib/style.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,8 +474,16 @@ class LineHeight {
474474
}
475475

476476
enum ListStyleType {
477+
LOWER_ALPHA,
478+
UPPER_ALPHA,
479+
LOWER_LATIN,
480+
UPPER_LATIN,
481+
CIRCLE,
477482
DISC,
478483
DECIMAL,
484+
LOWER_ROMAN,
485+
UPPER_ROMAN,
486+
SQUARE,
479487
}
480488

481489
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)