Skip to content

Commit 6dfb1da

Browse files
committed
content: Add start attribute support for ordered list
Fixes: #59
1 parent 9517336 commit 6dfb1da

File tree

4 files changed

+90
-19
lines changed

4 files changed

+90
-19
lines changed

lib/model/content.dart

+32-4
Original file line numberDiff line numberDiff line change
@@ -253,11 +253,11 @@ class HeadingNode extends BlockInlineContainerNode {
253253

254254
enum ListStyle { ordered, unordered }
255255

256-
class ListNode extends BlockContentNode {
257-
const ListNode(this.style, this.items, {super.debugHtmlNode});
256+
abstract class ListNode extends BlockContentNode {
257+
const ListNode({required this.items, super.debugHtmlNode});
258258

259-
final ListStyle style;
260259
final List<List<BlockContentNode>> items;
260+
ListStyle get style;
261261

262262
@override
263263
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@@ -275,6 +275,28 @@ class ListNode extends BlockContentNode {
275275
}
276276
}
277277

278+
class OrderedListNode extends ListNode {
279+
const OrderedListNode({this.start = 1, required super.items, super.debugHtmlNode});
280+
281+
final int start;
282+
283+
@override
284+
ListStyle get style => ListStyle.ordered;
285+
286+
@override
287+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
288+
super.debugFillProperties(properties);
289+
properties.add(IntProperty('start', start));
290+
}
291+
}
292+
293+
class UnorderedListNode extends ListNode {
294+
const UnorderedListNode({required super.items, super.debugHtmlNode});
295+
296+
@override
297+
ListStyle get style => ListStyle.unordered;
298+
}
299+
278300
class QuotationNode extends BlockContentNode {
279301
const QuotationNode(this.nodes, {super.debugHtmlNode});
280302

@@ -1081,7 +1103,13 @@ class _ZulipContentParser {
10811103
items.add(parseImplicitParagraphBlockContentList(item.nodes));
10821104
}
10831105

1084-
return ListNode(listStyle!, items, debugHtmlNode: debugHtmlNode);
1106+
if (listStyle == ListStyle.ordered) {
1107+
final startAttr = element.attributes['start'];
1108+
final start = startAttr != null ? (int.tryParse(startAttr) ?? 1) : 1;
1109+
return OrderedListNode(start: start, items: items, debugHtmlNode: debugHtmlNode);
1110+
} else {
1111+
return UnorderedListNode(items: items, debugHtmlNode: debugHtmlNode);
1112+
}
10851113
}
10861114

10871115
BlockContentNode parseSpoilerNode(dom.Element divElement) {

lib/widgets/content.dart

+1-3
Original file line numberDiff line numberDiff line change
@@ -491,8 +491,7 @@ class ListNodeWidget extends StatelessWidget {
491491
// but that comes out too close to item; not sure what's fixing that
492492
// in a browser
493493
case ListStyle.unordered: marker = "• "; break;
494-
// TODO(#59) ordered lists starting not at 1
495-
case ListStyle.ordered: marker = "${index+1}. "; break;
494+
case ListStyle.ordered: marker = "${(node as OrderedListNode).start + index}. "; break;
496495
}
497496
return ListItemWidget(marker: marker, nodes: item);
498497
});
@@ -516,7 +515,6 @@ class ListItemWidget extends StatelessWidget {
516515
textBaseline: localizedTextBaseline(context),
517516
children: [
518517
SizedBox(
519-
width: 20, // TODO handle long numbers in <ol>, like https://github.com/zulip/zulip/pull/25063
520518
child: Align(
521519
alignment: AlignmentDirectional.topEnd, child: Text(marker))),
522520
Expanded(child: BlockContentList(nodes: nodes)),

test/model/content_test.dart

+42-12
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,8 @@ class ContentExample {
306306
'<p><em>italic</em> <a href="https://zulip.com/">zulip</a></p>\n'
307307
'</div></div>',
308308
[SpoilerNode(
309-
header: [ListNode(ListStyle.ordered, [
310-
[ListNode(ListStyle.unordered, [
309+
header: [OrderedListNode(items: [
310+
[UnorderedListNode(items: [
311311
[HeadingNode(level: HeadingLevel.h2, links: null, nodes: [
312312
TextNode('hello'),
313313
])]
@@ -763,7 +763,7 @@ class ContentExample {
763763
'<div class="message_inline_image">'
764764
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">'
765765
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div></li>\n</ul>', [
766-
ListNode(ListStyle.unordered, [[
766+
UnorderedListNode(items: [[
767767
ImageNodeList([
768768
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png',
769769
thumbnailUrl: null, loading: false,
@@ -785,7 +785,7 @@ class ContentExample {
785785
'<div class="message_inline_image">'
786786
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2" title="icon.png">'
787787
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2"></a></div></li>\n</ul>', [
788-
ListNode(ListStyle.unordered, [[
788+
UnorderedListNode(items: [[
789789
ParagraphNode(wasImplicit: true, links: null, nodes: [
790790
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
791791
TextNode(' '),
@@ -814,7 +814,7 @@ class ContentExample {
814814
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png" title="icon.png">'
815815
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div>'
816816
'more text</li>\n</ul>', [
817-
ListNode(ListStyle.unordered, [[
817+
UnorderedListNode(items: [[
818818
const ParagraphNode(wasImplicit: true, links: null, nodes: [
819819
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
820820
TextNode(' '),
@@ -1382,7 +1382,7 @@ void main() {
13821382
testParse('<ol>',
13831383
// "1. first\n2. then"
13841384
'<ol>\n<li>first</li>\n<li>then</li>\n</ol>', const [
1385-
ListNode(ListStyle.ordered, [
1385+
OrderedListNode(items: [
13861386
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('first')])],
13871387
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('then')])],
13881388
]),
@@ -1391,7 +1391,7 @@ void main() {
13911391
testParse('<ul>',
13921392
// "* something\n* another"
13931393
'<ul>\n<li>something</li>\n<li>another</li>\n</ul>', const [
1394-
ListNode(ListStyle.unordered, [
1394+
UnorderedListNode(items: [
13951395
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('something')])],
13961396
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('another')])],
13971397
]),
@@ -1400,7 +1400,7 @@ void main() {
14001400
testParse('implicit paragraph with internal <br>',
14011401
// "* a\n b"
14021402
'<ul>\n<li>a<br>\n b</li>\n</ul>', const [
1403-
ListNode(ListStyle.unordered, [
1403+
UnorderedListNode(items: [
14041404
[ParagraphNode(wasImplicit: true, links: null, nodes: [
14051405
TextNode('a'),
14061406
LineBreakInlineNode(),
@@ -1412,13 +1412,43 @@ void main() {
14121412
testParse('explicit paragraphs',
14131413
// "* a\n\n b"
14141414
'<ul>\n<li>\n<p>a</p>\n<p>b</p>\n</li>\n</ul>', const [
1415-
ListNode(ListStyle.unordered, [
1415+
UnorderedListNode(items: [
14161416
[
14171417
ParagraphNode(links: null, nodes: [TextNode('a')]),
14181418
ParagraphNode(links: null, nodes: [TextNode('b')]),
14191419
],
14201420
]),
14211421
]);
1422+
1423+
testParse('ordered list - large start number (9999)',
1424+
// "9999. first\n10000. second"
1425+
'<ol start="9999">\n<li>first</li>\n<li>second</li>\n</ol>', const [
1426+
OrderedListNode(
1427+
start: 9999,
1428+
items: [
1429+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('first')])],
1430+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('second')])],
1431+
]),
1432+
]);
1433+
1434+
testParse('ordered list - default start (1)',
1435+
'<ol>\n<li>first</li>\n<li>second</li>\n</ol>', const [
1436+
OrderedListNode(
1437+
items: [
1438+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('first')])],
1439+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('second')])],
1440+
]),
1441+
]);
1442+
1443+
testParse('ordered list - custom start (5)',
1444+
'<ol start="5">\n<li>fifth</li>\n<li>sixth</li>\n</ol>', const [
1445+
OrderedListNode(
1446+
start: 5,
1447+
items: [
1448+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('fifth')])],
1449+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('sixth')])],
1450+
]),
1451+
]);
14221452
});
14231453

14241454
testParseExample(ContentExample.spoilerDefaultHeader);
@@ -1451,7 +1481,7 @@ void main() {
14511481
testParse('link in list item',
14521482
// "* [t](/u)"
14531483
'<ul>\n<li><a href="/u">t</a></li>\n</ul>', const [
1454-
ListNode(ListStyle.unordered, [
1484+
UnorderedListNode(items: [
14551485
[ParagraphNode(links: null, wasImplicit: true, nodes: [
14561486
LinkNode(url: '/u', nodes: [TextNode('t')]),
14571487
])],
@@ -1509,10 +1539,10 @@ void main() {
15091539
'<ol>\n<li>\n<blockquote>\n<h6>two</h6>\n<ul>\n<li>three</li>\n'
15101540
'</ul>\n</blockquote>\n<div class="codehilite"><pre><span></span>'
15111541
'<code>four\n</code></pre></div>\n\n</li>\n</ol>', const [
1512-
ListNode(ListStyle.ordered, [[
1542+
OrderedListNode(items: [[
15131543
QuotationNode([
15141544
HeadingNode(level: HeadingLevel.h6, links: null, nodes: [TextNode('two')]),
1515-
ListNode(ListStyle.unordered, [[
1545+
UnorderedListNode(items: [[
15161546
ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('three')]),
15171547
]]),
15181548
]),

test/widgets/content_test.dart

+15
Original file line numberDiff line numberDiff line change
@@ -1158,4 +1158,19 @@ void main() {
11581158
check(linkText.textAlign).equals(TextAlign.center);
11591159
});
11601160
});
1161+
1162+
group('ListNodeWidget', () {
1163+
testWidgets('ordered list with custom start', (tester) async {
1164+
await prepareContent(tester, plainContent('''
1165+
<ol start="3">
1166+
<li>third</li>
1167+
<li>fourth</li>
1168+
</ol>'''));
1169+
1170+
expect(find.text('3. '), findsOneWidget);
1171+
expect(find.text('4. '), findsOneWidget);
1172+
expect(find.text('third'), findsOneWidget);
1173+
expect(find.text('fourth'), findsOneWidget);
1174+
});
1175+
});
11611176
}

0 commit comments

Comments
 (0)