@@ -504,6 +504,58 @@ class EmbedVideoNode extends BlockContentNode {
504
504
}
505
505
}
506
506
507
+ // See:
508
+ // https://ogp.me/
509
+ // https://oembed.com/
510
+ // https://zulip.com/help/image-video-and-website-previews#configure-whether-website-previews-are-shown
511
+ class WebsitePreviewNode extends BlockContentNode {
512
+ const WebsitePreviewNode ({
513
+ super .debugHtmlNode,
514
+ required this .hrefUrl,
515
+ required this .imageSrcUrl,
516
+ required this .title,
517
+ required this .description,
518
+ });
519
+
520
+ /// The URL from which this preview data was retrieved.
521
+ final String hrefUrl;
522
+
523
+ /// The image URL representing the webpage, content value
524
+ /// of `og:image` HTML meta property.
525
+ final String imageSrcUrl;
526
+
527
+ /// Represents the webpage title, derived from either
528
+ /// the content of the `og:title` HTML meta property or
529
+ /// the <title> HTML element.
530
+ final String ? title;
531
+
532
+ /// Description about the webpage, content value of
533
+ /// `og:description` HTML meta property.
534
+ final String ? description;
535
+
536
+ @override
537
+ bool operator == (Object other) {
538
+ return other is WebsitePreviewNode
539
+ && other.hrefUrl == hrefUrl
540
+ && other.imageSrcUrl == imageSrcUrl
541
+ && other.title == title
542
+ && other.description == description;
543
+ }
544
+
545
+ @override
546
+ int get hashCode =>
547
+ Object .hash ('WebsitePreviewNode' , hrefUrl, imageSrcUrl, title, description);
548
+
549
+ @override
550
+ void debugFillProperties (DiagnosticPropertiesBuilder properties) {
551
+ super .debugFillProperties (properties);
552
+ properties.add (StringProperty ('hrefUrl' , hrefUrl));
553
+ properties.add (StringProperty ('imageSrcUrl' , imageSrcUrl));
554
+ properties.add (StringProperty ('title' , title));
555
+ properties.add (StringProperty ('description' , description));
556
+ }
557
+ }
558
+
507
559
class TableNode extends BlockContentNode {
508
560
const TableNode ({super .debugHtmlNode, required this .rows});
509
561
@@ -1339,6 +1391,113 @@ class _ZulipContentParser {
1339
1391
return EmbedVideoNode (hrefUrl: href, previewImageSrcUrl: imgSrc, debugHtmlNode: debugHtmlNode);
1340
1392
}
1341
1393
1394
+ static final _websitePreviewImageSrcRegexp = RegExp (r'background-image: url\(("?)(.+?)\1\)' );
1395
+
1396
+ BlockContentNode parseWebsitePreviewNode (dom.Element divElement) {
1397
+ assert (divElement.localName == 'div'
1398
+ && divElement.className == 'message_embed' );
1399
+
1400
+ final debugHtmlNode = kDebugMode ? divElement : null ;
1401
+ final result = () {
1402
+ if (divElement.nodes case [
1403
+ dom.Element (
1404
+ localName: 'a' ,
1405
+ className: 'message_embed_image' ,
1406
+ attributes: {
1407
+ 'href' : final String imageHref,
1408
+ 'style' : final String imageStyleAttr,
1409
+ },
1410
+ nodes: []),
1411
+ dom.Element (
1412
+ localName: 'div' ,
1413
+ className: 'data-container' ,
1414
+ nodes: [...]) && final dataContainer,
1415
+ ]) {
1416
+ final match = _websitePreviewImageSrcRegexp.firstMatch (imageStyleAttr);
1417
+ if (match == null ) return null ;
1418
+ final imageSrcUrl = match.group (2 );
1419
+ if (imageSrcUrl == null ) return null ;
1420
+
1421
+ String ? parseTitle (dom.Element element) {
1422
+ assert (element.localName == 'div' &&
1423
+ element.className == 'message_embed_title' );
1424
+ if (element.nodes case [
1425
+ dom.Element (localName: 'a' , className: '' ) && final child,
1426
+ ]) {
1427
+ final titleHref = child.attributes['href' ];
1428
+ // Make sure both image hyperlink and title hyperlink are same.
1429
+ if (imageHref != titleHref) return null ;
1430
+
1431
+ if (child.nodes case [dom.Text (text: final title)]) {
1432
+ return title;
1433
+ }
1434
+ }
1435
+ return null ;
1436
+ }
1437
+
1438
+ String ? parseDescription (dom.Element element) {
1439
+ assert (element.localName == 'div' &&
1440
+ element.className == 'message_embed_description' );
1441
+ if (element.nodes case [dom.Text (text: final description)]) {
1442
+ return description;
1443
+ }
1444
+ return null ;
1445
+ }
1446
+
1447
+ String ? title, description;
1448
+ switch (dataContainer.nodes) {
1449
+ case [
1450
+ dom.Element (
1451
+ localName: 'div' ,
1452
+ className: 'message_embed_title' ) && final first,
1453
+ dom.Element (
1454
+ localName: 'div' ,
1455
+ className: 'message_embed_description' ) && final second,
1456
+ ]:
1457
+ title = parseTitle (first);
1458
+ if (title == null ) return null ;
1459
+ description = parseDescription (second);
1460
+ if (description == null ) return null ;
1461
+
1462
+ case [dom.Element (localName: 'div' ) && final single]:
1463
+ switch (single.className) {
1464
+ case 'message_embed_title' :
1465
+ title = parseTitle (single);
1466
+ if (title == null ) return null ;
1467
+
1468
+ case 'message_embed_description' :
1469
+ description = parseDescription (single);
1470
+ if (description == null ) return null ;
1471
+
1472
+ default :
1473
+ return null ;
1474
+ }
1475
+
1476
+ case []:
1477
+ // Server generates an empty `<div class="data-container"></div>`
1478
+ // if website HTML has neither title (derived from
1479
+ // `og:title` or `<title>…</title>`) nor description (derived from
1480
+ // `og:description`).
1481
+ break ;
1482
+
1483
+ default :
1484
+ return null ;
1485
+ }
1486
+
1487
+ return WebsitePreviewNode (
1488
+ hrefUrl: imageHref,
1489
+ imageSrcUrl: imageSrcUrl,
1490
+ title: title,
1491
+ description: description,
1492
+ debugHtmlNode: debugHtmlNode);
1493
+ } else {
1494
+ return null ;
1495
+ }
1496
+ }();
1497
+
1498
+ return result ?? UnimplementedBlockContentNode (htmlNode: divElement);
1499
+ }
1500
+
1342
1501
BlockContentNode parseTableContent (dom.Element tableElement) {
1343
1502
assert (tableElement.localName == 'table'
1344
1503
&& tableElement.className.isEmpty);
@@ -1583,6 +1742,10 @@ class _ZulipContentParser {
1583
1742
}
1584
1743
}
1585
1744
1745
+ if (localName == 'div' && className == 'message_embed' ) {
1746
+ return parseWebsitePreviewNode (element);
1747
+ }
1748
+
1586
1749
// TODO more types of node
1587
1750
return UnimplementedBlockContentNode (htmlNode: node);
1588
1751
}
0 commit comments