diff --git a/src/Docfx.MarkdigEngine.Extensions/QuoteSectionNote/QuoteSectionNoteRender.cs b/src/Docfx.MarkdigEngine.Extensions/QuoteSectionNote/QuoteSectionNoteRender.cs
index d96394903bb..b5c19ad5073 100644
--- a/src/Docfx.MarkdigEngine.Extensions/QuoteSectionNote/QuoteSectionNoteRender.cs
+++ b/src/Docfx.MarkdigEngine.Extensions/QuoteSectionNote/QuoteSectionNoteRender.cs
@@ -1,13 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using System.Text.RegularExpressions;
using System.Web;
using Markdig.Renderers;
using Markdig.Renderers.Html;
namespace Docfx.MarkdigEngine.Extensions;
-public class QuoteSectionNoteRender : HtmlObjectRenderer
+public partial class QuoteSectionNoteRender : HtmlObjectRenderer
{
private readonly MarkdownContext _context;
private readonly Dictionary _notes;
@@ -100,13 +103,14 @@ private static void WriteVideo(HtmlRenderer renderer, QuoteSectionNoteBlock obj)
public static string FixUpLink(string link)
{
- if (!link.Contains("https"))
+ if (link.StartsWith("http:"))
{
- link = link.Replace("http", "https");
+ link = "https:" + link.Substring("http:".Length);
}
if (Uri.TryCreate(link, UriKind.Absolute, out Uri videoLink))
{
var host = videoLink.Host;
+ var path = videoLink.LocalPath;
var query = videoLink.Query;
if (query.Length > 1)
{
@@ -125,16 +129,115 @@ public static string FixUpLink(string link)
query += "&nocookie=true";
}
}
- else if (host.Equals("youtube.com", StringComparison.OrdinalIgnoreCase) || host.Equals("www.youtube.com", StringComparison.OrdinalIgnoreCase))
+ else if (hostsYouTube.Contains(host, StringComparer.OrdinalIgnoreCase))
{
// case 2, YouTube video
- host = "www.youtube-nocookie.com";
+ var idYouTube = GetYouTubeId(host, path, ref query);
+ if (idYouTube != null)
+ {
+ host = "www.youtube-nocookie.com";
+ path = "/embed/" + idYouTube;
+ query = AddYouTubeRel(query);
+ }
+ else
+ {
+ //YouTube Playlist
+ var listYouTube = GetYouTubeList(query);
+ if (listYouTube != null)
+ {
+ host = "www.youtube-nocookie.com";
+ path = "/embed/videoseries";
+ query = "list=" + listYouTube;
+ query = AddYouTubeRel(query);
+ }
+ }
+
+ //Keep this to preserve previous behavior
+ if (host.Equals("youtube.com", StringComparison.OrdinalIgnoreCase) || host.Equals("www.youtube.com", StringComparison.OrdinalIgnoreCase))
+ {
+ host = "www.youtube-nocookie.com";
+ }
}
- var builder = new UriBuilder(videoLink) { Host = host, Query = query };
+ var builder = new UriBuilder(videoLink) { Host = host, Path = path, Query = query };
link = builder.Uri.ToString();
}
return link;
}
+
+ ///
+ /// Only related videos from the same channel
+ /// https://developers.google.com/youtube/player_parameters
+ ///
+ private static string AddYouTubeRel(string query)
+ {
+ // Add rel=0 unless specified in the original link
+ if (query.Split('&').Any(q => q.StartsWith("rel=")) == false)
+ {
+ if (query.Length == 0)
+ return "rel=0";
+ else
+ return query + "&rel=0";
+ }
+
+ return query;
+ }
+
+ private static readonly ReadOnlyCollection hostsYouTube = new string[] {
+ "youtube.com",
+ "www.youtube.com",
+ "youtu.be",
+ "www.youtube-nocookie.com",
+ }.AsReadOnly();
+
+ private static string GetYouTubeId(string host, string path, ref string query)
+ {
+ if (host == "youtu.be")
+ {
+ return path.Substring(1);
+ }
+
+ var match = ReYouTubeQueryVideo().Match(query);
+ if (match.Success)
+ {
+ //Remove from query
+ query = query.Replace(match.Groups[0].Value, "").Trim('&').Replace("&&", "&");
+ return match.Groups[2].Value;
+ }
+
+ match = ReYouTubePathId().Match(path);
+ if (match.Success)
+ {
+ var id = match.Groups[1].Value;
+
+ if (id == "videoseries")
+ return null;
+
+ return id;
+ }
+
+ return null;
+ }
+
+ [GeneratedRegex(@"(^|&)v=([^&]+)")]
+ private static partial Regex ReYouTubeQueryVideo();
+
+ [GeneratedRegex(@"(^|&)list=([^&]+)")]
+ private static partial Regex ReYouTubeQueryList();
+
+ [GeneratedRegex(@"/embed/([^/]+)$")]
+ private static partial Regex ReYouTubePathId();
+
+ private static string GetYouTubeList(string query)
+ {
+ var match = ReYouTubeQueryList().Match(query);
+ if (match.Success)
+ {
+ return match.Groups[2].Value;
+ }
+
+ return null;
+ }
+
}
diff --git a/test/Docfx.MarkdigEngine.Extensions.Tests/QuoteSectionNoteTest.cs b/test/Docfx.MarkdigEngine.Extensions.Tests/QuoteSectionNoteTest.cs
index de4d5279a1d..43a42ff810e 100644
--- a/test/Docfx.MarkdigEngine.Extensions.Tests/QuoteSectionNoteTest.cs
+++ b/test/Docfx.MarkdigEngine.Extensions.Tests/QuoteSectionNoteTest.cs
@@ -421,6 +421,19 @@ public void TestVideoBlock_Normal()
TestUtility.VerifyMarkup(source, expected);
}
+ [Fact]
+ [Trait("Related", "DfmMarkdown")]
+ public void TestVideoBlock_Http()
+ {
+ var source = @"# Article 2
+> [!VIDEO http://microsoft.com:8080?query=http+A#bookmark]
+";
+ var expected = @"Article 2
+
+";
+ TestUtility.VerifyMarkup(source, expected);
+ }
+
[Fact]
[Trait("Related", "DfmMarkdown")]
public void TestVideoBlock_Channel9()
diff --git a/test/Docfx.MarkdigEngine.Extensions.Tests/VideoTest.cs b/test/Docfx.MarkdigEngine.Extensions.Tests/VideoTest.cs
index 544216da32f..f09d0034673 100644
--- a/test/Docfx.MarkdigEngine.Extensions.Tests/VideoTest.cs
+++ b/test/Docfx.MarkdigEngine.Extensions.Tests/VideoTest.cs
@@ -13,14 +13,18 @@ public class VideoTest
")]
[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"":::", @"
-
+
+
+")]
+ [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"":::", @"
+
")]
[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"":::
:::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"":::
", @"
-