马良AI写作初始化仓库
This commit is contained in:
410
AINoval/lib/utils/quill_helper.dart
Normal file
410
AINoval/lib/utils/quill_helper.dart
Normal file
@@ -0,0 +1,410 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
import 'package:ainoval/utils/logger.dart';
|
||||
|
||||
/// Quill富文本编辑器格式处理工具类
|
||||
///
|
||||
/// 用于统一处理Quill富文本编辑器的内容格式,确保正确转换和验证Delta格式
|
||||
class QuillHelper {
|
||||
static const String _tag = 'QuillHelper';
|
||||
|
||||
/// 确保内容是标准的Quill格式
|
||||
///
|
||||
/// 将{"ops":[...]}格式转换为更简洁的[...]格式
|
||||
/// 将非JSON文本转换为基本的Quill格式
|
||||
///
|
||||
/// @param content 输入的内容
|
||||
/// @return 标准化后的Quill Delta格式
|
||||
static String ensureQuillFormat(String content) {
|
||||
if (content.isEmpty) {
|
||||
return jsonEncode([{"insert": "\n"}]);
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查内容是否是纯文本(不是JSON格式)
|
||||
try {
|
||||
jsonDecode(content);
|
||||
} catch (e) {
|
||||
// 如果解析失败,说明是纯文本,直接转换为Delta格式
|
||||
return jsonEncode([{"insert": "$content\n"}]);
|
||||
}
|
||||
|
||||
// 尝试解析为JSON,检查是否已经是Quill格式
|
||||
final dynamic parsed = jsonDecode(content);
|
||||
|
||||
// 如果已经是数组格式,检查是否符合Quill格式要求
|
||||
if (parsed is List) {
|
||||
List<Map<String, dynamic>> ops = parsed.cast<Map<String, dynamic>>();
|
||||
bool isValidQuill = ops.isNotEmpty &&
|
||||
ops.every((item) => item is Map && (item.containsKey('insert') || item.containsKey('attributes')));
|
||||
|
||||
if (isValidQuill) {
|
||||
// 🚀 新增:检查和记录样式属性保存情况
|
||||
bool hasStyleAttributes = false;
|
||||
for (final op in ops) {
|
||||
if (op.containsKey('attributes')) {
|
||||
hasStyleAttributes = true;
|
||||
final attributes = op['attributes'] as Map<String, dynamic>?;
|
||||
if (attributes != null && (attributes.containsKey('color') || attributes.containsKey('background'))) {
|
||||
AppLogger.d('QuillHelper/ensureQuillFormat',
|
||||
'🎨 保存样式属性: ${attributes.keys.join(', ')}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasStyleAttributes) {
|
||||
AppLogger.i('QuillHelper/ensureQuillFormat',
|
||||
'🎨 确保包含样式属性的Quill格式,操作数量: ${ops.length}');
|
||||
}
|
||||
|
||||
// 确保最后一个操作以换行符结尾
|
||||
if (ops.isNotEmpty) {
|
||||
final lastOp = ops.last;
|
||||
if (lastOp.containsKey('insert')) {
|
||||
final insertText = lastOp['insert'].toString();
|
||||
if (!insertText.endsWith('\n')) {
|
||||
// 如果最后一个insert不以换行符结尾,添加一个新的换行符操作
|
||||
ops.add({'insert': '\n'});
|
||||
}
|
||||
} else {
|
||||
// 如果最后一个操作不包含insert,添加换行符
|
||||
ops.add({'insert': '\n'});
|
||||
}
|
||||
}
|
||||
return jsonEncode(ops); // 返回修正后的Quill格式
|
||||
} else {
|
||||
// 转换为纯文本后重新格式化
|
||||
String plainText = _extractTextFromList(parsed);
|
||||
return jsonEncode([{"insert": "$plainText\n"}]);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是对象格式,检查是否符合Delta格式
|
||||
if (parsed is Map && parsed.containsKey('ops') && parsed['ops'] is List) {
|
||||
List<Map<String, dynamic>> ops = (parsed['ops'] as List).cast<Map<String, dynamic>>();
|
||||
|
||||
// 确保最后一个操作以换行符结尾
|
||||
if (ops.isNotEmpty) {
|
||||
final lastOp = ops.last;
|
||||
if (lastOp.containsKey('insert')) {
|
||||
final insertText = lastOp['insert'].toString();
|
||||
if (!insertText.endsWith('\n')) {
|
||||
// 如果最后一个insert不以换行符结尾,添加一个新的换行符操作
|
||||
ops.add({'insert': '\n'});
|
||||
}
|
||||
} else {
|
||||
// 如果最后一个操作不包含insert,添加换行符
|
||||
ops.add({'insert': '\n'});
|
||||
}
|
||||
} else {
|
||||
// 如果ops为空,添加一个换行符
|
||||
ops = [{'insert': '\n'}];
|
||||
}
|
||||
|
||||
return jsonEncode(ops);
|
||||
}
|
||||
|
||||
// 其他JSON格式,转换为纯文本
|
||||
return jsonEncode([{"insert": "${jsonEncode(parsed)}\n"}]);
|
||||
} catch (e) {
|
||||
// 不是JSON格式,作为纯文本处理
|
||||
AppLogger.w('QuillHelper', '内容不是标准格式,作为纯文本处理');
|
||||
// 转义特殊字符,确保JSON格式有效
|
||||
String safeText = content
|
||||
.replaceAll('\\', '\\\\')
|
||||
.replaceAll('"', '\\"')
|
||||
.replaceAll('\n', '\\n')
|
||||
.replaceAll('\r', '\\r')
|
||||
.replaceAll('\t', '\\t');
|
||||
|
||||
return jsonEncode([{"insert": "$safeText\n"}]);
|
||||
}
|
||||
}
|
||||
|
||||
/// 将纯文本内容转换为Quill Delta格式
|
||||
///
|
||||
/// @param text 纯文本内容
|
||||
/// @return Quill Delta格式的字符串
|
||||
static String textToDelta(String text) {
|
||||
if (text.isEmpty) {
|
||||
return standardEmptyDelta;
|
||||
}
|
||||
|
||||
final String escapedText = _escapeQuillText(text);
|
||||
return '[{"insert":"$escapedText\\n"}]';
|
||||
}
|
||||
|
||||
/// 将Quill Delta格式转换为纯文本
|
||||
///
|
||||
/// @param delta Quill Delta格式的字符串
|
||||
/// @return 纯文本内容
|
||||
static String deltaToText(String deltaContent) {
|
||||
try {
|
||||
final dynamic parsed = jsonDecode(deltaContent);
|
||||
|
||||
if (parsed is List) {
|
||||
return _extractTextFromList(parsed);
|
||||
} else if (parsed is Map && parsed.containsKey('ops') && parsed['ops'] is List) {
|
||||
return _extractTextFromList(parsed['ops'] as List);
|
||||
}
|
||||
|
||||
// 如果不是标准格式,返回原始内容
|
||||
return deltaContent;
|
||||
} catch (e) {
|
||||
// 如果解析失败,返回原始内容
|
||||
return deltaContent;
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证内容是否为有效的Quill格式
|
||||
///
|
||||
/// @param content 要验证的内容
|
||||
/// @return 是否为有效的Quill格式
|
||||
static bool isValidQuillFormat(String content) {
|
||||
try {
|
||||
final parsed = jsonDecode(content);
|
||||
if (parsed is List) {
|
||||
return parsed.every((item) => item is Map && item.containsKey('insert'));
|
||||
}
|
||||
return false;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取标准的空Quill Delta格式
|
||||
static String get standardEmptyDelta => '[{"insert":"\\n"}]';
|
||||
|
||||
/// 获取包含ops的空Quill Delta格式
|
||||
static String get opsWrappedEmptyDelta => '{"ops":[{"insert":"\\n"}]}';
|
||||
|
||||
/// 转义Quill文本中的特殊字符
|
||||
static String _escapeQuillText(String text) {
|
||||
return text
|
||||
.replaceAll('\\', '\\\\')
|
||||
.replaceAll('"', '\\"')
|
||||
.replaceAll('\n', '\\n');
|
||||
}
|
||||
|
||||
/// 检测内容格式,确定是否需要转换
|
||||
///
|
||||
/// @param content 输入的内容
|
||||
/// @return 是否需要转换为标准格式
|
||||
static bool needsFormatConversion(String content) {
|
||||
if (content.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
final dynamic contentJson = jsonDecode(content);
|
||||
return contentJson is Map && contentJson.containsKey('ops');
|
||||
} catch (e) {
|
||||
return !content.startsWith('[{');
|
||||
}
|
||||
}
|
||||
|
||||
/// 计算Quill Delta内容的字数统计
|
||||
///
|
||||
/// @param delta Quill Delta格式的字符串
|
||||
/// @return 内容的字数
|
||||
static int countWords(String delta) {
|
||||
final String text = deltaToText(delta);
|
||||
if (text.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 移除所有换行符后计算字数
|
||||
final String cleanText = text.replaceAll('\n', '');
|
||||
return cleanText.length;
|
||||
}
|
||||
|
||||
/// 从List中提取文本内容
|
||||
static String _extractTextFromList(List list) {
|
||||
StringBuffer buffer = StringBuffer();
|
||||
for (var item in list) {
|
||||
if (item is Map && item.containsKey('insert')) {
|
||||
buffer.write(item['insert']);
|
||||
} else if (item is String) {
|
||||
buffer.write(item);
|
||||
} else {
|
||||
buffer.write(jsonEncode(item));
|
||||
}
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// 将纯文本转换为Quill Delta格式
|
||||
static String convertPlainTextToQuillDelta(String text) {
|
||||
if (text.isEmpty) {
|
||||
return jsonEncode([{"insert": "\n"}]);
|
||||
}
|
||||
|
||||
// 处理换行符,确保JSON格式正确
|
||||
String safeText = text
|
||||
.replaceAll('\\', '\\\\')
|
||||
.replaceAll('"', '\\"')
|
||||
.replaceAll('\n', '\\n')
|
||||
.replaceAll('\r', '\\r')
|
||||
.replaceAll('\t', '\\t');
|
||||
|
||||
// 构建基本的Quill格式
|
||||
return jsonEncode([{"insert": "$safeText\n"}]);
|
||||
}
|
||||
|
||||
/// 验证并修复Delta格式
|
||||
///
|
||||
/// 确保Delta格式符合Flutter Quill的要求,特别是最后一个操作必须以换行符结尾
|
||||
///
|
||||
/// @param deltaJson Delta格式的JSON字符串
|
||||
/// @return 修复后的有效Delta格式
|
||||
static String validateAndFixDelta(String deltaJson) {
|
||||
if (deltaJson.isEmpty) {
|
||||
return jsonEncode([{"insert": "\n"}]);
|
||||
}
|
||||
|
||||
try {
|
||||
final dynamic parsed = jsonDecode(deltaJson);
|
||||
List<Map<String, dynamic>> ops;
|
||||
|
||||
if (parsed is List) {
|
||||
ops = parsed.cast<Map<String, dynamic>>();
|
||||
} else if (parsed is Map && parsed.containsKey('ops') && parsed['ops'] is List) {
|
||||
ops = (parsed['ops'] as List).cast<Map<String, dynamic>>();
|
||||
} else {
|
||||
// 不是有效的Delta格式,转换为纯文本
|
||||
return jsonEncode([{"insert": "$deltaJson\n"}]);
|
||||
}
|
||||
|
||||
// 确保最后一个操作以换行符结尾
|
||||
if (ops.isEmpty) {
|
||||
ops = [{"insert": "\n"}];
|
||||
} else {
|
||||
final lastOp = ops.last;
|
||||
if (lastOp.containsKey('insert')) {
|
||||
final insertText = lastOp['insert'].toString();
|
||||
if (!insertText.endsWith('\n')) {
|
||||
// 如果最后一个insert不以换行符结尾,添加一个新的换行符操作
|
||||
ops.add({"insert": "\n"});
|
||||
}
|
||||
} else {
|
||||
// 如果最后一个操作不包含insert,添加换行符
|
||||
ops.add({"insert": "\n"});
|
||||
}
|
||||
}
|
||||
|
||||
return jsonEncode(ops);
|
||||
} catch (e) {
|
||||
// 解析失败,作为纯文本处理
|
||||
AppLogger.w('QuillHelper', 'Delta验证失败,转换为纯文本: ${e.toString()}');
|
||||
return jsonEncode([{"insert": "$deltaJson\n"}]);
|
||||
}
|
||||
}
|
||||
|
||||
/// 🚀 新增:测试样式属性的保存和解析
|
||||
///
|
||||
/// 用于验证包含颜色、背景等样式属性的内容是否能正确保存和加载
|
||||
static Map<String, dynamic> testStyleAttributeHandling() {
|
||||
final testResults = <String, dynamic>{};
|
||||
|
||||
try {
|
||||
// 测试数据:包含各种样式属性的Quill内容
|
||||
final testContents = [
|
||||
// 1. 包含背景颜色的内容
|
||||
'[{"insert":"这是红色背景的文字","attributes":{"background":"#f44336"}},{"insert":"\\n"}]',
|
||||
|
||||
// 2. 包含文字颜色的内容
|
||||
'[{"insert":"这是蓝色的文字","attributes":{"color":"#2196f3"}},{"insert":"\\n"}]',
|
||||
|
||||
// 3. 包含多种样式的内容
|
||||
'[{"insert":"粗体红色背景","attributes":{"bold":true,"background":"#f44336"}},{"insert":" 普通文字 "},{"insert":"蓝色斜体","attributes":{"color":"#2196f3","italic":true}},{"insert":"\\n"}]',
|
||||
|
||||
// 4. ops格式的内容
|
||||
'{"ops":[{"insert":"绿色背景文字","attributes":{"background":"#4caf50"}},{"insert":"\\n"}]}',
|
||||
];
|
||||
|
||||
final results = <Map<String, dynamic>>[];
|
||||
|
||||
for (int i = 0; i < testContents.length; i++) {
|
||||
final testContent = testContents[i];
|
||||
final testName = 'Test${i + 1}';
|
||||
|
||||
AppLogger.i('QuillHelper/testStyleAttributeHandling',
|
||||
'🧪 开始测试 $testName: ${testContent.length} 字符');
|
||||
|
||||
try {
|
||||
// 1. 测试ensureQuillFormat处理
|
||||
final processedContent = ensureQuillFormat(testContent);
|
||||
|
||||
// 2. 解析处理后的内容
|
||||
final parsedData = jsonDecode(processedContent);
|
||||
|
||||
// 3. 检查样式属性是否保留
|
||||
bool foundStyles = false;
|
||||
final foundAttributes = <String, dynamic>{};
|
||||
|
||||
if (parsedData is List) {
|
||||
for (final op in parsedData) {
|
||||
if (op is Map && op.containsKey('attributes')) {
|
||||
foundStyles = true;
|
||||
final attributes = op['attributes'] as Map<String, dynamic>;
|
||||
foundAttributes.addAll(attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.add({
|
||||
'testName': testName,
|
||||
'originalLength': testContent.length,
|
||||
'processedLength': processedContent.length,
|
||||
'foundStyles': foundStyles,
|
||||
'attributes': foundAttributes,
|
||||
'success': foundStyles,
|
||||
'originalContent': testContent.substring(0, math.min(100, testContent.length)),
|
||||
'processedContent': processedContent.substring(0, math.min(100, processedContent.length)),
|
||||
});
|
||||
|
||||
AppLogger.i('QuillHelper/testStyleAttributeHandling',
|
||||
'✅ $testName 成功: 找到样式=$foundStyles, 属性=${foundAttributes.keys.join(',')}');
|
||||
|
||||
} catch (e) {
|
||||
results.add({
|
||||
'testName': testName,
|
||||
'success': false,
|
||||
'error': e.toString(),
|
||||
});
|
||||
|
||||
AppLogger.e('QuillHelper/testStyleAttributeHandling',
|
||||
'❌ $testName 失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 汇总结果
|
||||
final successCount = results.where((r) => r['success'] == true).length;
|
||||
final totalCount = results.length;
|
||||
|
||||
testResults['summary'] = {
|
||||
'totalTests': totalCount,
|
||||
'successCount': successCount,
|
||||
'failureCount': totalCount - successCount,
|
||||
'successRate': totalCount > 0 ? (successCount / totalCount * 100).toStringAsFixed(1) + '%' : '0%',
|
||||
};
|
||||
|
||||
testResults['details'] = results;
|
||||
testResults['overallSuccess'] = successCount == totalCount;
|
||||
|
||||
AppLogger.i('QuillHelper/testStyleAttributeHandling',
|
||||
'🏁 测试完成: $successCount/$totalCount 成功 (${testResults['summary']['successRate']})');
|
||||
|
||||
} catch (e) {
|
||||
testResults['error'] = e.toString();
|
||||
testResults['overallSuccess'] = false;
|
||||
|
||||
AppLogger.e('QuillHelper/testStyleAttributeHandling',
|
||||
'💥 测试过程出错: $e');
|
||||
}
|
||||
|
||||
return testResults;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user