namespace MeetingAssistant.Workflow; internal enum WorkflowRulesEditorMarkdownInlineStyle { Normal, Italic, Bold, Code } internal abstract record WorkflowRulesEditorMarkdownBlock; internal sealed record WorkflowRulesEditorMarkdownParagraph( IReadOnlyList Inlines) : WorkflowRulesEditorMarkdownBlock; internal sealed record WorkflowRulesEditorMarkdownCodeBlock(string Text) : WorkflowRulesEditorMarkdownBlock; internal sealed record WorkflowRulesEditorMarkdownTable( IReadOnlyList Header, IReadOnlyList> Rows) : WorkflowRulesEditorMarkdownBlock; internal sealed record WorkflowRulesEditorMarkdownInline( string Text, WorkflowRulesEditorMarkdownInlineStyle Style); internal static class WorkflowRulesEditorMarkdown { public static IReadOnlyList Parse(string text) { if (string.IsNullOrEmpty(text)) { return []; } var blocks = new List(); var paragraph = new List(); var code = new List(); var inCodeBlock = false; var lines = ReadLines(text).ToArray(); for (var index = 0; index < lines.Length; index++) { var line = lines[index]; if (line.TrimStart().StartsWith("```", StringComparison.Ordinal)) { if (inCodeBlock) { blocks.Add(new WorkflowRulesEditorMarkdownCodeBlock(string.Join(Environment.NewLine, code))); code.Clear(); inCodeBlock = false; } else { FlushParagraph(blocks, paragraph); inCodeBlock = true; } continue; } if (inCodeBlock) { code.Add(line); continue; } if (IsTableStart(lines, index)) { FlushParagraph(blocks, paragraph); var header = ParseTableRow(lines[index]); index += 2; var rows = new List>(); while (index < lines.Length && IsTableRow(lines[index])) { rows.Add(ParseTableRow(lines[index])); index++; } blocks.Add(new WorkflowRulesEditorMarkdownTable(header, rows)); index--; continue; } if (string.IsNullOrWhiteSpace(line)) { FlushParagraph(blocks, paragraph); } else { paragraph.Add(line); } } if (inCodeBlock) { blocks.Add(new WorkflowRulesEditorMarkdownCodeBlock(string.Join(Environment.NewLine, code))); } FlushParagraph(blocks, paragraph); return blocks; } public static IReadOnlyList ParseInline(string text) { var result = new List(); var index = 0; while (index < text.Length) { var next = FindNextMarker(text, index); if (next < 0) { AddInline(result, text[index..], WorkflowRulesEditorMarkdownInlineStyle.Normal); break; } if (next > index) { AddInline(result, text[index..next], WorkflowRulesEditorMarkdownInlineStyle.Normal); } if (text[next] == '`') { var end = text.IndexOf('`', next + 1); if (end > next) { AddInline(result, text[(next + 1)..end], WorkflowRulesEditorMarkdownInlineStyle.Code); index = end + 1; continue; } } else if (text.AsSpan(next).StartsWith("**", StringComparison.Ordinal)) { var end = text.IndexOf("**", next + 2, StringComparison.Ordinal); if (end > next) { AddInline(result, text[(next + 2)..end], WorkflowRulesEditorMarkdownInlineStyle.Bold); index = end + 2; continue; } } else if (text[next] == '*') { var end = text.IndexOf('*', next + 1); if (end > next) { AddInline(result, text[(next + 1)..end], WorkflowRulesEditorMarkdownInlineStyle.Italic); index = end + 1; continue; } } AddInline(result, text[next].ToString(), WorkflowRulesEditorMarkdownInlineStyle.Normal); index = next + 1; } return result; } private static void FlushParagraph( List blocks, List paragraph) { if (paragraph.Count == 0) { return; } blocks.Add(new WorkflowRulesEditorMarkdownParagraph(ParseInline(string.Join('\n', paragraph)))); paragraph.Clear(); } private static bool IsTableStart(IReadOnlyList lines, int index) { return index + 1 < lines.Count && IsTableRow(lines[index]) && IsTableSeparator(lines[index + 1]); } private static bool IsTableRow(string line) { return !string.IsNullOrWhiteSpace(line) && line.Contains('|', StringComparison.Ordinal); } private static bool IsTableSeparator(string line) { var cells = ParseTableRow(line); return cells.Count > 0 && cells.All(cell => { var trimmed = cell.Trim(); return trimmed.Contains('-', StringComparison.Ordinal) && trimmed.All(character => character is '-' or ':' or ' '); }); } private static IReadOnlyList ParseTableRow(string line) { var trimmed = line.Trim(); if (trimmed.StartsWith('|')) { trimmed = trimmed[1..]; } if (trimmed.EndsWith('|')) { trimmed = trimmed[..^1]; } return trimmed .Split('|') .Select(cell => cell.Trim()) .ToArray(); } private static void AddInline( List result, string text, WorkflowRulesEditorMarkdownInlineStyle style) { if (text.Length == 0) { return; } if (result.Count > 0 && result[^1].Style == style) { result[^1] = result[^1] with { Text = result[^1].Text + text }; return; } result.Add(new WorkflowRulesEditorMarkdownInline(text, style)); } private static int FindNextMarker(string text, int start) { var code = text.IndexOf('`', start); var bold = text.IndexOf("**", start, StringComparison.Ordinal); var italic = text.IndexOf('*', start); if (italic >= 0 && italic + 1 < text.Length && text[italic + 1] == '*') { italic = text.IndexOf('*', italic + 2); } return new[] { code, bold, italic } .Where(index => index >= 0) .DefaultIfEmpty(-1) .Min(); } private static IEnumerable ReadLines(string text) { using var reader = new StringReader(text); while (reader.ReadLine() is { } line) { yield return line; } } }