Skip to content

Commit 5b4ad3b

Browse files
committed
content: Handle clusters of images in parseImplicitParagraphBlockContentList
Fixes: zulip#193
1 parent ac8fe80 commit 5b4ad3b

File tree

4 files changed

+157
-1
lines changed

4 files changed

+157
-1
lines changed

lib/model/content.dart

+25-1
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,7 @@ class _ZulipContentParser {
10161016
assert(_debugParserContext == _ParserContext.block);
10171017
final List<BlockContentNode> result = [];
10181018
final List<dom.Node> currentParagraph = [];
1019+
List<ImageNode> imageNodes = [];
10191020
void consumeParagraph() {
10201021
final parsed = parseBlockInline(currentParagraph);
10211022
result.add(ParagraphNode(
@@ -1029,13 +1030,36 @@ class _ZulipContentParser {
10291030
if (node is dom.Text && (node.text == '\n')) continue;
10301031

10311032
if (_isPossibleInlineNode(node)) {
1033+
if (imageNodes.isNotEmpty) {
1034+
result.add(ImageNodeList(imageNodes));
1035+
imageNodes = [];
1036+
// In a context where paragraphs are implicit it
1037+
// should be impossible to have more paragraph
1038+
// content after image previews.
1039+
result.add(ParagraphNode(
1040+
wasImplicit: true,
1041+
links: null,
1042+
nodes: [UnimplementedInlineContentNode(htmlNode: node)]
1043+
));
1044+
continue;
1045+
}
10321046
currentParagraph.add(node);
10331047
continue;
10341048
}
10351049
if (currentParagraph.isNotEmpty) consumeParagraph();
1036-
result.add(parseBlockContent(node));
1050+
final block = parseBlockContent(node);
1051+
if (block is ImageNode) {
1052+
imageNodes.add(block);
1053+
continue;
1054+
}
1055+
if (imageNodes.isNotEmpty) {
1056+
result.add(ImageNodeList(imageNodes));
1057+
imageNodes = [];
1058+
}
1059+
result.add(block);
10371060
}
10381061
if (currentParagraph.isNotEmpty) consumeParagraph();
1062+
if (imageNodes.isNotEmpty) result.add(ImageNodeList(imageNodes));
10391063

10401064
return result;
10411065
}

lib/widgets/content.dart

+4
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ class BlockContentList extends StatelessWidget {
8787
} else if (node is ImageNodeList) {
8888
return MessageImageList(node: node);
8989
} else if (node is ImageNode) {
90+
assert(false,
91+
"[ImageNode] not allowed in [BlockContentList]. "
92+
"It should be wrapped in [ImageNodeList]."
93+
);
9094
return MessageImage(node: node);
9195
} else if (node is UnimplementedBlockContentNode) {
9296
return Text.rich(_errorUnimplemented(node));

test/model/content_test.dart

+92
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,33 @@ void main() {
508508
]),
509509
]);
510510

511+
testParse('content after image cluster',
512+
'<p>content '
513+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">icon.png</a> '
514+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2">icon.png</a></p>\n'
515+
'<div class="message_inline_image">'
516+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png" title="icon.png">'
517+
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div>'
518+
'<div class="message_inline_image">'
519+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2" title="icon.png">'
520+
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2"></a></div>'
521+
'<p>more content</p>',
522+
const [
523+
ParagraphNode(links: null, nodes: [
524+
TextNode('content '),
525+
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
526+
TextNode(' '),
527+
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2', nodes: [TextNode('icon.png')]),
528+
]),
529+
ImageNodeList([
530+
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
531+
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2'),
532+
]),
533+
ParagraphNode(links: null, nodes: [
534+
TextNode('more content'),
535+
]),
536+
]);
537+
511538
testParse('multiple clusters of images',
512539
// "https://en.wikipedia.org/static/images/icons/wikipedia.png\nhttps://en.wikipedia.org/static/images/icons/wikipedia.png?v=1\n\nTest\n\nhttps://en.wikipedia.org/static/images/icons/wikipedia.png?v=2\nhttps://en.wikipedia.org/static/images/icons/wikipedia.png?v=3"
513540
'<p>'
@@ -554,6 +581,71 @@ void main() {
554581
ImageNode(srcUrl: 'https://uploads.zulipusercontent.net/51b70540cf6a5b3c8a0b919c893b8abddd447e88/68747470733a2f2f656e2e77696b6970656469612e6f72672f7374617469632f696d616765732f69636f6e732f77696b6970656469612e706e673f763d33'),
555582
]),
556583
]);
584+
585+
// TODO: maybe delete this
586+
testParse('image as immediate child in implicit paragraph',
587+
// "* https://chat.zulip.org/user_avatars/2/realm/icon.png"
588+
'<ul>\n'
589+
'<li>'
590+
'<div class="message_inline_image">'
591+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">'
592+
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div></li>\n</ul>',
593+
const [
594+
ListNode(ListStyle.unordered, [[
595+
ImageNodeList([
596+
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
597+
]),
598+
]]),
599+
]);
600+
601+
testParse('image cluster in implicit paragraph',
602+
// "* [icon.png](https://chat.zulip.org/user_avatars/2/realm/icon.png) [icon.png](https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2)"
603+
'<ul>\n'
604+
'<li>'
605+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">icon.png</a> '
606+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2">icon.png</a>'
607+
'<div class="message_inline_image">'
608+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png" title="icon.png">'
609+
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div>'
610+
'<div class="message_inline_image">'
611+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2" title="icon.png">'
612+
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2"></a></div></li>\n</ul>',
613+
const [
614+
ListNode(ListStyle.unordered, [[
615+
ParagraphNode(wasImplicit: true, links: null, nodes: [
616+
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
617+
TextNode(' '),
618+
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2', nodes: [TextNode('icon.png')]),
619+
]),
620+
ImageNodeList([
621+
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
622+
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2'),
623+
]),
624+
]]),
625+
]);
626+
627+
testParse('impossible content after image cluster in implicit paragraph',
628+
'<ul>\n'
629+
'<li>'
630+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">icon.png</a> '
631+
'<div class="message_inline_image">'
632+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png" title="icon.png">'
633+
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div>'
634+
'<span>Some content</span></li>\n</ul>',
635+
[
636+
ListNode(ListStyle.unordered, [[
637+
const ParagraphNode(wasImplicit: true, links: null, nodes: [
638+
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
639+
TextNode(' '),
640+
]),
641+
const ImageNodeList([
642+
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
643+
]),
644+
ParagraphNode(wasImplicit: true, links: null, nodes: [
645+
inlineUnimplemented('<span>Some content</span>'),
646+
])
647+
]]),
648+
]);
557649
});
558650

559651
testParse('parse nested lists, quotes, headings, code blocks',

test/widgets/content_test.dart

+36
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,42 @@ void main() {
388388
'https://uploads.zulipusercontent.net/51b70540cf6a5b3c8a0b919c893b8abddd447e88/68747470733a2f2f656e2e77696b6970656469612e6f72672f7374617469632f696d616765732f69636f6e732f77696b6970656469612e706e673f763d33',
389389
]);
390390
});
391+
392+
testWidgets('image as immediate child in list item', (tester) async {
393+
// "* https://chat.zulip.org/user_avatars/2/realm/icon.png"
394+
await prepareContent(tester,
395+
'<ul>\n'
396+
'<li>'
397+
'<div class="message_inline_image">'
398+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">'
399+
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div></li>\n</ul>');
400+
final images = tester.widgetList<RealmContentNetworkImage>(find.byType(RealmContentNetworkImage));
401+
check(images.map((i) => i.src.toString()).toList())
402+
.deepEquals([
403+
'https://chat.zulip.org/user_avatars/2/realm/icon.png',
404+
]);
405+
});
406+
407+
testWidgets('image cluster in list item', (tester) async {
408+
// "* [icon.png](https://chat.zulip.org/user_avatars/2/realm/icon.png) [icon.png](https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2)"
409+
await prepareContent(tester,
410+
'<ul>\n'
411+
'<li>'
412+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">icon.png</a> '
413+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2">icon.png</a>'
414+
'<div class="message_inline_image">'
415+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png" title="icon.png">'
416+
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div>'
417+
'<div class="message_inline_image">'
418+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2" title="icon.png">'
419+
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2"></hia></div></li>\n</ul>');
420+
final images = tester.widgetList<RealmContentNetworkImage>(find.byType(RealmContentNetworkImage));
421+
check(images.map((i) => i.src.toString()).toList())
422+
.deepEquals([
423+
'https://chat.zulip.org/user_avatars/2/realm/icon.png',
424+
'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2',
425+
]);
426+
});
391427
});
392428

393429
group('RealmContentNetworkImage', () {

0 commit comments

Comments
 (0)