Skip to content

Commit 0c6fda4

Browse files
committed
content: Add start attribute support for ordered list
Fixes: zulip#59
1 parent a055486 commit 0c6fda4

File tree

4 files changed

+96
-43
lines changed

4 files changed

+96
-43
lines changed

lib/model/content.dart

+27-19
Original file line numberDiff line numberDiff line change
@@ -251,21 +251,11 @@ class HeadingNode extends BlockInlineContainerNode {
251251
}
252252
}
253253

254-
enum ListStyle { ordered, unordered }
254+
sealed class ListNode extends BlockContentNode {
255+
const ListNode({required this.items, super.debugHtmlNode});
255256

256-
class ListNode extends BlockContentNode {
257-
const ListNode(this.style, this.items, {super.debugHtmlNode});
258-
259-
final ListStyle style;
260257
final List<List<BlockContentNode>> items;
261258

262-
@override
263-
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
264-
super.debugFillProperties(properties);
265-
properties.add(FlagProperty('ordered', value: style == ListStyle.ordered,
266-
ifTrue: 'ordered', ifFalse: 'unordered'));
267-
}
268-
269259
@override
270260
List<DiagnosticsNode> debugDescribeChildren() {
271261
return items
@@ -275,6 +265,22 @@ class ListNode extends BlockContentNode {
275265
}
276266
}
277267

268+
class UnorderedListNode extends ListNode {
269+
const UnorderedListNode({required super.items, super.debugHtmlNode});
270+
}
271+
272+
class OrderedListNode extends ListNode {
273+
const OrderedListNode({required super.items, required this.start, super.debugHtmlNode});
274+
275+
final int start;
276+
277+
@override
278+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
279+
super.debugFillProperties(properties);
280+
properties.add(IntProperty('start', start));
281+
}
282+
}
283+
278284
class QuotationNode extends BlockContentNode {
279285
const QuotationNode(this.nodes, {super.debugHtmlNode});
280286

@@ -1063,12 +1069,6 @@ class _ZulipContentParser {
10631069
}
10641070

10651071
BlockContentNode parseListNode(dom.Element element) {
1066-
ListStyle? listStyle;
1067-
switch (element.localName) {
1068-
case 'ol': listStyle = ListStyle.ordered; break;
1069-
case 'ul': listStyle = ListStyle.unordered; break;
1070-
}
1071-
assert(listStyle != null);
10721072
assert(element.className.isEmpty);
10731073

10741074
final debugHtmlNode = kDebugMode ? element : null;
@@ -1081,7 +1081,15 @@ class _ZulipContentParser {
10811081
items.add(parseImplicitParagraphBlockContentList(item.nodes));
10821082
}
10831083

1084-
return ListNode(listStyle!, items, debugHtmlNode: debugHtmlNode);
1084+
if (element.localName == 'ol') {
1085+
final startAttr = element.attributes['start'];
1086+
final start = startAttr == null ? 1
1087+
: int.tryParse(startAttr, radix: 10);
1088+
if (start == null) return UnimplementedBlockContentNode(htmlNode: element);
1089+
return OrderedListNode(start: start, items: items, debugHtmlNode: debugHtmlNode);
1090+
} else {
1091+
return UnorderedListNode(items: items, debugHtmlNode: debugHtmlNode);
1092+
}
10851093
}
10861094

10871095
BlockContentNode parseSpoilerNode(dom.Element divElement) {

lib/widgets/content.dart

+3-4
Original file line numberDiff line numberDiff line change
@@ -482,17 +482,16 @@ class ListNodeWidget extends StatelessWidget {
482482
final items = List.generate(node.items.length, (index) {
483483
final item = node.items[index];
484484
String marker;
485-
switch (node.style) {
485+
switch (node) {
486486
// TODO(#161): different unordered marker styles at different levels of nesting
487487
// see:
488488
// https://html.spec.whatwg.org/multipage/rendering.html#lists
489489
// https://www.w3.org/TR/css-counter-styles-3/#simple-symbolic
490490
// TODO proper alignment of unordered marker; should be "• ", one space,
491491
// but that comes out too close to item; not sure what's fixing that
492492
// in a browser
493-
case ListStyle.unordered: marker = "• "; break;
494-
// TODO(#59) ordered lists starting not at 1
495-
case ListStyle.ordered: marker = "${index+1}. "; break;
493+
case UnorderedListNode(): marker = "• "; break;
494+
case OrderedListNode(:final start): marker = "${start + index}. "; break;
496495
}
497496
return ListItemWidget(marker: marker, nodes: item);
498497
});

test/model/content_test.dart

+55-20
Original file line numberDiff line numberDiff line change
@@ -306,13 +306,16 @@ 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, [
311-
[HeadingNode(level: HeadingLevel.h2, links: null, nodes: [
312-
TextNode('hello'),
313-
])]
314-
])],
315-
])],
309+
header: [OrderedListNode(
310+
start: 1,
311+
items: [
312+
[UnorderedListNode(items: [
313+
[HeadingNode(level: HeadingLevel.h2, links: null, nodes: [
314+
TextNode('hello'),
315+
])]
316+
])],
317+
]),
318+
],
316319
content: [ParagraphNode(links: null, nodes: [
317320
EmphasisNode(nodes: [TextNode('italic')]),
318321
TextNode(' '),
@@ -763,7 +766,7 @@ class ContentExample {
763766
'<div class="message_inline_image">'
764767
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">'
765768
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div></li>\n</ul>', [
766-
ListNode(ListStyle.unordered, [[
769+
UnorderedListNode(items: [[
767770
ImageNodeList([
768771
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png',
769772
thumbnailUrl: null, loading: false,
@@ -785,7 +788,7 @@ class ContentExample {
785788
'<div class="message_inline_image">'
786789
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2" title="icon.png">'
787790
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2"></a></div></li>\n</ul>', [
788-
ListNode(ListStyle.unordered, [[
791+
UnorderedListNode(items: [[
789792
ParagraphNode(wasImplicit: true, links: null, nodes: [
790793
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
791794
TextNode(' '),
@@ -814,7 +817,7 @@ class ContentExample {
814817
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png" title="icon.png">'
815818
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div>'
816819
'more text</li>\n</ul>', [
817-
ListNode(ListStyle.unordered, [[
820+
UnorderedListNode(items: [[
818821
const ParagraphNode(wasImplicit: true, links: null, nodes: [
819822
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
820823
TextNode(' '),
@@ -1155,6 +1158,32 @@ class ContentExample {
11551158
], isHeader: false),
11561159
]),
11571160
]);
1161+
1162+
static const orderedListLargeStart = ContentExample(
1163+
'ordered list with large start number',
1164+
'9999. first\n10000. second',
1165+
'<ol start="9999">\n<li>first</li>\n<li>second</li>\n</ol>',
1166+
[OrderedListNode(
1167+
start: 9999,
1168+
items: [
1169+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('first')])],
1170+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('second')])],
1171+
])
1172+
],
1173+
);
1174+
1175+
static const orderedListCustomStart = ContentExample(
1176+
'ordered list with custom start',
1177+
'5. fifth\n6. sixth',
1178+
'<ol start="5">\n<li>fifth</li>\n<li>sixth</li>\n</ol>',
1179+
[OrderedListNode(
1180+
start: 5,
1181+
items: [
1182+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('fifth')])],
1183+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('sixth')])],
1184+
])
1185+
],
1186+
);
11581187
}
11591188

11601189
UnimplementedBlockContentNode blockUnimplemented(String html) {
@@ -1382,16 +1411,18 @@ void main() {
13821411
testParse('<ol>',
13831412
// "1. first\n2. then"
13841413
'<ol>\n<li>first</li>\n<li>then</li>\n</ol>', const [
1385-
ListNode(ListStyle.ordered, [
1386-
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('first')])],
1387-
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('then')])],
1388-
]),
1414+
OrderedListNode(
1415+
start: 1,
1416+
items: [
1417+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('first')])],
1418+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('then')])],
1419+
]),
13891420
]);
13901421

13911422
testParse('<ul>',
13921423
// "* something\n* another"
13931424
'<ul>\n<li>something</li>\n<li>another</li>\n</ul>', const [
1394-
ListNode(ListStyle.unordered, [
1425+
UnorderedListNode(items: [
13951426
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('something')])],
13961427
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('another')])],
13971428
]),
@@ -1400,7 +1431,7 @@ void main() {
14001431
testParse('implicit paragraph with internal <br>',
14011432
// "* a\n b"
14021433
'<ul>\n<li>a<br>\n b</li>\n</ul>', const [
1403-
ListNode(ListStyle.unordered, [
1434+
UnorderedListNode(items: [
14041435
[ParagraphNode(wasImplicit: true, links: null, nodes: [
14051436
TextNode('a'),
14061437
LineBreakInlineNode(),
@@ -1412,7 +1443,7 @@ void main() {
14121443
testParse('explicit paragraphs',
14131444
// "* a\n\n b"
14141445
'<ul>\n<li>\n<p>a</p>\n<p>b</p>\n</li>\n</ul>', const [
1415-
ListNode(ListStyle.unordered, [
1446+
UnorderedListNode(items: [
14161447
[
14171448
ParagraphNode(links: null, nodes: [TextNode('a')]),
14181449
ParagraphNode(links: null, nodes: [TextNode('b')]),
@@ -1451,7 +1482,7 @@ void main() {
14511482
testParse('link in list item',
14521483
// "* [t](/u)"
14531484
'<ul>\n<li><a href="/u">t</a></li>\n</ul>', const [
1454-
ListNode(ListStyle.unordered, [
1485+
UnorderedListNode(items: [
14551486
[ParagraphNode(links: null, wasImplicit: true, nodes: [
14561487
LinkNode(url: '/u', nodes: [TextNode('t')]),
14571488
])],
@@ -1503,16 +1534,20 @@ void main() {
15031534
testParseExample(ContentExample.tableMissingOneBodyColumnInMarkdown);
15041535
testParseExample(ContentExample.tableWithDifferentTextAlignmentInColumns);
15051536
testParseExample(ContentExample.tableWithLinkCenterAligned);
1537+
testParseExample(ContentExample.orderedListLargeStart);
1538+
testParseExample(ContentExample.orderedListCustomStart);
15061539

15071540
testParse('parse nested lists, quotes, headings, code blocks',
15081541
// "1. > ###### two\n > * three\n\n four"
15091542
'<ol>\n<li>\n<blockquote>\n<h6>two</h6>\n<ul>\n<li>three</li>\n'
15101543
'</ul>\n</blockquote>\n<div class="codehilite"><pre><span></span>'
15111544
'<code>four\n</code></pre></div>\n\n</li>\n</ol>', const [
1512-
ListNode(ListStyle.ordered, [[
1545+
OrderedListNode(
1546+
start: 1,
1547+
items: [[
15131548
QuotationNode([
15141549
HeadingNode(level: HeadingLevel.h6, links: null, nodes: [TextNode('two')]),
1515-
ListNode(ListStyle.unordered, [[
1550+
UnorderedListNode(items: [[
15161551
ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('three')]),
15171552
]]),
15181553
]),

test/widgets/content_test.dart

+11
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,17 @@ void main() {
230230
});
231231
});
232232

233+
group('ListNodeWidget', () {
234+
testWidgets('ordered list with custom start', (tester) async {
235+
await prepareContent(tester, plainContent('<ol start="3">\n<li>third</li>\n<li>fourth</li>\n</ol>'));
236+
237+
expect(find.text('3. '), findsOneWidget);
238+
expect(find.text('4. '), findsOneWidget);
239+
expect(find.text('third'), findsOneWidget);
240+
expect(find.text('fourth'), findsOneWidget);
241+
});
242+
});
243+
233244
group('Spoiler', () {
234245
testContentSmoke(ContentExample.spoilerDefaultHeader);
235246
testContentSmoke(ContentExample.spoilerPlainCustomHeader);

0 commit comments

Comments
 (0)