using Dpz.Core.Public.ViewModel.Response;
namespace Dpz.Core.Service.Mediator.Features.Article.Contracts;
/// <summary>
/// 文章搜索出参
/// </summary>
public class ArticleResponseSearchResultResponse : ArticleResponse
{
/// <summary>
/// 标题搜索结果
/// </summary>
public List<SearchResult> TitleSearchResult { get; set; } = [];
/// <summary>
/// 内容搜索结果 --> Markdown
/// </summary>
public List<SearchResult> ContentSearchResult { get; set; } = [];
/// <summary>
/// 生成高亮后的标题。
/// </summary>
public string HighlightTitle()
{
if (TitleSearchResult.Count == 0)
{
return Title;
}
return HighlightText(TitleSearchResult, Title);
}
/// <summary>
/// 生成高亮后的正文内容。
/// </summary>
public string HighlightContent(bool readAll = false)
{
if (ContentSearchResult.Count == 0)
{
return "";
}
return HighlightText(ContentSearchResult, Markdown, readAll);
}
private static string HighlightText(
List<SearchResult> searchResults,
string text,
bool readAll = false
)
{
var highlightedText = new StringBuilder();
using var reader = new StringReader(text);
var lineNumber = 1;
// 维护 fenced code block (``` / ~~~) 的跨行状态。
var codeFenceState = new CodeFenceState();
var groupedResults = searchResults
.GroupBy(x => x.LineNumber)
.ToDictionary(x => x.Key, x => x.ToList());
while (reader.ReadLine() is { } line)
{
// 先更新当前行是否触发代码围栏开闭,再决定本行是否属于代码块。
var isCodeFenceDelimiter = TryUpdateCodeFenceState(line, codeFenceState);
var isInsideCodeFence = codeFenceState.IsInsideFence || isCodeFenceDelimiter;
if (groupedResults.TryGetValue(lineNumber, out var matchesInLine))
{
// 记录不允许插入 <mark> 的区间,避免破坏 Markdown 结构。
var unsafeRanges = new List<(int Start, int End)>();
if (isInsideCodeFence)
{
// 代码块整行禁用高亮。
unsafeRanges.Add((0, line.Length));
}
var linkMatches = System.Text.RegularExpressions.Regex.Matches(
line,
@"!?\[.*?\]\((.*?)\)"
);
foreach (System.Text.RegularExpressions.Match m in linkMatches)
{
if (m.Groups.Count > 1)
{
// 仅保护链接目标 URL 区域,避免将链接语法打断。
var g = m.Groups[1];
unsafeRanges.Add((g.Index, g.Index + g.Length));
}
}
// 行内代码 (`...`) 区域同样不执行高亮。
unsafeRanges.AddRange(GetInlineCodeRanges(line));
var sortedMatches = matchesInLine.OrderBy(x => x.StartIndex).ToList();
var highlightedLine = line;
var offset = 0;
foreach (var result in sortedMatches)
{
if (
unsafeRanges.Any(r =>
result.StartIndex >= r.Start && result.EndIndex < r.End
)
)
{
continue;
}
var adjustedStartIndex = result.StartIndex + offset;
var adjustedEndIndex = result.EndIndex + offset;
var beforeMatch = highlightedLine[..adjustedStartIndex];
var match = highlightedLine.Substring(
adjustedStartIndex,
adjustedEndIndex - adjustedStartIndex + 1
);
var afterMatch = highlightedLine[(adjustedEndIndex + 1)..];
highlightedLine = $"{beforeMatch}{HighlightTag(match)}{afterMatch}";
// 每插入一对 <mark></mark> 都会改变后续索引,需累计偏移。
offset += HighlightTag(null).Length;
}
highlightedText.AppendLine(highlightedLine);
}
else if (readAll)
{
highlightedText.AppendLine(line);
}
lineNumber++;
}
return highlightedText.ToString();
string HighlightTag(string? match)
{
return $"<mark>{match}</mark>";
}
}
private sealed class CodeFenceState
{
/// <summary>
/// 是否已进入 fenced code block。
/// </summary>
public bool IsInsideFence { get; set; }
/// <summary>
/// 围栏字符:` 或 ~。
/// </summary>
public char FenceChar { get; set; }
/// <summary>
/// 围栏长度,结束时要求同字符且数量不少于起始长度。
/// </summary>
public int FenceLength { get; set; }
}
private static bool TryUpdateCodeFenceState(string line, CodeFenceState state)
{
// 仅识别行首(忽略缩进)围栏,避免误判正文中的反引号。
var trimmed = line.TrimStart();
if (trimmed.Length < 3)
{
return false;
}
var firstChar = trimmed[0];
if (firstChar != '`' && firstChar != '~')
{
return false;
}
var length = 0;
while (length < trimmed.Length && trimmed[length] == firstChar)
{
length++;
}
if (length < 3)
{
return false;
}
if (!state.IsInsideFence)
{
// 命中起始围栏,进入代码块模式。
state.IsInsideFence = true;
state.FenceChar = firstChar;
state.FenceLength = length;
return true;
}
if (state.FenceChar == firstChar && length >= state.FenceLength)
{
// 命中匹配的结束围栏,退出代码块模式。
state.IsInsideFence = false;
state.FenceChar = default;
state.FenceLength = 0;
return true;
}
return false;
}
private static IEnumerable<(int Start, int End)> GetInlineCodeRanges(string line)
{
// 支持多个连续反引号包裹的行内代码(如 ``code `x` ``)。
var i = 0;
while (i < line.Length)
{
if (line[i] != '`')
{
i++;
continue;
}
var start = i;
var tickCount = 0;
while (i < line.Length && line[i] == '`')
{
tickCount++;
i++;
}
var contentStart = i;
var matched = false;
while (i < line.Length)
{
if (line[i] != '`')
{
i++;
continue;
}
var closeTickCount = 0;
var closeStart = i;
while (i < line.Length && line[i] == '`')
{
closeTickCount++;
i++;
}
if (closeTickCount == tickCount)
{
var endExclusive = closeStart + closeTickCount;
// 与现有逻辑保持一致:使用 [start, endExclusive) 语义。
yield return (start, endExclusive);
matched = true;
break;
}
}
if (!matched)
{
i = contentStart;
}
}
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
这是一个用于文章搜索结果的响应类,继承自ArticleResponse,主要功能是在搜索结果中对匹配的文本进行高亮显示。让我详细解释一下这个代码:
类的结构
基本属性
public class ArticleResponseSearchResultResponse : ArticleResponse
{
public List<SearchResult> TitleSearchResult { get; set; } = [];
public List<SearchResult> ContentSearchResult { get; set; } = [];
}
- 继承自
ArticleResponse基类 - 包含标题和内容的搜索结果列表
核心功能方法
1. 高亮标题
public string HighlightTitle()
- 如果有标题搜索结果,就对标题进行高亮处理
- 如果没有搜索结果,直接返回原标题
2. 高亮内容
public string HighlightContent(bool readAll = false)
- 对内容搜索结果进行高亮处理
readAll参数决定是否显示所有内容(未匹配的行也显示)
核心算法:文本高亮处理
主要逻辑(HighlightText方法)
- 按行处理文本:逐行读取文本内容
- 代码块识别:识别Markdown中的代码围栏(```或~~~)
- 安全区域保护:避免在特定区域进行高亮,包括:
- 代码块内容
- Markdown链接的URL部分
- 行内代码(用反引号包裹的内容)
代码围栏状态管理
private sealed class CodeFenceState
{
public bool IsInsideFence { get; set; } // 是否在代码块内
public char FenceChar { get; set; } // 围栏字符(` 或 ~)
public int FenceLength { get; set; } // 围栏长度
}
关键特性
智能高亮:
- 只在安全的文本区域进行高亮
- 避免破坏Markdown语法结构
代码块处理:
- 正确识别代码围栏的开始和结束
- 支持不同长度的围栏(至少3个字符)
- 支持两种围栏字符(` 和 ~)
行内代码保护:
- 识别用反引号包裹的行内代码
- 支持多个连续反引号的情况
链接保护:
- 使用正则表达式识别Markdown链接
- 只保护链接URL部分,不影响链接文本
使用场景
这个类主要用于:
- 全文搜索功能的结果展示
- 在搜索结果中突出显示匹配的关键词
- 保持Markdown格式的完整性
- 提供良好的用户搜索体验
高亮标记使用HTML的<mark>标签包裹匹配的文本,最终在前端显示时会以醒目的样式突出显示搜索关键词。
评论加载中...