using System.Text.RegularExpressions; using Microsoft.AspNetCore.Components; using Model; namespace Shared.Services; public class CardRenderingService { private readonly Dictionary _cardLookup; private readonly Dictionary _docLookup; private readonly List _allTerms; private readonly Regex _termRegex; public CardRenderingService() { var allCards = CardDatabase.Cards .Where(c => !string.IsNullOrWhiteSpace(c.Name)) .ToList(); _cardLookup = allCards .GroupBy(c => c.Name, StringComparer.OrdinalIgnoreCase) .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase); var allDocs = DocDatabase.Docs .Where(d => !string.IsNullOrWhiteSpace(d.Title)) .ToList(); _docLookup = allDocs .GroupBy(d => d.Title, StringComparer.OrdinalIgnoreCase) .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase); _allTerms = _cardLookup.Keys .Concat(_docLookup.Keys) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderByDescending(n => n.Length) .ToList(); var pattern = string.Join("|", _allTerms.Select(n => $@"\b{Regex.Escape(n)}\b")); _termRegex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled); } public RenderFragment RenderDescription(string description, Action onCardClick, Action? onDocClick = null, CardPreviewService? previewService = null) { return builder => { var sequence = 0; var paragraphs = description.Split('\n', StringSplitOptions.RemoveEmptyEntries); foreach (var paragraph in paragraphs) { builder.OpenElement(sequence++, "p"); var matches = _termRegex.Matches(paragraph); var lastIndex = 0; foreach (Match match in matches) { if (match.Index > lastIndex) builder.AddContent(sequence++, paragraph.Substring(lastIndex, match.Index - lastIndex)); var term = match.Value; var card = LookupCard(term); var doc = LookupDoc(term); builder.OpenElement(sequence++, "button"); builder.AddAttribute(sequence++, "class", "inline-card-btn"); if (card != null) { builder.AddAttribute(sequence++, "onclick", EventCallback.Factory.Create(this, () => onCardClick(card))); if (previewService != null) { builder.AddAttribute(sequence++, "onmouseenter", EventCallback.Factory.Create(this, e => previewService.Show(card, e.ClientX, e.ClientY))); builder.AddAttribute(sequence++, "onmouseleave", EventCallback.Factory.Create(this, () => previewService.Hide())); } } else if (doc != null && onDocClick != null) { builder.AddAttribute(sequence++, "onclick", EventCallback.Factory.Create(this, () => onDocClick(doc))); } builder.AddContent(sequence++, term); builder.CloseElement(); lastIndex = match.Index + match.Length; } if (lastIndex < paragraph.Length) builder.AddContent(sequence++, paragraph.Substring(lastIndex)); builder.CloseElement(); } }; } private CardData? LookupCard(string name) { return _cardLookup.GetValueOrDefault(name); } private DocData? LookupDoc(string name) { return _docLookup.GetValueOrDefault(name); } }