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;
}
}
}
}