Skip to content

Commit 61032ad

Browse files
sirpengignprice
authored andcommitted
content: Handle clusters of images in parseImplicitParagraphBlockContentList
Fixes: #193
1 parent 72efa3e commit 61032ad

File tree

4 files changed

+114
-1
lines changed

4 files changed

+114
-1
lines changed

lib/model/content.dart

Lines changed: 20 additions & 1 deletion
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,31 @@ 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 should be impossible
1037+
// to have more paragraph content after image previews.
1038+
result.add(UnimplementedBlockContentNode(htmlNode: node));
1039+
continue;
1040+
}
10321041
currentParagraph.add(node);
10331042
continue;
10341043
}
10351044
if (currentParagraph.isNotEmpty) consumeParagraph();
1036-
result.add(parseBlockContent(node));
1045+
final block = parseBlockContent(node);
1046+
if (block is ImageNode) {
1047+
imageNodes.add(block);
1048+
continue;
1049+
}
1050+
if (imageNodes.isNotEmpty) {
1051+
result.add(ImageNodeList(imageNodes));
1052+
imageNodes = [];
1053+
}
1054+
result.add(block);
10371055
}
10381056
if (currentParagraph.isNotEmpty) consumeParagraph();
1057+
if (imageNodes.isNotEmpty) result.add(ImageNodeList(imageNodes));
10391058

10401059
return result;
10411060
}

lib/widgets/content.dart

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,71 @@ class ContentExample {
365365
ImageNode(srcUrl: 'https://uploads.zulipusercontent.net/51b70540cf6a5b3c8a0b919c893b8abddd447e88/68747470733a2f2f656e2e77696b6970656469612e6f72672f7374617469632f696d616765732f69636f6e732f77696b6970656469612e706e673f763d33'),
366366
]),
367367
]);
368+
369+
static const imageInImplicitParagraph = ContentExample(
370+
'image as immediate child in implicit paragraph',
371+
"* https://chat.zulip.org/user_avatars/2/realm/icon.png",
372+
'<ul>\n'
373+
'<li>'
374+
'<div class="message_inline_image">'
375+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">'
376+
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div></li>\n</ul>', [
377+
ListNode(ListStyle.unordered, [[
378+
ImageNodeList([
379+
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
380+
]),
381+
]]),
382+
]);
383+
384+
static const imageClusterInImplicitParagraph = ContentExample(
385+
'image cluster in implicit paragraph',
386+
"* [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)",
387+
'<ul>\n'
388+
'<li>'
389+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">icon.png</a> '
390+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2">icon.png</a>'
391+
'<div class="message_inline_image">'
392+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png" title="icon.png">'
393+
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div>'
394+
'<div class="message_inline_image">'
395+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2" title="icon.png">'
396+
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2"></a></div></li>\n</ul>', [
397+
ListNode(ListStyle.unordered, [[
398+
ParagraphNode(wasImplicit: true, links: null, nodes: [
399+
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
400+
TextNode(' '),
401+
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2', nodes: [TextNode('icon.png')]),
402+
]),
403+
ImageNodeList([
404+
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
405+
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2'),
406+
]),
407+
]]),
408+
]);
409+
410+
static final imageClusterInImplicitParagraphThenContent = ContentExample(
411+
'impossible content after image cluster in implicit paragraph',
412+
// Image previews are always inserted at the end of the paragraph
413+
// so it would be impossible to have content after.
414+
null,
415+
'<ul>\n'
416+
'<li>'
417+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">icon.png</a> '
418+
'<div class="message_inline_image">'
419+
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png" title="icon.png">'
420+
'<img src="https://chat.zulip.org/user_avatars/2/realm/icon.png"></a></div>'
421+
'more text</li>\n</ul>', [
422+
ListNode(ListStyle.unordered, [[
423+
const ParagraphNode(wasImplicit: true, links: null, nodes: [
424+
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
425+
TextNode(' '),
426+
]),
427+
const ImageNodeList([
428+
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
429+
]),
430+
blockUnimplemented('more text'),
431+
]]),
432+
]);
368433
}
369434

370435
UnimplementedBlockContentNode blockUnimplemented(String html) {
@@ -689,6 +754,9 @@ void main() {
689754
testParseExample(ContentExample.imageCluster);
690755
testParseExample(ContentExample.imageClusterThenContent);
691756
testParseExample(ContentExample.imageMultipleClusters);
757+
testParseExample(ContentExample.imageInImplicitParagraph);
758+
testParseExample(ContentExample.imageClusterInImplicitParagraph);
759+
testParseExample(ContentExample.imageClusterInImplicitParagraphThenContent);
692760

693761
testParse('parse nested lists, quotes, headings, code blocks',
694762
// "1. > ###### two\n > * three\n\n four"

test/widgets/content_test.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,28 @@ void main() {
321321
check(images.map((i) => i.src.toString()).toList())
322322
.deepEquals(expectedImages.map((n) => n.srcUrl));
323323
});
324+
325+
testWidgets('image as immediate child in implicit paragraph', (tester) async {
326+
const example = ContentExample.imageInImplicitParagraph;
327+
await prepareContent(tester, example.html);
328+
final expectedImages = ((example.expectedNodes[0] as ListNode)
329+
.items[0][0] as ImageNodeList).images;
330+
final images = tester.widgetList<RealmContentNetworkImage>(
331+
find.byType(RealmContentNetworkImage));
332+
check(images.map((i) => i.src.toString()).toList())
333+
.deepEquals(expectedImages.map((n) => n.srcUrl));
334+
});
335+
336+
testWidgets('image cluster in implicit paragraph', (tester) async {
337+
const example = ContentExample.imageClusterInImplicitParagraph;
338+
await prepareContent(tester, example.html);
339+
final expectedImages = ((example.expectedNodes[0] as ListNode)
340+
.items[0][1] as ImageNodeList).images;
341+
final images = tester.widgetList<RealmContentNetworkImage>(
342+
find.byType(RealmContentNetworkImage));
343+
check(images.map((i) => i.src.toString()).toList())
344+
.deepEquals(expectedImages.map((n) => n.srcUrl));
345+
});
324346
});
325347

326348
group('RealmContentNetworkImage', () {

0 commit comments

Comments
 (0)