Skip to content

Commit fe367e2

Browse files
cjlotzyufeih
authored andcommitted
feat: Added extensions configuration point and PlantUml support (dotnet#9574)
* Added extensions configuration point and PlantUmlExtension * Address review comments and improve error handling of PlantUmlExtension * Remove output formatters * Update JsonConverterTest.cs to ignoreLineEndingDifferences * Update JsonConverterTest.cs --------- Co-authored-by: Yufei Huang <yufeih@live.com>
1 parent 9f534aa commit fe367e2

File tree

16 files changed

+365
-247
lines changed

16 files changed

+365
-247
lines changed

Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<PackageVersion Include="OneOf" Version="3.0.263" />
3030
<PackageVersion Include="OneOf.SourceGenerator" Version="3.0.263" />
3131
<PackageVersion Include="PdfPig" Version="0.1.9-alpha-20231119-4537e" />
32+
<PackageVersion Include="PlantUml.Net" Version="1.4.80" />
3233
<PackageVersion Include="Spectre.Console" Version="0.48.0" />
3334
<PackageVersion Include="Spectre.Console.Cli" Version="0.48.0" />
3435
<PackageVersion Include="Stubble.Core" Version="1.10.8" />
@@ -46,4 +47,4 @@
4647
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.4" />
4748
<PackageVersion Include="xunit" Version="2.6.2" />
4849
</ItemGroup>
49-
</Project>
50+
</Project>

src/Docfx.MarkdigEngine.Extensions/Docfx.MarkdigEngine.Extensions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<ItemGroup>
33
<PackageReference Include="Markdig" />
44
<PackageReference Include="Newtonsoft.Json" />
5+
<PackageReference Include="PlantUml.Net" />
56
</ItemGroup>
67

78
</Project>

src/Docfx.MarkdigEngine.Extensions/MarkdownContext.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ public class MarkdownContext
3636
/// <returns>Image url bound to the path</returns>
3737
public delegate string GetImageLinkDelegate(string path, MarkdownObject origin, string altText);
3838

39+
/// <summary>
40+
/// Allows configuration of extensions
41+
/// </summary>
42+
/// <param name="extension">Name of the extension being configured</param>
43+
/// <returns>Object representing the configuration for the extension</returns>
44+
public delegate object GetExtensionConfigurationDelegate(string extension);
45+
3946
/// <summary>
4047
/// Reads a file as text.
4148
/// </summary>
@@ -51,6 +58,11 @@ public class MarkdownContext
5158
/// </summary>
5259
public GetImageLinkDelegate GetImageLink { get; }
5360

61+
/// <summary>
62+
/// Get the configuration for a given extension
63+
/// </summary>
64+
public GetExtensionConfigurationDelegate GetExtensionConfiguration { get; }
65+
5466
/// <summary>
5567
/// Log info
5668
/// </summary>
@@ -86,12 +98,14 @@ public MarkdownContext(
8698
LogActionDelegate logError = null,
8799
ReadFileDelegate readFile = null,
88100
GetLinkDelegate getLink = null,
89-
GetImageLinkDelegate getImageLink = null)
101+
GetImageLinkDelegate getImageLink = null,
102+
GetExtensionConfigurationDelegate getConfig = null)
90103
{
91104
_getToken = getToken ?? (_ => null);
92105
ReadFile = readFile ?? ((a, b) => (a, a));
93106
GetLink = getLink ?? ((a, b) => a);
94107
GetImageLink = getImageLink ?? ((a, b, c) => a);
108+
GetExtensionConfiguration = getConfig ?? (_ => null);
95109
LogInfo = logInfo ?? ((a, b, c, d) => { });
96110
LogSuggestion = logSuggestion ?? ((a, b, c, d) => { });
97111
LogWarning = logWarning ?? ((a, b, c, d) => { });

src/Docfx.MarkdigEngine.Extensions/MarkdownExtensions.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Docfx.MarkdigEngine.Extensions;
1111

1212
public static class MarkdownExtensions
1313
{
14-
public static MarkdownPipelineBuilder UseDocfxExtensions(this MarkdownPipelineBuilder pipeline, MarkdownContext context, Dictionary<string, string> notes = null)
14+
public static MarkdownPipelineBuilder UseDocfxExtensions(this MarkdownPipelineBuilder pipeline, MarkdownContext context)
1515
{
1616
return pipeline
1717
.UseMathematics()
@@ -24,7 +24,7 @@ public static MarkdownPipelineBuilder UseDocfxExtensions(this MarkdownPipelineBu
2424
.UseIncludeFile(context)
2525
.UseCodeSnippet(context)
2626
.UseDFMCodeInfoPrefix()
27-
.UseQuoteSectionNote(context, notes)
27+
.UseQuoteSectionNote(context)
2828
.UseXref()
2929
.UseEmojiAndSmiley(false)
3030
.UseTabGroup(context)
@@ -35,6 +35,7 @@ public static MarkdownPipelineBuilder UseDocfxExtensions(this MarkdownPipelineBu
3535
.UseTripleColon(context)
3636
.UseNoloc()
3737
.UseResolveLink(context)
38+
.UsePlantUml(context)
3839
.RemoveUnusedExtensions();
3940
}
4041

@@ -99,9 +100,9 @@ public static MarkdownPipelineBuilder UseDFMCodeInfoPrefix(this MarkdownPipeline
99100
return pipeline;
100101
}
101102

102-
public static MarkdownPipelineBuilder UseQuoteSectionNote(this MarkdownPipelineBuilder pipeline, MarkdownContext context, Dictionary<string, string> notes = null)
103+
public static MarkdownPipelineBuilder UseQuoteSectionNote(this MarkdownPipelineBuilder pipeline, MarkdownContext context)
103104
{
104-
pipeline.Extensions.AddIfNotAlready(new QuoteSectionNoteExtension(context, notes));
105+
pipeline.Extensions.AddIfNotAlready(new QuoteSectionNoteExtension(context));
105106
return pipeline;
106107
}
107108

@@ -111,6 +112,12 @@ public static MarkdownPipelineBuilder UseLineNumber(this MarkdownPipelineBuilder
111112
return pipeline;
112113
}
113114

115+
public static MarkdownPipelineBuilder UsePlantUml(this MarkdownPipelineBuilder pipeline, MarkdownContext context)
116+
{
117+
pipeline.Extensions.AddIfNotAlready(new PlantUmlExtension(context));
118+
return pipeline;
119+
}
120+
114121
public static MarkdownPipelineBuilder UseResolveLink(this MarkdownPipelineBuilder pipeline, MarkdownContext context)
115122
{
116123
pipeline.Extensions.AddIfNotAlready(new ResolveLinkExtension(context));
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using static System.Text.Encoding;
2+
3+
using Markdig.Renderers;
4+
using Markdig.Syntax;
5+
using Markdig.Renderers.Html;
6+
using PlantUml.Net;
7+
8+
namespace Docfx.MarkdigEngine.Extensions;
9+
10+
/// <summary>
11+
/// An HTML renderer for a <see cref="CodeBlock"/> and <see cref="FencedCodeBlock"/>.
12+
/// </summary>
13+
/// <seealso cref="HtmlObjectRenderer{CodeBlock}" />
14+
public class CustomCodeBlockRenderer : CodeBlockRenderer
15+
{
16+
private readonly MarkdownContext _context;
17+
private readonly DocfxPlantUmlSettings _settings;
18+
private readonly RendererFactory rendererFactory;
19+
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="CodeBlockRenderer"/> class.
22+
/// </summary>
23+
/// <param name="context"></param>
24+
/// <param name="settings"></param>
25+
public CustomCodeBlockRenderer(MarkdownContext context, DocfxPlantUmlSettings settings)
26+
{
27+
_context = context;
28+
_settings = settings;
29+
30+
rendererFactory = new RendererFactory();
31+
}
32+
33+
protected override void Write(HtmlRenderer renderer, CodeBlock obj)
34+
{
35+
if (obj is FencedCodeBlock fencedCodeBlock
36+
&& fencedCodeBlock.Info is string info
37+
&& info.Equals("plantuml", StringComparison.OrdinalIgnoreCase))
38+
{
39+
IPlantUmlRenderer plantUmlRenderer = rendererFactory.CreateRenderer(_settings);
40+
41+
// Get PlantUML code.
42+
var plantUmlCode = fencedCodeBlock.Lines.ToString();
43+
44+
try
45+
{
46+
byte[] output = plantUmlRenderer.Render(plantUmlCode, _settings.OutputFormat);
47+
48+
renderer.EnsureLine();
49+
renderer.Write(FormatOutput(_settings.OutputFormat, output));
50+
renderer.EnsureLine();
51+
}
52+
catch (RenderingException ex)
53+
{
54+
_context.LogWarning(nameof(PlantUmlExtension), ex.Message, null);
55+
}
56+
catch (Exception ex)
57+
{
58+
_context.LogError(nameof(PlantUmlExtension), ex.Message, null);
59+
60+
// If the error is not related to rendering a specific diagram, re-throw to abort
61+
throw;
62+
}
63+
64+
return;
65+
}
66+
67+
// Fallback to default CodeBlockRenderer
68+
base.Write(renderer, obj);
69+
}
70+
71+
private static string FormatOutput(OutputFormat format, byte[] output)
72+
{
73+
switch (format)
74+
{
75+
case OutputFormat.Svg:
76+
string svg = UTF8.GetString(output);
77+
return $"<div class=\"lang-plantUml\">{svg}</div>";
78+
79+
case OutputFormat.Ascii:
80+
string ascii = ASCII.GetString(output);
81+
return $"<div class=\"lang-plantUml\"><pre>{ascii}</pre></div>";
82+
83+
case OutputFormat.Ascii_Unicode:
84+
string asciiUnicode = UTF8.GetString(output);
85+
return $"<div class=\"lang-plantUml\"><pre>{asciiUnicode}</pre></div>";
86+
87+
case OutputFormat.Png:
88+
case OutputFormat.Eps:
89+
case OutputFormat.Pdf:
90+
case OutputFormat.Vdx:
91+
case OutputFormat.Xmi:
92+
case OutputFormat.Scxml:
93+
case OutputFormat.Html:
94+
case OutputFormat.LaTeX:
95+
default:
96+
throw new NotSupportedException($"Output format {format} is currently not supported");
97+
}
98+
}
99+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using Markdig;
2+
using Markdig.Renderers;
3+
using Markdig.Renderers.Html;
4+
using PlantUml.Net;
5+
6+
namespace Docfx.MarkdigEngine.Extensions;
7+
8+
public class DocfxPlantUmlSettings : PlantUmlSettings
9+
{
10+
public DocfxPlantUmlSettings() : base()
11+
{
12+
}
13+
14+
public DocfxPlantUmlSettings(IReadOnlyDictionary<string, string> config) : this()
15+
{
16+
if (config.TryGetValue("remoteUrl", out var url))
17+
RemoteUrl = url;
18+
if (config.TryGetValue("outputFormat", out var format))
19+
OutputFormat = Enum.Parse<OutputFormat>(format, true);
20+
if (config.TryGetValue("javaPath", out var path))
21+
JavaPath = path;
22+
if (config.TryGetValue("localPlantUmlPath", out path))
23+
LocalPlantUmlPath = path;
24+
if (config.TryGetValue("localGraphvizDotPath", out path))
25+
LocalGraphvizDotPath = path;
26+
if (config.TryGetValue("renderingMode", out var renderMode))
27+
RenderingMode = Enum.Parse<RenderingMode>(renderMode, true);
28+
}
29+
30+
public OutputFormat OutputFormat { get; set; } = OutputFormat.Svg;
31+
}
32+
33+
internal class PlantUmlExtension : IMarkdownExtension
34+
{
35+
private readonly MarkdownContext _context;
36+
private readonly DocfxPlantUmlSettings _settings;
37+
38+
public PlantUmlExtension(MarkdownContext context)
39+
{
40+
_context = context;
41+
_settings = new();
42+
43+
if (_context.GetExtensionConfiguration("PlantUml") is Dictionary<string, string> config)
44+
_settings = new DocfxPlantUmlSettings(config);
45+
}
46+
47+
public void Setup(MarkdownPipelineBuilder pipeline)
48+
{
49+
}
50+
51+
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
52+
{
53+
if (renderer is HtmlRenderer { ObjectRenderers: not null } htmlRenderer)
54+
{
55+
var customRenderer = new CustomCodeBlockRenderer(_context, _settings);
56+
var renderers = htmlRenderer.ObjectRenderers;
57+
58+
if (renderers.Contains<CodeBlockRenderer>())
59+
{
60+
renderers.InsertBefore<CodeBlockRenderer>(customRenderer);
61+
}
62+
else
63+
{
64+
renderers.AddIfNotAlready(customRenderer);
65+
}
66+
}
67+
}
68+
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
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.Text.Json.Serialization;
45
using Markdig;
56
using Markdig.Parsers;
67
using Markdig.Renderers;
78
using Markdig.Renderers.Html;
9+
using Newtonsoft.Json;
810

911
namespace Docfx.MarkdigEngine.Extensions;
1012

@@ -20,13 +22,13 @@ public class QuoteSectionNoteExtension : IMarkdownExtension
2022
["CAUTION"] = "CAUTION",
2123
};
2224

23-
public QuoteSectionNoteExtension(MarkdownContext context, Dictionary<string, string> notes = null)
25+
public QuoteSectionNoteExtension(MarkdownContext context)
2426
{
2527
_context = context;
2628

27-
if (notes != null)
29+
if (_context.GetExtensionConfiguration("Alerts") is Dictionary<string, string> config)
2830
{
29-
foreach (var (key, value) in notes)
31+
foreach (var (key, value) in config)
3032
_notes[key] = value;
3133
}
3234
}

src/Docfx.MarkdigEngine/MarkdigMarkdownService.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Collections.Immutable;
5+
using System.Text;
56
using Docfx.Common;
67
using Docfx.MarkdigEngine.Extensions;
78
using Docfx.Plugins;
@@ -33,7 +34,8 @@ public MarkdigMarkdownService(
3334
(code, message, origin, line) => Logger.LogError(message, null, InclusionContext.File.ToString(), line?.ToString(), code),
3435
ReadFile,
3536
GetLink,
36-
GetImageLink);
37+
GetImageLink,
38+
GetExtensionConfiguration);
3739
}
3840

3941
public MarkupResult Markup(string content, string filePath)
@@ -127,7 +129,7 @@ private MarkdownPipeline CreateMarkdownPipeline(bool isInline, bool multipleYaml
127129

128130
var builder = new MarkdownPipelineBuilder();
129131

130-
builder.UseDocfxExtensions(_context, _parameters.Extensions?.Alerts);
132+
builder.UseDocfxExtensions(_context);
131133
builder.Extensions.Insert(0, new YamlHeaderExtension(_context) { AllowInMiddleOfDocument = multipleYamlHeader });
132134

133135
if (enableSourceInfo)
@@ -185,6 +187,8 @@ private static string GetLink(string path, MarkdownObject origin)
185187
return path;
186188
}
187189

190+
private object GetExtensionConfiguration(string extension) => _parameters.GetExtensionConfiguration(extension);
191+
188192
private static string GetImageLink(string href, MarkdownObject origin, string altText) => GetLink(href, origin);
189193

190194
private static void ReportDependency(RelativePath filePathToDocset, string parentFileDirectoryToDocset)

src/Docfx.Plugins/MarkdownServiceParameters.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ public class MarkdownServiceProperties
3535
[JsonProperty("alerts")]
3636
[JsonPropertyName("alerts")]
3737
public Dictionary<string, string> Alerts { get; set; }
38+
39+
/// <summary>
40+
/// PlantUml extension configuration parameters
41+
/// </summary>
42+
[JsonProperty("plantUml")]
43+
[JsonPropertyName("plantUml")]
44+
public Dictionary<string, string> PlantUml { get; set; }
3845
}
3946

4047
public class MarkdownServiceParameters
@@ -43,4 +50,16 @@ public class MarkdownServiceParameters
4350
public string TemplateDir { get; set; }
4451
public MarkdownServiceProperties Extensions { get; set; } = new();
4552
public ImmutableDictionary<string, string> Tokens { get; set; } = ImmutableDictionary<string, string>.Empty;
53+
54+
public object GetExtensionConfiguration(string extension)
55+
{
56+
if (!string.IsNullOrEmpty(extension) && Extensions != null)
57+
{
58+
var property = typeof(MarkdownServiceProperties).GetProperty(extension);
59+
if (property != null)
60+
return property.GetValue(Extensions);
61+
}
62+
63+
return null;
64+
}
4665
}

0 commit comments

Comments
 (0)