using System.Text; using System.Text.RegularExpressions; using Chrono.Model; var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..")); var docsDir = Path.Combine(repoRoot, "chrono.docs"); var webWwwRoot = Path.Combine(repoRoot, "Chrono", "Web", "wwwroot"); var generatedFile = Path.Combine(repoRoot, "Chrono", "Web", "Generated", "Cards.g.cs"); Console.WriteLine($"Repo root: {repoRoot}"); Console.WriteLine($"Docs dir: {docsDir}"); Console.WriteLine($"Generated: {generatedFile}"); if (!Directory.Exists(docsDir)) { Console.Error.WriteLine($"ERROR: docs dir not found: {docsDir}"); return 1; } var mdFiles = Directory.GetFiles(docsDir, "*.md"); var cards = new List(); foreach (var file in mdFiles) { var content = Encoding.UTF8.GetString(File.ReadAllBytes(file)); if (!content.StartsWith("---")) continue; var endIndex = content.IndexOf("---", 3, StringComparison.Ordinal); if (endIndex < 0) continue; var frontmatter = content[3..endIndex].Trim().Replace("\r\n", "\n").Replace("\r", "\n"); var yaml = ParseYaml(frontmatter); var name = Path.GetFileNameWithoutExtension(file); var category = yaml.GetValueOrDefault("category"); if (category == null) continue; var imageFile = StripWikiLink(yaml.GetValueOrDefault("imageLink")); if (imageFile != null && !imageFile.EndsWith(".png")) imageFile += ".png"; var card = new CardData { Name = name, Category = category, Cost = ParseInt(yaml, "cost"), Attack = ParseInt(yaml, "attack"), Health = ParseInt(yaml, "health"), Description = StripWikiLinks(yaml.GetValueOrDefault("description")), Faction = StripWikiLink(yaml.GetValueOrDefault("faction")), Set = StripWikiLink(yaml.GetValueOrDefault("set")), Speed = StripWikiLink(yaml.GetValueOrDefault("speed")), Archetypes = ParseList(yaml, "archetypes").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(), ImmortalizeTo = yaml.ContainsKey("immortalizeTo") ? ParseListOrScalar(yaml, "immortalizeTo").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList() : null, ImmortalizeFrom = StripWikiLink(yaml.GetValueOrDefault("immortalizeFrom")), ImmortalizeWhen = NullIfNa(StripWikiLinks(yaml.GetValueOrDefault("immortalizeWhen"))), ImageFile = imageFile }; cards.Add(card); } // Copy PNGs to wwwroot/cards var cardsDir = Path.Combine(webWwwRoot, "cards"); Directory.CreateDirectory(cardsDir); foreach (var card in cards) { if (card.ImageFile == null) continue; var src = Path.Combine(docsDir, card.ImageFile); var dst = Path.Combine(cardsDir, card.ImageFile); if (File.Exists(src)) File.Copy(src, dst, true); } // Generate C# source file Directory.CreateDirectory(Path.GetDirectoryName(generatedFile)!); using var writer = new StreamWriter(generatedFile, false, Encoding.UTF8); writer.WriteLine("// "); writer.WriteLine("#nullable enable"); writer.WriteLine(); writer.WriteLine("namespace Chrono.Model;"); writer.WriteLine(); writer.WriteLine("public static class CardDatabase"); writer.WriteLine("{"); writer.WriteLine(" public static readonly System.Collections.Generic.List Cards ="); writer.WriteLine(" ["); for (var i = 0; i < cards.Count; i++) { var c = cards[i]; writer.WriteLine(" new()"); writer.WriteLine(" {"); WriteProp(writer, "Name", c.Name, 3); WriteProp(writer, "Category", c.Category, 3); WriteNullProp(writer, "Cost", c.Cost, 3); WriteNullProp(writer, "Attack", c.Attack, 3); WriteNullProp(writer, "Health", c.Health, 3); WriteStrProp(writer, "Description", c.Description, 3); WriteStrProp(writer, "Faction", c.Faction, 3); WriteStrProp(writer, "Set", c.Set, 3); WriteStrProp(writer, "Speed", c.Speed, 3); WriteListProp(writer, "Archetypes", c.Archetypes, 3); WriteListProp(writer, "ImmortalizeTo", c.ImmortalizeTo, 3); WriteStrProp(writer, "ImmortalizeFrom", c.ImmortalizeFrom, 3); WriteStrProp(writer, "ImmortalizeWhen", c.ImmortalizeWhen, 3); WriteStrProp(writer, "ImageFile", c.ImageFile, 3); var comma = i < cards.Count - 1 ? "," : ""; writer.WriteLine($" }}{comma}"); } writer.WriteLine(" ];"); writer.WriteLine("}"); Console.WriteLine($"Generated {cards.Count} cards in {generatedFile}"); // ── Decks ── var decksDir = Path.Combine(docsDir, "Decks"); var deckFiles = Directory.Exists(decksDir) ? Directory.GetFiles(decksDir, "*.md") : []; var decks = new List(); foreach (var file in deckFiles) { var content = Encoding.UTF8.GetString(File.ReadAllBytes(file)); if (!content.StartsWith("---")) continue; var endIndex = content.IndexOf("---", 3, StringComparison.Ordinal); if (endIndex < 0) continue; var frontmatter = content[3..endIndex].Trim().Replace("\r\n", "\n").Replace("\r", "\n"); var yaml = ParseYaml(frontmatter); var name = Path.GetFileNameWithoutExtension(file); var isVisible = yaml.GetValueOrDefault("isVisible") == "true"; var deck = new DeckData { Name = name, Cards = ParseList(yaml, "cards").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(), Keycards = ParseList(yaml, "keycards").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(), Divers = ParseList(yaml, "divers").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(), Description = StripWikiLinks(NullIfNa(yaml.GetValueOrDefault("description")))?.Replace("\r\n", "\n") .Replace("\r", "\n").Replace("\n\n", "\n").Replace("\n\n", "\n"), Factions = ParseList(yaml, "factions").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(), IsVisible = isVisible }; decks.Add(deck); } Console.WriteLine($"Parsed {decks.Count} deck files"); // Generate Decks.g.cs var deckGeneratedFile = Path.Combine(repoRoot, "Chrono", "Web", "Generated", "Decks.g.cs"); Directory.CreateDirectory(Path.GetDirectoryName(deckGeneratedFile)!); using var deckWriter = new StreamWriter(deckGeneratedFile, false, Encoding.UTF8); deckWriter.WriteLine("// "); deckWriter.WriteLine("#nullable enable"); deckWriter.WriteLine(); deckWriter.WriteLine("namespace Chrono.Model;"); deckWriter.WriteLine(); deckWriter.WriteLine("public static class DeckDatabase"); deckWriter.WriteLine("{"); deckWriter.WriteLine(" public static readonly System.Collections.Generic.List Decks ="); deckWriter.WriteLine(" ["); for (var i = 0; i < decks.Count; i++) { var d = decks[i]; deckWriter.WriteLine(" new()"); deckWriter.WriteLine(" {"); WriteProp(deckWriter, "Name", d.Name, 3); WriteListProp(deckWriter, "Cards", d.Cards, 3); WriteListProp(deckWriter, "Keycards", d.Keycards, 3); WriteListProp(deckWriter, "Divers", d.Divers, 3); WriteStrProp(deckWriter, "Description", d.Description, 3); WriteListProp(deckWriter, "Factions", d.Factions, 3); deckWriter.WriteLine($" IsVisible = {(d.IsVisible ? "true" : "false")},"); var comma = i < decks.Count - 1 ? "," : ""; deckWriter.WriteLine($" }}{comma}"); } deckWriter.WriteLine(" ];"); deckWriter.WriteLine("}"); Console.WriteLine($"Generated {decks.Count} decks in {deckGeneratedFile}"); return 0; // --- Helpers --- static int? ParseInt(Dictionary yaml, string key) { if (!yaml.TryGetValue(key, out var val)) return null; if (int.TryParse(val, out var i)) return i; return null; } static string? StripWikiLink(string? s) { if (s == null || s == "N/A") return null; return Regex.Replace(s, @"\[\[([^\]]*)\]\]", "$1").Trim('"'); } static string? StripWikiLinks(string? s) { if (s == null || s == "N/A") return null; return Regex.Replace(s.Trim('"'), @"\[\[([^\]]*)\]\]", "$1").Trim(); } static string? NullIfNa(string? s) { return s is "N/A" or null ? null : s.Trim('"'); } static List ParseList(Dictionary yaml, string key) { if (!yaml.TryGetValue(key, out var raw)) return []; var result = new List(); foreach (var item in raw.Split('\n', StringSplitOptions.RemoveEmptyEntries)) { var trimmed = item.TrimStart('-', ' ').Trim(' ', '"'); if (trimmed.Length > 0) result.Add(trimmed); } return result; } static List ParseListOrScalar(Dictionary yaml, string key) { if (!yaml.TryGetValue(key, out var raw)) return []; raw = raw.Trim(); if (!raw.StartsWith('-')) return [raw.Trim('"')]; var result = new List(); foreach (var item in raw.Split('\n', StringSplitOptions.RemoveEmptyEntries)) { var trimmed = item.TrimStart('-', ' ').Trim(' ', '"'); if (trimmed.Length > 0) result.Add(trimmed); } return result; } static Dictionary ParseYaml(string yaml) { var dict = new Dictionary(); var lines = yaml.Split('\n'); string? currentKey = null; var listBuffer = new List(); var inBlockScalar = false; string? blockScalarKey = null; var blockScalarLines = new List(); string? quoteKey = null; var quoteLines = new List(); foreach (var line in lines) { var trimmed = line.Trim(); if (inBlockScalar) { if (line.Length == 0 || line[0] == ' ' || line[0] == '\t') { blockScalarLines.Add(trimmed); continue; } if (blockScalarKey != null) dict[blockScalarKey] = string.Join("\n", blockScalarLines); inBlockScalar = false; blockScalarKey = null; blockScalarLines.Clear(); } if (quoteKey != null) { if (line.Length == 0 || line[0] == ' ' || line[0] == '\t') { quoteLines.Add(trimmed); if (trimmed.EndsWith("\"")) { dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml)).TrimEnd('"'); quoteKey = null; quoteLines.Clear(); } continue; } dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml)); quoteKey = null; quoteLines.Clear(); } if (trimmed.Length == 0) continue; if (trimmed.StartsWith("- ")) { listBuffer.Add(trimmed); continue; } if (listBuffer.Count > 0 && currentKey != null) { dict[currentKey] = string.Join("\n", listBuffer); listBuffer.Clear(); } var colonIndex = trimmed.IndexOf(':'); if (colonIndex < 0) continue; currentKey = trimmed[..colonIndex].Trim(); var value = trimmed[(colonIndex + 1)..].Trim(); if (value is "|" or "|-") { inBlockScalar = true; blockScalarKey = currentKey; blockScalarLines.Clear(); continue; } if (value.StartsWith("\"") && !value.EndsWith("\"")) { quoteKey = currentKey; quoteLines.Clear(); quoteLines.Add(value.TrimStart('"')); continue; } dict[currentKey] = value; } if (listBuffer.Count > 0 && currentKey != null) dict[currentKey] = string.Join("\n", listBuffer); if (inBlockScalar && blockScalarKey != null) dict[blockScalarKey] = string.Join("\n", blockScalarLines); if (quoteKey != null) dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml)); return dict; } static string UnescapeYaml(string s) { var sb = new StringBuilder(); for (var i = 0; i < s.Length; i++) if (s[i] == '\\' && i + 1 < s.Length) switch (s[i + 1]) { case 'r': sb.Append('\r'); i++; break; case 'n': sb.Append('\n'); i++; break; case 't': sb.Append('\t'); i++; break; case '\\': sb.Append('\\'); i++; break; case '"': sb.Append('"'); i++; break; default: sb.Append(s[i]); break; } else sb.Append(s[i]); return sb.ToString(); } static void WriteProp(StreamWriter w, string name, string value, int indent) { w.WriteLine($"{new string(' ', indent * 4)}{name} = {ToLiteral(value)},"); } static void WriteStrProp(StreamWriter w, string name, string? value, int indent) { if (value == null) return; w.WriteLine($"{new string(' ', indent * 4)}{name} = {ToLiteral(value)},"); } static void WriteNullProp(StreamWriter w, string name, int? value, int indent) { if (value == null) return; w.WriteLine($"{new string(' ', indent * 4)}{name} = {value},"); } static void WriteListProp(StreamWriter w, string name, List? values, int indent) { if (values == null) return; var pad = new string(' ', indent * 4); w.WriteLine($"{pad}{name} = ["); foreach (var v in values) w.WriteLine($"{pad} {ToLiteral(v)},"); w.WriteLine($"{pad}],"); } static string ToLiteral(string? s) { if (s == null) return "null"; var sb = new StringBuilder(); sb.Append('"'); foreach (var c in s) switch (c) { case '"': sb.Append("\\\""); break; case '\\': sb.Append("\\\\"); break; case '\n': sb.Append("\\n"); break; case '\r': sb.Append("\\r"); break; case '\t': sb.Append("\\t"); break; case '\0': sb.Append("\\0"); break; default: sb.Append(c); break; } sb.Append('"'); return sb.ToString(); }