Skip to content

Commit b84ca7d

Browse files
authored
Markdig version update (#8292)
* update markdig to 0.22.0 * migrate to markdig with feature flag * add unit test * add disable html extension, auto link, emjo, table, tasklist
1 parent e724d7f commit b84ca7d

4 files changed

Lines changed: 315 additions & 28 deletions

File tree

src/NuGetGallery.Services/Configuration/FeatureFlagService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public class FeatureFlagService : IFeatureFlagService
4040
private const string PackageRenamesFeatureName = GalleryPrefix + "PackageRenames";
4141
private const string EmbeddedReadmeFlightName = GalleryPrefix + "EmbeddedReadmes";
4242
private const string LicenseMdRenderingFlightName = GalleryPrefix + "LicenseMdRendering";
43+
private const string MarkdigMdRenderingFlightName = GalleryPrefix + "MarkdigMdRendering";
4344
private const string DeletePackageApiFlightName = GalleryPrefix + "DeletePackageApi";
4445

4546
private const string ODataV1GetAllNonHijackedFeatureName = GalleryPrefix + "ODataV1GetAllNonHijacked";
@@ -286,6 +287,11 @@ public bool IsODataV2SearchCountNonHijackedEnabled()
286287
return _client.IsEnabled(ODataV2SearchCountNonHijackedFeatureName, defaultValue: true);
287288
}
288289

290+
public bool IsMarkdigMdRenderingEnabled()
291+
{
292+
return _client.IsEnabled(MarkdigMdRenderingFlightName, defaultValue: false);
293+
}
294+
289295
public bool IsDeletePackageApiEnabled(User user)
290296
{
291297
return _client.IsEnabled(DeletePackageApiFlightName, user, defaultValue: false);

src/NuGetGallery.Services/Configuration/IFeatureFlagService.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,10 @@ public interface IFeatureFlagService
227227
bool IsODataV2SearchCountNonHijackedEnabled();
228228

229229
/// <summary>
230+
/// Whether rendering Markdown content to HTML using Markdig is enabled
231+
/// </summary>
232+
bool IsMarkdigMdRenderingEnabled();
233+
230234
/// Whether or not the user can delete a package through the API.
231235
/// </summary>
232236
bool IsDeletePackageApiEnabled(User user);

src/NuGetGallery/Services/MarkdownService.cs

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,74 @@
33

44
using System;
55
using System.IO;
6+
using System.Linq;
7+
using System.Management;
68
using System.Text.RegularExpressions;
9+
using System.Timers;
710
using System.Web;
811
using CommonMark;
912
using CommonMark.Syntax;
13+
using Markdig;
14+
using Markdig.Parsers;
15+
using Markdig.Renderers;
16+
using Markdig.Syntax;
17+
using Markdig.Syntax.Inlines;
1018

1119
namespace NuGetGallery
1220
{
1321
public class MarkdownService : IMarkdownService
1422
{
1523
private static readonly TimeSpan RegexTimeout = TimeSpan.FromMinutes(1);
1624
private static readonly Regex EncodedBlockQuotePattern = new Regex("^ {0,3}&gt;", RegexOptions.Multiline, RegexTimeout);
17-
private static readonly Regex CommonMarkLinkPattern = new Regex("<a href=([\"\']).*?\\1", RegexOptions.None, RegexTimeout);
25+
private static readonly Regex LinkPattern = new Regex("<a href=([\"\']).*?\\1", RegexOptions.None, RegexTimeout);
26+
27+
private readonly IFeatureFlagService _features;
28+
29+
public MarkdownService(IFeatureFlagService features)
30+
{
31+
_features = features ?? throw new ArgumentNullException(nameof(features));
32+
}
1833

1934
public RenderedMarkdownResult GetHtmlFromMarkdown(string markdownString)
2035
{
21-
return GetHtmlFromMarkdown(markdownString, 1);
36+
if (markdownString == null)
37+
{
38+
throw new ArgumentNullException(nameof(markdownString));
39+
}
40+
41+
if (_features.IsMarkdigMdRenderingEnabled())
42+
{
43+
return GetHtmlFromMarkdownMarkdig(markdownString, 1);
44+
}
45+
else
46+
{
47+
return GetHtmlFromMarkdownCommonMark(markdownString, 1);
48+
}
2249
}
2350

2451
public RenderedMarkdownResult GetHtmlFromMarkdown(string markdownString, int incrementHeadersBy)
52+
{
53+
if (markdownString == null)
54+
{
55+
throw new ArgumentNullException(nameof(markdownString));
56+
}
57+
58+
if (incrementHeadersBy < 0)
59+
{
60+
throw new ArgumentOutOfRangeException(nameof(incrementHeadersBy), $"{nameof(incrementHeadersBy)} must be greater than or equal to 0");
61+
}
62+
63+
if (_features.IsMarkdigMdRenderingEnabled())
64+
{
65+
return GetHtmlFromMarkdownMarkdig(markdownString, incrementHeadersBy);
66+
}
67+
else
68+
{
69+
return GetHtmlFromMarkdownCommonMark(markdownString, incrementHeadersBy);
70+
}
71+
}
72+
73+
private RenderedMarkdownResult GetHtmlFromMarkdownCommonMark(string markdownString, int incrementHeadersBy)
2574
{
2675
var output = new RenderedMarkdownResult()
2776
{
@@ -106,7 +155,84 @@ public RenderedMarkdownResult GetHtmlFromMarkdown(string markdownString, int inc
106155
{
107156
CommonMarkConverter.ProcessStage3(document, htmlWriter, settings);
108157

109-
output.Content = CommonMarkLinkPattern.Replace(htmlWriter.ToString(), "$0" + " rel=\"nofollow\"").Trim();
158+
output.Content = LinkPattern.Replace(htmlWriter.ToString(), "$0" + " rel=\"nofollow\"").Trim();
159+
return output;
160+
}
161+
}
162+
163+
private RenderedMarkdownResult GetHtmlFromMarkdownMarkdig(string markdownString, int incrementHeadersBy)
164+
{
165+
var output = new RenderedMarkdownResult()
166+
{
167+
ImagesRewritten = false,
168+
Content = ""
169+
};
170+
171+
var readmeWithoutBom = markdownString.TrimStart('\ufeff');
172+
173+
var pipeline = new MarkdownPipelineBuilder()
174+
.UseGridTables()
175+
.UsePipeTables()
176+
.UseListExtras()
177+
.UseTaskLists()
178+
.UseSoftlineBreakAsHardlineBreak()
179+
.UseEmojiAndSmiley()
180+
.UseAutoLinks()
181+
.UseReferralLinks("nofollow")
182+
.DisableHtml() //block inline html
183+
.Build();
184+
185+
using (var htmlWriter = new StringWriter())
186+
{
187+
var renderer = new HtmlRenderer(htmlWriter);
188+
pipeline.Setup(renderer);
189+
190+
var document = Markdown.Parse(readmeWithoutBom, pipeline);
191+
192+
foreach (var node in document.Descendants())
193+
{
194+
if (node is Markdig.Syntax.Block)
195+
{
196+
// Demote heading tags so they don't overpower expander headings.
197+
if (node is HeadingBlock heading)
198+
{
199+
heading.Level = Math.Min(heading.Level + incrementHeadersBy, 6);
200+
}
201+
}
202+
else if (node is Markdig.Syntax.Inlines.Inline)
203+
{
204+
if (node is LinkInline linkInline)
205+
{
206+
if (linkInline.IsImage)
207+
{
208+
if (!PackageHelper.TryPrepareUrlForRendering(linkInline.Url, out string readyUriString, rewriteAllHttp: true))
209+
{
210+
linkInline.Url = string.Empty;
211+
}
212+
else
213+
{
214+
output.ImagesRewritten = output.ImagesRewritten || (linkInline.Url != readyUriString);
215+
linkInline.Url = readyUriString;
216+
}
217+
}
218+
else
219+
{
220+
// Allow only http or https links in markdown. Transform link to https for known domains.
221+
if (!PackageHelper.TryPrepareUrlForRendering(linkInline.Url, out string readyUriString))
222+
{
223+
linkInline.Url = string.Empty;
224+
}
225+
else
226+
{
227+
linkInline.Url = readyUriString;
228+
}
229+
}
230+
}
231+
}
232+
}
233+
234+
renderer.Render(document);
235+
output.Content = htmlWriter.ToString().Trim();
110236
return output;
111237
}
112238
}

0 commit comments

Comments
 (0)