Skip to content

Commit

Permalink
content: Add start attribute support for ordered list
Browse files Browse the repository at this point in the history
Fixes: #59
  • Loading branch information
lakshya1goel committed Feb 14, 2025
1 parent a055486 commit 0c6fda4
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 43 deletions.
46 changes: 27 additions & 19 deletions lib/model/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -251,21 +251,11 @@ class HeadingNode extends BlockInlineContainerNode {
}
}

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

class ListNode extends BlockContentNode {
const ListNode(this.style, this.items, {super.debugHtmlNode});

final ListStyle style;
final List<List<BlockContentNode>> items;

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagProperty('ordered', value: style == ListStyle.ordered,
ifTrue: 'ordered', ifFalse: 'unordered'));
}

@override
List<DiagnosticsNode> debugDescribeChildren() {
return items
Expand All @@ -275,6 +265,22 @@ class ListNode extends BlockContentNode {
}
}

class UnorderedListNode extends ListNode {
const UnorderedListNode({required super.items, super.debugHtmlNode});
}

class OrderedListNode extends ListNode {
const OrderedListNode({required super.items, required this.start, super.debugHtmlNode});

final int start;

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('start', start));
}
}

class QuotationNode extends BlockContentNode {
const QuotationNode(this.nodes, {super.debugHtmlNode});

Expand Down Expand Up @@ -1063,12 +1069,6 @@ class _ZulipContentParser {
}

BlockContentNode parseListNode(dom.Element element) {
ListStyle? listStyle;
switch (element.localName) {
case 'ol': listStyle = ListStyle.ordered; break;
case 'ul': listStyle = ListStyle.unordered; break;
}
assert(listStyle != null);
assert(element.className.isEmpty);

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

return ListNode(listStyle!, items, debugHtmlNode: debugHtmlNode);
if (element.localName == 'ol') {
final startAttr = element.attributes['start'];
final start = startAttr == null ? 1
: int.tryParse(startAttr, radix: 10);
if (start == null) return UnimplementedBlockContentNode(htmlNode: element);
return OrderedListNode(start: start, items: items, debugHtmlNode: debugHtmlNode);
} else {
return UnorderedListNode(items: items, debugHtmlNode: debugHtmlNode);
}
}

BlockContentNode parseSpoilerNode(dom.Element divElement) {
Expand Down
7 changes: 3 additions & 4 deletions lib/widgets/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -482,17 +482,16 @@ class ListNodeWidget extends StatelessWidget {
final items = List.generate(node.items.length, (index) {
final item = node.items[index];
String marker;
switch (node.style) {
switch (node) {
// TODO(#161): different unordered marker styles at different levels of nesting
// see:
// https://html.spec.whatwg.org/multipage/rendering.html#lists
// https://www.w3.org/TR/css-counter-styles-3/#simple-symbolic
// TODO proper alignment of unordered marker; should be "• ", one space,
// but that comes out too close to item; not sure what's fixing that
// in a browser
case ListStyle.unordered: marker = "• "; break;
// TODO(#59) ordered lists starting not at 1
case ListStyle.ordered: marker = "${index+1}. "; break;
case UnorderedListNode(): marker = "• "; break;
case OrderedListNode(:final start): marker = "${start + index}. "; break;
}
return ListItemWidget(marker: marker, nodes: item);
});
Expand Down
75 changes: 55 additions & 20 deletions test/model/content_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -306,13 +306,16 @@ class ContentExample {
'<p><em>italic</em> <a href="https://zulip.com/">zulip</a></p>\n'
'</div></div>',
[SpoilerNode(
header: [ListNode(ListStyle.ordered, [
[ListNode(ListStyle.unordered, [
[HeadingNode(level: HeadingLevel.h2, links: null, nodes: [
TextNode('hello'),
])]
])],
])],
header: [OrderedListNode(
start: 1,
items: [
[UnorderedListNode(items: [
[HeadingNode(level: HeadingLevel.h2, links: null, nodes: [
TextNode('hello'),
])]
])],
]),
],
content: [ParagraphNode(links: null, nodes: [
EmphasisNode(nodes: [TextNode('italic')]),
TextNode(' '),
Expand Down Expand Up @@ -763,7 +766,7 @@ class ContentExample {
'<div class="message_inline_image">'
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">'
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div></li>\n</ul>', [
ListNode(ListStyle.unordered, [[
UnorderedListNode(items: [[
ImageNodeList([
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png',
thumbnailUrl: null, loading: false,
Expand All @@ -785,7 +788,7 @@ class ContentExample {
'<div class="message_inline_image">'
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2" title="icon.png">'
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2"></a></div></li>\n</ul>', [
ListNode(ListStyle.unordered, [[
UnorderedListNode(items: [[
ParagraphNode(wasImplicit: true, links: null, nodes: [
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
TextNode(' '),
Expand Down Expand Up @@ -814,7 +817,7 @@ class ContentExample {
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png" title="icon.png">'
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div>'
'more text</li>\n</ul>', [
ListNode(ListStyle.unordered, [[
UnorderedListNode(items: [[
const ParagraphNode(wasImplicit: true, links: null, nodes: [
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
TextNode(' '),
Expand Down Expand Up @@ -1155,6 +1158,32 @@ class ContentExample {
], isHeader: false),
]),
]);

static const orderedListLargeStart = ContentExample(
'ordered list with large start number',
'9999. first\n10000. second',
'<ol start="9999">\n<li>first</li>\n<li>second</li>\n</ol>',
[OrderedListNode(
start: 9999,
items: [
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('first')])],
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('second')])],
])
],
);

static const orderedListCustomStart = ContentExample(
'ordered list with custom start',
'5. fifth\n6. sixth',
'<ol start="5">\n<li>fifth</li>\n<li>sixth</li>\n</ol>',
[OrderedListNode(
start: 5,
items: [
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('fifth')])],
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('sixth')])],
])
],
);
}

UnimplementedBlockContentNode blockUnimplemented(String html) {
Expand Down Expand Up @@ -1382,16 +1411,18 @@ void main() {
testParse('<ol>',
// "1. first\n2. then"
'<ol>\n<li>first</li>\n<li>then</li>\n</ol>', const [
ListNode(ListStyle.ordered, [
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('first')])],
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('then')])],
]),
OrderedListNode(
start: 1,
items: [
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('first')])],
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('then')])],
]),
]);

testParse('<ul>',
// "* something\n* another"
'<ul>\n<li>something</li>\n<li>another</li>\n</ul>', const [
ListNode(ListStyle.unordered, [
UnorderedListNode(items: [
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('something')])],
[ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('another')])],
]),
Expand All @@ -1400,7 +1431,7 @@ void main() {
testParse('implicit paragraph with internal <br>',
// "* a\n b"
'<ul>\n<li>a<br>\n b</li>\n</ul>', const [
ListNode(ListStyle.unordered, [
UnorderedListNode(items: [
[ParagraphNode(wasImplicit: true, links: null, nodes: [
TextNode('a'),
LineBreakInlineNode(),
Expand All @@ -1412,7 +1443,7 @@ void main() {
testParse('explicit paragraphs',
// "* a\n\n b"
'<ul>\n<li>\n<p>a</p>\n<p>b</p>\n</li>\n</ul>', const [
ListNode(ListStyle.unordered, [
UnorderedListNode(items: [
[
ParagraphNode(links: null, nodes: [TextNode('a')]),
ParagraphNode(links: null, nodes: [TextNode('b')]),
Expand Down Expand Up @@ -1451,7 +1482,7 @@ void main() {
testParse('link in list item',
// "* [t](/u)"
'<ul>\n<li><a href="/u">t</a></li>\n</ul>', const [
ListNode(ListStyle.unordered, [
UnorderedListNode(items: [
[ParagraphNode(links: null, wasImplicit: true, nodes: [
LinkNode(url: '/u', nodes: [TextNode('t')]),
])],
Expand Down Expand Up @@ -1503,16 +1534,20 @@ void main() {
testParseExample(ContentExample.tableMissingOneBodyColumnInMarkdown);
testParseExample(ContentExample.tableWithDifferentTextAlignmentInColumns);
testParseExample(ContentExample.tableWithLinkCenterAligned);
testParseExample(ContentExample.orderedListLargeStart);
testParseExample(ContentExample.orderedListCustomStart);

testParse('parse nested lists, quotes, headings, code blocks',
// "1. > ###### two\n > * three\n\n four"
'<ol>\n<li>\n<blockquote>\n<h6>two</h6>\n<ul>\n<li>three</li>\n'
'</ul>\n</blockquote>\n<div class="codehilite"><pre><span></span>'
'<code>four\n</code></pre></div>\n\n</li>\n</ol>', const [
ListNode(ListStyle.ordered, [[
OrderedListNode(
start: 1,
items: [[
QuotationNode([
HeadingNode(level: HeadingLevel.h6, links: null, nodes: [TextNode('two')]),
ListNode(ListStyle.unordered, [[
UnorderedListNode(items: [[
ParagraphNode(wasImplicit: true, links: null, nodes: [TextNode('three')]),
]]),
]),
Expand Down
11 changes: 11 additions & 0 deletions test/widgets/content_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,17 @@ void main() {
});
});

group('ListNodeWidget', () {
testWidgets('ordered list with custom start', (tester) async {
await prepareContent(tester, plainContent('<ol start="3">\n<li>third</li>\n<li>fourth</li>\n</ol>'));

expect(find.text('3. '), findsOneWidget);
expect(find.text('4. '), findsOneWidget);
expect(find.text('third'), findsOneWidget);
expect(find.text('fourth'), findsOneWidget);
});
});

group('Spoiler', () {
testContentSmoke(ContentExample.spoilerDefaultHeader);
testContentSmoke(ContentExample.spoilerPlainCustomHeader);
Expand Down

0 comments on commit 0c6fda4

Please sign in to comment.