using System.Text;
using System.Text.RegularExpressions;
using System.Web;
namespace TinyChat.Messages.Formatting;
///
{quoteText}"; }); } // Process lists BEFORE inline formatting to preserve markers if (IsSupported("ul") && IsSupported("li")) { text = ConvertUnorderedLists(text); } if (IsSupported("ol") && IsSupported("li")) { text = ConvertOrderedLists(text); } // Process inline formatting (bold, italic, strikethrough) AFTER lists text = ProcessInlineFormatting(text); return text; } private string ProcessInlineFormatting(string text) { // Process bold+italic (***) first to handle triple asterisks correctly if (IsSupported("b") && IsSupported("i")) { text = MarkdownBoldItalicRegex().Replace(text, "$2"); } else if (IsSupported("b")) { // If only bold is supported, treat *** as bold text = MarkdownBoldItalicRegex().Replace(text, "$2"); } else if (IsSupported("i")) { // If only italic is supported, treat *** as italic text = MarkdownBoldItalicRegex().Replace(text, "$2"); } else { // Neither supported, strip text = MarkdownBoldItalicRegex().Replace(text, "$2"); } // Process bold (before italic to handle *** correctly) if (IsSupported("b")) { text = MarkdownBoldRegex().Replace(text, match => { var content = match.Groups[2].Value; // Recursively process inner content content = ProcessInlineFormattingWithoutBold(content); return $"{content}"; }); } else { text = MarkdownBoldRegex().Replace(text, match => { var content = match.Groups[2].Value; return ProcessInlineFormattingWithoutBold(content); }); } // Process italic if (IsSupported("i")) { text = MarkdownItalicRegex().Replace(text, "$2"); } else { text = MarkdownItalicRegex().Replace(text, "$2"); } // Process strikethrough if (IsSupported("s")) { text = MarkdownStrikethroughRegex().Replace(text, "
and to if font is supported but code/pre are not
if ((normalizedTag == "code" || normalizedTag == "pre") && !IsSupported(normalizedTag) && IsSupported("font") && !string.IsNullOrWhiteSpace(DefaultCodeFontName))
{
// Don't use HtmlAttributeEncode for font name - DevExpress uses special syntax
var fontFace = DefaultCodeFontName.Replace("\"", """);
// Don't double-encode - content is already processed
return $"{processedContent}";
}
// Convert h1-h6 tags to size+bold if h-tags not supported but size/b are
if (normalizedTag.Length == 2 && normalizedTag[0] == 'h' && char.IsDigit(normalizedTag[1]))
{
var level = int.Parse(normalizedTag[1].ToString());
if (!IsSupported(normalizedTag))
{
// If h-tag not supported, use size+bold format (DevExpress style)
var hasBold = IsSupported("b");
var hasSize = IsSupported("size");
if (!hasBold && !hasSize)
return processedContent;
var result = processedContent;
if (hasBold)
result = $"{result}";
if (hasSize && level < 6)
{
var sizeIncrease = 6 - level; // h1=+5, h2=+4, h3=+3, h4=+2, h5=+1
result = $"{result} ";
}
return result;
}
}
if (IsSupported(normalizedTag))
{
// For specific tags, keep only essential attributes
var attributes = "";
if (normalizedTag == "a")
{
// Extract href attribute only
var hrefMatch = HrefAttributeRegex().Match(match.Value);
if (hrefMatch.Success)
attributes = $" href=\"{hrefMatch.Groups[1].Value}\"";
}
else if (normalizedTag == "img")
{
// Extract src and alt attributes only
var srcMatch = SrcAttributeRegex().Match(match.Value);
var altMatch = AltAttributeRegex().Match(match.Value);
if (srcMatch.Success)
attributes += $" src=\"{srcMatch.Groups[1].Value}\"";
if (altMatch.Success)
attributes += $" alt=\"{altMatch.Groups[1].Value}\"";
attributes = attributes.Trim();
if (attributes.Length > 0)
attributes = " " + attributes;
}
else if (normalizedTag == "font")
{
// DevExpress uses syntax instead of
// Extract both face attribute and the simplified DevExpress format
var devExpressMatch = FontDevExpressFormatRegex().Match(match.Value);
var faceMatch = FontFaceAttributeRegex().Match(match.Value);
if (devExpressMatch.Success)
attributes = $"=\"{devExpressMatch.Groups[1].Value}\"";
else if (faceMatch.Success)
attributes = $"=\"{faceMatch.Groups[1].Value}\"";
}
else if (normalizedTag == "size")
{
// Extract size value (e.g., )
var sizeMatch = SizeAttributeRegex().Match(match.Value);
if (sizeMatch.Success)
attributes = $"={sizeMatch.Groups[1].Value}";
}
else if (normalizedTag == "color")
{
// Extract color value (e.g., or )
var colorMatch = ColorAttributeRegex().Match(match.Value);
if (colorMatch.Success)
attributes = $"={colorMatch.Groups[1].Value}";
}
else if (normalizedTag == "backcolor")
{
// Extract backcolor value
var backcolorMatch = BackColorAttributeRegex().Match(match.Value);
if (backcolorMatch.Success)
attributes = $"={backcolorMatch.Groups[1].Value}";
}
if (normalizedTag == "size" || normalizedTag == "color" || normalizedTag == "backcolor")
{
return $"<{normalizedTag}{attributes}>{processedContent}{normalizedTag}>";
}
return $"<{normalizedTag}{attributes}>{processedContent}{normalizedTag}>";
}
else
{
// Strip tag but keep processed content
return processedContent;
}
});
// Process self-closing tags
text = SelfClosingHtmlTagsRegex().Replace(text, match =>
{
var tagName = match.Groups[1].Value;
var normalizedTag = NormalizeTag(tagName);
if (IsSupported(normalizedTag))
{
return match.Value; // Keep the tag as-is with attributes for self-closing tags like
}
else
{
return string.Empty; // Remove unsupported self-closing tags
}
});
if (text != originalText)
changed = true;
} while (changed);
return text;
}
private string ConvertHtmlListsIfNotSupported(string text)
{
// We need to process nested lists from innermost to outermost
// So we keep replacing until no more or tags are found
var changed = true;
while (changed)
{
var originalText = text;
// Convert unordered lists to "- " prefixed items if ul/li not supported
if (!IsSupported("ul") || !IsSupported("li"))
{
text = HtmlUnorderedListRegex().Replace(text, match =>
{
var listContent = match.Groups[1].Value;
var items = HtmlListItemRegex().Matches(listContent);
// Strip HTML tags from list items when converting to plain text
var result = string.Join("\n", items.Cast().Select(m => $"- {StripHtmlTags(m.Groups[1].Value.Trim())}"));
return result;
});
}
// Convert ordered lists to numbered items if ol/li not supported
if (!IsSupported("ol") || !IsSupported("li"))
{
text = HtmlOrderedListRegex().Replace(text, match =>
{
var listContent = match.Groups[1].Value;
var items = HtmlListItemRegex().Matches(listContent);
// Strip HTML tags from list items when converting to plain text
var result = string.Join("\n", items.Cast().Select((m, i) => $"{i + 1}. {StripHtmlTags(m.Groups[1].Value.Trim())}"));
return result;
});
}
changed = text != originalText;
}
return text;
}
private static string StripHtmlTags(string text)
{
// Recursively strip all HTML tags but keep content
var changed = true;
while (changed)
{
var originalText = text;
// Replace tags with their content
text = HtmlTagsRegex().Replace(text, "$2");
// Remove self-closing tags
text = SelfClosingHtmlTagsRegex().Replace(text, string.Empty);
changed = text != originalText;
}
return text;
}
private string ConvertColorSpansToDevExpressFormat(string text)
{
// Convert HTML to DevExpress
if (IsSupported("color"))
{
text = HtmlColorSpanRegex().Replace(text, match =>
{
var color = ParseColorValue(match.Groups[1].Value);
var content = match.Groups[2].Value;
return $"{content} ";
});
}
// Convert HTML to DevExpress
if (IsSupported("backcolor"))
{
text = HtmlBackColorSpanRegex().Replace(text, match =>
{
var color = ParseColorValue(match.Groups[1].Value);
var content = match.Groups[2].Value;
return $"{content} ";
});
}
return text;
}
private static string ParseColorValue(string colorValue)
{
// Normalize color value - keep as is (named colors, hex, rgb)
// DevExpress supports: red, #FF0000, 255,0,0, 255,255,0,0 (ARGB)
colorValue = colorValue.Trim();
// If it's rgb() or rgba() format, extract values
var rgbMatch = RgbColorRegex().Match(colorValue);
if (rgbMatch.Success)
{
var r = rgbMatch.Groups[1].Value;
var g = rgbMatch.Groups[2].Value;
var b = rgbMatch.Groups[3].Value;
var a = rgbMatch.Groups[4].Success ? rgbMatch.Groups[4].Value : null;
if (a != null)
{
// Convert alpha from 0-1 or 0-255 to 0-255
var alphaValue = double.Parse(a);
if (alphaValue <= 1.0)
alphaValue *= 255;
return $"{(int)alphaValue},{r},{g},{b}";
}
return $"{r},{g},{b}";
}
return colorValue;
}
private string ProcessHtmlTagsInner(string content)
{
// Single pass recursive processing for nested tags
var processed = HtmlTagsRegex().Replace(content, match =>
{
var tagName = match.Groups[1].Value;
var innerContent = match.Groups[2].Value;
var normalizedTag = NormalizeTag(tagName);
// Recursively process inner content
var processedInner = ProcessHtmlTagsInner(innerContent);
// Handle special conversions for HTML tags (same as in ProcessHtmlTags)
// Convert and to if font is supported but code/pre are not
if ((normalizedTag == "code" || normalizedTag == "pre") && !IsSupported(normalizedTag) && IsSupported("font") && !string.IsNullOrWhiteSpace(DefaultCodeFontName))
{
// Don't use HtmlAttributeEncode for font name - DevExpress uses special syntax
var fontFace = DefaultCodeFontName.Replace("\"", """);
return $"{processedInner}";
}
// Convert h1-h6 tags to size+bold if h-tags not supported but size/b are
if (normalizedTag.Length == 2 && normalizedTag[0] == 'h' && char.IsDigit(normalizedTag[1]))
{
var level = int.Parse(normalizedTag[1].ToString());
if (!IsSupported(normalizedTag))
{
// If h-tag not supported, use size+bold format (DevExpress style)
var hasBold = IsSupported("b");
var hasSize = IsSupported("size");
if (!hasBold && !hasSize)
return processedInner;
var result = processedInner;
if (hasBold)
result = $"{result}";
if (hasSize && level < 6)
{
var sizeIncrease = 6 - level; // h1=+5, h2=+4, h3=+3, h4=+2, h5=+1
result = $"{result} ";
}
return result;
}
}
if (IsSupported(normalizedTag))
{
// For nested tags, extract and keep essential attributes
var attributes = "";
if (normalizedTag == "a")
{
var hrefMatch = HrefAttributeRegex().Match(match.Value);
if (hrefMatch.Success)
attributes = $" href=\"{hrefMatch.Groups[1].Value}\"";
}
else if (normalizedTag == "img")
{
var srcMatch = SrcAttributeRegex().Match(match.Value);
var altMatch = AltAttributeRegex().Match(match.Value);
if (srcMatch.Success)
attributes += $" src=\"{srcMatch.Groups[1].Value}\"";
if (altMatch.Success)
attributes += $" alt=\"{altMatch.Groups[1].Value}\"";
attributes = attributes.Trim();
if (attributes.Length > 0)
attributes = " " + attributes;
}
else if (normalizedTag == "font")
{
var devExpressMatch = FontDevExpressFormatRegex().Match(match.Value);
var faceMatch = FontFaceAttributeRegex().Match(match.Value);
if (devExpressMatch.Success)
attributes = $"=\"{devExpressMatch.Groups[1].Value}\"";
else if (faceMatch.Success)
attributes = $"=\"{faceMatch.Groups[1].Value}\"";
}
else if (normalizedTag == "size")
{
var sizeMatch = SizeAttributeRegex().Match(match.Value);
if (sizeMatch.Success)
attributes = $"={sizeMatch.Groups[1].Value}";
}
else if (normalizedTag == "color")
{
var colorMatch = ColorAttributeRegex().Match(match.Value);
if (colorMatch.Success)
attributes = $"={colorMatch.Groups[1].Value}";
}
else if (normalizedTag == "backcolor")
{
var backcolorMatch = BackColorAttributeRegex().Match(match.Value);
if (backcolorMatch.Success)
attributes = $"={backcolorMatch.Groups[1].Value}";
}
if (normalizedTag == "size" || normalizedTag == "color" || normalizedTag == "backcolor")
{
return $"<{normalizedTag}{attributes}>{processedInner}{normalizedTag}>";
}
return $"<{normalizedTag}{attributes}>{processedInner}{normalizedTag}>";
}
else
{
// Strip tag but keep content
return processedInner;
}
});
return processed;
}
private bool IsSupported(string tag)
{
return _supportedTags.Contains(NormalizeTag(tag));
}
private static string NormalizeTag(string tag)
{
if (_tagAliases.TryGetValue(tag, out var normalized))
return normalized;
return tag.ToLowerInvariant();
}
}