using System.Net.Http.Json; using Markdig; using Web.Models; namespace Web.Services; public class DocsService { private readonly HttpClient _http; private NotesIndex? _index; private static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder() .UseYamlFrontMatter() .Build(); public DocsService(HttpClient http) { _http = http; } public async Task GetIndexAsync() { if (_index != null) return _index; _index = await _http.GetFromJsonAsync("docs/notes-index.json") ?? new NotesIndex(); return _index; } public async Task GetNoteAsync(string slug) { var index = await GetIndexAsync(); var noteInfo = index.Notes.FirstOrDefault(n => string.Equals(n.Slug, slug, StringComparison.OrdinalIgnoreCase)); if (noteInfo == null) return null; try { var markdown = await _http.GetStringAsync($"docs/notes/{slug}.md"); return ParseDocument(slug, noteInfo.Title, markdown); } catch { return null; } } private static NoteDocument ParseDocument(string slug, string title, string markdown) { var doc = new NoteDocument { Slug = slug, Title = title }; var frontmatterLines = new List(); var bodyLines = new List(); var inFrontmatter = false; var frontmatterDone = false; foreach (var line in markdown.Replace("\r\n", "\n").Split('\n')) { var trimmed = line.Trim(); if (!frontmatterDone && trimmed == "---") { if (!inFrontmatter) { inFrontmatter = true; continue; } inFrontmatter = false; frontmatterDone = true; continue; } if (inFrontmatter) frontmatterLines.Add(line); else if (frontmatterDone || !string.IsNullOrWhiteSpace(line)) bodyLines.Add(line); } if (frontmatterLines.Count > 0) { foreach (var line in frontmatterLines) { var colonIdx = line.IndexOf(':'); if (colonIdx > 0) { var key = line[..colonIdx].Trim(); if (string.Equals(key, "category", StringComparison.OrdinalIgnoreCase)) { doc.Category = line[(colonIdx + 1)..].Trim().Trim('"'); } } } var fmHtml = new System.Text.StringBuilder(); fmHtml.Append(""); foreach (var line in frontmatterLines) { var colonIdx = line.IndexOf(':'); if (colonIdx > 0) { var key = line[..colonIdx].Trim(); var value = line[(colonIdx + 1)..].Trim().Trim('"'); fmHtml.Append(""); } else { fmHtml.Append(""); } } fmHtml.Append("
"); fmHtml.Append(System.Net.WebUtility.HtmlEncode(key)); fmHtml.Append(""); fmHtml.Append(System.Net.WebUtility.HtmlEncode(value)); fmHtml.Append("
"); fmHtml.Append(System.Net.WebUtility.HtmlEncode(line.Trim())); fmHtml.Append("
"); doc.FrontmatterHtml = fmHtml.ToString(); } var body = string.Join("\n", bodyLines); body = ConvertWikiLinks(body); doc.HtmlContent = Markdown.ToHtml(body, Pipeline); return doc; } private static string ConvertWikiLinks(string text) { return System.Text.RegularExpressions.Regex.Replace( text, @"\[\[([^\]]+)\]\]", match => { var content = match.Groups[1].Value; var parts = content.Split('|'); var linkText = parts.Length > 1 ? parts[1].Trim() : parts[0].Trim(); var target = parts[0].Trim(); var slug = Slugify(target); return $"{System.Net.WebUtility.HtmlEncode(linkText)}"; }); } public static string Slugify(string title) { var slug = title.ToLowerInvariant() .Replace(' ', '-') .Replace("'", "") .Replace(".", "") .Replace("(", "") .Replace(")", ""); slug = System.Text.RegularExpressions.Regex.Replace(slug, @"[^a-z0-9\-]", ""); slug = System.Text.RegularExpressions.Regex.Replace(slug, @"-+", "-"); return slug.Trim('-'); } }