mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
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)
143 lines
4.2 KiB
C#
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));
|
|
}
|
|
}
|
|
}
|