/
valzem
/
MatControl
Обзор
Документация
Войти
/
valzem
/
MatControl
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/MatControl.UI/MarkdownRenderer.cs
337 строк
10 KB
MatControl Dev
Помощь с инструкцией, названия разделов ОС, PDF-экспорт для всех отчетов, сообщения об успехе экспорта; скиллы и задачи обновлены
08 авг 2026, 23:57
08 авг 2026, 23:57
e112694
Код
Авторство
О чём код?
using System.Text; using System.Text.RegularExpressions; namespace MatControl.UI; internal sealed record TocEntry(string Title, int Level, int Offset); internal static class MarkdownRenderer { private static readonly Font TitleFont = new("Segoe UI", 18f, FontStyle.Bold); private static readonly Font ChapterFont = new("Segoe UI", 14f, FontStyle.Bold); private static readonly Font SectionFont = new("Segoe UI", 11.5f, FontStyle.Bold); private static readonly Font BodyFont = new("Segoe UI", 10f); private static readonly Font BodyBold = new("Segoe UI", 10f, FontStyle.Bold); private static readonly Font BodyItalic = new("Segoe UI", 10f, FontStyle.Italic); private static readonly Font CodeFont = new("Consolas", 9.5f); private static readonly Regex NumberedPattern = new(@"^\d+\.\s+", RegexOptions.Compiled); public static List<TocEntry> Render(RichTextBox box, string markdown) { box.ReadOnly = true; box.BorderStyle = BorderStyle.None; box.DetectUrls = false; box.WordWrap = true; box.ScrollBars = RichTextBoxScrollBars.Both; box.Clear(); box.SelectionStart = 0; box.SelectionLength = 0; var toc = new List<TocEntry>(); var lines = markdown.Split('\n'); var index = 0; while (index < lines.Length) { var line = lines[index].Trim(); if (line.Length == 0) { index++; continue; } if (line.StartsWith("### ")) { AppendHeading(box, line[4..].Trim(), 3, toc); index++; } else if (line.StartsWith("## ")) { AppendHeading(box, line[3..].Trim(), 2, toc); index++; } else if (line.StartsWith("# ")) { AppendHeading(box, line[2..].Trim(), 1, toc); index++; } else if (line == "---") { AppendDivider(box); index++; } else if (line.StartsWith("|")) { index = RenderTable(box, lines, index); } else if (line.StartsWith(">")) { var quote = new StringBuilder(); while (index < lines.Length) { var q = lines[index].Trim(); if (q.Length == 0 || !q.StartsWith(">")) { break; } if (quote.Length > 0) { quote.Append(' '); } quote.Append(q[1..].Trim()); index++; } AppendQuote(box, quote.ToString()); } else if (line.StartsWith("- ")) { AppendBullet(box, line[2..].Trim()); index++; } else if (NumberedPattern.IsMatch(line)) { AppendNumbered(box, NumberedPattern.Replace(line, string.Empty)); index++; } else { AppendParagraph(box, line); index++; } } box.SelectionStart = 0; box.SelectionLength = 0; return toc; } private static void AppendHeading(RichTextBox box, string text, int level, List<TocEntry> toc) { if (level <= 3) { toc.Add(new TocEntry(text, level, box.TextLength)); } if (level > 1) { AppendBlankLine(box); } var font = level switch { 1 => TitleFont, 2 => ChapterFont, _ => SectionFont }; var color = level == 1 ? Theme.Accent : Theme.TextMain; box.SelectionStart = box.TextLength; box.SelectionLength = 0; box.SelectionFont = font; box.SelectionColor = color; box.SelectedText = text + "\n"; if (level == 1) { AppendDivider(box); } else { AppendBlankLine(box); } } private static int RenderTable(RichTextBox box, string[] lines, int start) { var rows = new List<string[]>(); var index = start; while (index < lines.Length && lines[index].Trim().StartsWith("|")) { rows.Add(ParseCells(lines[index])); index++; } var hasSeparator = rows.Count > 1 && rows[1].All(c => IsSeparator(c)); AppendSpacing(box); for (var r = 0; r < rows.Count; r++) { if (rows[r].All(c => IsSeparator(c))) { continue; } var isHeader = r == 0 && hasSeparator; AppendTableRow(box, rows[r], isHeader); } AppendSpacing(box); return index; } private static void AppendTableRow(RichTextBox box, string[] cells, bool isHeader) { var sb = new StringBuilder(); for (var c = 0; c < cells.Length; c++) { if (c > 0) { sb.Append(" │ "); } sb.Append(cells[c]); } AppendInline(box, sb.ToString(), isHeader ? BodyBold : BodyFont, isHeader ? Theme.Accent : Theme.TextMain); box.SelectionStart = box.TextLength; box.SelectionLength = 0; box.SelectionFont = BodyFont; box.SelectionColor = Theme.TextMain; box.SelectedText = "\n"; } private static void AppendBullet(RichTextBox box, string text) { AppendInline(box, "\u2022 ", BodyFont, Theme.Accent); AppendInline(box, text, BodyFont, Theme.TextMain); AppendLine(box); } private static void AppendNumbered(RichTextBox box, string text) { AppendInline(box, text, BodyFont, Theme.TextMain); AppendLine(box); } private static void AppendParagraph(RichTextBox box, string text) { AppendInline(box, text, BodyFont, Theme.TextMain); AppendBlankLine(box); } private static void AppendQuote(RichTextBox box, string text) { AppendSpacing(box); box.SelectionStart = box.TextLength; box.SelectionLength = 0; box.SelectionFont = BodyItalic; box.SelectionColor = Theme.TextSecondary; box.SelectedText = text; AppendLine(box); AppendSpacing(box); } private static void AppendDivider(RichTextBox box) { AppendSpacing(box); box.SelectionStart = box.TextLength; box.SelectionLength = 0; box.SelectionFont = BodyFont; box.SelectionColor = Theme.HeaderBack; box.SelectedText = new string('\u2500', 90) + "\n"; AppendSpacing(box); } private static void AppendInline(RichTextBox box, string text, Font baseFont, Color baseColor) { var index = 0; while (index < text.Length) { if (text[index] == '`') { var end = text.IndexOf('`', index + 1); if (end > index) { AppendRun(box, text.Substring(index + 1, end - index - 1), CodeFont, Theme.TextSecondary, Theme.RowBack); index = end + 1; continue; } } if (text[index] == '*' && index + 1 < text.Length && text[index + 1] == '*') { var end = text.IndexOf("**", index + 2); if (end > index) { AppendInline(box, text.Substring(index + 2, end - index - 2), BodyBold, Theme.TextMain); index = end + 2; continue; } } if (text[index] == '*') { var end = text.IndexOf('*', index + 1); if (end > index) { AppendInline(box, text.Substring(index + 1, end - index - 1), BodyItalic, Theme.TextMain); index = end + 1; continue; } } AppendRun(box, text[index].ToString(), baseFont, baseColor, Color.Empty); index++; } } private static void AppendRun(RichTextBox box, string text, Font font, Color color, Color back) { if (text.Length == 0) { return; } box.SelectionStart = box.TextLength; box.SelectionLength = 0; box.SelectionFont = font; box.SelectionColor = color; box.SelectionBackColor = back; box.SelectedText = text; box.SelectionBackColor = Color.Empty; } private static void AppendLine(RichTextBox box) { box.SelectionStart = box.TextLength; box.SelectionLength = 0; box.SelectionFont = BodyFont; box.SelectionColor = Theme.TextMain; box.SelectedText = "\n"; } private static void AppendSpacing(RichTextBox box) { box.SelectionStart = box.TextLength; box.SelectionLength = 0; box.SelectionFont = BodyFont; box.SelectionColor = Theme.TextMain; box.SelectedText = "\n\n"; box.SelectionStart = box.TextLength; box.SelectionLength = 0; } private static void AppendBlankLine(RichTextBox box) { AppendSpacing(box); } private static bool IsSeparator(string cell) { var t = cell.Trim(); return t.Length > 0 && t.All(c => c == '-' || c == ':'); } private static string[] ParseCells(string line) { return line.Trim() .Split('|', StringSplitOptions.RemoveEmptyEntries) .Select(c => c.Trim()) .ToArray(); } }