using System.Text; namespace Nuuru.Server.Services.BBCode { /// /// Serializes a BBCode AST back to raw BBCode text. /// public static class BbSerializer { public static string Serialize(List nodes) { var sb = new StringBuilder(); foreach (var node in nodes) SerializeNode(node, sb); return sb.ToString(); } /// /// Extract plain text from AST nodes (no tags, no HTML). /// public static string ExtractText(List nodes) { var sb = new StringBuilder(); foreach (var node in nodes) ExtractTextNode(node, sb); return sb.ToString(); } private static void SerializeNode(BbNode node, StringBuilder sb) { switch (node) { case TextNode t: sb.Append(t.Content); break; case NewlineNode: sb.Append('\n'); break; case EmoteNode em: sb.Append(':').Append(em.Name).Append(':'); break; case ThumbNode t: sb.Append("[thumb]").Append(t.PostId).Append("[/thumb]"); break; case MentionNode m: sb.Append("[mention userguid=").Append(m.UserId).Append(']') .Append(m.UserName).Append("[/mention]"); break; case AttachmentNode a: sb.Append("[attachment"); if (a.IsThumbnail) sb.Append("=thumb"); else { if (a.Width.HasValue) sb.Append(" width=").Append(a.Width); if (a.Height.HasValue) sb.Append(" height=").Append(a.Height); } sb.Append(']').Append(a.AttachmentId).Append("[/attachment]"); break; case UrlNode u: sb.Append("[url=").Append(u.Href).Append(']'); foreach (var child in u.Children) SerializeNode(child, sb); sb.Append("[/url]"); break; case QuoteNode q: SerializeQuote(q, sb); break; case ElementNode e: SerializeElement(e, sb); break; } } private static void SerializeQuote(QuoteNode q, StringBuilder sb) { sb.Append("[quote"); if (!string.IsNullOrEmpty(q.SourceType) && !string.IsNullOrEmpty(q.SourceId)) { var idAttr = q.SourceType == "forum" ? "postId" : "commentId"; sb.Append(' ').Append(idAttr).Append('=').Append(q.SourceId); if (!string.IsNullOrEmpty(q.AuthorName)) sb.Append(" author=").Append(q.AuthorName); if (!string.IsNullOrEmpty(q.ProvidedHash)) sb.Append(" hash=").Append(q.ProvidedHash); } else if (!string.IsNullOrEmpty(q.AuthorName)) { sb.Append('=').Append(q.AuthorName); } sb.Append(']'); foreach (var child in q.Children) SerializeNode(child, sb); sb.Append("[/quote]"); } private static void SerializeElement(ElementNode e, StringBuilder sb) { var tag = e.Tag; // Greentext/orangetext lines use >text / '); foreach (var child in e.Children) SerializeNode(child, sb); return; } if (e is { Tag: "orangetext", Attribute: "line" }) { sb.Append('<'); foreach (var child in e.Children) SerializeNode(child, sb); return; } // List items: [*] with no closing tag if (tag == "*") { sb.Append("[*]"); foreach (var child in e.Children) SerializeNode(child, sb); return; } // Inline mark shortcuts var shortcut = GetInlineShortcut(tag, e.Attribute); if (shortcut != null) { sb.Append(shortcut.Value.open); foreach (var child in e.Children) SerializeNode(child, sb); sb.Append(shortcut.Value.close); return; } // General [tag] or [tag=attr] ... [/tag] sb.Append('[').Append(tag); if (e.Attribute != null) sb.Append('=').Append(e.Attribute); sb.Append(']'); foreach (var child in e.Children) SerializeNode(child, sb); sb.Append("[/").Append(tag).Append(']'); } private static (string open, string close)? GetInlineShortcut(string tag, string? _) { return tag.ToLowerInvariant() switch { "redtext" => ("==", "=="), "bluetext" => ("--", "--"), "glowtext" => ("%%", "%%"), "redglow" => ("!!", "!!"), "yellowglow" => ("::", "::"), "rainbow" => ("~-~", "~-~"), _ => null }; } private static void ExtractTextNode(BbNode node, StringBuilder sb) { switch (node) { case TextNode t: sb.Append(t.Content); break; case NewlineNode: sb.Append('\n'); break; case ElementNode e: foreach (var child in e.Children) ExtractTextNode(child, sb); break; case QuoteNode q: foreach (var child in q.Children) ExtractTextNode(child, sb); break; case UrlNode u: foreach (var child in u.Children) ExtractTextNode(child, sb); break; case MentionNode m: sb.Append('@').Append(m.UserName); break; case EmoteNode em: sb.Append(':').Append(em.Name).Append(':'); break; } } } }