Skip to content

Commit 2b96569

Browse files
committed
content: Add start attribute support for ordered list
Fixes: #59
1 parent e0df0ed commit 2b96569

File tree

4 files changed

+84
-43
lines changed

4 files changed

+84
-43
lines changed

lib/model/content.dart

Lines changed: 28 additions & 19 deletions
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(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(super.items, {super.debugHtmlNode});
270+
}
271+
272+
class OrderedListNode extends ListNode {
273+
const OrderedListNode(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

@@ -1108,12 +1114,7 @@ class _ZulipContentParser {
11081114
}
11091115

11101116
BlockContentNode parseListNode(dom.Element element) {
1111-
ListStyle? listStyle;
1112-
switch (element.localName) {
1113-
case 'ol': listStyle = ListStyle.ordered; break;
1114-
case 'ul': listStyle = ListStyle.unordered; break;
1115-
}
1116-
assert(listStyle != null);
1117+
assert(element.localName == 'ol' || element.localName == 'ul');
11171118
assert(element.className.isEmpty);
11181119

11191120
final debugHtmlNode = kDebugMode ? element : null;
@@ -1126,7 +1127,15 @@ class _ZulipContentParser {
11261127
items.add(parseImplicitParagraphBlockContentList(item.nodes));
11271128
}
11281129

1129-
return ListNode(listStyle!, items, debugHtmlNode: debugHtmlNode);
1130+
if (element.localName == 'ol') {
1131+
final startAttr = element.attributes['start'];
1132+
final start = startAttr == null ? 1
1133+
: int.tryParse(startAttr, radix: 10);
1134+
if (start == null) return UnimplementedBlockContentNode(htmlNode: element);
1135+
return OrderedListNode(items, start: start, debugHtmlNode: debugHtmlNode);
1136+
} else {
1137+
return UnorderedListNode(items, debugHtmlNode: debugHtmlNode);
1138+
}
11301139
}
11311140

11321141
BlockContentNode parseSpoilerNode(dom.Element divElement) {

lib/widgets/content.dart

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -492,17 +492,16 @@ class ListNodeWidget extends StatelessWidget {
492492
final items = List.generate(node.items.length, (index) {
493493
final item = node.items[index];
494494
String marker;
495-
switch (node.style) {
495+
switch (node) {
496496
// TODO(#161): different unordered marker styles at different levels of nesting
497497
// see:
498498
// https://html.spec.whatwg.org/multipage/rendering.html#lists
499499
// https://www.w3.org/TR/css-counter-styles-3/#simple-symbolic
500500
// TODO proper alignment of unordered marker; should be "• ", one space,
501501
// but that comes out too close to item; not sure what's fixing that
502502
// in a browser
503-
case ListStyle.unordered: marker = "• "; break;
504-
// TODO(#59) ordered lists starting not at 1
505-
case ListStyle.ordered: marker = "${index+1}. "; break;
503+
case UnorderedListNode(): marker = "• "; break;
504+
case OrderedListNode(:final start): marker = "${start + index}. "; break;
506505
}
507506
return ListItemWidget(marker: marker, nodes: item);
508507
});

test/model/content_test.dart

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,26 @@ class ContentExample {
269269
url: '/#narrow/channel/378-api-design/topic/notation.20for.20near.20links/near/1972281',
270270
nodes: [TextNode('#api design > notation for near links @ 💬')]));
271271

272+
static const orderedListCustomStart = ContentExample(
273+
'ordered list with custom start',
274+
'5. fifth\n6. sixth',
275+
'<ol start="5">\n<li>fifth</li>\n<li>sixth</li>\n</ol>',
276+
[OrderedListNode(start: 5, [
277+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('fifth')])],
278+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('sixth')])],
279+
])],
280+
);
281+
282+
static const orderedListLargeStart = ContentExample(
283+
'ordered list with large start number',
284+
'9999. first\n10000. second',
285+
'<ol start="9999">\n<li>first</li>\n<li>second</li>\n</ol>',
286+
[OrderedListNode(start: 9999, [
287+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('first')])],
288+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('second')])],
289+
])],
290+
);
291+
272292
static const spoilerDefaultHeader = ContentExample(
273293
'spoiler with default header',
274294
'```spoiler\nhello world\n```',
@@ -306,13 +326,13 @@ class ContentExample {
306326
'<p><em>italic</em> <a href="https://zulip.com/">zulip</a></p>\n'
307327
'</div></div>',
308328
[SpoilerNode(
309-
header: [ListNode(ListStyle.ordered, [
310-
[ListNode(ListStyle.unordered, [
311-
[HeadingNode(level: HeadingLevel.h2, links: null, nodes: [
312-
TextNode('hello'),
329+
header: [OrderedListNode(start: 1, [[
330+
UnorderedListNode([
331+
[HeadingNode(level: HeadingLevel.h2, links: null, nodes: [
332+
TextNode('hello'),
333+
])]
313334
])]
314335
])],
315-
])],
316336
content: [ParagraphNode(links: null, nodes: [
317337
EmphasisNode(nodes: [TextNode('italic')]),
318338
TextNode(' '),
@@ -836,7 +856,7 @@ class ContentExample {
836856
'<div class="message_inline_image">'
837857
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">'
838858
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div></li>\n</ul>', [
839-
ListNode(ListStyle.unordered, [[
859+
UnorderedListNode([[
840860
ImageNodeList([
841861
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png',
842862
thumbnailUrl: null, loading: false,
@@ -858,7 +878,7 @@ class ContentExample {
858878
'<div class="message_inline_image">'
859879
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2" title="icon.png">'
860880
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2"></a></div></li>\n</ul>', [
861-
ListNode(ListStyle.unordered, [[
881+
UnorderedListNode([[
862882
ParagraphNode(wasImplicit: true, links: null, nodes: [
863883
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
864884
TextNode(' '),
@@ -887,7 +907,7 @@ class ContentExample {
887907
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png" title="icon.png">'
888908
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div>'
889909
'more text</li>\n</ul>', [
890-
ListNode(ListStyle.unordered, [[
910+
UnorderedListNode([[
891911
const ParagraphNode(wasImplicit: true, links: null, nodes: [
892912
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
893913
TextNode(' '),
@@ -1559,16 +1579,16 @@ void main() {
15591579
testParse('<ol>',
15601580
// "1. first\n2. then"
15611581
'<ol>\n<li>first</li>\n<li>then</li>\n</ol>', const [
1562-
ListNode(ListStyle.ordered, [
1563-
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('first')])],
1564-
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('then')])],
1565-
]),
1582+
OrderedListNode(start: 1, [
1583+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('first')])],
1584+
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('then')])],
1585+
]),
15661586
]);
15671587

15681588
testParse('<ul>',
15691589
// "* something\n* another"
15701590
'<ul>\n<li>something</li>\n<li>another</li>\n</ul>', const [
1571-
ListNode(ListStyle.unordered, [
1591+
UnorderedListNode([
15721592
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('something')])],
15731593
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('another')])],
15741594
]),
@@ -1577,8 +1597,8 @@ void main() {
15771597
testParse('implicit paragraph with internal <br>',
15781598
// "* a\n b"
15791599
'<ul>\n<li>a<br>\n b</li>\n</ul>', const [
1580-
ListNode(ListStyle.unordered, [
1581-
[ParagraphNode(wasImplicit: true, links: null, nodes: [
1600+
UnorderedListNode([[
1601+
ParagraphNode(wasImplicit: true, links: null, nodes: [
15821602
TextNode('a'),
15831603
LineBreakInlineNode(),
15841604
TextNode('\n b'), // TODO: this renders misaligned
@@ -1589,13 +1609,15 @@ void main() {
15891609
testParse('explicit paragraphs',
15901610
// "* a\n\n b"
15911611
'<ul>\n<li>\n<p>a</p>\n<p>b</p>\n</li>\n</ul>', const [
1592-
ListNode(ListStyle.unordered, [
1593-
[
1612+
UnorderedListNode([[
15941613
ParagraphNode(links: null, nodes: [TextNode('a')]),
15951614
ParagraphNode(links: null, nodes: [TextNode('b')]),
15961615
],
15971616
]),
15981617
]);
1618+
1619+
testParseExample(ContentExample.orderedListCustomStart);
1620+
testParseExample(ContentExample.orderedListLargeStart);
15991621
});
16001622

16011623
testParseExample(ContentExample.spoilerDefaultHeader);
@@ -1628,7 +1650,7 @@ void main() {
16281650
testParse('link in list item',
16291651
// "* [t](/u)"
16301652
'<ul>\n<li><a href="/u">t</a></li>\n</ul>', const [
1631-
ListNode(ListStyle.unordered, [
1653+
UnorderedListNode([
16321654
[ParagraphNode(links: null, wasImplicit: true, nodes: [
16331655
LinkNode(url: '/u', nodes: [TextNode('t')]),
16341656
])],
@@ -1695,10 +1717,10 @@ void main() {
16951717
'<ol>\n<li>\n<blockquote>\n<h6>two</h6>\n<ul>\n<li>three</li>\n'
16961718
'</ul>\n</blockquote>\n<div class="codehilite"><pre><span></span>'
16971719
'<code>four\n</code></pre></div>\n\n</li>\n</ol>', const [
1698-
ListNode(ListStyle.ordered, [[
1720+
OrderedListNode(start: 1, [[
16991721
QuotationNode([
17001722
HeadingNode(level: HeadingLevel.h6, links: null, nodes: [TextNode('two')]),
1701-
ListNode(ListStyle.unordered, [[
1723+
UnorderedListNode([[
17021724
ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('three')]),
17031725
]]),
17041726
]),

test/widgets/content_test.dart

Lines changed: 11 additions & 0 deletions
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)