Files
OwnCord/Client/OwnCord.Client/Services/MessageContentParser.cs
T
jevb c1c25ed26c feat: implement full client UI from mockup — 10 phases, 331 tests
Client UI:
- Design system: Colors, Typography, Controls resource dictionaries
- Message actions: reply compose bar, hover edit/delete/reply buttons
- Rich content: code blocks, attachments, system messages, content parser
- Server strip: 72px sidebar with server icons, home button, add server
- Status picker: popup for changing online/idle/dnd/invisible status
- ConnectPage: server health check dots with auto-refresh
- User popup: profile card with banner, avatar, roles, member since
- Emoji picker: 6 categories, search, grid of Unicode emojis
- Settings overlay: full-screen with sidebar navigation
- Friends/DM view: sidebar + friends list with tabs (online/all/pending)
- Toast notifications: auto-dismiss after 3s with fade animation

Models & services:
- Attachment model added to Message, ApiMessage, ChatMessagePayload
- EditMessageAsync, DeleteMessageAsync, SendStatusChangeAsync APIs
- MessageContentParser (code blocks, inline code, bold, italic)
- EmojiData, ToastService, HealthStatusToBrushConverter

Server (from prior session):
- Voice room management, SFU, speaker detection
- ACME/TLS support, config improvements
- Protocol and schema updates

Tests: 331 passing (61 converter + 24 voice service + 34 voice VM +
41 parser + 9 edit/delete + existing)
2026-03-15 11:42:25 +01:00

143 lines
4.2 KiB
C#

using System.Text.RegularExpressions;
namespace OwnCord.Client.Services;
/// <summary>
/// Parses message content into segments for rich rendering.
/// Handles code blocks (```), inline code (`), bold (**), italic (*), and plain text.
/// </summary>
public static class MessageContentParser
{
public enum SegmentType { Text, CodeBlock, InlineCode, Bold, Italic }
public record ContentSegment(SegmentType Type, string Text, string? Language = null);
// Matches ```language\n...\n``` (multiline)
private static readonly Regex CodeBlockRegex = new(
@"```(\w*)\n?([\s\S]*?)```",
RegexOptions.Compiled);
// Matches `...` (single backtick inline code, no newlines)
private static readonly Regex InlineCodeRegex = new(
@"`([^`\n]+)`",
RegexOptions.Compiled);
// Matches **...** (bold)
private static readonly Regex BoldRegex = new(
@"\*\*(.+?)\*\*",
RegexOptions.Compiled);
// Matches *...* (italic, but not **)
private static readonly Regex ItalicRegex = new(
@"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)",
RegexOptions.Compiled);
public static IReadOnlyList<ContentSegment> Parse(string content)
{
if (string.IsNullOrEmpty(content))
return Array.Empty<ContentSegment>();
var segments = new List<ContentSegment>();
ParseCodeBlocks(content, segments);
return segments;
}
private static void ParseCodeBlocks(string text, List<ContentSegment> segments)
{
var lastIndex = 0;
foreach (Match match in CodeBlockRegex.Matches(text))
{
if (match.Index > lastIndex)
{
ParseInlineCode(text[lastIndex..match.Index], segments);
}
var language = match.Groups[1].Value;
var code = match.Groups[2].Value;
segments.Add(new ContentSegment(
SegmentType.CodeBlock,
code,
string.IsNullOrEmpty(language) ? null : language));
lastIndex = match.Index + match.Length;
}
if (lastIndex < text.Length)
{
ParseInlineCode(text[lastIndex..], segments);
}
}
private static void ParseInlineCode(string text, List<ContentSegment> segments)
{
var lastIndex = 0;
foreach (Match match in InlineCodeRegex.Matches(text))
{
if (match.Index > lastIndex)
{
ParseBoldAndItalic(text[lastIndex..match.Index], segments);
}
segments.Add(new ContentSegment(SegmentType.InlineCode, match.Groups[1].Value));
lastIndex = match.Index + match.Length;
}
if (lastIndex < text.Length)
{
ParseBoldAndItalic(text[lastIndex..], segments);
}
}
private static void ParseBoldAndItalic(string text, List<ContentSegment> segments)
{
var lastIndex = 0;
foreach (Match match in BoldRegex.Matches(text))
{
if (match.Index > lastIndex)
{
ParseItalic(text[lastIndex..match.Index], segments);
}
segments.Add(new ContentSegment(SegmentType.Bold, match.Groups[1].Value));
lastIndex = match.Index + match.Length;
}
if (lastIndex < text.Length)
{
ParseItalic(text[lastIndex..], segments);
}
}
private static void ParseItalic(string text, List<ContentSegment> segments)
{
var lastIndex = 0;
foreach (Match match in ItalicRegex.Matches(text))
{
if (match.Index > lastIndex)
{
AddTextSegment(text[lastIndex..match.Index], segments);
}
segments.Add(new ContentSegment(SegmentType.Italic, match.Groups[1].Value));
lastIndex = match.Index + match.Length;
}
if (lastIndex < text.Length)
{
AddTextSegment(text[lastIndex..], segments);
}
}
private static void AddTextSegment(string text, List<ContentSegment> segments)
{
if (!string.IsNullOrEmpty(text))
{
segments.Add(new ContentSegment(SegmentType.Text, text));
}
}
}