@@ -361,6 +361,34 @@ class ImageNode extends BlockContentNode {
361
361
}
362
362
}
363
363
364
+ class VideoNode extends BlockContentNode {
365
+ const VideoNode ({
366
+ super .debugHtmlNode,
367
+ required this .srcUrl,
368
+ this .previewImageUrl,
369
+ });
370
+
371
+ final String srcUrl;
372
+ final String ? previewImageUrl;
373
+
374
+ @override
375
+ bool operator == (Object other) {
376
+ return other is VideoNode
377
+ && other.srcUrl == srcUrl
378
+ && other.previewImageUrl == previewImageUrl;
379
+ }
380
+
381
+ @override
382
+ int get hashCode => Object .hash ('VideoNode' , srcUrl, previewImageUrl);
383
+
384
+ @override
385
+ void debugFillProperties (DiagnosticPropertiesBuilder properties) {
386
+ super .debugFillProperties (properties);
387
+ properties.add (StringProperty ('srcUrl' , srcUrl));
388
+ properties.add (StringProperty ('previewImageUrl' , previewImageUrl));
389
+ }
390
+ }
391
+
364
392
/// A content node that expects an inline layout context from its parent.
365
393
///
366
394
/// When rendered into a Flutter widget tree, an inline content node
@@ -694,6 +722,29 @@ class _ZulipContentParser {
694
722
}();
695
723
static final _emojiCodeFromClassNameRegexp = RegExp (r"emoji-([^ ]+)" );
696
724
725
+ static final _videoClassNameRegexp = () {
726
+ const sourceType = r"(message_inline_video)?(youtube-video)?(embed-video)?" ;
727
+ return RegExp ("^message_inline_image $sourceType |$sourceType message_inline_image\$ " );
728
+ }();
729
+
730
+ bool _isVideoClassNameInlineVideo (String input) {
731
+ if (! _videoClassNameRegexp.hasMatch (input)) return false ;
732
+ final groups = _videoClassNameRegexp.firstMatch (input)! .groups ([1 , 4 ]);
733
+ return groups.nonNulls.firstOrNull == 'message_inline_video' ;
734
+ }
735
+
736
+ bool _isVideoClassNameYoutubeVideo (String input) {
737
+ if (! _videoClassNameRegexp.hasMatch (input)) return false ;
738
+ final groups = _videoClassNameRegexp.firstMatch (input)! .groups ([2 , 5 ]);
739
+ return groups.nonNulls.firstOrNull == 'youtube-video' ;
740
+ }
741
+
742
+ bool _isVideoClassNameEmbedVideo (String input) {
743
+ if (! _videoClassNameRegexp.hasMatch (input)) return false ;
744
+ final groups = _videoClassNameRegexp.firstMatch (input)! .groups ([3 , 6 ]);
745
+ return groups.nonNulls.firstOrNull == 'embed-video' ;
746
+ }
747
+
697
748
InlineContentNode parseInlineContent (dom.Node node) {
698
749
assert (_debugParserContext == _ParserContext .inline);
699
750
final debugHtmlNode = kDebugMode ? node : null ;
@@ -948,6 +999,85 @@ class _ZulipContentParser {
948
999
return ImageNode (srcUrl: src, debugHtmlNode: debugHtmlNode);
949
1000
}
950
1001
1002
+ BlockContentNode parseVideoNode (dom.Element divElement) {
1003
+ assert (_debugParserContext == _ParserContext .block);
1004
+ assert (divElement.localName == 'div'
1005
+ && _videoClassNameRegexp.hasMatch (divElement.className));
1006
+
1007
+ if (_isVideoClassNameInlineVideo (divElement.className)) {
1008
+ return _parseInlineVideo (divElement);
1009
+ }
1010
+ if (_isVideoClassNameYoutubeVideo (divElement.className)
1011
+ || _isVideoClassNameEmbedVideo (divElement.className)) {
1012
+ return _parseEmbedVideoWithPreviewImage (divElement);
1013
+ }
1014
+ return UnimplementedBlockContentNode (htmlNode: divElement);
1015
+ }
1016
+
1017
+ BlockContentNode _parseInlineVideo (dom.Element divElement) {
1018
+ final videoElement = () {
1019
+ if (divElement.nodes.length != 1 ) return null ;
1020
+ final child = divElement.nodes[0 ];
1021
+ if (child is ! dom.Element ) return null ;
1022
+ if (child.localName != 'a' ) return null ;
1023
+ if (child.className.isNotEmpty) return null ;
1024
+
1025
+ if (child.nodes.length != 1 ) return null ;
1026
+ final grandchild = child.nodes[0 ];
1027
+ if (grandchild is ! dom.Element ) return null ;
1028
+ if (grandchild.localName != 'video' ) return null ;
1029
+ if (grandchild.className.isNotEmpty) return null ;
1030
+ return grandchild;
1031
+ }();
1032
+
1033
+ final debugHtmlNode = kDebugMode ? divElement : null ;
1034
+ if (videoElement == null ) {
1035
+ return UnimplementedBlockContentNode (htmlNode: divElement);
1036
+ }
1037
+
1038
+ final src = videoElement.attributes['src' ];
1039
+ if (src == null ) {
1040
+ return UnimplementedBlockContentNode (htmlNode: divElement);
1041
+ }
1042
+
1043
+ return VideoNode (srcUrl: src, debugHtmlNode: debugHtmlNode);
1044
+ }
1045
+
1046
+ BlockContentNode _parseEmbedVideoWithPreviewImage (dom.Element divElement) {
1047
+ final result = () {
1048
+ if (divElement.nodes.length != 1 ) return null ;
1049
+ final child = divElement.nodes[0 ];
1050
+ if (child is ! dom.Element ) return null ;
1051
+ if (child.localName != 'a' ) return null ;
1052
+ if (child.className.isNotEmpty) return null ;
1053
+
1054
+ if (child.nodes.length != 1 ) return null ;
1055
+ final grandchild = child.nodes[0 ];
1056
+ if (grandchild is ! dom.Element ) return null ;
1057
+ if (grandchild.localName != 'img' ) return null ;
1058
+ if (grandchild.className.isNotEmpty) return null ;
1059
+ return (child, grandchild);
1060
+ }();
1061
+
1062
+ final debugHtmlNode = kDebugMode ? divElement : null ;
1063
+ if (result == null ) {
1064
+ return UnimplementedBlockContentNode (htmlNode: divElement);
1065
+ }
1066
+ final (anchorElement, imgElement) = result;
1067
+
1068
+ final imgSrc = imgElement.attributes['src' ];
1069
+ if (imgSrc == null ) {
1070
+ return UnimplementedBlockContentNode (htmlNode: divElement);
1071
+ }
1072
+
1073
+ final href = anchorElement.attributes['href' ];
1074
+ if (href == null ) {
1075
+ return UnimplementedBlockContentNode (htmlNode: divElement);
1076
+ }
1077
+
1078
+ return VideoNode (srcUrl: href, previewImageUrl: imgSrc, debugHtmlNode: debugHtmlNode);
1079
+ }
1080
+
951
1081
BlockContentNode parseBlockContent (dom.Node node) {
952
1082
assert (_debugParserContext == _ParserContext .block);
953
1083
final debugHtmlNode = kDebugMode ? node : null ;
@@ -1024,6 +1154,10 @@ class _ZulipContentParser {
1024
1154
return parseImageNode (element);
1025
1155
}
1026
1156
1157
+ if (localName == 'div' && _videoClassNameRegexp.hasMatch (className)) {
1158
+ return parseVideoNode (element);
1159
+ }
1160
+
1027
1161
// TODO more types of node
1028
1162
return UnimplementedBlockContentNode (htmlNode: node);
1029
1163
}
0 commit comments