Skip to content

Commit

Permalink
content: Handle clusters of images in parseImplicitParagraphBlockCont…
Browse files Browse the repository at this point in the history
…entList

Fixes: zulip#193
  • Loading branch information
sirpengi committed Feb 9, 2024
1 parent ac8fe80 commit 5b4ad3b
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 1 deletion.
26 changes: 25 additions & 1 deletion lib/model/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,7 @@ class _ZulipContentParser {
assert(_debugParserContext == _ParserContext.block);
final List<BlockContentNode> result = [];
final List<dom.Node> currentParagraph = [];
List<ImageNode> imageNodes = [];
void consumeParagraph() {
final parsed = parseBlockInline(currentParagraph);
result.add(ParagraphNode(
Expand All @@ -1029,13 +1030,36 @@ class _ZulipContentParser {
if (node is dom.Text && (node.text == '\n')) continue;

if (_isPossibleInlineNode(node)) {
if (imageNodes.isNotEmpty) {
result.add(ImageNodeList(imageNodes));
imageNodes = [];
// In a context where paragraphs are implicit it
// should be impossible to have more paragraph
// content after image previews.
result.add(ParagraphNode(
wasImplicit: true,
links: null,
nodes: [UnimplementedInlineContentNode(htmlNode: node)]
));
continue;
}
currentParagraph.add(node);
continue;
}
if (currentParagraph.isNotEmpty) consumeParagraph();
result.add(parseBlockContent(node));
final block = parseBlockContent(node);
if (block is ImageNode) {
imageNodes.add(block);
continue;
}
if (imageNodes.isNotEmpty) {
result.add(ImageNodeList(imageNodes));
imageNodes = [];
}
result.add(block);
}
if (currentParagraph.isNotEmpty) consumeParagraph();
if (imageNodes.isNotEmpty) result.add(ImageNodeList(imageNodes));

return result;
}
Expand Down
4 changes: 4 additions & 0 deletions lib/widgets/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ class BlockContentList extends StatelessWidget {
} else if (node is ImageNodeList) {
return MessageImageList(node: node);
} else if (node is ImageNode) {
assert(false,
"[ImageNode] not allowed in [BlockContentList]. "
"It should be wrapped in [ImageNodeList]."
);
return MessageImage(node: node);
} else if (node is UnimplementedBlockContentNode) {
return Text.rich(_errorUnimplemented(node));
Expand Down
92 changes: 92 additions & 0 deletions test/model/content_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,33 @@ void main() {
]),
]);

testParse('content after image cluster',
'<p>content '
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">icon.png</a> '
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2">icon.png</a></p>\n'
'<div class="message_inline_image">'
'<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>'
'<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>'
'<p>more content</p>',
const [
ParagraphNode(links: null, nodes: [
TextNode('content '),
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
TextNode(' '),
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2', nodes: [TextNode('icon.png')]),
]),
ImageNodeList([
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2'),
]),
ParagraphNode(links: null, nodes: [
TextNode('more content'),
]),
]);

testParse('multiple clusters of images',
// "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"
'<p>'
Expand Down Expand Up @@ -554,6 +581,71 @@ void main() {
ImageNode(srcUrl: 'https://uploads.zulipusercontent.net/51b70540cf6a5b3c8a0b919c893b8abddd447e88/68747470733a2f2f656e2e77696b6970656469612e6f72672f7374617469632f696d616765732f69636f6e732f77696b6970656469612e706e673f763d33'),
]),
]);

// TODO: maybe delete this
testParse('image as immediate child in implicit paragraph',
// "* https://chat.zulip.org/user_avatars/2/realm/icon.png"
'<ul>\n'
'<li>'
'<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>',
const [
ListNode(ListStyle.unordered, [[
ImageNodeList([
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
]),
]]),
]);

testParse('image cluster in implicit paragraph',
// "* [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)"
'<ul>\n'
'<li>'
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">icon.png</a> '
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2">icon.png</a>'
'<div class="message_inline_image">'
'<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>'
'<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>',
const [
ListNode(ListStyle.unordered, [[
ParagraphNode(wasImplicit: true, links: null, nodes: [
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
TextNode(' '),
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2', nodes: [TextNode('icon.png')]),
]),
ImageNodeList([
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2'),
]),
]]),
]);

testParse('impossible content after image cluster in implicit paragraph',
'<ul>\n'
'<li>'
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">icon.png</a> '
'<div class="message_inline_image">'
'<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>'
'<span>Some content</span></li>\n</ul>',
[
ListNode(ListStyle.unordered, [[
const ParagraphNode(wasImplicit: true, links: null, nodes: [
LinkNode(url: 'https://chat.zulip.org/user_avatars/2/realm/icon.png', nodes: [TextNode('icon.png')]),
TextNode(' '),
]),
const ImageNodeList([
ImageNode(srcUrl: 'https://chat.zulip.org/user_avatars/2/realm/icon.png'),
]),
ParagraphNode(wasImplicit: true, links: null, nodes: [
inlineUnimplemented('<span>Some content</span>'),
])
]]),
]);
});

testParse('parse nested lists, quotes, headings, code blocks',
Expand Down
36 changes: 36 additions & 0 deletions test/widgets/content_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,42 @@ void main() {
'https://uploads.zulipusercontent.net/51b70540cf6a5b3c8a0b919c893b8abddd447e88/68747470733a2f2f656e2e77696b6970656469612e6f72672f7374617469632f696d616765732f69636f6e732f77696b6970656469612e706e673f763d33',
]);
});

testWidgets('image as immediate child in list item', (tester) async {
// "* https://chat.zulip.org/user_avatars/2/realm/icon.png"
await prepareContent(tester,
'<ul>\n'
'<li>'
'<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>');
final images = tester.widgetList<RealmContentNetworkImage>(find.byType(RealmContentNetworkImage));
check(images.map((i) => i.src.toString()).toList())
.deepEquals([
'https://chat.zulip.org/user_avatars/2/realm/icon.png',
]);
});

testWidgets('image cluster in list item', (tester) async {
// "* [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)"
await prepareContent(tester,
'<ul>\n'
'<li>'
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png">icon.png</a> '
'<a href="https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2">icon.png</a>'
'<div class="message_inline_image">'
'<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>'
'<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"></hia></div></li>\n</ul>');
final images = tester.widgetList<RealmContentNetworkImage>(find.byType(RealmContentNetworkImage));
check(images.map((i) => i.src.toString()).toList())
.deepEquals([
'https://chat.zulip.org/user_avatars/2/realm/icon.png',
'https://chat.zulip.org/user_avatars/2/realm/icon.png?version=2',
]);
});
});

group('RealmContentNetworkImage', () {
Expand Down

0 comments on commit 5b4ad3b

Please sign in to comment.