diff --git a/src/Docfx.MarkdigEngine.Extensions/QuoteSectionNote/QuoteSectionNoteRender.cs b/src/Docfx.MarkdigEngine.Extensions/QuoteSectionNote/QuoteSectionNoteRender.cs index d96394903bb..b08e825ecf9 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; @@ -107,6 +110,7 @@ public static string FixUpLink(string link) 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,107 @@ 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; + } + else + { + //YouTube Playlist + var listYouTube = GetYouTubeList(query); + if (listYouTube != null) + { + host = "www.youtube-nocookie.com"; + path = "/embed/videoseries"; + query = "list=" + listYouTube; + //No need to modify query parameters, "list=..." is the same + } + } + + //Only related videos from the same channel + //https://developers.google.com/youtube/player_parameters + //Add rel=0 unless specified in the original link + if (query.Split('&').Any(q => q.StartsWith("rel=")) == false) + { + if (query.Length == 0) + query = "rel=0"; + else + query += "&rel=0"; + } + + //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; } + + 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; + } + }