using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
using Yarn.Markup;
namespace SLSUtilities.Narrative.UI
{
///
/// 关键词文本处理器。
/// 扫描台词文本,将已注册的关键词包裹为 TMP 标签,
/// 使其可被悬停检测和高亮显示。
///
public static class KeywordProcessor
{
///
/// 关键词的 link ID 前缀,用于与其他 link 类型区分。
/// 例如:
///
public const string LinkPrefix = "kw:";
// 已编译的关键词匹配正则(所有触发词按长度降序排列)
private static Regex _keywordPattern;
// 触发词 → KeywordData 的映射表(大小写不敏感)
private static Dictionary _triggerLookup;
// 主关键词 → KeywordData 的映射表(用于通过 link ID 反查)
private static Dictionary _primaryLookup;
// 用于识别 RichText 标签的正则,避免在标签内部匹配关键词
private static readonly Regex RichTextTagPattern =
new Regex(@"<[^>]+>", RegexOptions.Compiled);
///
/// 从数据库构建关键词缓存。
/// 应在对话开始时调用一次,或在关键词列表变更后重新调用。
///
public static void BuildCache(List keywords)
{
_triggerLookup = new Dictionary(System.StringComparer.OrdinalIgnoreCase);
_primaryLookup = new Dictionary(System.StringComparer.OrdinalIgnoreCase);
if (keywords == null || keywords.Count == 0)
{
_keywordPattern = null;
return;
}
var allTriggers = new List();
foreach (var kw in keywords)
{
if (kw == null || string.IsNullOrWhiteSpace(kw.keyword)) continue;
// 注册主关键词到 primaryLookup
if (!_primaryLookup.ContainsKey(kw.keyword))
_primaryLookup[kw.keyword] = kw;
// 注册所有触发词(主关键词 + 别名)到 triggerLookup
foreach (var trigger in kw.GetAllTriggerWords())
{
if (!string.IsNullOrWhiteSpace(trigger) && !_triggerLookup.ContainsKey(trigger))
{
_triggerLookup[trigger] = kw;
allTriggers.Add(trigger);
}
}
}
if (allTriggers.Count == 0)
{
_keywordPattern = null;
return;
}
// 按长度降序排列,确保"灵能者协会"优先于"灵能者"匹配
allTriggers.Sort((a, b) => b.Length.CompareTo(a.Length));
// 构建正则:将所有触发词用 | 连接
var escapedTriggers = allTriggers.Select(Regex.Escape);
string pattern = string.Join("|", escapedTriggers);
_keywordPattern = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
}
///
/// 处理台词文本:扫描并将匹配的关键词包裹为带高亮样式的 TMP link 标签。
/// 会自动跳过已有的 RichText 标签内部,避免破坏现有排版。
///
/// 原始台词文本(可能已包含 RichText 标签)
/// 关键词高亮颜色(十六进制,如 "#AADDFF")
/// 处理后的文本
public static string Process(string rawText, string highlightColor = "#AADDFF")
{
if (_keywordPattern == null || string.IsNullOrEmpty(rawText))
return rawText;
// 将文本按"RichText 标签"和"纯文本"交替切分,
// 只对纯文本段进行关键词替换,标签段原样保留。
var result = new StringBuilder(rawText.Length * 2);
int lastIndex = 0;
foreach (Match tagMatch in RichTextTagPattern.Matches(rawText))
{
// 处理标签前的纯文本段
if (tagMatch.Index > lastIndex)
{
string segment = rawText.Substring(lastIndex, tagMatch.Index - lastIndex);
result.Append(ReplaceKeywordsInSegment(segment, highlightColor));
}
// 标签本身原样追加
result.Append(tagMatch.Value);
lastIndex = tagMatch.Index + tagMatch.Length;
}
// 处理最后一个标签之后的剩余纯文本
if (lastIndex < rawText.Length)
{
string remaining = rawText.Substring(lastIndex);
result.Append(ReplaceKeywordsInSegment(remaining, highlightColor));
}
return result.ToString();
}
///
/// 基于 Yarn Markup 属性处理台词/选项文本:
/// 将 [kw] 和 [kw id="主关键词"] 标签转换为 TMP link/style 标签。
///
/// Yarn 标签语法:
/// [kw]迎雾森林[/kw] —— 关键词 = 标签内的文本
/// [kw id="迎雾森林"]林地[/kw] —— 关键词 = “迎雾森林”,显示文本 = “林地”
///
/// 来自 Yarn 的 MarkupParseResult(LocalizedLine.TextWithoutCharacterName 等)
/// 包含 TMP 忌工标签的处理后字符串
public static string ProcessWithMarkup(MarkupParseResult markup)
{
string plainText = markup.Text;
if (string.IsNullOrEmpty(plainText)) return plainText;
// 收集所有 "kw" 属性
var kwAttributes = new List();
foreach (var attr in markup.Attributes)
{
if (attr.Name == "kw")
kwAttributes.Add(attr);
}
// 没有关键词标签,直接返回原始文本
if (kwAttributes.Count == 0) return plainText;
// 按位置降序排列,从字符串末尾开始插入,避免早期插入造成索引偏移
kwAttributes.Sort((a, b) => b.Position.CompareTo(a.Position));
var sb = new StringBuilder(plainText);
foreach (var attr in kwAttributes)
{
// 确定关键词:优先使用 id 属性,否则取标签覆盖的文本
string keyword;
if (attr.Properties.TryGetValue("id", out var idProp))
{
keyword = idProp.StringValue;
}
else
{
keyword = plainText.Substring(attr.Position, attr.Length);
}
// 在数据库中查找该关键词,找不到则跳过(不插入任何标签)
if (FindByPrimaryKeyword(keyword) == null) continue;
// 先插入闭合标签(索引较大),再插入开放标签
sb.Insert(attr.Position + attr.Length, "");
sb.Insert(attr.Position, $"";
});
}
}
}