Commit 9af54c51 authored by 段廷瑞's avatar 段廷瑞

pref: markdown渲染优化

parent 3b9cf83e
...@@ -15,7 +15,8 @@ namespace Tools.MyAgent ...@@ -15,7 +15,8 @@ namespace Tools.MyAgent
@"{\rtf1\ansi\deff0" + @"{\rtf1\ansi\deff0" +
@"{\fonttbl" + @"{\fonttbl" +
@"{\f0\fnil\fcharset134 Microsoft YaHei;}" + @"{\f0\fnil\fcharset134 Microsoft YaHei;}" +
@"{\f1\fnil\fcharset134 Courier New;}}" + @"{\f1\fnil\fcharset134 Courier New;}" +
@"{\f2\fnil\fcharset0 Segoe UI Emoji;}}" + // emoji 回退字体(D1/D3:微软雅黑无 emoji 字形)
@"{\colortbl" + @"{\colortbl" +
@";\red0\green120\blue212" + // 1: user blue @";\red0\green120\blue212" + // 1: user blue
@";\red128\green128\blue128" + // 2: timestamp gray @";\red128\green128\blue128" + // 2: timestamp gray
...@@ -162,6 +163,7 @@ namespace Tools.MyAgent ...@@ -162,6 +163,7 @@ namespace Tools.MyAgent
var lines = markdown.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); var lines = markdown.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
bool inCodeBlock = false; bool inCodeBlock = false;
bool codeHasCjk = false; // 代码块是否含 CJK(在收集阶段检测原始文本;转义后文本无法检测)(D5)
var codeBuf = new StringBuilder(); var codeBuf = new StringBuilder();
for (int i = 0; i < lines.Length; i++) for (int i = 0; i < lines.Length; i++)
...@@ -211,8 +213,9 @@ namespace Tools.MyAgent ...@@ -211,8 +213,9 @@ namespace Tools.MyAgent
else else
{ {
inCodeBlock = false; inCodeBlock = false;
RenderCodeBlock(sb, codeBuf.ToString()); RenderCodeBlock(sb, codeBuf.ToString(), codeHasCjk);
codeBuf.Clear(); codeBuf.Clear();
codeHasCjk = false;
} }
continue; continue;
} }
...@@ -221,6 +224,8 @@ namespace Tools.MyAgent ...@@ -221,6 +224,8 @@ namespace Tools.MyAgent
{ {
if (codeBuf.Length > 0) if (codeBuf.Length > 0)
codeBuf.Append(@"\line "); codeBuf.Append(@"\line ");
if (ContainsCjk(line))
codeHasCjk = true; // 原始行检测(转义后为 \uXXXX? 序列,无法用正则命中 CJK)
codeBuf.Append(Escape(line)); codeBuf.Append(Escape(line));
continue; continue;
} }
...@@ -293,7 +298,7 @@ namespace Tools.MyAgent ...@@ -293,7 +298,7 @@ namespace Tools.MyAgent
if (inCodeBlock) if (inCodeBlock)
{ {
RenderCodeBlock(sb, codeBuf.ToString()); RenderCodeBlock(sb, codeBuf.ToString(), codeHasCjk);
} }
} }
...@@ -338,13 +343,16 @@ namespace Tools.MyAgent ...@@ -338,13 +343,16 @@ namespace Tools.MyAgent
} }
} }
private static void RenderCodeBlock(StringBuilder sb, string code) private static void RenderCodeBlock(StringBuilder sb, string code, bool hasCjk)
{ {
string trimmed = code.Trim(); string trimmed = code.Trim();
// 代码字体:含 CJK 时切回微软雅黑(Courier New 无 CJK 字形,依赖系统字体链接不可靠)(D5)
// 注意:code 为已转义文本(\uXXXX?),CJK 判定必须由调用方在收集阶段基于原始行完成
string codeFont = hasCjk ? @"\f0" : @"\f1";
if (!trimmed.Contains("\n") && !trimmed.Contains("\r") && IsLikelyCommand(trimmed)) if (!trimmed.Contains("\n") && !trimmed.Contains("\r") && IsLikelyCommand(trimmed))
{ {
sb.Append(@"\pard\highlight"); sb.Append(ClCodeBg); sb.Append(@"\pard\highlight"); sb.Append(ClCodeBg);
sb.Append(@"\f1\fs18 "); sb.Append(codeFont); sb.Append(@"\fs18 ");
sb.Append(BuildCmdHyperlink(trimmed)); sb.Append(BuildCmdHyperlink(trimmed));
sb.Append(@"\highlight0\f0\fs20\par "); sb.Append(@"\highlight0\f0\fs20\par ");
return; return;
...@@ -352,7 +360,7 @@ namespace Tools.MyAgent ...@@ -352,7 +360,7 @@ namespace Tools.MyAgent
sb.Append(@"\pard\highlight"); sb.Append(ClCodeBg); sb.Append(@"\pard\highlight"); sb.Append(ClCodeBg);
sb.Append(@"\cf"); sb.Append(ClCodeFg); sb.Append(@"\cf"); sb.Append(ClCodeFg);
sb.Append(@"\f1\fs18 "); sb.Append(codeFont); sb.Append(@"\fs18 ");
sb.Append(code); sb.Append(code);
sb.Append(@"\highlight0\cf"); sb.Append(ClText); sb.Append(@"\highlight0\cf"); sb.Append(ClText);
sb.Append(@"\f0\fs20\par "); sb.Append(@"\f0\fs20\par ");
...@@ -412,16 +420,18 @@ namespace Tools.MyAgent ...@@ -412,16 +420,18 @@ namespace Tools.MyAgent
if (end > i) if (end > i)
{ {
string code = text.Substring(i + 1, end - i - 1); string code = text.Substring(i + 1, end - i - 1);
// 含 CJK 时切微软雅黑(Courier New 无 CJK 字形)(D5)
string codeFont = ContainsCjk(code) ? @"\f0" : @"\f1";
if (IsLikelyCommand(code)) if (IsLikelyCommand(code))
{ {
sb.Append(@"{\f1\fs18\highlight"); sb.Append(ClCodeBg); sb.Append("{"); sb.Append(codeFont); sb.Append(@"\fs18\highlight"); sb.Append(ClCodeBg);
sb.Append(" "); sb.Append(" ");
sb.Append(BuildCmdHyperlink(code)); sb.Append(BuildCmdHyperlink(code));
sb.Append(@"\highlight0\f0\fs20}"); sb.Append(@"\highlight0\f0\fs20}");
} }
else else
{ {
sb.Append(@"{\f1\fs18\highlight"); sb.Append(ClCodeBg); sb.Append("{"); sb.Append(codeFont); sb.Append(@"\fs18\highlight"); sb.Append(ClCodeBg);
sb.Append(@"\cf"); sb.Append(ClCodeFg); sb.Append(@"\cf"); sb.Append(ClCodeFg);
sb.Append(" "); sb.Append(" ");
sb.Append(Escape(code)); sb.Append(Escape(code));
...@@ -509,6 +519,14 @@ namespace Tools.MyAgent ...@@ -509,6 +519,14 @@ namespace Tools.MyAgent
continue; continue;
} }
// 非 BMP 代理对(4 字节 emoji):切 Segoe UI Emoji 字体以 \uN 双转义组合渲染(D1)
if (char.IsHighSurrogate(text[i]) && i + 1 < text.Length && char.IsLowSurrogate(text[i + 1]))
{
AppendSurrogatePair(sb, text[i], text[i + 1]);
i++;
continue;
}
sb.Append(EscapeChar(text[i])); sb.Append(EscapeChar(text[i]));
i++; i++;
} }
...@@ -524,11 +542,36 @@ namespace Tools.MyAgent ...@@ -524,11 +542,36 @@ namespace Tools.MyAgent
{ {
if (string.IsNullOrEmpty(text)) return ""; if (string.IsNullOrEmpty(text)) return "";
var sb = new StringBuilder(text.Length); var sb = new StringBuilder(text.Length);
foreach (char c in text) AppendEscaped(sb, text);
sb.Append(EscapeChar(c));
return sb.ToString(); return sb.ToString();
} }
/// <summary>
/// 逐字符转义;非 BMP 代理对(4 字节 emoji)切 Segoe UI Emoji 字体并以 \uN 双转义组合渲染(D1),
/// 避免被丢弃导致内容缺失。
/// </summary>
private static void AppendEscaped(StringBuilder sb, string text)
{
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
if (char.IsHighSurrogate(c) && i + 1 < text.Length && char.IsLowSurrogate(text[i + 1]))
{
AppendSurrogatePair(sb, c, text[i + 1]);
i++;
continue;
}
sb.Append(EscapeChar(c));
}
}
/// <summary>代理对(4 字节 emoji):切 Segoe UI Emoji 字体以 \uN 双转义组合渲染(D1)</summary>
private static void AppendSurrogatePair(StringBuilder sb, char high, char low)
{
sb.Append(@"{\f2 \u").Append(unchecked((short)high)).Append('?')
.Append(@"\u").Append(unchecked((short)low)).Append("?}");
}
private static string EscapeChar(char c) private static string EscapeChar(char c)
{ {
switch (c) switch (c)
...@@ -540,15 +583,60 @@ namespace Tools.MyAgent ...@@ -540,15 +583,60 @@ namespace Tools.MyAgent
case '\n': return @"\line "; case '\n': return @"\line ";
case '\t': return @"\tab "; case '\t': return @"\tab ";
default: default:
// 代理对(emoji 等非 BMP 字符):RTF \uN 无法表达,直接丢弃避免渲染为 "??" // C0 控制字符(除已处理)与 DEL:无展示价值,直接丢弃,避免原始控制字节破坏 RTF 解析(D2)
if (char.IsSurrogate(c)) if (c < 0x20 || c == 0x7F) return "";
return ""; // 孤立代理(数据损坏):丢弃,避免渲染残缺(D1 兜底)
if (char.IsSurrogate(c)) return "";
// 零宽/不可见格式字符(D4):丢弃,避免在无字形字体下显示为缺字形框
if (IsInvisibleFormatChar(c)) return "";
// 非 ASCII:\uN 转义(头部已声明 \uc1,后跟 1 个回退字符), // 非 ASCII:\uN 转义(头部已声明 \uc1,后跟 1 个回退字符),
// 避免字符不在系统 ANSI 代码页中而被 RichTextBox 渲染为 '?' // 避免字符不在系统 ANSI 代码页中而被 RichTextBox 渲染为 '?'
if (c >= 128) if (c >= 128)
{
// emoji/符号区段(D3):微软雅黑缺字形 → 切 Segoe UI Emoji 渲染,避免显示 □
if (IsEmojiSymbol(c))
return $@"{{\f2 \u{unchecked((short)c)}?}}";
return $@"\u{unchecked((short)c)}?"; return $@"\u{unchecked((short)c)}?";
}
return c.ToString(); return c.ToString();
} }
} }
/// <summary>零宽/不可见格式字符(D4):无展示价值且多数字体缺字形,丢弃避免显示 □</summary>
private static bool IsInvisibleFormatChar(char c)
{
switch (c)
{
case '\u00AD': // 软连字符
case '\u200B': // 零宽空格
case '\u200C': // 零宽非连接符
case '\u200D': // 零宽连接符 ZWJ(组合 emoji 拆散后残留,单独无展示价值)
case '\u200E': // 从左到右标记
case '\u200F': // 从右到左标记
case '\u2060': // 字词连接符
case '\uFE0F': // 变体选择符 VS16(emoji 已切 \f2 字体呈现,无需修饰符;丢弃避免残留字符)
case '\uFEFF': // BOM / 零宽不换行空格
return true;
default:
return false;
}
}
/// <summary>微软雅黑缺字形的 BMP emoji/符号区段(D3):切 Segoe UI Emoji 渲染(U+FE0F 变体选择符在 IsInvisibleFormatChar 中直接丢弃)</summary>
private static bool IsEmojiSymbol(char c)
{
return (c >= 0x2600 && c <= 0x27BF) // 杂项符号 + 装饰符号(含 ⚠ ✅ ⭐ 等)
|| (c >= 0x2B00 && c <= 0x2BFF); // 杂项符号和箭头(含 ⬆ ⬇)
}
/// <summary>代码内容是否含 CJK(D5):Courier New 无 CJK 字形,含 CJK 的代码块/行内代码切回微软雅黑</summary>
private static readonly Regex CjkPattern = new Regex(
@"[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\u3000-\u303F\uFF00-\uFFEF]",
RegexOptions.Compiled);
private static bool ContainsCjk(string text)
{
return !string.IsNullOrEmpty(text) && CjkPattern.IsMatch(text);
}
} }
} }
...@@ -13,9 +13,9 @@ namespace Tools.MyAgent ...@@ -13,9 +13,9 @@ namespace Tools.MyAgent
@"\b([A-Za-z]:[\\/](?:[^\s\\/:*?""<>|]+[\\/])*[^\s\\/:*?""<>|]+\.[a-zA-Z0-9]{1,10})\b", @"\b([A-Za-z]:[\\/](?:[^\s\\/:*?""<>|]+[\\/])*[^\s\\/:*?""<>|]+\.[a-zA-Z0-9]{1,10})\b",
RegexOptions.Compiled); RegexOptions.Compiled);
/// <summary>UNC 网络路径 \\server\share\...(到空白/引号/常见句读标点为止)</summary> /// <summary>UNC 网络路径 \\server\share\...(到空白/引号/常见句读标点为止;末尾英文句号视为句读,用负向后瞻排除——文件名中的句号不受影响,如 file.md)</summary>
public static readonly Regex UncFilePath = new Regex( public static readonly Regex UncFilePath = new Regex(
@"\\\\[^\s<\"",。;:、!?))】》>]+", @"\\\\[^\s<\"",。;:、!?))】》>]+(?<![.])",
RegexOptions.Compiled); RegexOptions.Compiled);
/// <summary>http/https 链接</summary> /// <summary>http/https 链接</summary>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment