@@ -581,6 +581,57 @@ class TableCellNode extends BlockInlineContainerNode {
581
581
}
582
582
}
583
583
584
+ // Ref:
585
+ // https://ogp.me/
586
+ // https://oembed.com/
587
+ class LinkPreviewNode extends BlockContentNode {
588
+ const LinkPreviewNode ({
589
+ super .debugHtmlNode,
590
+ required this .hrefUrl,
591
+ required this .imageSrcUrl,
592
+ required this .title,
593
+ required this .description,
594
+ });
595
+
596
+ /// The URL from which this preview data was retrieved.
597
+ final String hrefUrl;
598
+
599
+ /// The image URL representing the webpage, content value
600
+ /// of `og:image` HTML meta property.
601
+ final String imageSrcUrl;
602
+
603
+ /// Represents the webpage title, derived from either
604
+ /// the content of the `og:title` HTML meta property or
605
+ /// the <title> HTML element.
606
+ final String ? title;
607
+
608
+ /// Description about the webpage, content value of
609
+ /// `og:description` HTML meta property.
610
+ final String ? description;
611
+
612
+ @override
613
+ bool operator == (Object other) {
614
+ return other is LinkPreviewNode
615
+ && other.hrefUrl == hrefUrl
616
+ && other.imageSrcUrl == imageSrcUrl
617
+ && other.title == title
618
+ && other.description == description;
619
+ }
620
+
621
+ @override
622
+ int get hashCode =>
623
+ Object .hash ('LinkPreviewNode' , hrefUrl, imageSrcUrl, title, description);
624
+
625
+ @override
626
+ void debugFillProperties (DiagnosticPropertiesBuilder properties) {
627
+ super .debugFillProperties (properties);
628
+ properties.add (StringProperty ('hrefUrl' , hrefUrl));
629
+ properties.add (StringProperty ('imageSrcUrl' , imageSrcUrl));
630
+ properties.add (StringProperty ('title' , title));
631
+ properties.add (StringProperty ('description' , description));
632
+ }
633
+ }
634
+
584
635
/// A content node that expects an inline layout context from its parent.
585
636
///
586
637
/// When rendered into a Flutter widget tree, an inline content node
@@ -1448,6 +1499,81 @@ class _ZulipContentParser {
1448
1499
return tableNode ?? UnimplementedBlockContentNode (htmlNode: tableElement);
1449
1500
}
1450
1501
1502
+ static final _linkPreviewImageSrcRegexp = RegExp (r'background-image: url\("(.+)"\)' );
1503
+
1504
+ BlockContentNode parseLinkPreviewNode (dom.Element divElement) {
1505
+ assert (_debugParserContext == _ParserContext .block);
1506
+ assert (divElement.localName == 'div'
1507
+ && divElement.className == 'message_embed' );
1508
+
1509
+ final result = () {
1510
+ if (divElement.nodes.length != 2 ) return null ;
1511
+
1512
+ final first = divElement.nodes.first;
1513
+ if (first is ! dom.Element ) return null ;
1514
+ if (first.localName != 'a' ) return null ;
1515
+ if (first.className != 'message_embed_image' ) return null ;
1516
+ if (first.nodes.isNotEmpty) return null ;
1517
+
1518
+ final imageHref = first.attributes['href' ];
1519
+ if (imageHref == null ) return null ;
1520
+
1521
+ final styleAttr = first.attributes['style' ];
1522
+ if (styleAttr == null ) return null ;
1523
+ final match = _linkPreviewImageSrcRegexp.firstMatch (styleAttr);
1524
+ if (match == null ) return null ;
1525
+ final imageSrcUrl = match.group (1 );
1526
+ if (imageSrcUrl == null ) return null ;
1527
+
1528
+ final second = divElement.nodes.last;
1529
+ if (second is ! dom.Element ) return null ;
1530
+ if (second.localName != 'div' ) return null ;
1531
+ if (second.className != 'data-container' ) return null ;
1532
+ if (second.nodes.isEmpty) return null ;
1533
+ if (second.nodes.length > 2 ) return null ;
1534
+
1535
+ String ? title, description;
1536
+ for (final node in second.nodes) {
1537
+ if (node is ! dom.Element ) return null ;
1538
+ if (node.localName != 'div' ) return null ;
1539
+
1540
+ switch (node.className) {
1541
+ case 'message_embed_title' :
1542
+ if (node.nodes.length != 1 ) return null ;
1543
+ final child = node.nodes.single;
1544
+ if (child is ! dom.Element ) return null ;
1545
+ if (child.localName != 'a' ) return null ;
1546
+ if (child.className.isNotEmpty) return null ;
1547
+ if (child.nodes.length != 1 ) return null ;
1548
+
1549
+ final titleHref = child.attributes['href' ];
1550
+ // Make sure both image hyperlink and title hyperlink are same.
1551
+ if (imageHref != titleHref) return null ;
1552
+ final grandchild = child.nodes.single;
1553
+ if (grandchild is ! dom.Text ) return null ;
1554
+ title = grandchild.text;
1555
+
1556
+ case 'message_embed_description' :
1557
+ if (node.nodes.length != 1 ) return null ;
1558
+ final child = node.nodes.single;
1559
+ if (child is ! dom.Text ) return null ;
1560
+ description = child.text;
1561
+
1562
+ default :
1563
+ return null ;
1564
+ }
1565
+ }
1566
+
1567
+ return LinkPreviewNode (
1568
+ hrefUrl: imageHref,
1569
+ imageSrcUrl: imageSrcUrl,
1570
+ title: title,
1571
+ description: description);
1572
+ }();
1573
+
1574
+ return result ?? UnimplementedBlockContentNode (htmlNode: divElement);
1575
+ }
1576
+
1451
1577
BlockContentNode parseBlockContent (dom.Node node) {
1452
1578
assert (_debugParserContext == _ParserContext .block);
1453
1579
final debugHtmlNode = kDebugMode ? node : null ;
@@ -1545,6 +1671,10 @@ class _ZulipContentParser {
1545
1671
}
1546
1672
}
1547
1673
1674
+ if (localName == 'div' && className == 'message_embed' ) {
1675
+ return parseLinkPreviewNode (element);
1676
+ }
1677
+
1548
1678
// TODO more types of node
1549
1679
return UnimplementedBlockContentNode (htmlNode: node);
1550
1680
}
0 commit comments