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> ops = parsed.cast>(); 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?; 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> ops = (parsed['ops'] as List).cast>(); // 确保最后一个操作以换行符结尾 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> ops; if (parsed is List) { ops = parsed.cast>(); } else if (parsed is Map && parsed.containsKey('ops') && parsed['ops'] is List) { ops = (parsed['ops'] as List).cast>(); } 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 testStyleAttributeHandling() { final testResults = {}; 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 = >[]; 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 = {}; if (parsedData is List) { for (final op in parsedData) { if (op is Map && op.containsKey('attributes')) { foundStyles = true; final attributes = op['attributes'] as Map; 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; } }