using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.RegularExpressions; var exeDir = AppContext.BaseDirectory; var solutionDir = FindContainingDir(exeDir, "ET.sln") ?? throw new InvalidOperationException("Cannot find ET.sln"); var srcDir = Path.GetFullPath(Path.Combine(solutionDir, "..", "docs", "Notes")); var dstDir = Path.GetFullPath(Path.Combine(solutionDir, "Web", "wwwroot", "docs", "notes")); Console.WriteLine($"Source: {srcDir}"); Console.WriteLine($"Destination: {dstDir}"); if (!Directory.Exists(srcDir)) { Console.Error.WriteLine($"Source directory not found: {srcDir}"); return 1; } var entries = SyncNotes(srcDir, dstDir); CopyOverview(solutionDir, dstDir, entries); CopyImages(solutionDir, Path.GetFullPath(Path.Combine(solutionDir, "Web", "wwwroot", "docs"))); var indexJson = BuildIndex(entries); File.WriteAllText(Path.Combine(Path.GetDirectoryName(dstDir)!, "notes-index.json"), indexJson); GenerateMap(srcDir, Path.GetFullPath(Path.Combine(solutionDir, "Web", "wwwroot", "docs"))); GenerateOverviewPage(solutionDir); Console.WriteLine("Done."); return 0; static List SyncNotes(string srcDir, string dstDir) { Directory.CreateDirectory(dstDir); foreach (var file in Directory.GetFiles(dstDir)) File.Delete(file); var entries = new List(); foreach (var file in Directory.EnumerateFiles(srcDir, "*.md")) { var name = Path.GetFileNameWithoutExtension(file); var slug = Slugify(name); File.Copy(file, Path.Combine(dstDir, $"{slug}.md"), true); string? category = null; string? cost = null; string? gearCategory = null; string? effect = null; string? location = null; var content = File.ReadAllText(file); var fmMatch = Regex.Match(content, @"^---\s*\n(.*?)\n---", RegexOptions.Singleline); if (fmMatch.Success) { var fm = fmMatch.Groups[1].Value; var catMatch = Regex.Match(fm, @"(?m)^category:\s*(.+)$", RegexOptions.IgnoreCase); if (catMatch.Success) category = catMatch.Groups[1].Value.Trim().Trim('"'); var costMatch = Regex.Match(fm, @"(?m)^cost:\s*(.+)$", RegexOptions.IgnoreCase); if (costMatch.Success) cost = costMatch.Groups[1].Value.Trim().Trim('"'); var gcMatch = Regex.Match(fm, @"(?m)^gear category:\s*(.+)$", RegexOptions.IgnoreCase); if (gcMatch.Success) gearCategory = gcMatch.Groups[1].Value.Trim().Trim('"'); var effMatch = Regex.Match(fm, @"(?m)^effect:\s*(.+)$", RegexOptions.IgnoreCase); if (effMatch.Success) effect = effMatch.Groups[1].Value.Trim().Trim('"'); var locMatch = Regex.Match(fm, @"(?m)^location:\s*(.+)$", RegexOptions.IgnoreCase); if (locMatch.Success) location = locMatch.Groups[1].Value.Trim().Trim('"'); } var entry = new Dictionary { ["slug"] = slug, ["title"] = name }; if (category != null) entry["category"] = category; if (cost != null) entry["cost"] = cost; if (gearCategory != null) entry["gearCategory"] = gearCategory; if (effect != null) entry["effect"] = effect; if (location != null) entry["location"] = location; entries.Add(entry); } Console.WriteLine($"Copied {entries.Count} notes."); return entries; } static void CopyOverview(string solutionDir, string dstDir, List entries) { var srcOverview = Path.GetFullPath(Path.Combine(solutionDir, "..", "docs", "Overview.md")); if (!File.Exists(srcOverview)) return; var slug = "overview"; File.Copy(srcOverview, Path.Combine(dstDir, $"{slug}.md"), true); entries.Add(new Dictionary { ["slug"] = slug, ["title"] = "Overview", ["category"] = "Overview" }); Console.WriteLine("Copied Overview.md."); } static void CopyImages(string solutionDir, string dstDir) { var srcImgs = Path.GetFullPath(Path.Combine(solutionDir, "..", "docs", "Images")); if (!Directory.Exists(srcImgs)) return; var dstImgs = Path.Combine(dstDir, "images"); Directory.CreateDirectory(dstImgs); foreach (var file in Directory.EnumerateFiles(srcImgs)) { var name = Path.GetFileName(file); File.Copy(file, Path.Combine(dstImgs, name), true); } Console.WriteLine($"Copied images from {srcImgs}."); } static string BuildIndex(List entries) { var index = new Dictionary { ["notes"] = entries }; return JsonSerializer.Serialize(index, new JsonSerializerOptions { WriteIndented = true }); } static void GenerateOverviewPage(string solutionDir) { var pagesDir = Path.GetFullPath(Path.Combine(solutionDir, "Web", "Pages")); Directory.CreateDirectory(pagesDir); var component = """ @page "/overview" @inject DocsService DocsService Overview @if (loading) {
Loading...
} else if (doc == null) {

Overview Not Found

The overview document could not be found.

} else {

@doc.Title

@if (!string.IsNullOrEmpty(doc.FrontmatterHtml)) {
Frontmatter @((MarkupString)doc.FrontmatterHtml)
}
@((MarkupString)doc.HtmlContent)
} @code { private NoteDocument? doc; private bool loading = true; protected override async Task OnInitializedAsync() { doc = await DocsService.GetNoteAsync("overview"); loading = false; } } """; var overviewPagePath = Path.Combine(pagesDir, "Overview.razor"); File.WriteAllText(overviewPagePath, component); Console.WriteLine($"Overview page written to: {overviewPagePath}"); } static void GenerateMap(string srcDir, string dstDir) { var terrainColors = new Dictionary { ["Grass"] = "#4caf50", ["Forest"] = "#2e7d32", ["Mountain"] = "#78909c", ["Water"] = "#42a5f5", ["Wasteland"] = "#8d6e63" }; var regionFiles = Directory.EnumerateFiles(srcDir, "*.md") .Select(f => (File: f, Content: File.ReadAllText(f))) .Where(t => Regex.IsMatch(t.Content, @"(?m)^category:\s*Region\s*$", RegexOptions.Multiline)) .ToList(); var regions = new List(); foreach (var (file, content) in regionFiles) { var fmMatch = Regex.Match(content, @"^---\s*\n(.*?)\n---", RegexOptions.Singleline); if (!fmMatch.Success) continue; var fm = fmMatch.Groups[1].Value; var name = Path.GetFileNameWithoutExtension(file); var xMatch = Regex.Match(fm, @"(?m)^X:\s*(\d+)"); var yMatch = Regex.Match(fm, @"(?m)^Y:\s*(\d+)"); if (!xMatch.Success || !yMatch.Success) continue; var connMatches = Regex.Matches(fm, @"\[\[([^\]]+)\]\]"); var conns = connMatches.Select(m => m.Groups[1].Value).ToList(); var landmarks = new List(); var lmSection = Regex.Match(fm, @"Landmarks:\s*\n((?:\s*-\s*""\[\[([^\]]+)\]\]""\s*\n?)*)"); if (lmSection.Success) { var lmEntries = Regex.Matches(lmSection.Groups[1].Value, @"\[\[([^\]]+)\]\]"); landmarks.AddRange(lmEntries.Select(m => m.Groups[1].Value)); } var terrain = name.Split(' ')[0]; regions.Add(new RegionData { Name = name, Slug = Slugify(name), Terrain = terrain, X = int.Parse(xMatch.Groups[1].Value), Y = int.Parse(yMatch.Groups[1].Value), Connections = conns, Landmarks = landmarks }); } var srcMapImage = Path.GetFullPath(Path.Combine(srcDir, "..", "Images", "Map.png")); var dstMapImage = Path.Combine(dstDir, "Map.png"); if (File.Exists(srcMapImage)) File.Copy(srcMapImage, dstMapImage, true); var nameLookup = regions.ToDictionary(r => r.Name); var pad = 60; var maxX = (regions.Count > 0 ? regions.Max(r => r.X) : 0) + pad * 2; var maxY = (regions.Count > 0 ? regions.Max(r => r.Y) : 0) + pad * 2; var svg = new StringBuilder(); svg.AppendLine(""); svg.AppendLine(""""""); svg.AppendLine(""""""); foreach (var (terrain, color) in terrainColors) { svg.AppendLine($""""""); svg.AppendLine($""""""); svg.AppendLine($""""""); svg.AppendLine(""); } svg.AppendLine(""); foreach (var region in regions) foreach (var conn in region.Connections) { if (!nameLookup.TryGetValue(conn, out var target) || string.Compare(region.Name, conn, StringComparison.OrdinalIgnoreCase) >= 0) continue; svg.AppendLine( $""""""); } foreach (var region in regions) { var cx = region.X; var cy = region.Y; var color = terrainColors.GetValueOrDefault(region.Terrain, "#888"); svg.AppendLine($""""""); svg.AppendLine($""""""); svg.AppendLine( $""""""); var labelX = cx + 24; svg.AppendLine( $""""""); svg.Append(HtmlEncoder.Default.Encode(region.Name)); svg.AppendLine(""); foreach (var lm in region.Landmarks) { svg.AppendLine( $""""""); svg.Append(HtmlEncoder.Default.Encode($"\u2605 {lm}")); svg.AppendLine(""); } svg.AppendLine(""); } /** var legendX = 700 - 160; var legendY = 20; svg.AppendLine($""""""); svg.AppendLine($"""Legend"""); var ly = legendY + 32; foreach (var (terrain, color) in terrainColors) { svg.AppendLine($""""""); svg.AppendLine($"""{terrain}"""); ly += 22; } */ svg.AppendLine(""); var mapPath = Path.Combine(dstDir, "map.svg"); File.WriteAllText(mapPath, svg.ToString()); Console.WriteLine($"Map written to: {mapPath} ({regions.Count} regions)"); } static string Slugify(string name) { var slug = name.ToLowerInvariant() .Replace("'", "") .Replace(".", "") .Replace("(", "") .Replace(")", ""); slug = Regex.Replace(slug, @"[^a-z0-9 -]", ""); slug = Regex.Replace(slug, @"\s+", "-"); slug = Regex.Replace(slug, @"-+", "-"); return slug.Trim('-'); } static string? FindContainingDir(string startDir, string markerFile) { var dir = new DirectoryInfo(startDir); while (dir != null) { if (File.Exists(Path.Combine(dir.FullName, markerFile))) return dir.FullName; dir = dir.Parent; } return null; } internal class RegionData { public string Name { get; set; } = ""; public string Slug { get; set; } = ""; public string Terrain { get; set; } = ""; public int X { get; set; } public int Y { get; set; } public List Connections { get; set; } = new(); public List Landmarks { get; set; } = new(); }