This commit is contained in:
parent
ecf000e466
commit
7bc9b5d709
|
|
@ -1115,191 +1115,201 @@ mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
|||
/// 递归解析富文本,支持嵌套格式
|
||||
///
|
||||
/// 支持的格式:
|
||||
/// - `**粗体**` - 加粗文本
|
||||
/// - `__斜体__` - 斜体文本
|
||||
/// - `` `代码` `` - 等宽字体代码
|
||||
/// - `[高亮]` - 主题色高亮
|
||||
/// - `**粗体**`
|
||||
/// - `__斜体__`
|
||||
/// - `` `代码` ``
|
||||
/// - `[高亮]`
|
||||
///
|
||||
/// 支持嵌套,例如:
|
||||
/// - `__在[学生管理]中添加详情页__` - 斜体中嵌套高亮
|
||||
/// - `[高亮**粗体**文本]` - 高亮中嵌套粗体
|
||||
/// - `` `代码**粗体**` `` - 代码中嵌套粗体
|
||||
///
|
||||
/// 处理逻辑:
|
||||
/// 1. 找到所有格式标记
|
||||
/// 2. 识别嵌套关系(被其他格式完全包围的为内层)
|
||||
/// 3. 优先处理最外层格式,递归处理内层内容
|
||||
/// 4. 继续处理剩余文本
|
||||
/// 采用自定义解析器,逐字符扫描并使用递归解析子内容,从而支持任意嵌套。
|
||||
List<TextSpan> _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 = <TextSpan>[];
|
||||
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'), // __斜体__ - 不包含 __ 的内容
|
||||
];
|
||||
|
||||
// 找到所有匹配的格式标记
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有匹配,返回普通文本
|
||||
if (allMatches.isEmpty) {
|
||||
return [
|
||||
TextSpan(
|
||||
text: content,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface.withOpacity(0.8),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// 按位置排序,找到最左边的匹配
|
||||
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);
|
||||
});
|
||||
|
||||
// 找到第一个不被其他格式完全包围的匹配(最外层的)
|
||||
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) {
|
||||
void flushBuffer() {
|
||||
if (buffer.isEmpty) return;
|
||||
spans.add(TextSpan(
|
||||
text: content.substring(lastIndex, selectedMatch.start),
|
||||
style: TextStyle(
|
||||
text: buffer.toString(),
|
||||
style: _defaultRichTextStyle(colorScheme),
|
||||
));
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
flushBuffer();
|
||||
return _RichTextParseResult(spans, index, false);
|
||||
}
|
||||
|
||||
TextStyle _defaultRichTextStyle(ColorScheme colorScheme) {
|
||||
return 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(
|
||||
TextStyle _boldTextStyle(ColorScheme colorScheme) {
|
||||
return TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface.withOpacity(0.9),
|
||||
height: 1.4,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
break;
|
||||
case 'italic':
|
||||
baseStyle = TextStyle(
|
||||
}
|
||||
|
||||
TextStyle _italicTextStyle(ColorScheme colorScheme) {
|
||||
return TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurface.withOpacity(0.9),
|
||||
height: 1.4,
|
||||
fontStyle: FontStyle.italic,
|
||||
);
|
||||
break;
|
||||
case 'code':
|
||||
baseStyle = TextStyle(
|
||||
}
|
||||
|
||||
TextStyle _codeTextStyle(ColorScheme colorScheme) {
|
||||
return TextStyle(
|
||||
fontSize: 12,
|
||||
color: colorScheme.primary,
|
||||
height: 1.4,
|
||||
fontFamily: 'monospace',
|
||||
backgroundColor: colorScheme.primaryContainer.withOpacity(0.2),
|
||||
);
|
||||
break;
|
||||
case 'highlight':
|
||||
baseStyle = TextStyle(
|
||||
}
|
||||
|
||||
TextStyle _highlightTextStyle(ColorScheme colorScheme) {
|
||||
return 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) {
|
||||
List<TextSpan> _applyStyleToSpans(List<TextSpan> spans, TextStyle style, String fallbackText) {
|
||||
if (spans.isEmpty) {
|
||||
return [
|
||||
TextSpan(
|
||||
text: fallbackText,
|
||||
style: style,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return spans.map((span) => _mergeTextSpanStyle(span, style)).toList();
|
||||
}
|
||||
|
||||
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(baseStyle) ?? baseStyle,
|
||||
children: span.children,
|
||||
style: span.style?.merge(style) ?? style,
|
||||
children: mergedChildren,
|
||||
);
|
||||
}).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;
|
||||
}
|
||||
|
||||
Widget _buildEnhancedDownloadProgress(BuildContext context, ColorScheme colorScheme) {
|
||||
|
|
@ -2207,3 +2217,11 @@ class _ToastWidget extends StatelessWidget {
|
|||
}
|
||||
|
||||
typedef BoolCallback = void Function(bool success);
|
||||
|
||||
class _RichTextParseResult {
|
||||
final List<TextSpan> spans;
|
||||
final int nextIndex;
|
||||
final bool closed;
|
||||
|
||||
const _RichTextParseResult(this.spans, this.nextIndex, this.closed);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue