using System.Text.RegularExpressions; using System.Text.Json; 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; } SyncNotes(srcDir, dstDir); GenerateMap(srcDir, Path.GetFullPath(Path.Combine(solutionDir, "Web", "wwwroot", "docs"))); Console.WriteLine("Done."); return 0; static void 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"), overwrite: true); string? category = null; var content = File.ReadAllText(file); var fmMatch = Regex.Match(content, @"^---\s*\n(.*?)\n---", RegexOptions.Singleline); if (fmMatch.Success) { var catMatch = Regex.Match(fmMatch.Groups[1].Value, @"(?m)^category:\s*(.+)$"); if (catMatch.Success) category = catMatch.Groups[1].Value.Trim().Trim('"'); } var entry = new Dictionary { ["slug"] = slug, ["title"] = name }; if (category != null) entry["category"] = category; entries.Add(entry); } var index = new Dictionary { ["notes"] = entries }; var json = JsonSerializer.Serialize(index, new JsonSerializerOptions { WriteIndented = true }); var indexPath = Path.Combine(Path.GetDirectoryName(dstDir)!, "notes-index.json"); File.WriteAllText(indexPath, json); Console.WriteLine($"Copied {entries.Count} notes."); Console.WriteLine($"Index written to: {indexPath}"); } 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$")) .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 nameLookup = regions.ToDictionary(r => r.Name); var pad = 60; var maxX = regions.Max(r => r.X) + pad * 2; var maxY = regions.Max(r => r.Y) + pad * 2; var svg = new System.Text.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 + pad; var cy = region.Y + pad; var color = terrainColors.GetValueOrDefault(region.Terrain, "#888"); svg.AppendLine($""""""); svg.AppendLine($""""""); svg.AppendLine($""""""); var labelX = cx + 24; svg.AppendLine($""""""); svg.Append(System.Text.Encodings.Web.HtmlEncoder.Default.Encode(region.Name)); svg.AppendLine(""); foreach (var lm in region.Landmarks) { svg.AppendLine($""""""); svg.Append(System.Text.Encodings.Web.HtmlEncoder.Default.Encode($"\u2605 {lm}")); svg.AppendLine(""); } svg.AppendLine(""); } var legendX = maxX - 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; } 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(); }