11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4+ using System . Collections . ObjectModel ;
5+ using System . Diagnostics ;
6+ using System . Text . RegularExpressions ;
47using System . Web ;
58using Markdig . Renderers ;
69using Markdig . Renderers . Html ;
710
811namespace Docfx . MarkdigEngine . Extensions ;
912
10- public class QuoteSectionNoteRender : HtmlObjectRenderer < QuoteSectionNoteBlock >
13+ public partial class QuoteSectionNoteRender : HtmlObjectRenderer < QuoteSectionNoteBlock >
1114{
1215 private readonly MarkdownContext _context ;
1316 private readonly Dictionary < string , string > _notes ;
@@ -100,13 +103,14 @@ private static void WriteVideo(HtmlRenderer renderer, QuoteSectionNoteBlock obj)
100103
101104 public static string FixUpLink ( string link )
102105 {
103- if ( ! link . Contains ( "https ") )
106+ if ( link . StartsWith ( "http: ") )
104107 {
105- link = link . Replace ( "http" , "https" ) ;
108+ link = "https:" + link . Substring ( "http:" . Length ) ;
106109 }
107110 if ( Uri . TryCreate ( link , UriKind . Absolute , out Uri videoLink ) )
108111 {
109112 var host = videoLink . Host ;
113+ var path = videoLink . LocalPath ;
110114 var query = videoLink . Query ;
111115 if ( query . Length > 1 )
112116 {
@@ -125,16 +129,115 @@ public static string FixUpLink(string link)
125129 query += "&nocookie=true" ;
126130 }
127131 }
128- else if ( host . Equals ( "youtube.com" , StringComparison . OrdinalIgnoreCase ) || host . Equals ( "www.youtube.com" , StringComparison . OrdinalIgnoreCase ) )
132+ else if ( hostsYouTube . Contains ( host , StringComparer . OrdinalIgnoreCase ) )
129133 {
130134 // case 2, YouTube video
131- host = "www.youtube-nocookie.com" ;
135+ var idYouTube = GetYouTubeId ( host , path , ref query ) ;
136+ if ( idYouTube != null )
137+ {
138+ host = "www.youtube-nocookie.com" ;
139+ path = "/embed/" + idYouTube ;
140+ query = AddYouTubeRel ( query ) ;
141+ }
142+ else
143+ {
144+ //YouTube Playlist
145+ var listYouTube = GetYouTubeList ( query ) ;
146+ if ( listYouTube != null )
147+ {
148+ host = "www.youtube-nocookie.com" ;
149+ path = "/embed/videoseries" ;
150+ query = "list=" + listYouTube ;
151+ query = AddYouTubeRel ( query ) ;
152+ }
153+ }
154+
155+ //Keep this to preserve previous behavior
156+ if ( host . Equals ( "youtube.com" , StringComparison . OrdinalIgnoreCase ) || host . Equals ( "www.youtube.com" , StringComparison . OrdinalIgnoreCase ) )
157+ {
158+ host = "www.youtube-nocookie.com" ;
159+ }
132160 }
133161
134- var builder = new UriBuilder ( videoLink ) { Host = host , Query = query } ;
162+ var builder = new UriBuilder ( videoLink ) { Host = host , Path = path , Query = query } ;
135163 link = builder . Uri . ToString ( ) ;
136164 }
137165
138166 return link ;
139167 }
168+
169+ /// <summary>
170+ /// Only related videos from the same channel
171+ /// https://developers.google.com/youtube/player_parameters
172+ /// </summary>
173+ private static string AddYouTubeRel ( string query )
174+ {
175+ // Add rel=0 unless specified in the original link
176+ if ( query . Split ( '&' ) . Any ( q => q . StartsWith ( "rel=" ) ) == false )
177+ {
178+ if ( query . Length == 0 )
179+ return "rel=0" ;
180+ else
181+ return query + "&rel=0" ;
182+ }
183+
184+ return query ;
185+ }
186+
187+ private static readonly ReadOnlyCollection < string > hostsYouTube = new string [ ] {
188+ "youtube.com" ,
189+ "www.youtube.com" ,
190+ "youtu.be" ,
191+ "www.youtube-nocookie.com" ,
192+ } . AsReadOnly ( ) ;
193+
194+ private static string GetYouTubeId ( string host , string path , ref string query )
195+ {
196+ if ( host == "youtu.be" )
197+ {
198+ return path . Substring ( 1 ) ;
199+ }
200+
201+ var match = ReYouTubeQueryVideo ( ) . Match ( query ) ;
202+ if ( match . Success )
203+ {
204+ //Remove from query
205+ query = query . Replace ( match . Groups [ 0 ] . Value , "" ) . Trim ( '&' ) . Replace ( "&&" , "&" ) ;
206+ return match . Groups [ 2 ] . Value ;
207+ }
208+
209+ match = ReYouTubePathId ( ) . Match ( path ) ;
210+ if ( match . Success )
211+ {
212+ var id = match . Groups [ 1 ] . Value ;
213+
214+ if ( id == "videoseries" )
215+ return null ;
216+
217+ return id ;
218+ }
219+
220+ return null ;
221+ }
222+
223+ [ GeneratedRegex ( @"(^|&)v=([^&]+)" ) ]
224+ private static partial Regex ReYouTubeQueryVideo ( ) ;
225+
226+ [ GeneratedRegex ( @"(^|&)list=([^&]+)" ) ]
227+ private static partial Regex ReYouTubeQueryList ( ) ;
228+
229+ [ GeneratedRegex ( @"/embed/([^/]+)$" ) ]
230+ private static partial Regex ReYouTubePathId ( ) ;
231+
232+ private static string GetYouTubeList ( string query )
233+ {
234+ var match = ReYouTubeQueryList ( ) . Match ( query ) ;
235+ if ( match . Success )
236+ {
237+ return match . Groups [ 2 ] . Value ;
238+ }
239+
240+ return null ;
241+ }
242+
140243}
0 commit comments