diff --git a/lib/app_upgrade_simple.dart b/lib/app_upgrade_simple.dart index 2ed8ef0..21279ab 100644 --- a/lib/app_upgrade_simple.dart +++ b/lib/app_upgrade_simple.dart @@ -1115,191 +1115,201 @@ mixin _UpgradeDialogLogic on State { /// 递归解析富文本,支持嵌套格式 /// /// 支持的格式: - /// - `**粗体**` - 加粗文本 - /// - `__斜体__` - 斜体文本 - /// - `` `代码` `` - 等宽字体代码 - /// - `[高亮]` - 主题色高亮 + /// - `**粗体**` + /// - `__斜体__` + /// - `` `代码` `` + /// - `[高亮]` /// - /// 支持嵌套,例如: - /// - `__在[学生管理]中添加详情页__` - 斜体中嵌套高亮 - /// - `[高亮**粗体**文本]` - 高亮中嵌套粗体 - /// - `` `代码**粗体**` `` - 代码中嵌套粗体 - /// - /// 处理逻辑: - /// 1. 找到所有格式标记 - /// 2. 识别嵌套关系(被其他格式完全包围的为内层) - /// 3. 优先处理最外层格式,递归处理内层内容 - /// 4. 继续处理剩余文本 + /// 采用自定义解析器,逐字符扫描并使用递归解析子内容,从而支持任意嵌套。 List _parseRichText(String content, ColorScheme colorScheme) { + final result = _parseRichTextInternal(content, colorScheme, 0, null); + return result.spans; + } + + _RichTextParseResult _parseRichTextInternal( + String text, + ColorScheme colorScheme, + int startIndex, + String? endToken, + ) { final spans = []; + final buffer = StringBuffer(); + var index = startIndex; - // 定义格式标记:使用严格的正则表达式,避免匹配不完整的标记 - // 优先级:[] > ` > ** > __ (内层格式优先) - final patterns = [ - (regex: RegExp(r'\[([^\]]+)\]'), type: 'highlight'), // [高亮] - 不包含 ] 的内容 - (regex: RegExp(r'`([^`]+)`'), type: 'code'), // `代码` - 不包含 ` 的内容 - (regex: RegExp(r'\*\*([^*]+(?:\*(?!\*)[^*]*)*)\*\*'), type: 'bold'), // **粗体** - 不包含 ** 的内容 - (regex: RegExp(r'__([^_]+(?:_(?!_)[^_]*)*)__'), type: 'italic'), // __斜体__ - 不包含 __ 的内容 - ]; + void flushBuffer() { + if (buffer.isEmpty) return; + spans.add(TextSpan( + text: buffer.toString(), + style: _defaultRichTextStyle(colorScheme), + )); + buffer.clear(); + } - // 找到所有匹配的格式标记 - final allMatches = <({Match match, String type, int priority})>[]; - for (int i = 0; i < patterns.length; i++) { - final pattern = patterns[i]; - for (final match in pattern.regex.allMatches(content)) { - // 确保匹配的内容不为空 - if (match.group(1) != null && match.group(1)!.isNotEmpty) { - allMatches.add((match: match, type: pattern.type, priority: i)); + while (index < text.length) { + // 如果遇到结束标记,返回到上层 + if (endToken != null && text.startsWith(endToken, index)) { + flushBuffer(); + return _RichTextParseResult(spans, index + endToken.length, true); + } + + final currentChar = text[index]; + var handled = false; + + // [高亮] + if (currentChar == '[') { + final innerResult = _parseRichTextInternal(text, colorScheme, index + 1, ']'); + if (innerResult.closed) { + flushBuffer(); + final innerText = text.substring(index + 1, innerResult.nextIndex - 1); + spans.addAll(_applyStyleToSpans( + innerResult.spans, + _highlightTextStyle(colorScheme), + innerText, + )); + index = innerResult.nextIndex; + handled = true; + } else { + // 无法找到匹配的 ],按普通文本处理 + buffer.write(currentChar); + index++; + handled = true; } } + // `代码` + else if (currentChar == '`') { + final closingIndex = text.indexOf('`', index + 1); + if (closingIndex != -1) { + flushBuffer(); + final codeText = text.substring(index + 1, closingIndex); + spans.add(TextSpan( + text: codeText, + style: _codeTextStyle(colorScheme), + )); + index = closingIndex + 1; + handled = true; + } else { + // 没有找到闭合的 `,按普通文本处理 + buffer.write(currentChar); + index++; + handled = true; + } + } + // **粗体** + else if (text.startsWith('**', index)) { + final innerResult = _parseRichTextInternal(text, colorScheme, index + 2, '**'); + if (innerResult.closed) { + flushBuffer(); + final innerText = text.substring(index + 2, innerResult.nextIndex - 2); + spans.addAll(_applyStyleToSpans( + innerResult.spans, + _boldTextStyle(colorScheme), + innerText, + )); + index = innerResult.nextIndex; + handled = true; + } else { + // 没有匹配的 ** + buffer.write('**'); + index += 2; + handled = true; + } + } + // __斜体__ + else if (text.startsWith('__', index)) { + final innerResult = _parseRichTextInternal(text, colorScheme, index + 2, '__'); + if (innerResult.closed) { + flushBuffer(); + final innerText = text.substring(index + 2, innerResult.nextIndex - 2); + spans.addAll(_applyStyleToSpans( + innerResult.spans, + _italicTextStyle(colorScheme), + innerText, + )); + index = innerResult.nextIndex; + handled = true; + } else { + buffer.write('__'); + index += 2; + handled = true; + } + } + + if (!handled) { + buffer.write(currentChar); + index++; + } } - // 如果没有匹配,返回普通文本 - if (allMatches.isEmpty) { + flushBuffer(); + return _RichTextParseResult(spans, index, false); + } + + TextStyle _defaultRichTextStyle(ColorScheme colorScheme) { + return TextStyle( + fontSize: 13, + color: colorScheme.onSurface.withOpacity(0.8), + height: 1.4, + ); + } + + TextStyle _boldTextStyle(ColorScheme colorScheme) { + return TextStyle( + fontSize: 13, + color: colorScheme.onSurface.withOpacity(0.9), + height: 1.4, + fontWeight: FontWeight.bold, + ); + } + + TextStyle _italicTextStyle(ColorScheme colorScheme) { + return TextStyle( + fontSize: 13, + color: colorScheme.onSurface.withOpacity(0.9), + height: 1.4, + fontStyle: FontStyle.italic, + ); + } + + TextStyle _codeTextStyle(ColorScheme colorScheme) { + return TextStyle( + fontSize: 12, + color: colorScheme.primary, + height: 1.4, + fontFamily: 'monospace', + backgroundColor: colorScheme.primaryContainer.withOpacity(0.2), + ); + } + + TextStyle _highlightTextStyle(ColorScheme colorScheme) { + return TextStyle( + fontSize: 13, + color: colorScheme.primary, + height: 1.4, + fontWeight: FontWeight.w600, + ); + } + + List _applyStyleToSpans(List spans, TextStyle style, String fallbackText) { + if (spans.isEmpty) { return [ TextSpan( - text: content, - style: TextStyle( - fontSize: 13, - color: colorScheme.onSurface.withOpacity(0.8), - height: 1.4, - ), + text: fallbackText, + style: style, ), ]; } - // 按位置排序,找到最左边的匹配 - allMatches.sort((a, b) { - // 先按开始位置排序 - if (a.match.start != b.match.start) { - return a.match.start.compareTo(b.match.start); - } - // 如果开始位置相同,按优先级排序(高优先级在前,即 [] 和 ` 优先) - if (a.priority != b.priority) { - return a.priority.compareTo(b.priority); - } - // 如果优先级也相同,按结束位置排序(短的在前) - return a.match.end.compareTo(b.match.end); - }); + return spans.map((span) => _mergeTextSpanStyle(span, style)).toList(); + } - // 找到第一个不被其他格式完全包围的匹配(最外层的) - Match? selectedMatch; - String? selectedType; - - for (final item in allMatches) { - bool isNested = false; - // 检查是否被其他格式完全包围(开始位置更早且结束位置更晚) - for (final other in allMatches) { - if (other.match != item.match && - other.match.start <= item.match.start && - other.match.end >= item.match.end && - (other.match.start < item.match.start || other.match.end > item.match.end)) { - isNested = true; - break; - } - } - // 选择第一个不被嵌套的匹配 - if (!isNested) { - selectedMatch = item.match; - selectedType = item.type; - break; - } - } - - // 如果没找到(理论上不应该发生),使用第一个匹配 - selectedMatch ??= allMatches.first.match; - selectedType ??= allMatches.first.type; - - int lastIndex = 0; - - // 添加匹配前的普通文本 - if (selectedMatch.start > lastIndex) { - spans.add(TextSpan( - text: content.substring(lastIndex, selectedMatch.start), - style: TextStyle( - fontSize: 13, - color: colorScheme.onSurface.withOpacity(0.8), - height: 1.4, - ), - )); - } - - // 处理匹配到的格式内容(递归解析,支持嵌套) - final matchedContent = selectedMatch.group(1)!; - final nestedSpans = _parseRichText(matchedContent, colorScheme); - - // 应用当前格式的样式 - TextStyle baseStyle; - switch (selectedType) { - case 'bold': - baseStyle = TextStyle( - fontSize: 13, - color: colorScheme.onSurface.withOpacity(0.9), - height: 1.4, - fontWeight: FontWeight.bold, - ); - break; - case 'italic': - baseStyle = TextStyle( - fontSize: 13, - color: colorScheme.onSurface.withOpacity(0.9), - height: 1.4, - fontStyle: FontStyle.italic, - ); - break; - case 'code': - baseStyle = TextStyle( - fontSize: 12, - color: colorScheme.primary, - height: 1.4, - fontFamily: 'monospace', - backgroundColor: colorScheme.primaryContainer.withOpacity(0.2), - ); - break; - case 'highlight': - baseStyle = TextStyle( - fontSize: 13, - color: colorScheme.primary, - height: 1.4, - fontWeight: FontWeight.w600, - ); - break; - default: - baseStyle = TextStyle( - fontSize: 13, - color: colorScheme.onSurface.withOpacity(0.8), - height: 1.4, - ); - } - - // 如果嵌套解析有结果,应用样式;否则直接使用匹配的内容 - if (nestedSpans.isNotEmpty && - (nestedSpans.length > 1 || (nestedSpans.isNotEmpty && nestedSpans[0].text != matchedContent))) { - // 有嵌套格式,应用样式到所有嵌套的spans - final styledSpans = nestedSpans.map((span) { - return TextSpan( - text: span.text, - style: span.style?.merge(baseStyle) ?? baseStyle, - children: span.children, - ); - }).toList(); - spans.addAll(styledSpans); - } else { - // 无嵌套格式,直接应用样式 - spans.add(TextSpan( - text: matchedContent, - style: baseStyle, - )); - } - - // 继续解析剩余部分 - lastIndex = selectedMatch.end; - if (lastIndex < content.length) { - final remainingSpans = _parseRichText(content.substring(lastIndex), colorScheme); - spans.addAll(remainingSpans); - } - - return spans; + TextSpan _mergeTextSpanStyle(TextSpan span, TextStyle style) { + final mergedChildren = + span.children?.map((child) => child is TextSpan ? _mergeTextSpanStyle(child, style) : child).toList(); + return TextSpan( + text: span.text, + style: span.style?.merge(style) ?? style, + children: mergedChildren, + ); } Widget _buildEnhancedDownloadProgress(BuildContext context, ColorScheme colorScheme) { @@ -2207,3 +2217,11 @@ class _ToastWidget extends StatelessWidget { } typedef BoolCallback = void Function(bool success); + +class _RichTextParseResult { + final List spans; + final int nextIndex; + final bool closed; + + const _RichTextParseResult(this.spans, this.nextIndex, this.closed); +}