193 lines
6.5 KiB
C#
193 lines
6.5 KiB
C#
using System.Net;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace PlotLine.Services;
|
|
|
|
public interface IHelpMarkdownRenderer
|
|
{
|
|
string Render(string markdown);
|
|
}
|
|
|
|
public sealed partial class HelpMarkdownRenderer(IHelpService help) : IHelpMarkdownRenderer
|
|
{
|
|
public string Render(string markdown)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(markdown))
|
|
{
|
|
return "<p class=\"muted\">This help article has no body content yet.</p>";
|
|
}
|
|
|
|
var lines = markdown.Replace("\r\n", "\n").Split('\n');
|
|
var html = new StringBuilder();
|
|
var paragraph = new List<string>();
|
|
var listItems = new List<string>();
|
|
var inTable = false;
|
|
|
|
void FlushParagraph()
|
|
{
|
|
if (paragraph.Count == 0) return;
|
|
html.Append("<p>").Append(RenderInline(string.Join(" ", paragraph))).AppendLine("</p>");
|
|
paragraph.Clear();
|
|
}
|
|
|
|
void FlushList()
|
|
{
|
|
if (listItems.Count == 0) return;
|
|
html.AppendLine("<ul>");
|
|
foreach (var item in listItems)
|
|
{
|
|
html.Append("<li>").Append(RenderInline(item)).AppendLine("</li>");
|
|
}
|
|
html.AppendLine("</ul>");
|
|
listItems.Clear();
|
|
}
|
|
|
|
void CloseTable()
|
|
{
|
|
if (!inTable) return;
|
|
html.AppendLine("</tbody></table>");
|
|
inTable = false;
|
|
}
|
|
|
|
for (var index = 0; index < lines.Length; index++)
|
|
{
|
|
var line = lines[index].Trim();
|
|
if (string.IsNullOrWhiteSpace(line))
|
|
{
|
|
FlushParagraph();
|
|
FlushList();
|
|
CloseTable();
|
|
continue;
|
|
}
|
|
|
|
if (line.StartsWith("#", StringComparison.Ordinal))
|
|
{
|
|
FlushParagraph();
|
|
FlushList();
|
|
CloseTable();
|
|
var level = Math.Clamp(line.TakeWhile(ch => ch == '#').Count(), 1, 4);
|
|
var text = line[level..].Trim();
|
|
html.Append("<h").Append(level).Append(">")
|
|
.Append(RenderInline(text))
|
|
.Append("</h").Append(level).AppendLine(">");
|
|
continue;
|
|
}
|
|
|
|
if (line.StartsWith("- ", StringComparison.Ordinal) || line.StartsWith("* ", StringComparison.Ordinal))
|
|
{
|
|
FlushParagraph();
|
|
CloseTable();
|
|
listItems.Add(line[2..].Trim());
|
|
continue;
|
|
}
|
|
|
|
if (LooksLikeTableRow(line))
|
|
{
|
|
FlushParagraph();
|
|
FlushList();
|
|
if (index + 1 < lines.Length && LooksLikeTableSeparator(lines[index + 1].Trim()))
|
|
{
|
|
if (inTable) CloseTable();
|
|
html.AppendLine("<table class=\"table table-sm help-markdown-table\"><thead><tr>");
|
|
foreach (var cell in SplitTableCells(line))
|
|
{
|
|
html.Append("<th>").Append(RenderInline(cell)).AppendLine("</th>");
|
|
}
|
|
html.AppendLine("</tr></thead><tbody>");
|
|
inTable = true;
|
|
index++;
|
|
continue;
|
|
}
|
|
|
|
if (inTable)
|
|
{
|
|
html.AppendLine("<tr>");
|
|
foreach (var cell in SplitTableCells(line))
|
|
{
|
|
html.Append("<td>").Append(RenderInline(cell)).AppendLine("</td>");
|
|
}
|
|
html.AppendLine("</tr>");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
paragraph.Add(line);
|
|
}
|
|
|
|
FlushParagraph();
|
|
FlushList();
|
|
CloseTable();
|
|
return html.ToString();
|
|
}
|
|
|
|
private static bool LooksLikeTableRow(string line) => line.Contains('|');
|
|
|
|
private static bool LooksLikeTableSeparator(string line) =>
|
|
line.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
|
.All(cell => cell.Length > 0 && cell.All(ch => ch is '-' or ':'));
|
|
|
|
private static IReadOnlyList<string> SplitTableCells(string line) =>
|
|
line.Trim('|').Split('|', StringSplitOptions.TrimEntries).ToList();
|
|
|
|
private string RenderInline(string value)
|
|
{
|
|
var encoded = WebUtility.HtmlEncode(value);
|
|
encoded = ImageRegex().Replace(encoded, match =>
|
|
{
|
|
var alt = match.Groups["alt"].Value;
|
|
var url = match.Groups["url"].Value;
|
|
return $"<img src=\"/HelpDrawer/Image?file={WebUtility.UrlEncode(url)}\" alt=\"{alt}\" class=\"help-article-image\" />";
|
|
});
|
|
encoded = LinkRegex().Replace(encoded, match =>
|
|
{
|
|
var text = match.Groups["text"].Value;
|
|
var url = RewriteHelpUrl(match.Groups["url"].Value);
|
|
return $"<a href=\"{url}\" target=\"_blank\" rel=\"noopener\">{text}</a>";
|
|
});
|
|
encoded = StrongRegex().Replace(encoded, "<strong>$1</strong>");
|
|
encoded = EmphasisRegex().Replace(encoded, "<em>$1</em>");
|
|
return encoded;
|
|
}
|
|
|
|
private string RewriteHelpUrl(string url)
|
|
{
|
|
var decodedUrl = WebUtility.HtmlDecode(url);
|
|
const string articlePrefix = "/Help/Article/";
|
|
if (decodedUrl.StartsWith(articlePrefix, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var key = Uri.UnescapeDataString(decodedUrl[articlePrefix.Length..]);
|
|
var article = help.GetByContextKey(key);
|
|
if (article is not null)
|
|
{
|
|
return article.Url;
|
|
}
|
|
}
|
|
|
|
const string categoryPrefix = "/Help/Category/";
|
|
if (decodedUrl.StartsWith(categoryPrefix, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var category = Uri.UnescapeDataString(decodedUrl[categoryPrefix.Length..]);
|
|
var categoryName = help.GetByCategory(category).FirstOrDefault()?.Category;
|
|
if (!string.IsNullOrWhiteSpace(categoryName))
|
|
{
|
|
return $"/help/{HelpService.Slugify(categoryName)}";
|
|
}
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
[GeneratedRegex(@"!\[(?<alt>[^\]]*)\]\((?<url>[^)]+)\)")]
|
|
private static partial Regex ImageRegex();
|
|
|
|
[GeneratedRegex(@"\[(?<text>[^\]]+)\]\((?<url>[^)]+)\)")]
|
|
private static partial Regex LinkRegex();
|
|
|
|
[GeneratedRegex(@"\*\*([^*]+)\*\*")]
|
|
private static partial Regex StrongRegex();
|
|
|
|
[GeneratedRegex(@"\*([^*]+)\*")]
|
|
private static partial Regex EmphasisRegex();
|
|
}
|