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 "

This help article has no body content yet.

"; } var lines = markdown.Replace("\r\n", "\n").Split('\n'); var html = new StringBuilder(); var paragraph = new List(); var listItems = new List(); var inTable = false; void FlushParagraph() { if (paragraph.Count == 0) return; html.Append("

").Append(RenderInline(string.Join(" ", paragraph))).AppendLine("

"); paragraph.Clear(); } void FlushList() { if (listItems.Count == 0) return; html.AppendLine(""); listItems.Clear(); } void CloseTable() { if (!inTable) return; html.AppendLine(""); 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("") .Append(RenderInline(text)) .Append(""); 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(""); foreach (var cell in SplitTableCells(line)) { html.Append(""); } html.AppendLine(""); inTable = true; index++; continue; } if (inTable) { html.AppendLine(""); foreach (var cell in SplitTableCells(line)) { html.Append(""); } html.AppendLine(""); 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 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 $"\"{alt}\""; }); encoded = LinkRegex().Replace(encoded, match => { var text = match.Groups["text"].Value; var url = RewriteHelpUrl(match.Groups["url"].Value); return $"{text}"; }); encoded = StrongRegex().Replace(encoded, "$1"); encoded = EmphasisRegex().Replace(encoded, "$1"); 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(@"!\[(?[^\]]*)\]\((?[^)]+)\)")] private static partial Regex ImageRegex(); [GeneratedRegex(@"\[(?[^\]]+)\]\((?[^)]+)\)")] private static partial Regex LinkRegex(); [GeneratedRegex(@"\*\*([^*]+)\*\*")] private static partial Regex StrongRegex(); [GeneratedRegex(@"\*([^*]+)\*")] private static partial Regex EmphasisRegex(); }
").Append(RenderInline(cell)).AppendLine("
").Append(RenderInline(cell)).AppendLine("