Skip to content

Commit d9a3032

Browse files
committed
feat: Reformat YouTube videos to the embeded URL
Extract the YouTube video or list ID and generates the embed URL. Simplifies embedding YouTube videos by allowing one to copy paste the direct YouTube URL rather than extracting the embed URL from the <iframe> source. Also supports short youtu.be links. Add "rel=0" (unless explicitly specified) to only show related videos from the same channel. Add test for link with rel=1 Fix https enforcer to only replace at start of link. Add test for http replacement.
1 parent 5b067ac commit d9a3032

File tree

3 files changed

+128
-8
lines changed

3 files changed

+128
-8
lines changed

src/Docfx.MarkdigEngine.Extensions/QuoteSectionNote/QuoteSectionNoteRender.cs

Lines changed: 109 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
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;
47
using System.Web;
58
using Markdig.Renderers;
69
using Markdig.Renderers.Html;
710

811
namespace 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
}

test/Docfx.MarkdigEngine.Extensions.Tests/QuoteSectionNoteTest.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,19 @@ public void TestVideoBlock_Normal()
421421
TestUtility.VerifyMarkup(source, expected);
422422
}
423423

424+
[Fact]
425+
[Trait("Related", "DfmMarkdown")]
426+
public void TestVideoBlock_Http()
427+
{
428+
var source = @"# Article 2
429+
> [!VIDEO http://microsoft.com:8080?query=http+A#bookmark]
430+
";
431+
var expected = @"<h1 id=""article-2"">Article 2</h1>
432+
<div class=""embeddedvideo""><iframe src=""https://microsoft.com:8080/?query=http+A#bookmark"" frameborder=""0"" allowfullscreen=""true""></iframe></div>
433+
";
434+
TestUtility.VerifyMarkup(source, expected);
435+
}
436+
424437
[Fact]
425438
[Trait("Related", "DfmMarkdown")]
426439
public void TestVideoBlock_Channel9()

test/Docfx.MarkdigEngine.Extensions.Tests/VideoTest.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@ public class VideoTest
1313
</div></p>
1414
")]
1515
[InlineData(@":::video source=""https://www.youtube.com/embed/wV11_nbT2XE"" title=""Video: Build-Your-First-Android-App-with-Visual-Studio-2019-and-Xamarin"" thumbnail=""media/3-eclipse-install-button.png"" upload-date=""07/27/2020"":::", @"<p><div class=""embeddedvideo"">
16-
<iframe src=""https://www.youtube-nocookie.com/embed/wV11_nbT2XE"" allowFullScreen=""true"" frameBorder=""0"" title=""Video: Build-Your-First-Android-App-with-Visual-Studio-2019-and-Xamarin"" thumbnail=""media/3-eclipse-install-button.png"" upload-date=""07/27/2020""></iframe>
16+
<iframe src=""https://www.youtube-nocookie.com/embed/wV11_nbT2XE?rel=0"" allowFullScreen=""true"" frameBorder=""0"" title=""Video: Build-Your-First-Android-App-with-Visual-Studio-2019-and-Xamarin"" thumbnail=""media/3-eclipse-install-button.png"" upload-date=""07/27/2020""></iframe>
17+
</div></p>
18+
")]
19+
[InlineData(@":::video source=""https://www.youtube.com/embed/wV11_nbT2XE?rel=1"" title=""Video: Build-Your-First-Android-App-with-Visual-Studio-2019-and-Xamarin"" thumbnail=""media/3-eclipse-install-button.png"" upload-date=""07/27/2020"":::", @"<p><div class=""embeddedvideo"">
20+
<iframe src=""https://www.youtube-nocookie.com/embed/wV11_nbT2XE?rel=1"" allowFullScreen=""true"" frameBorder=""0"" title=""Video: Build-Your-First-Android-App-with-Visual-Studio-2019-and-Xamarin"" thumbnail=""media/3-eclipse-install-button.png"" upload-date=""07/27/2020""></iframe>
1721
</div></p>
1822
")]
1923
[InlineData(
2024
@":::video source=""https://www.youtube.com/embed/wV11_nbT2XE"" title=""Video: Build-Your-First-Android-App-with-Visual-Studio-2019-and-Xamarin"" thumbnail=""media/3-eclipse-install-button.png"" upload-date=""07/27/2020"":::
2125
:::video source=""https://channel9.msdn.com/Shows/XamarinShow/Build-Your-First-Android-App-with-Visual-Studio-2019-and-Xamarin/player?nocookie=true"" title=""Video: Build-Your-First-Android-App-with-Visual-Studio-2019-and-Xamarin"" max-width=""400"" thumbnail=""media/3-eclipse-install-button.png"" upload-date=""07/27/2020"":::
2226
", @"<p><div class=""embeddedvideo"">
23-
<iframe src=""https://www.youtube-nocookie.com/embed/wV11_nbT2XE"" allowFullScreen=""true"" frameBorder=""0"" title=""Video: Build-Your-First-Android-App-with-Visual-Studio-2019-and-Xamarin"" thumbnail=""media/3-eclipse-install-button.png"" upload-date=""07/27/2020"">
27+
<iframe src=""https://www.youtube-nocookie.com/embed/wV11_nbT2XE?rel=0"" allowFullScreen=""true"" frameBorder=""0"" title=""Video: Build-Your-First-Android-App-with-Visual-Studio-2019-and-Xamarin"" thumbnail=""media/3-eclipse-install-button.png"" upload-date=""07/27/2020"">
2428
</iframe></div>
2529
<div class=""embeddedvideo"">
2630
<iframe src=""https://channel9.msdn.com/Shows/XamarinShow/Build-Your-First-Android-App-with-Visual-Studio-2019-and-Xamarin/player?nocookie=true&amp;nocookie=true"" allowFullScreen=""true"" frameBorder=""0"" title=""Video: Build-Your-First-Android-App-with-Visual-Studio-2019-and-Xamarin"" style=""max-width:400px;"" thumbnail=""media/3-eclipse-install-button.png"" upload-date=""07/27/2020"">

0 commit comments

Comments
 (0)