Skip to content

Commit a6d96a3

Browse files
committed
content: Handle clusters of images in parseImplicitParagraphBlockContentList
Fixes: #193
1 parent ab9dd8e commit a6d96a3

File tree

4 files changed

+123
-2
lines changed

4 files changed

+123
-2
lines changed

lib/model/content.dart

Lines changed: 25 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,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

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

362429
UnimplementedBlockContentNode blockUnimplemented(String html) {
@@ -685,6 +752,9 @@ void main() {
685752
testParseExample(ContentExample.multipleImages);
686753
testParseExample(ContentExample.contentAfterImageCluster);
687754
testParseExample(ContentExample.multipleImageClusters);
755+
testParseExample(ContentExample.imageInImplicitParagraph);
756+
testParseExample(ContentExample.imageClusterInImplicitParagraph);
757+
testParseExample(ContentExample.contentAfterImageClusterInImplicitParagraph);
688758

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

test/widgets/content_test.dart

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,30 @@ void main() {
314314
await prepareContent(tester, example.html);
315315
final expectedImages = (example.expectedNodes[1] as ImageNodeList).images
316316
+ (example.expectedNodes[4] as ImageNodeList).images;
317-
final images = tester.widgetList<RealmContentNetworkImage>(find.byType(RealmContentNetworkImage));
317+
final images = tester.widgetList<RealmContentNetworkImage>(
318+
find.byType(RealmContentNetworkImage));
319+
check(images.map((i) => i.src.toString()).toList())
320+
.deepEquals(expectedImages.map((n) => n.srcUrl));
321+
});
322+
323+
testWidgets('image as immediate child in implicit paragraph', (tester) async {
324+
const example = ContentExample.imageInImplicitParagraph;
325+
await prepareContent(tester, example.html);
326+
final expectedImages = ((example.expectedNodes[0] as ListNode)
327+
.items[0][0] as ImageNodeList).images;
328+
final images = tester.widgetList<RealmContentNetworkImage>(
329+
find.byType(RealmContentNetworkImage));
330+
check(images.map((i) => i.src.toString()).toList())
331+
.deepEquals(expectedImages.map((n) => n.srcUrl));
332+
});
333+
334+
testWidgets('image cluster in implicit paragraph', (tester) async {
335+
const example = ContentExample.imageClusterInImplicitParagraph;
336+
await prepareContent(tester, example.html);
337+
final expectedImages = ((example.expectedNodes[0] as ListNode)
338+
.items[0][1] as ImageNodeList).images;
339+
final images = tester.widgetList<RealmContentNetworkImage>(
340+
find.byType(RealmContentNetworkImage));
318341
check(images.map((i) => i.src.toString()).toList())
319342
.deepEquals(expectedImages.map((n) => n.srcUrl));
320343
});

0 commit comments

Comments
 (0)