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方法)

  1. 按行处理文本:逐行读取文本内容
  2. 代码块识别:识别Markdown中的代码围栏(```或~~~)
  3. 安全区域保护:避免在特定区域进行高亮,包括:
    • 代码块内容
    • Markdown链接的URL部分
    • 行内代码(用反引号包裹的内容)

代码围栏状态管理

private sealed class CodeFenceState
{
    public bool IsInsideFence { get; set; }     // 是否在代码块内
    public char FenceChar { get; set; }         // 围栏字符(` 或 ~)
    public int FenceLength { get; set; }        // 围栏长度
}

关键特性

  1. 智能高亮

    • 只在安全的文本区域进行高亮
    • 避免破坏Markdown语法结构
  2. 代码块处理

    • 正确识别代码围栏的开始和结束
    • 支持不同长度的围栏(至少3个字符)
    • 支持两种围栏字符(` 和 ~)
  3. 行内代码保护

    • 识别用反引号包裹的行内代码
    • 支持多个连续反引号的情况
  4. 链接保护

    • 使用正则表达式识别Markdown链接
    • 只保护链接URL部分,不影响链接文本

使用场景

这个类主要用于:

  • 全文搜索功能的结果展示
  • 在搜索结果中突出显示匹配的关键词
  • 保持Markdown格式的完整性
  • 提供良好的用户搜索体验

高亮标记使用HTML的<mark>标签包裹匹配的文本,最终在前端显示时会以醒目的样式突出显示搜索关键词。

评论加载中...