Files
MaliangAINovalWriter/AINoval/lib/utils/setting_reference_processor.dart
2025-09-10 00:07:52 +08:00

811 lines
27 KiB
Dart
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.

import 'dart:async';
import 'dart:convert';
import 'dart:isolate';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:ainoval/models/novel_setting_item.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:flutter/scheduler.dart';
/// AC自动机节点
class _ACNode {
Map<String, _ACNode> children = {};
_ACNode? failure;
List<String> outputs = [];
void addOutput(String settingId) {
outputs.add(settingId);
}
}
/// Aho-Corasick 自动机
class _AhoCorasick {
final _ACNode root = _ACNode();
void build(Map<String, String> patterns) {
// 构建 Trie
patterns.forEach((name, settingId) {
_ACNode current = root;
for (int i = 0; i < name.length; i++) {
final char = name[i];
current.children[char] ??= _ACNode();
current = current.children[char]!;
}
current.addOutput(settingId);
});
// 构建失败函数
_buildFailure();
}
void _buildFailure() {
final queue = <_ACNode>[];
// 第一层节点的失败函数指向根节点
root.children.values.forEach((node) {
node.failure = root;
queue.add(node);
});
while (queue.isNotEmpty) {
final current = queue.removeAt(0);
current.children.forEach((char, child) {
queue.add(child);
_ACNode? temp = current.failure;
while (temp != null && !temp.children.containsKey(char)) {
temp = temp.failure;
}
child.failure = temp?.children[char] ?? root;
child.outputs.addAll(child.failure!.outputs);
});
}
}
List<SettingMatch> search(String text, Map<String, String> idToName) {
final matches = <SettingMatch>[];
_ACNode current = root;
for (int i = 0; i < text.length; i++) {
final char = text[i];
while (current != root && !current.children.containsKey(char)) {
current = current.failure!;
}
if (current.children.containsKey(char)) {
current = current.children[char]!;
}
for (final settingId in current.outputs) {
final name = idToName[settingId]!;
final start = i - name.length + 1;
matches.add(SettingMatch(
text: name,
start: start,
end: i + 1,
settingId: settingId,
settingName: name,
));
}
}
return matches;
}
}
/// 设定引用处理器缓存
class _ProcessorCache {
int textHash = 0;
String lastProcessedText = '';
List<SettingMatch> lastMatches = [];
int settingVersion = 0;
_AhoCorasick? automaton;
void updateHash(String text) {
textHash = text.hashCode;
lastProcessedText = text;
}
}
/// 设定引用匹配结果
class SettingMatch {
final String text; // 匹配的文本
final int start; // 开始位置
final int end; // 结束位置
final String settingId; // 设定ID
final String settingName; // 设定名称
SettingMatch({
required this.text,
required this.start,
required this.end,
required this.settingId,
required this.settingName,
});
@override
String toString() => 'SettingMatch(text: "$text", pos: $start-$end, id: $settingId)';
}
/// 设定引用处理器 - Flutter Quill原生实现
/// 使用Flutter Quill的Attribute系统来实现设定引用高亮
class SettingReferenceProcessor {
static const String _tag = 'SettingReferenceProcessor';
/// 设定引用的自定义属性名存储设定ID
static const String settingReferenceAttr = 'setting-reference';
/// 设定引用样式属性名用于CSS选择器识别
static const String settingStyleAttr = 'setting-style';
// 🚀 三层架构:全局缓存映射
static final Map<String, _ProcessorCache> _cacheMap = {};
static int _globalSettingVersion = 0;
/// 更新全局设定版本(当设定发生变化时调用)
static void updateSettingVersion() {
_globalSettingVersion++;
// 清空所有缓存的自动机,强制重建
_cacheMap.values.forEach((cache) {
cache.automaton = null;
cache.settingVersion = 0;
});
}
/// 【第二层扫描层】使用AC自动机进行高效匹配
static List<SettingMatch> _scanForMatches(
String sceneId,
String text,
List<NovelSettingItem> settings,
) {
final cache = _cacheMap[sceneId]!;
// 检查是否需要重建自动机
if (cache.automaton == null || cache.settingVersion != _globalSettingVersion) {
final patterns = <String, String>{};
final idToName = <String, String>{};
for (final setting in settings) {
final name = setting.name;
final id = setting.id;
if (name != null && name.trim().isNotEmpty && id != null && id.isNotEmpty) {
patterns[name] = id;
idToName[id] = name;
}
}
cache.automaton = _AhoCorasick();
cache.automaton!.build(patterns);
cache.settingVersion = _globalSettingVersion;
AppLogger.d(_tag, '重建AC自动机设定数量: ${patterns.length}');
}
// 使用自动机搜索
final idToName = <String, String>{};
for (final setting in settings) {
final name = setting.name;
final id = setting.id;
if (name != null && id != null) {
idToName[id] = name;
}
}
return cache.automaton!.search(text, idToName);
}
/// 【第三层:修改层】异步应用样式
static Future<void> _applyStylesAsync(
QuillController controller,
List<SettingMatch> matches,
) async {
if (matches.isEmpty) return;
SchedulerBinding.instance.addPostFrameCallback((_) {
try {
final originalSelection = controller.selection;
for (final match in matches.reversed) {
final refAttr = Attribute(settingReferenceAttr, AttributeScope.inline, match.settingId);
final styleAttr = Attribute(settingStyleAttr, AttributeScope.inline, 'reference');
controller.formatText(match.start, match.text.length, refAttr);
controller.formatText(match.start, match.text.length, styleAttr);
}
controller.updateSelection(originalSelection, ChangeSource.silent);
} catch (e) {
AppLogger.e(_tag, '样式应用失败', e);
}
});
}
/// 悬停状态管理
static String? _currentHoveredSettingId;
static QuillController? _currentHoveringController;
static int? _hoveredTextStart;
static int? _hoveredTextLength;
/// 🎯 主要方法:处理文档中的设定引用
/// 使用Flutter Quill原生Attribute系统添加样式
static void processSettingReferences({
required Document document,
required List<NovelSettingItem> settingItems,
required QuillController controller,
}) {
try {
// 🚀 第一层:检测层 - 快速检测是否需要处理
final currentText = document.toPlainText();
final textHash = currentText.hashCode;
// 使用文档hashCode作为临时sceneId
final sceneId = 'doc_${document.hashCode}';
final cache = _cacheMap.putIfAbsent(sceneId, () => _ProcessorCache());
if (textHash == cache.textHash) {
// 文本无变化,跳过处理
return;
}
AppLogger.i(_tag, '🎯 开始三层架构设定引用处理');
if (settingItems.isEmpty) {
//AppLogger.d(_tag, '无设定条目,跳过处理');
return;
}
// 🚀 第二层:扫描层 - 使用AC自动机进行高效匹配
final matches = _scanForMatches(sceneId, currentText, settingItems);
// 更新缓存
cache.updateHash(currentText);
cache.lastMatches = matches;
AppLogger.i(_tag, '🎉 找到 ${matches.length} 个设定引用匹配');
if (matches.isEmpty) {
//AppLogger.d(_tag, '未找到设定引用,跳过样式应用');
return;
}
// 🚀 第三层:修改层 - 异步应用样式
_applyStylesAsync(controller, matches);
AppLogger.i(_tag, '✅ 设定引用处理完成');
} catch (e) {
AppLogger.e(_tag, '设定引用处理失败', e);
}
}
/// 🔍 查找设定匹配项
static List<SettingMatch> findSettingMatches(String text, List<NovelSettingItem> settingItems) {
final matches = <SettingMatch>[];
try {
//AppLogger.d(_tag, '🔍 开始查找设定匹配,设定数量: ${settingItems.length}');
if (text.isEmpty || settingItems.isEmpty) {
return matches;
}
// 创建设定名称到ID的映射
final settingNameToId = <String, String>{};
for (final item in settingItems) {
final name = item.name;
final id = item.id;
if (name != null && name.isNotEmpty && id != null && id.isNotEmpty) {
settingNameToId[name] = id;
}
}
// 按长度排序设定名称,避免短名称覆盖长名称
final sortedNames = settingNameToId.keys.toList()..sort((a, b) => b.length.compareTo(a.length));
//AppLogger.d(_tag, '📚 设定名称列表: ${sortedNames.join(', ')}');
// 🚀 调试:特别检查"小胖"是否在文本中
final xiaoPangInText = text.contains('小胖');
//AppLogger.d(_tag, '🔍 特别检查"小胖"是否在文本中: $xiaoPangInText');
if (xiaoPangInText) {
final positions = <int>[];
int searchStart = 0;
while (true) {
final index = text.indexOf('小胖', searchStart);
if (index == -1) break;
positions.add(index);
searchStart = index + 1;
}
//AppLogger.d(_tag, '🔍 "小胖"在文本中的位置: $positions');
}
// 查找所有匹配
for (final settingName in sortedNames) {
final settingId = settingNameToId[settingName]!; // 使用!因为我们确定key存在
// 🚀 调试:特别关注"小胖"的处理过程
if (settingName == '小胖') {
//AppLogger.d(_tag, '🎯 开始处理设定"小胖", ID: $settingId');
}
int searchStart = 0;
while (true) {
final index = text.indexOf(settingName, searchStart);
if (index == -1) break;
// 🚀 调试:记录找到的位置
if (settingName == '小胖') {
//AppLogger.d(_tag, '🎯 找到"小胖"在位置: $index');
}
// 检查是否是完整的词(可选:避免部分匹配)
final isWordBoundary = _isWordBoundary(text, index, settingName.length);
// 🚀 调试:记录边界检查结果
if (settingName == '小胖') {
//AppLogger.d(_tag, '🎯 "小胖"边界检查结果: $isWordBoundary');
}
if (isWordBoundary) {
final match = SettingMatch(
text: settingName,
start: index,
end: index + settingName.length,
settingId: settingId,
settingName: settingName,
);
// 检查是否与已有匹配重叠
if (!_hasOverlap(matches, match)) {
matches.add(match);
////AppLogger.v(_tag, '✅ 添加匹配: $match');
} else {
// 🚀 调试:记录重叠情况
if (settingName == '小胖') {
//AppLogger.d(_tag, '🎯 "小胖"匹配被跳过(与已有匹配重叠)');
}
}
}
searchStart = index + 1;
}
}
// 按位置排序
matches.sort((a, b) => a.start.compareTo(b.start));
AppLogger.i(_tag, '🎉 总共找到 ${matches.length} 个有效匹配');
for (final match in matches) {
////AppLogger.v(_tag, ' 📍 ${match.settingName} (${match.start}-${match.end})');
}
} catch (e) {
AppLogger.e(_tag, '查找设定匹配失败', e);
}
return matches;
}
/// 🎨 应用Flutter Quill样式
static void _applyFlutterQuillStyles(QuillController controller, List<SettingMatch> matches) {
if (matches.isEmpty) return;
final settingRefAttribute = Attribute.clone(
Attribute.link,
'setting_reference',
);
final settingStyleAttribute = Attribute.clone(
Attribute.color,
const Color(0xFF0066CC).value,
);
try {
// 🚀 批量应用样式,避免多次触发 document change
final originalSelection = controller.selection;
// 逆序处理,避免位置偏移
for (final match in matches.reversed) {
controller.formatText(
match.start,
match.text.length,
settingRefAttribute,
);
controller.formatText(
match.start,
match.text.length,
settingStyleAttribute,
);
}
// 恢复原始选择
controller.updateSelection(originalSelection, ChangeSource.silent);
} catch (e) {
AppLogger.e(_tag, 'Flutter Quill样式应用失败', e);
}
}
/// 检查是否是完整的词边界
static bool _isWordBoundary(String text, int start, int length) {
// 🚀 修复:改进中文字符的词边界检查
final before = start > 0 ? text[start - 1] : ' ';
final after = start + length < text.length ? text[start + length] : ' ';
final beforeIsWord = _isWordChar(before);
final afterIsWord = _isWordChar(after);
// 🚀 调试:添加详细的边界检查日志
////AppLogger.v(_tag, '🔍 词边界检查: "${text.substring(start, start + length)}" | 前:"$before"(${beforeIsWord ? "词" : "非词"}) 后:"$after"(${afterIsWord ? "词" : "非词"})');
// 🚀 修复:对于中文,采用更宽松的边界检查
// 如果前后都不是字母数字,则认为是完整的词
return !beforeIsWord && !afterIsWord;
}
/// 检查字符是否是单词字符
static bool _isWordChar(String char) {
if (char.isEmpty) return false;
final code = char.codeUnitAt(0);
// 🚀 修复:简化单词字符判断,对中文更友好
// 只有字母和数字才算单词字符,中文字符不算
return (code >= 65 && code <= 90) || // A-Z
(code >= 97 && code <= 122) || // a-z
(code >= 48 && code <= 57); // 0-9
// 移除中文字符判断,这样中文前后的字符不会影响匹配
}
/// 检查匹配是否重叠
static bool _hasOverlap(List<SettingMatch> existingMatches, SettingMatch newMatch) {
for (final existing in existingMatches) {
if ((newMatch.start < existing.end && newMatch.end > existing.start)) {
return true;
}
}
return false;
}
/// 🛡️ 清除格式传播,防止设定引用样式影响后续输入
static void _clearFormattingPropagation(QuillController controller) {
try {
//AppLogger.d(_tag, '🛡️ 清除格式传播');
// 获取当前选择
final selection = controller.selection;
// 🎯 简化格式传播清除逻辑
// 不直接操作文档内容,而是通过设置光标样式状态来防止传播
if (selection.isCollapsed) {
final currentOffset = selection.baseOffset;
// 🛡️ 只在光标位置插入一个零宽字符来重置格式状态
// 这样不会影响已经应用的设定引用样式
try {
// 保存当前选择
final originalSelection = controller.selection;
// 临时在光标位置插入零宽空格,然后立即删除
// 这可以重置光标位置的格式继承状态
final zeroWidthSpace = '\u200B'; // 零宽空格
controller.replaceText(currentOffset, 0, zeroWidthSpace, TextSelection.collapsed(offset: currentOffset + 1));
controller.replaceText(currentOffset, 1, '', TextSelection.collapsed(offset: currentOffset));
// 恢复原始选择
controller.updateSelection(originalSelection, ChangeSource.silent);
////AppLogger.v(_tag, '✅ 已重置光标位置的格式继承状态');
} catch (e) {
AppLogger.w(_tag, '重置格式继承状态失败,使用备用方案', e);
// 备用方案:简单地清除当前选择的格式状态
// 注意这里不使用formatText避免影响已有的设定引用样式
////AppLogger.v(_tag, '✅ 使用备用格式传播清除方案');
}
}
} catch (e) {
AppLogger.w(_tag, '清除格式传播失败', e);
}
}
/// 🎯 移除设定引用样式
static void removeSettingReferenceStyles(QuillController controller) {
try {
AppLogger.i(_tag, '🗑️ 移除所有设定引用样式');
final document = controller.document;
final text = document.toPlainText();
if (text.isEmpty) return;
// 移除所有设定引用相关的属性
final removeAttributes = [
Attribute(settingReferenceAttr, AttributeScope.inline, null),
Attribute(settingStyleAttr, AttributeScope.inline, null),
];
for (final attr in removeAttributes) {
controller.formatText(0, text.length, attr);
}
AppLogger.i(_tag, '✅ 设定引用样式移除完成');
} catch (e) {
AppLogger.e(_tag, '移除设定引用样式失败', e);
}
}
/// 🔄 刷新设定引用样式
static void refreshSettingReferences({
required QuillController controller,
required List<NovelSettingItem> settingItems,
}) {
try {
AppLogger.i(_tag, '🔄 刷新设定引用样式');
// 1. 先移除现有样式
removeSettingReferenceStyles(controller);
// 2. 重新应用样式
processSettingReferences(
document: controller.document,
settingItems: settingItems,
controller: controller,
);
AppLogger.i(_tag, '✅ 设定引用样式刷新完成');
} catch (e) {
AppLogger.e(_tag, '刷新设定引用样式失败', e);
}
}
/// 🛡️ 清除光标位置的设定引用格式传播(公共方法)
/// 应在用户输入时调用,防止设定引用样式影响新输入的文本
static void clearFormattingPropagationAtCursor(QuillController controller) {
_clearFormattingPropagation(controller);
}
/// 🧹 用于保存时的设定引用样式过滤(保留原功能)
static String filterSettingReferenceStylesForSave(String deltaJson, {String? caller}) {
return filterSettingReferenceStyles(deltaJson, caller: caller ?? 'filterSettingReferenceStylesForSave');
}
/// 🔄 用于编辑时的内容处理(不过滤设定引用样式)
/// 在编辑过程中,我们要保留设定引用样式以便显示
static String processContentForEditing(String deltaJson) {
// 编辑时不过滤设定引用样式,直接返回原内容
return deltaJson;
}
/// 清理场景缓存
static void clearSceneCache(String sceneId) {
_cacheMap.remove(sceneId);
}
/// 清理所有缓存
static void clearAllCache() {
_cacheMap.clear();
}
/// 🧹 过滤设定引用相关的自定义样式,保留其他样式
/// 用于保存时清理临时的设定引用样式,但保留用户的格式化样式
static String filterSettingReferenceStyles(String deltaJson, {String? caller}) {
try {
// 🎯 优化:减少频繁日志输出,仅在调试模式或特定调用者时输出
if (caller == null || caller == 'debug') {
//AppLogger.d(_tag, '🧹 开始过滤设定引用样式${caller != null ? ' - 调用者: $caller' : ''}');
}
// 解析Delta JSON
final dynamic deltaData = jsonDecode(deltaJson);
List<dynamic> ops;
if (deltaData is List) {
// 格式1: 直接是ops数组 [{"insert": "text"}, ...]
////AppLogger.v(_tag, '📋 检测到直接ops数组格式');
ops = deltaData;
} else if (deltaData is Map<String, dynamic>) {
// 格式2: 标准Delta格式 {"ops": [{"insert": "text"}, ...]}
////AppLogger.v(_tag, '📋 检测到标准Delta格式');
final dynamic opsData = deltaData['ops'];
if (opsData is! List) {
AppLogger.w(_tag, '❌ ops数据不是预期的List格式');
return deltaJson;
}
ops = opsData;
} else {
AppLogger.w(_tag, '❌ Delta数据格式不支持: ${deltaData.runtimeType}');
return deltaJson;
}
// 过滤操作列表
final List<dynamic> filteredOps = [];
for (int i = 0; i < ops.length; i++) {
final dynamic op = ops[i];
// 只处理Map类型的操作
if (op is Map<String, dynamic>) {
// 创建新的操作副本
final Map<String, dynamic> newOp = <String, dynamic>{};
// 复制所有字段
op.forEach((key, value) {
newOp[key] = value;
});
// 检查是否有attributes字段
if (newOp.containsKey('attributes') && newOp['attributes'] is Map) {
final dynamic attributesData = newOp['attributes'];
if (attributesData is Map<String, dynamic>) {
// 创建属性副本
final Map<String, dynamic> attributes = <String, dynamic>{};
attributesData.forEach((key, value) {
attributes[key] = value;
});
// 移除设定引用相关的属性
bool hasRemovedAttrs = false;
if (attributes.containsKey(settingReferenceAttr)) {
attributes.remove(settingReferenceAttr);
hasRemovedAttrs = true;
}
if (attributes.containsKey(settingStyleAttr)) {
attributes.remove(settingStyleAttr);
hasRemovedAttrs = true;
}
// // 如果移除了属性,记录日志
// if (hasRemovedAttrs) {
// ////AppLogger.v(_tag, '🗑️ 已移除设定引用属性: op[$i]');
// }
// 如果还有其他属性保留attributes否则移除整个attributes字段
if (attributes.isNotEmpty) {
newOp['attributes'] = attributes;
} else {
newOp.remove('attributes');
}
}
}
filteredOps.add(newOp);
} else {
// 非Map类型的操作直接保留通常不应该发生
////AppLogger.v(_tag, '⚠️ 跳过非Map类型的操作: ${op.runtimeType}');
filteredOps.add(op);
}
}
// 重新构造Delta保持原有格式
final dynamic filteredResult;
if (deltaData is List) {
// 如果原始数据是数组格式,返回数组
filteredResult = filteredOps;
} else {
// 如果原始数据是标准Delta格式返回包含ops的对象
filteredResult = {
'ops': filteredOps,
};
}
final String filteredJson = jsonEncode(filteredResult);
// 🎯 优化:减少频繁日志输出
if (caller == null || caller == 'debug') {
//AppLogger.d(_tag, '✅ 设定引用样式过滤完成${caller != null ? ' - 调用者: $caller' : ''}');
////AppLogger.v(_tag, ' 原始长度: ${deltaJson.length}, 过滤后长度: ${filteredJson.length}');
}
return filteredJson;
} catch (e, stackTrace) {
AppLogger.w(_tag, '过滤设定引用样式失败,返回原始内容', e);
////AppLogger.v(_tag, '错误详情', e, stackTrace);
return deltaJson; // 出错时返回原始内容
}
}
/// 🎯 处理设定引用悬停开始 - 使用精确位置(新版本,推荐使用)
static void handleSettingReferenceHoverStartWithPosition({
required QuillController controller,
required String settingId,
required int textStart,
required int textLength,
}) {
try {
//AppLogger.d(_tag, '🖱️ 开始处理设定引用悬停(使用精确位置): $settingId (位置: $textStart-${textStart + textLength})');
// 如果当前已有悬停状态,先清除
if (_currentHoveredSettingId != null) {
handleSettingReferenceHoverEnd();
}
// 直接使用传递的位置信息,不再计算
_currentHoveredSettingId = settingId;
_currentHoveringController = controller;
_hoveredTextStart = textStart;
_hoveredTextLength = textLength;
// 添加黄色背景属性使用Flutter Quill标准background属性
final hoverBackgroundAttribute = Attribute(
'background',
AttributeScope.inline,
'#FFF3CD', // 浅黄色背景
);
// 保存当前选择状态
final originalSelection = controller.selection;
// 应用悬停背景
controller.formatText(
_hoveredTextStart!,
_hoveredTextLength!,
hoverBackgroundAttribute,
);
// 恢复选择状态
controller.updateSelection(originalSelection, ChangeSource.silent);
////AppLogger.v(_tag, '✅ 已添加悬停背景(精确位置): $settingId (${_hoveredTextStart}-${_hoveredTextStart! + _hoveredTextLength!})');
} catch (e) {
AppLogger.e(_tag, '处理设定引用悬停开始失败(精确位置): $settingId', e);
}
}
/// 🎯 处理设定引用悬停结束 - 移除黄色背景
static void handleSettingReferenceHoverEnd() {
try {
if (_currentHoveredSettingId == null ||
_currentHoveringController == null ||
_hoveredTextStart == null ||
_hoveredTextLength == null) {
return;
}
//AppLogger.d(_tag, '🖱️ 结束处理设定引用悬停: $_currentHoveredSettingId');
// 移除悬停背景属性使用Flutter Quill标准background属性
final removeHoverBackgroundAttribute = Attribute(
'background',
AttributeScope.inline,
null, // null值表示移除属性
);
// 保存当前选择状态
final originalSelection = _currentHoveringController!.selection;
// 移除悬停背景
_currentHoveringController!.formatText(
_hoveredTextStart!,
_hoveredTextLength!,
removeHoverBackgroundAttribute,
);
// 恢复选择状态
_currentHoveringController!.updateSelection(originalSelection, ChangeSource.silent);
////AppLogger.v(_tag, '✅ 已移除悬停背景: $_currentHoveredSettingId');
// 清除悬停状态
_currentHoveredSettingId = null;
_currentHoveringController = null;
_hoveredTextStart = null;
_hoveredTextLength = null;
} catch (e) {
AppLogger.e(_tag, '处理设定引用悬停结束失败', e);
}
}
}