@using System.Text.RegularExpressions @using Microsoft.AspNetCore.Components.Rendering @namespace Chrono.Components @if (Card != null) {
@if (HasImage) {
@Card.Name
}

@Card.Name

@Card.Category @if (Card.Cost.HasValue) { @Card.Cost } @if (Card.Attack.HasValue) { @Card.Attack } @if (Card.Health.HasValue) { @Card.Health } @if (Card.Speed != null) { @Card.Speed }
@if (Card.Syndicate != null) {
Syndicate @Card.Syndicate
} @if (Card.Description != null) {
@RenderDescription(Card.Description)
} @if (Card.Set != null) {
Set @Card.Set
} @if (Card.Archetypes is { Count: > 0 }) {
Archetypes @string.Join(", ", Card.Archetypes)
} @if (Card.ImmortalizeWhen != null) {
Immortalize When @Card.ImmortalizeWhen
} @if (Card.HasImmortalize) {
Immortalizes To @foreach (var name in Card.ImmortalizeTo!) { var targetName = name; var target = LookupCard(targetName); if (target != null) { } else { @targetName } }
} @if (Card.ImmortalizeFrom != null) {
Immortalizes From @{ var fromCard = LookupCard(Card.ImmortalizeFrom); if (fromCard != null) { } else { @Card.ImmortalizeFrom } }
} @if (ShowNotes && Card.IsAgent) {
Personal Note @if (isSaving) { Saving... }
}
} @code { [Parameter] public CardData? Card { get; set; } [Parameter] public EventCallback OnClose { get; set; } [Parameter] public EventCallback OnNavigate { get; set; } [Parameter] public bool ShowNotes { get; set; } [Inject] private HttpClient Http { get; set; } = default!; private bool HasImage => Card?.ImageFile is { Length: > 0 } && Card.ImageFile != "placeholder.png"; private string currentNote = ""; private bool isSaving; protected override void OnParametersSet() { if (Card != null && Card.IsAgent) { currentNote = ""; _ = LoadNote(); } } private async Task LoadNote() { if (Card == null || !Card.IsAgent) return; try { var note = await Http.GetFromJsonAsync($"api/notes/{Uri.EscapeDataString(Card.Name)}"); currentNote = note?.Note ?? ""; } catch { currentNote = ""; } } private async Task SaveNote() { if (Card == null || !Card.IsAgent) return; isSaving = true; try { await Http.PostAsJsonAsync("api/notes", new CardNote { CardName = Card.Name, Note = currentNote }); } catch { } finally { isSaving = false; } } private void HandleClose() { _ = OnClose.InvokeAsync(); } private void HandleBackdropClick() { _ = OnClose.InvokeAsync(); } private void Navigate(string name) { var card = LookupCard(name); if (card != null) { Card = card; _ = LoadNote(); StateHasChanged(); } else { _ = OnNavigate.InvokeAsync(name); } } private static CardData? LookupCard(string cardName) { return CardDatabase.Cards.FirstOrDefault(c => string.Equals(c.Name, cardName, StringComparison.OrdinalIgnoreCase)); } private static DocData? LookupDoc(string title) { return DocDatabase.Docs.FirstOrDefault(d => string.Equals(d.Title, title, StringComparison.OrdinalIgnoreCase)); } private RenderFragment RenderDescription(string content) { return builder => { if (string.IsNullOrWhiteSpace(content)) return; var seq = 0; var lines = content.Split(['\r', '\n'], StringSplitOptions.None); var inList = false; var tableLines = new List(); var inTable = false; for (var i = 0; i < lines.Length; i++) { var line = lines[i]; var trimmedLine = line.Trim(); // Handle Tables if (trimmedLine.StartsWith("|") && trimmedLine.EndsWith("|")) { inTable = true; tableLines.Add(trimmedLine); continue; } if (inTable) { RenderTable(builder, ref seq, tableLines); tableLines.Clear(); inTable = false; } // Handle Lists if (trimmedLine.StartsWith("- ") || trimmedLine.StartsWith("* ")) { if (!inList) { builder.OpenElement(seq++, "ul"); inList = true; } var isCardListItem = trimmedLine.Contains("[[") && trimmedLine.Contains("]]"); builder.OpenElement(seq++, "li"); RenderInlines(builder, ref seq, trimmedLine[2..], true); builder.CloseElement(); continue; } if (inList) { builder.CloseElement(); // Close ul inList = false; } // Handle Headings if (trimmedLine.StartsWith("### ")) { builder.OpenElement(seq++, "h4"); RenderInlines(builder, ref seq, trimmedLine[4..], false); builder.CloseElement(); } else if (trimmedLine.StartsWith("## ")) { builder.OpenElement(seq++, "h3"); RenderInlines(builder, ref seq, trimmedLine[3..], false); builder.CloseElement(); } else if (trimmedLine.StartsWith("# ")) { builder.OpenElement(seq++, "h2"); RenderInlines(builder, ref seq, trimmedLine[2..], false); builder.CloseElement(); } else if (!string.IsNullOrWhiteSpace(trimmedLine)) { builder.OpenElement(seq++, "p"); RenderInlines(builder, ref seq, line, false); builder.CloseElement(); } } if (inTable) RenderTable(builder, ref seq, tableLines); if (inList) builder.CloseElement(); }; } private void RenderTable(RenderTreeBuilder builder, ref int seq, List lines) { if (lines.Count < 2) return; builder.OpenElement(seq++, "div"); builder.AddAttribute(seq++, "class", "table-responsive"); builder.OpenElement(seq++, "table"); builder.AddAttribute(seq++, "class", "markdown-table"); var hasHeader = lines.Count > 2 && lines[1].Contains("---"); var startIdx = 0; if (hasHeader) { builder.OpenElement(seq++, "thead"); builder.OpenElement(seq++, "tr"); var cells = lines[0].Split('|', StringSplitOptions.RemoveEmptyEntries); foreach (var cell in cells) { builder.OpenElement(seq++, "th"); RenderInlines(builder, ref seq, cell.Trim(), false); builder.CloseElement(); } builder.CloseElement(); builder.CloseElement(); startIdx = 2; } builder.OpenElement(seq++, "tbody"); for (var i = startIdx; i < lines.Count; i++) { if (i == 1 && hasHeader) continue; builder.OpenElement(seq++, "tr"); var cells = lines[i].Split('|', StringSplitOptions.RemoveEmptyEntries); foreach (var cell in cells) { builder.OpenElement(seq++, "td"); RenderInlines(builder, ref seq, cell.Trim(), false); builder.CloseElement(); } builder.CloseElement(); } builder.CloseElement(); builder.CloseElement(); builder.CloseElement(); } private void RenderInlines(RenderTreeBuilder builder, ref int seq, string text, bool renderAsImage) { var remaining = text; while (!string.IsNullOrEmpty(remaining)) { var boldMatch = Regex.Match(remaining, @"\*\*(.*?)\*\*"); var italicMatch = Regex.Match(remaining, @"\*(.*?)\*"); var cardMatch = Regex.Match(remaining, @"\[\[(.*?)\]\]"); var bestIdx = int.MaxValue; Match? bestMatch = null; var matchType = 0; // 1: bold, 2: italic, 3: card if (boldMatch.Success && boldMatch.Index < bestIdx) { bestIdx = boldMatch.Index; bestMatch = boldMatch; matchType = 1; } if (italicMatch.Success && italicMatch.Index < bestIdx) { bestIdx = italicMatch.Index; bestMatch = italicMatch; matchType = 2; } if (cardMatch.Success && cardMatch.Index < bestIdx) { bestIdx = cardMatch.Index; bestMatch = cardMatch; matchType = 3; } if (bestMatch != null) { if (bestIdx > 0) builder.AddContent(seq++, remaining[..bestIdx]); var innerText = bestMatch.Groups[1].Value; if (matchType == 1) // Bold { builder.OpenElement(seq++, "strong"); builder.AddContent(seq++, innerText); builder.CloseElement(); } else if (matchType == 2) // Italic { builder.OpenElement(seq++, "em"); builder.AddContent(seq++, innerText); builder.CloseElement(); } else if (matchType == 3) // Card Link / Doc Link { var card = LookupCard(innerText); var doc = LookupDoc(innerText); if (card != null || doc != null) { builder.OpenElement(seq++, "button"); builder.AddAttribute(seq++, "class", "inline-card-btn"); builder.AddAttribute(seq++, "onclick", EventCallback.Factory.Create(this, () => Navigate(innerText))); builder.AddContent(seq++, innerText); builder.CloseElement(); } else { builder.AddContent(seq++, innerText); } } remaining = remaining[(bestIdx + bestMatch.Length)..]; } else { builder.AddContent(seq++, remaining); remaining = ""; } } } }