Files
better-genshin-impact/BetterGenshinImpact/Helpers/MarkdownToFlowDocumentConverter.cs

1404 lines
50 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using BetterGenshinImpact.View.Windows;
namespace BetterGenshinImpact.Helpers;
/// <summary>
/// Markdown To FlowDocument的转换器
/// 支持显示:分割线、图片、多级标题、表格、任务列表、多级无序列表、多级有序列表、普通段落、粗斜体、粗体、斜体、删除线、下划线、内联代码和超链接
/// 不支持显示代码块、脚注、引用、HTML标签、数学公式
/// </summary>
public static class MarkdownToFlowDocumentConverter
{
/// <summary>
/// 将Markdown文本转换为FlowDocument
/// </summary>
/// <param name="markdown">Markdown文本内容</param>
/// <returns>转换后的FlowDocument</returns>
public static FlowDocument ConvertToFlowDocument(string markdown)
{
var doc = new FlowDocument();
// 尝试获取App.xaml中定义的默认字体
if (Application.Current?.Resources["TextThemeFontFamily"] is FontFamily appFont)
doc.FontFamily = appFont;
var lines = markdown.Split(["\r\n", "\n"], StringSplitOptions.None);
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i];
if (string.IsNullOrWhiteSpace(line))
{
// 为空行创建一个较小间距的段落,减少空行占用的垂直空间
doc.Blocks.Add(new Paragraph()
{
Margin = new Thickness(0, 0, 0, 0),
FontSize = 6
});
continue;
}
var trimmedLine = line.Trim();
// 分割线 - 支持 ---, ***, ___ (至少3个字符)
if (IsHorizontalRule(trimmedLine))
{
var hrParagraph = new Paragraph
{
Margin = new Thickness(0, 10, 0, 10)
};
// 创建分割线效果 - 使用下划线的Run
var hrRun = new Run(new string('─', 15)) // 使用Unicode水平线字符
{
Foreground = Brushes.Gray,
FontSize = 24
};
hrParagraph.Inlines.Add(hrRun);
hrParagraph.TextAlignment = TextAlignment.Center;
doc.Blocks.Add(hrParagraph);
continue;
}
// 独立图片行处理 - 直接显示图片
if (IsStandaloneImage(trimmedLine))
{
var imageMatch = Regex.Match(trimmedLine, @"^!\[(.*?)\]\((.*?)\)$");
if (imageMatch.Success)
{
string altText = imageMatch.Groups[1].Value;
string imageContent = imageMatch.Groups[2].Value;
// 解析URL和标题
ParseImageUrlAndTitle(imageContent, out string url, out string? title);
var imageParagraph = new Paragraph
{
TextAlignment = TextAlignment.Center,
Margin = new Thickness(0, 10, 0, 10)
};
// 尝试创建并添加图片
if (TryCreateInlineImage(url, altText, out var inlineImage))
{
imageParagraph.Inlines.Add(inlineImage);
// 如果有标题,添加到图片下方
if (!string.IsNullOrWhiteSpace(title))
{
imageParagraph.Inlines.Add(new LineBreak());
imageParagraph.Inlines.Add(new Run(title)
{
FontStyle = FontStyles.Normal,
FontSize = 12
});
}
}
else
{
// 加载图片失败,回退到创建超链接
string displayText = !string.IsNullOrWhiteSpace(title) ? title : altText;
if (TryCreateImageHyperlink(url, displayText, out Hyperlink? imageLink))
{
imageParagraph.Inlines.Add(imageLink);
}
else
{
// 创建普通文本显示
string fallbackText = !string.IsNullOrWhiteSpace(displayText) ? $"[图片: {displayText}]" : $"[图片: {url}]";
imageParagraph.Inlines.Add(new Run(fallbackText)
{
FontStyle = FontStyles.Normal,
Foreground = Brushes.Gray
});
}
}
doc.Blocks.Add(imageParagraph);
continue;
}
}
// 标题
if (trimmedLine.StartsWith("#"))
{
int level = trimmedLine.TakeWhile(c => c == '#').Count();
string text = trimmedLine[level..].Trim();
var para = new Paragraph
{
FontWeight = FontWeights.Bold,
FontSize = 24 - (level - 1) * 2,
Margin = new Thickness(0, 10, 0, 5)
};
// 标题支持粗体、斜体、删除线、下划线、内联代码和超链接格式
AddInlinesWithFormatting(para.Inlines, text);
doc.Blocks.Add(para);
continue;
}
// 检查是否为表格开始
if (IsTableRow(trimmedLine))
{
var tableLines = new List<string> { line };
// 收集表格的所有行
int j = i + 1;
while (j < lines.Length)
{
var nextLine = lines[j];
var nextTrimmed = nextLine.Trim();
if (string.IsNullOrWhiteSpace(nextTrimmed))
{
break; // 空行表示表格结束
}
if (IsTableRow(nextTrimmed) || IsTableSeparatorRow(nextTrimmed))
{
tableLines.Add(nextLine);
j++;
}
else
{
break; // 非表格行表示表格结束
}
}
// 创建表格
var table = CreateTable(tableLines);
if (table != null)
{
doc.Blocks.Add(table);
i = j - 1; // 调整索引,跳过已处理的表格行
continue;
}
}
// 任务列表项
if (IsTaskListItem(line, out int taskIndentLevel, out string taskContent, out bool isCompleted))
{
var para = new Paragraph
{
Margin = new Thickness(20 + (taskIndentLevel * 20), 2, 0, 2) // 根据缩进级别调整左边距
};
// 根据任务完成状态添加不同的符号
string taskSymbol = isCompleted ? "✅ " : "❎ ";
para.Inlines.Add(new Run(taskSymbol) { FontWeight = FontWeights.Bold });
// 处理任务内容,支持粗体、斜体、删除线、下划线、内联代码和超链接
AddInlinesWithFormatting(para.Inlines, taskContent);
doc.Blocks.Add(para);
continue;
}
// 多级无序列表
if (IsUnorderedListItem(line, out int unorderedIndentLevel, out string unorderedContent))
{
var para = new Paragraph
{
Margin = new Thickness(20 + (unorderedIndentLevel * 20), 2, 0, 2) // 根据缩进级别调整左边距
};
// 根据缩进级别选择不同的项目符号
string bullet = GetUnorderedListBullet(unorderedIndentLevel);
para.Inlines.Add(new Run(bullet + " ") { FontWeight = FontWeights.Bold });
// 处理列表项内容,支持粗体、斜体、删除线、下划线、内联代码和超链接
AddInlinesWithFormatting(para.Inlines, unorderedContent);
doc.Blocks.Add(para);
continue;
}
// 多级有序列表
if (IsOrderedListItem(line, out int orderedIndentLevel, out string orderedContent, out string numberPrefix))
{
var para = new Paragraph
{
Margin = new Thickness(20 + (orderedIndentLevel * 20), 2, 0, 2) // 根据缩进级别调整左边距
};
// 添加序号前缀
para.Inlines.Add(new Run(numberPrefix + " ") { FontWeight = FontWeights.Bold });
// 处理有序列表内容,支持粗体、斜体、删除线、下划线、内联代码和超链接
AddInlinesWithFormatting(para.Inlines, orderedContent);
doc.Blocks.Add(para);
continue;
}
// 普通段落,支持粗体、斜体、删除线、下划线、内联代码和超链接
var paragraph = new Paragraph
{
Margin = new Thickness(0, 2, 0, 2)
};
// 处理普通段落,支持粗体、斜体、删除线、下划线、内联代码和超链接
AddInlinesWithFormatting(paragraph.Inlines, line);
doc.Blocks.Add(paragraph);
}
return doc;
}
/// <summary>
/// 检查是否为任务列表项
/// </summary>
/// <param name="line">文本行</param>
/// <param name="indentLevel">缩进级别0为顶级</param>
/// <param name="content">任务内容</param>
/// <param name="isCompleted">任务是否已完成</param>
/// <returns>是否为任务列表项</returns>
private static bool IsTaskListItem(string line, out int indentLevel, out string content, out bool isCompleted)
{
indentLevel = 0;
content = string.Empty;
isCompleted = false;
// 匹配任务列表项:支持空格缩进 + "- [ ] " 或 "- [x] " 格式
// 每两个空格为一级缩进
var match = Regex.Match(line, @"^(\s*)-\s*\[([ xX])\]\s+(.*)$");
if (match.Success)
{
string indentString = match.Groups[1].Value;
string checkMark = match.Groups[2].Value;
content = match.Groups[3].Value;
// 计算缩进级别每2个空格为一级
indentLevel = indentString.Length / 2;
// 判断任务是否已完成
isCompleted = checkMark.ToLower() == "x";
return true;
}
return false;
}
//图片处理
/// <summary>
/// 检查是否为独立的图片行
/// </summary>
/// <param name="line">文本行</param>
/// <returns>是否为独立图片</returns>
private static bool IsStandaloneImage(string line)
{
// 检查整行是否只包含一个图片标记,支持带标题的格式
var pattern = @"^!\[.*?\]\([^\)]*\)$";
return Regex.IsMatch(line, pattern);
}
/// <summary>
/// 解析图片标记,分离 URL 和标题
/// </summary>
/// <param name="imageContent">图片括号内容url "title" 或 url</param>
/// <param name="url">解析出的URL</param>
/// <param name="title">解析出的标题(可选)</param>
private static void ParseImageUrlAndTitle(string imageContent, out string url, out string? title)
{
url = imageContent.Trim();
title = null;
// 匹配 URL 和可选标题url "title" 或 url 'title' 或纯 url
var match = Regex.Match(imageContent.Trim(), @"^(.+?)\s+[""']([^""']*)[""']\s*$");
if (match.Success)
{
url = match.Groups[1].Value.Trim();
title = match.Groups[2].Value;
}
}
/// <summary>
/// 尝试创建内联图片
/// </summary>
/// <param name="url">图片URL或路径</param>
/// <param name="altText">替代文本</param>
/// <param name="inlineImage">创建的内联图片</param>
/// <returns>是否成功创建图片</returns>
private static bool TryCreateInlineImage(string url, string altText, out InlineUIContainer? inlineImage)
{
inlineImage = null;
if (string.IsNullOrWhiteSpace(url))
{
return false;
}
try
{
// 尝试解析为绝对URI
Uri imageUri;
if (Uri.TryCreate(url, UriKind.Absolute, out var absoluteUri) &&
(absoluteUri.Scheme == Uri.UriSchemeHttp || absoluteUri.Scheme == Uri.UriSchemeHttps || absoluteUri.Scheme == Uri.UriSchemeFile))
{
imageUri = absoluteUri;
}
else if (File.Exists(url))
{
// 本地文件路径
imageUri = new Uri(Path.GetFullPath(url), UriKind.Absolute);
}
else
{
// 尝试作为相对路径处理
var fullPath = Path.Combine(Environment.CurrentDirectory, url);
if (File.Exists(fullPath))
{
imageUri = new Uri(fullPath, UriKind.Absolute);
}
else
{
return false;
}
}
// 创建图片
var image = new Image
{
Stretch = Stretch.Uniform,
StretchDirection = StretchDirection.DownOnly,
MaxWidth = 400, // 限制最大宽度,避免图片过大
MaxHeight = 300, // 限制最大高度
ToolTip = !string.IsNullOrWhiteSpace(altText) ? altText : "图片"
};
// 设置图片源
BitmapImage bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = imageUri;
bitmap.CacheOption = BitmapCacheOption.OnLoad; // 加载后缓存,避免文件锁定
bitmap.EndInit();
image.Source = bitmap;
// 创建内联容器
inlineImage = new InlineUIContainer(image);
return true;
}
catch (Exception ex)
{
Debug.WriteLine($"加载图片失败: {url}, 错误: {ex.Message}");
return false;
}
}
/// <summary>
/// 尝试创建图片超链接
/// </summary>
/// <param name="url">图片URL或路径</param>
/// <param name="altText">替代文本</param>
/// <param name="imageLink">创建的超链接对象</param>
/// <returns>是否成功创建超链接</returns>
private static bool TryCreateImageHyperlink(string url, string altText, out Hyperlink? imageLink)
{
imageLink = null;
if (string.IsNullOrWhiteSpace(url))
{
return false;
}
try
{
Uri? imageUri = null;
string displayText = string.IsNullOrWhiteSpace(altText) ? $"🖼️ 查看图片: {Path.GetFileName(url)}" : $"🖼️ {altText}";
// 处理不同类型的URL
if (Uri.TryCreate(url, UriKind.Absolute, out imageUri))
{
// 绝对URLhttp/https/file等
}
else if (File.Exists(url))
{
// 相对路径或绝对文件路径
var fullPath = Path.GetFullPath(url);
imageUri = new Uri(fullPath, UriKind.Absolute);
}
else
{
// 尝试作为相对路径处理
var fullPath = Path.Combine(Environment.CurrentDirectory, url);
if (File.Exists(fullPath))
{
imageUri = new Uri(fullPath, UriKind.Absolute);
}
else
{
// 文件不存在但仍然创建链接可能是网络URL
if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out imageUri))
{
// 创建相对URL的绝对路径
var baseUri = new Uri(Environment.CurrentDirectory + Path.DirectorySeparatorChar);
imageUri = new Uri(baseUri, imageUri);
}
}
}
if (imageUri != null)
{
imageLink = new Hyperlink(new Run(displayText))
{
NavigateUri = imageUri,
Foreground = Brushes.DodgerBlue,
TextDecorations = TextDecorations.Underline,
ToolTip = $"点击在浏览器中打开图片: {url}"
};
// 添加点击事件处理
imageLink.RequestNavigate += (sender, e) =>
{
try
{
Process.Start(new ProcessStartInfo(e.Uri.ToString()) { UseShellExecute = true });
}
catch (Exception ex)
{
Debug.WriteLine($"无法打开图片: {e.Uri}, 错误: {ex.Message}");
}
e.Handled = true;
};
return true;
}
}
catch (Exception ex)
{
Debug.WriteLine($"创建图片链接失败 - URL: {url}, 错误: {ex.Message}");
}
return false;
}
//分割线处理
/// <summary>
/// 检查是否为分割线
/// </summary>
/// <param name="line">文本行</param>
/// <returns>是否为分割线</returns>
private static bool IsHorizontalRule(string line)
{
if (line.Length < 3) return false;
// 检查是否全部为相同的分割线字符至少3个
if (line.All(c => c == '-') && line.Length >= 3) return true;
if (line.All(c => c == '*') && line.Length >= 3) return true;
if (line.All(c => c == '_') && line.Length >= 3) return true;
// 支持带空格的分割线格式,如 "- - -" 或 "* * *"
var withoutSpaces = line.Replace(" ", "");
if (withoutSpaces.Length >= 3)
{
if (withoutSpaces.All(c => c == '-') ||
withoutSpaces.All(c => c == '*') ||
withoutSpaces.All(c => c == '_'))
{
return true;
}
}
return false;
}
//表格处理
/// <summary>
/// 检查是否为表格行
/// </summary>
/// <param name="line">文本行</param>
/// <returns>是否为表格行</returns>
private static bool IsTableRow(string line)
{
if (string.IsNullOrWhiteSpace(line))
return false;
// 简单检查:包含至少一个 | 字符,且不是表格分隔行
return line.Contains('|') && !IsTableSeparatorRow(line);
}
/// <summary>
/// 检查是否为表格分隔行(如 |---|---|
/// </summary>
/// <param name="line">文本行</param>
/// <returns>是否为表格分隔行</returns>
private static bool IsTableSeparatorRow(string line)
{
if (string.IsNullOrWhiteSpace(line))
return false;
// 匹配表格分隔行:|---|---|--- 或 | :--- | ---: | :---: |
var pattern = @"^\s*\|(\s*:?-+:?\s*\|)+\s*$";
return Regex.IsMatch(line, pattern);
}
/// <summary>
/// 创建表格
/// </summary>
/// <param name="tableLines">表格行列表</param>
/// <returns>创建的表格对象</returns>
private static Table? CreateTable(List<string> tableLines)
{
if (tableLines.Count < 1)
return null;
// 解析表格数据
var rows = new List<List<string>>();
List<TextAlignment>? alignments = null;
bool hasSeparatorRow = false;
foreach (var line in tableLines)
{
var trimmedLine = line.Trim();
if (IsTableSeparatorRow(trimmedLine))
{
// 解析对齐方式
alignments = ParseTableAlignment(trimmedLine);
hasSeparatorRow = true;
continue;
}
// 解析表格行
var cells = ParseTableRow(trimmedLine);
if (cells.Count > 0)
{
rows.Add(cells);
}
}
if (rows.Count == 0)
return null;
// 创建WPF表格
var table = new Table
{
CellSpacing = 0,
BorderBrush = Brushes.Gray,
BorderThickness = new Thickness(1),
Margin = new Thickness(0, 10, 0, 10)
};
// 确定列数
int columnCount = rows.Max(row => row.Count);
// 创建列定义
for (int i = 0; i < columnCount; i++)
{
table.Columns.Add(new TableColumn());
}
// 创建表格行组
var tableRowGroup = new TableRowGroup();
for (int rowIndex = 0; rowIndex < rows.Count; rowIndex++)
{
var row = rows[rowIndex];
var tableRow = new TableRow();
// 第一行作为表头(如果有分隔行的话)
bool isHeader = hasSeparatorRow && rowIndex == 0;
for (int colIndex = 0; colIndex < columnCount; colIndex++)
{
var cellContent = colIndex < row.Count ? row[colIndex] : "";
var tableCell = new TableCell();
// 设置单元格边框
tableCell.BorderBrush = Brushes.Gray;
tableCell.BorderThickness = new Thickness(1);
tableCell.Padding = new Thickness(8, 4, 8, 4);
// 设置对齐方式
if (alignments != null && colIndex < alignments.Count)
{
tableCell.TextAlignment = alignments[colIndex];
}
// 创建段落
var paragraph = new Paragraph
{
Margin = new Thickness(0)
};
// 表头加粗
if (isHeader)
{
paragraph.FontWeight = FontWeights.Bold;
tableCell.Background = new SolidColorBrush(Color.FromArgb(20, 128, 128, 128)); // 淡灰色背景
}
// 添加格式化文本
AddInlinesWithFormatting(paragraph.Inlines, cellContent);
tableCell.Blocks.Add(paragraph);
tableRow.Cells.Add(tableCell);
}
tableRowGroup.Rows.Add(tableRow);
}
table.RowGroups.Add(tableRowGroup);
return table;
}
/// <summary>
/// 解析表格行,提取单元格内容
/// </summary>
/// <param name="line">表格行</param>
/// <returns>单元格内容列表</returns>
private static List<string> ParseTableRow(string line)
{
var cells = new List<string>();
// 移除首尾的管道符
var trimmed = line.Trim();
if (trimmed.StartsWith("|"))
trimmed = trimmed[1..];
if (trimmed.EndsWith("|"))
trimmed = trimmed[..^1];
// 分割单元格,但要处理转义的管道符
var parts = SplitTableCells(trimmed);
foreach (var part in parts)
{
cells.Add(part.Trim());
}
return cells;
}
/// <summary>
/// 分割表格单元格,处理转义的管道符
/// </summary>
/// <param name="content">单元格内容</param>
/// <returns>单元格列表</returns>
private static List<string> SplitTableCells(string content)
{
var cells = new List<string>();
var currentCell = "";
bool inCodeSpan = false;
for (int i = 0; i < content.Length; i++)
{
char c = content[i];
if (c == '`')
{
inCodeSpan = !inCodeSpan;
currentCell += c;
}
else if (c == '\\' && i + 1 < content.Length && content[i + 1] == '|')
{
// 转义的管道符
currentCell += '|';
i++; // 跳过下一个字符
}
else if (c == '|' && !inCodeSpan)
{
// 分隔符
cells.Add(currentCell);
currentCell = "";
}
else
{
currentCell += c;
}
}
// 添加最后一个单元格
if (!string.IsNullOrEmpty(currentCell) || cells.Count > 0)
{
cells.Add(currentCell);
}
return cells;
}
/// <summary>
/// 解析表格对齐方式
/// </summary>
/// <param name="separatorLine">分隔行</param>
/// <returns>对齐方式列表</returns>
private static List<TextAlignment> ParseTableAlignment(string separatorLine)
{
var alignments = new List<TextAlignment>();
// 移除首尾的管道符和空格
var trimmed = separatorLine.Trim().Trim('|');
var parts = trimmed.Split('|');
foreach (var part in parts)
{
var cell = part.Trim();
if (cell.StartsWith(":") && cell.EndsWith(":"))
{
alignments.Add(TextAlignment.Center);
}
else if (cell.EndsWith(":"))
{
alignments.Add(TextAlignment.Right);
}
else
{
alignments.Add(TextAlignment.Left);
}
}
return alignments;
}
//列表处理
/// <summary>
/// 检查是否为多级无序列表项
/// </summary>
/// <param name="line">文本行</param>
/// <param name="indentLevel">缩进级别0为顶级</param>
/// <param name="content">列表项内容</param>
/// <returns>是否为无序列表项</returns>
private static bool IsUnorderedListItem(string line, out int indentLevel, out string content)
{
indentLevel = 0;
content = string.Empty;
// 匹配多级无序列表:支持空格缩进 + "- " 格式
// 每两个空格为一级缩进
var match = Regex.Match(line, @"^(\s*)- (.*)$");
if (match.Success)
{
string indentString = match.Groups[1].Value;
content = match.Groups[2].Value;
// 计算缩进级别每2个空格为一级
indentLevel = indentString.Length / 2;
return true;
}
return false;
}
/// <summary>
/// 检查是否为多级有序列表项
/// </summary>
/// <param name="line">文本行</param>
/// <param name="indentLevel">缩进级别0为顶级</param>
/// <param name="content">列表项内容</param>
/// <param name="numberPrefix">序号前缀</param>
/// <returns>是否为有序列表项</returns>
private static bool IsOrderedListItem(string line, out int indentLevel, out string content, out string numberPrefix)
{
indentLevel = 0;
content = string.Empty;
numberPrefix = string.Empty;
// 匹配多级有序列表:支持空格缩进 + "数字." 格式
// 每两个空格为一级缩进
var match = Regex.Match(line, @"^(\s*)(\d+)\.\s+(.*)$");
if (match.Success)
{
string indentString = match.Groups[1].Value;
numberPrefix = match.Groups[2].Value + ".";
content = match.Groups[3].Value;
// 计算缩进级别每2个空格为一级
indentLevel = indentString.Length / 2;
return true;
}
return false;
}
/// <summary>
/// 根据缩进级别获取无序列表的项目符号
/// </summary>
/// <param name="indentLevel">缩进级别</param>
/// <returns>项目符号字符</returns>
private static string GetUnorderedListBullet(int indentLevel)
{
return indentLevel switch
{
0 => "•", // 顶级:实心圆点
1 => "◦", // 二级:空心圆点
2 => "▪", // 三级:小方块
_ => "‣" // 更深级别:三角形
};
}
//文本处理
/// <summary>
/// 处理文本格式(粗体、斜体、粗斜体、删除线、下划线、内联代码、图片、超链接等)
/// </summary>
/// <param name="inlines">内联集合</param>
/// <param name="text">要处理的文本</param>
private static void AddInlinesWithFormatting(InlineCollection inlines, string text)
{
// 首先处理转义字符
text = ProcessEscapeCharacters(text);
int idx = 0;
while (idx < text.Length)
{
// 查找下一个格式标记的位置(包括图片)
int nextFormatIndex = FindNextFormatMark(text, idx);
if (nextFormatIndex == -1)
{
// 没有更多格式标记,处理剩余文本中的超链接
if (idx < text.Length)
AddTextWithHyperlinks(inlines, text[idx..]);
break;
}
// 处理格式标记之前的普通文本,包含超链接检测
if (nextFormatIndex > idx)
AddTextWithHyperlinks(inlines, text[idx..nextFormatIndex]);
// 处理格式标记(包括图片)
int formatEndIndex = ProcessFormatMark(inlines, text, nextFormatIndex);
if (formatEndIndex > nextFormatIndex)
{
idx = formatEndIndex;
}
else
{
// 如果格式处理失败,跳过当前字符继续
idx = nextFormatIndex + 1;
}
}
}
/// <summary>
/// 处理转义字符
/// </summary>
/// <param name="text">原始文本</param>
/// <returns>处理转义字符后的文本</returns>
private static string ProcessEscapeCharacters(string text)
{
// 使用特殊标记替换转义字符,避免在格式处理时被识别
const string ESCAPED_ASTERISK = "\x00ESCAPED_ASTERISK\x00";
const string ESCAPED_UNDERSCORE = "\x00ESCAPED_UNDERSCORE\x00";
const string ESCAPED_TILDE = "\x00ESCAPED_TILDE\x00";
const string ESCAPED_BACKSLASH = "\x00ESCAPED_BACKSLASH\x00";
const string ESCAPED_BACKTICK = "\x00ESCAPED_BACKTICK\x00";
var result = text
.Replace(@"\*", ESCAPED_ASTERISK)
.Replace(@"\_", ESCAPED_UNDERSCORE)
.Replace(@"\~", ESCAPED_TILDE)
.Replace(@"\\", ESCAPED_BACKSLASH)
.Replace(@"\`", ESCAPED_BACKTICK);
return result;
}
/// <summary>
/// 恢复转义字符
/// </summary>
/// <param name="text">包含转义标记的文本</param>
/// <returns>恢复转义字符后的文本</returns>
private static string RestoreEscapeCharacters(string text)
{
const string ESCAPED_ASTERISK = "\x00ESCAPED_ASTERISK\x00";
const string ESCAPED_UNDERSCORE = "\x00ESCAPED_UNDERSCORE\x00";
const string ESCAPED_TILDE = "\x00ESCAPED_TILDE\x00";
const string ESCAPED_BACKSLASH = "\x00ESCAPED_BACKSLASH\x00";
const string ESCAPED_BACKTICK = "\x00ESCAPED_BACKTICK\x00";
return text
.Replace(ESCAPED_ASTERISK, "*")
.Replace(ESCAPED_UNDERSCORE, "_")
.Replace(ESCAPED_TILDE, "~")
.Replace(ESCAPED_BACKSLASH, "\\")
.Replace(ESCAPED_BACKTICK, "`");
}
/// <summary>
/// 查找下一个格式标记的位置
/// </summary>
/// <param name="text">文本内容</param>
/// <param name="startIndex">开始搜索的位置</param>
/// <returns>格式标记的位置,如果没有找到返回-1</returns>
private static int FindNextFormatMark(string text, int startIndex)
{
int boldItalicIndex = text.IndexOf("***", startIndex, StringComparison.Ordinal);
int boldIndex = text.IndexOf("**", startIndex, StringComparison.Ordinal);
int italicIndex = text.IndexOf('*', startIndex);
int strikethroughIndex = text.IndexOf("~~", startIndex, StringComparison.Ordinal);
int underlineIndex = text.IndexOf("__", startIndex, StringComparison.Ordinal);
int inlineCodeIndex = text.IndexOf('`', startIndex);
int imageIndex = text.IndexOf("![", startIndex, StringComparison.Ordinal);
int autoLinkIndex = text.IndexOf("<http", startIndex, StringComparison.OrdinalIgnoreCase);
int linkIndex = text.IndexOf('[', startIndex);
// 避免将 *** 或 ** 中的 * 识别为斜体标记
if (italicIndex != -1)
{
if (italicIndex == boldItalicIndex)
{
// 跳过 *** 中的第一个 *
italicIndex = text.IndexOf('*', boldItalicIndex + 3);
}
else if (italicIndex == boldIndex)
{
// 跳过 ** 中的第一个 *
italicIndex = text.IndexOf('*', boldIndex + 2);
}
}
// 避免将 *** 识别为 ** + *
if (boldIndex != -1 && boldIndex == boldItalicIndex)
{
boldIndex = text.IndexOf("**", boldItalicIndex + 3, StringComparison.Ordinal);
}
// 避免将 __ 识别为两个单独的下划线字符
if (underlineIndex != -1)
{
// 确保找到的是完整的 __ 标记
var nextUnderline = text.IndexOf("__", underlineIndex + 2, StringComparison.Ordinal);
if (nextUnderline == -1)
{
underlineIndex = -1; // 没有配对的下划线,忽略
}
}
// 收集所有有效的格式标记位置
var indices = new[] { boldItalicIndex, boldIndex, italicIndex, strikethroughIndex, underlineIndex, inlineCodeIndex, imageIndex, autoLinkIndex, linkIndex }
.Where(i => i != -1)
.ToArray();
// 返回最近的格式标记位置
return indices.Length > 0 ? indices.Min() : -1;
}
/// <summary>
/// 处理格式标记
/// </summary>
/// <param name="inlines">内联集合</param>
/// <param name="text">文本内容</param>
/// <param name="markIndex">标记位置</param>
/// <returns>处理结束的位置</returns>
private static int ProcessFormatMark(InlineCollection inlines, string text, int markIndex)
{
// 标准链接格式 [text](url)
if (markIndex < text.Length && text[markIndex] == '[')
{
int closeBracketIndex = text.IndexOf(']', markIndex);
if (closeBracketIndex != -1 && closeBracketIndex + 1 < text.Length && text[closeBracketIndex + 1] == '(')
{
int closeParenIndex = text.IndexOf(')', closeBracketIndex + 1);
if (closeParenIndex != -1)
{
string linkText = text.Substring(markIndex + 1, closeBracketIndex - markIndex - 1);
string url = text.Substring(closeBracketIndex + 2, closeParenIndex - closeBracketIndex - 2);
// 创建超链接
if (TryCreateHyperlinkWithText(url, linkText, out Hyperlink? hyperlink))
{
inlines.Add(hyperlink);
return closeParenIndex + 1;
}
}
}
}
// 自动链接格式 <https://example.com>
else if (markIndex < text.Length && text[markIndex] == '<' &&
markIndex + 1 < text.Length &&
(text.IndexOf("http:", markIndex, StringComparison.OrdinalIgnoreCase) == markIndex + 1 ||
text.IndexOf("https:", markIndex, StringComparison.OrdinalIgnoreCase) == markIndex + 1))
{
int autoLinkEndIndex = text.IndexOf('>', markIndex);
if (autoLinkEndIndex != -1)
{
// 提取URL (不包含 < >)
string url = text.Substring(markIndex + 1, autoLinkEndIndex - markIndex - 1);
if (TryCreateHyperlink(url, out Hyperlink? hyperlink))
{
inlines.Add(hyperlink);
return autoLinkEndIndex + 1;
}
}
}
// 图片处理 ![alt text](url) - 尝试直接显示图片
else if (markIndex + 1 < text.Length && text[markIndex] == '!' && text[markIndex + 1] == '[')
{
int imageEndIndex = ProcessImageMark(inlines, text, markIndex);
if (imageEndIndex > markIndex)
{
return imageEndIndex;
}
}
// 粗斜体处理 ***text*** (必须在粗体和斜体之前处理)
else if (markIndex + 2 < text.Length &&
text[markIndex] == '*' && text[markIndex + 1] == '*' && text[markIndex + 2] == '*')
{
int boldItalicEndIndex = text.IndexOf("***", markIndex + 3, StringComparison.Ordinal);
if (boldItalicEndIndex != -1)
{
string boldItalicText = text[(markIndex + 3)..boldItalicEndIndex];
if (!string.IsNullOrWhiteSpace(boldItalicText))
{
// 创建粗斜体Bold 包含 Italic
var boldRun = new Bold();
var italicRun = new Italic();
// 递归处理粗斜体文本中的其他格式
AddInlinesWithFormatting(italicRun.Inlines, boldItalicText);
boldRun.Inlines.Add(italicRun);
inlines.Add(boldRun);
return boldItalicEndIndex + 3;
}
}
}
// 删除线处理 ~~text~~
else if (markIndex + 1 < text.Length && text[markIndex] == '~' && text[markIndex + 1] == '~')
{
int strikethroughEndIndex = text.IndexOf("~~", markIndex + 2, StringComparison.Ordinal);
if (strikethroughEndIndex != -1)
{
string strikethroughText = text[(markIndex + 2)..strikethroughEndIndex];
if (!string.IsNullOrWhiteSpace(strikethroughText))
{
var strikethroughRun = new Run(RestoreEscapeCharacters(strikethroughText))
{
TextDecorations = TextDecorations.Strikethrough
};
inlines.Add(strikethroughRun);
return strikethroughEndIndex + 2;
}
}
}
// 下划线处理 __text__ (注意与粗体 **text** 区分)
else if (markIndex + 1 < text.Length && text[markIndex] == '_' && text[markIndex + 1] == '_')
{
int underlineEndIndex = text.IndexOf("__", markIndex + 2, StringComparison.Ordinal);
if (underlineEndIndex != -1)
{
string underlineText = text[(markIndex + 2)..underlineEndIndex];
if (!string.IsNullOrWhiteSpace(underlineText))
{
var underlineRun = new Run(RestoreEscapeCharacters(underlineText))
{
TextDecorations = TextDecorations.Underline
};
inlines.Add(underlineRun);
return underlineEndIndex + 2;
}
}
}
// 粗体处理 **text**
else if (markIndex + 1 < text.Length && text[markIndex] == '*' && text[markIndex + 1] == '*')
{
int boldEndIndex = text.IndexOf("**", markIndex + 2, StringComparison.Ordinal);
if (boldEndIndex != -1)
{
string boldText = text[(markIndex + 2)..boldEndIndex];
if (!string.IsNullOrWhiteSpace(boldText))
{
// 递归处理粗体文本中的其他格式
var boldRun = new Bold();
AddInlinesWithFormatting(boldRun.Inlines, boldText);
inlines.Add(boldRun);
return boldEndIndex + 2;
}
}
}
// 斜体处理 *text*
else if (text[markIndex] == '*')
{
int italicEndIndex = text.IndexOf("*", markIndex + 1, StringComparison.Ordinal);
if (italicEndIndex != -1)
{
string italicText = text[(markIndex + 1)..italicEndIndex];
if (!string.IsNullOrWhiteSpace(italicText))
{
// 递归处理斜体文本中的其他格式
var italicRun = new Italic();
AddInlinesWithFormatting(italicRun.Inlines, italicText);
inlines.Add(italicRun);
return italicEndIndex + 1;
}
}
}
// 内联代码处理 `code`
else if (text[markIndex] == '`')
{
int codeEndIndex = text.IndexOf('`', markIndex + 1);
if (codeEndIndex != -1)
{
string codeText = text[(markIndex + 1)..codeEndIndex];
if (!string.IsNullOrEmpty(codeText)) // 允许空代码但不允许null
{
// 创建包含内联代码的容器,用于添加间距
var codeContainer = new InlineUIContainer();
// 创建边框元素来提供间距和背景
var border = new Border
{
Background = new SolidColorBrush(Color.FromArgb(40, 128, 128, 128)),
CornerRadius = new CornerRadius(3),
Padding = new Thickness(4, 2, 4, 2),
Margin = new Thickness(2, 0, 2, 0),
Cursor = System.Windows.Input.Cursors.Hand,
ToolTip = "单击复制代码",
Child = new TextBlock
{
Text = RestoreEscapeCharacters(codeText),
FontFamily = new FontFamily("Consolas, 宋体, monospace"),
VerticalAlignment = VerticalAlignment.Center
}
};
// 添加单击复制功能
border.MouseDown += (sender, e) =>
{
try
{
Clipboard.SetText(RestoreEscapeCharacters(codeText));
ThemedMessageBox.Information("代码已复制到剪贴板!", "复制成功");
}
catch (Exception ex)
{
Debug.WriteLine($"复制代码失败: {ex.Message}");
border.ToolTip = "复制失败";
}
};
codeContainer.Child = border;
inlines.Add(codeContainer);
return codeEndIndex + 1;
}
}
}
// 如果格式处理失败,作为普通文本处理
AddTextWithHyperlinks(inlines, text[markIndex].ToString());
return markIndex + 1;
}
/// <summary>
/// 处理图片格式标记 ![alt text](url) - 尝试直接显示图片
/// </summary>
/// <param name="inlines">内联集合</param>
/// <param name="text">文本内容</param>
/// <param name="markIndex">标记位置</param>
/// <returns>处理结束的位置</returns>
private static int ProcessImageMark(InlineCollection inlines, string text, int markIndex)
{
// 查找 ![alt](url) 或 ![alt](url "title") 格式
var imagePattern = @"^!\[(.*?)\]\((.*?)\)";
var match = Regex.Match(text[markIndex..], imagePattern);
if (match.Success)
{
string altText = match.Groups[1].Value;
string imageContent = match.Groups[2].Value;
// 解析URL和标题
ParseImageUrlAndTitle(imageContent, out string url, out string? title);
// 尝试创建内联图片
if (TryCreateInlineImage(url, altText, out var inlineImage))
{
inlines.Add(inlineImage);
// 如果有标题且不是空字符串,添加标题文本
if (!string.IsNullOrWhiteSpace(title))
{
inlines.Add(new Run(" " + title) { FontStyle = FontStyles.Normal, FontSize = 12 });
}
}
else
{
// 如果图片加载失败,回退到超链接
string displayText = !string.IsNullOrWhiteSpace(title) ? title : altText;
if (TryCreateImageHyperlink(url, displayText, out Hyperlink? imageLink))
{
inlines.Add(imageLink);
}
else
{
// 如果链接创建失败,显示替代文本
string fallbackText = !string.IsNullOrWhiteSpace(displayText) ? $"[图片: {displayText}]" : $"[图片: {url}]";
inlines.Add(new Run(fallbackText));
}
}
return markIndex + match.Length;
}
return markIndex;
}
//超链接处理
/// <summary>
/// 添加文本并自动检测超链接
/// </summary>
/// <param name="inlines">内联集合</param>
/// <param name="text">要处理的文本</param>
private static void AddTextWithHyperlinks(InlineCollection inlines, string text)
{
// 恢复转义字符
text = RestoreEscapeCharacters(text);
// 更严格的URL正则表达式匹配完整的http或https URL
var urlPattern = @"https?://(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?(?::\d+)?(?:/[^\s\[\]()]*)?";
var matches = Regex.Matches(text, urlPattern);
if (matches.Count == 0)
{
// 没有找到URL直接添加文本
inlines.Add(new Run(text));
return;
}
int lastIndex = 0;
foreach (Match match in matches)
{
// 添加URL之前的文本
if (match.Index > lastIndex)
{
inlines.Add(new Run(text[lastIndex..match.Index]));
}
// 验证并创建超链接
if (TryCreateHyperlink(match.Value, out Hyperlink? hyperlink))
{
inlines.Add(hyperlink);
}
else
{
// 如果URI无效作为普通文本添加
inlines.Add(new Run(match.Value));
}
lastIndex = match.Index + match.Length;
}
// 添加最后一个URL之后的文本
if (lastIndex < text.Length)
{
inlines.Add(new Run(text[lastIndex..]));
}
}
/// <summary>
/// 尝试创建超链接
/// </summary>
/// <param name="urlText">URL文本</param>
/// <param name="hyperlink">创建的超链接对象</param>
/// <returns>是否成功创建超链接</returns>
private static bool TryCreateHyperlink(string urlText, out Hyperlink? hyperlink)
{
hyperlink = null;
try
{
// 验证URI是否有效
if (Uri.TryCreate(urlText, UriKind.Absolute, out Uri? uri) &&
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
{
hyperlink = new Hyperlink(new Run(urlText))
{
NavigateUri = uri,
Foreground = Brushes.DodgerBlue,
TextDecorations = TextDecorations.Underline
};
// 添加点击事件处理
hyperlink.RequestNavigate += (sender, e) =>
{
try
{
Process.Start(new ProcessStartInfo(e.Uri.ToString()) { UseShellExecute = true });
}
catch
{
// 默认浏览器打开失败静默处理
}
e.Handled = true;
};
return true;
}
}
catch (UriFormatException)
{
// URI格式异常返回false
}
catch (Exception)
{
// 其他异常返回false
}
return false;
}
/// <summary>
/// 尝试创建带自定义文本的超链接
/// </summary>
/// <param name="urlText">URL文本</param>
/// <param name="displayText">显示的文本</param>
/// <param name="hyperlink">创建的超链接对象</param>
/// <returns>是否成功创建超链接</returns>
private static bool TryCreateHyperlinkWithText(string urlText, string displayText, out Hyperlink? hyperlink)
{
hyperlink = null;
try
{
// 验证URI是否有效
if (Uri.TryCreate(urlText, UriKind.Absolute, out Uri? uri) &&
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
{
hyperlink = new Hyperlink(new Run(displayText))
{
NavigateUri = uri,
Foreground = Brushes.DodgerBlue,
TextDecorations = TextDecorations.Underline,
ToolTip = urlText
};
// 添加点击事件处理
hyperlink.RequestNavigate += (sender, e) =>
{
try
{
Process.Start(new ProcessStartInfo(e.Uri.ToString()) { UseShellExecute = true });
}
catch
{
// 默认浏览器打开失败静默处理
}
e.Handled = true;
};
return true;
}
}
catch (UriFormatException)
{
// URI格式异常返回false
}
catch (Exception)
{
// 其他异常返回false
}
return false;
}
}