54 lines
2.3 KiB
Dart
54 lines
2.3 KiB
Dart
/// 中西文混排自动加间隙(W3C clreq §3.2.2)。
|
||
///
|
||
/// 规范要求:横排时汉字与西文字母/数字之间留约 1/4 em 间隙。CSS 有
|
||
/// `text-autospace`,Flutter 没有对应能力,因此在文本层插入
|
||
/// U+2009 THIN SPACE(约 1/5 em,视觉接近规范建议,且远比全角空格克制)。
|
||
///
|
||
/// 规则(与 pangu 习惯一致):
|
||
/// - 汉字 ↔ 字母/数字 相邻 → 插 thin space(「下降2.2%」→「下降 2.2%」);
|
||
/// - 西文 run 末尾的 % ‰ 视作 run 的一部分(「2.2%的降幅」→「2.2% 的降幅」);
|
||
/// - 全角标点(,。:等)自带空隙,不加;
|
||
/// - markdown 版本额外处理强调/行内码边界(「至**138美元**」→「至 **138美元**」),
|
||
/// 并跳过 fenced code block;表格管道符、链接 URL 天然不受影响。
|
||
library;
|
||
|
||
const String _thin = '\u2009'; // THIN SPACE
|
||
|
||
// CJK 统一表意文字(基本区 + 扩展 A + 兼容区)
|
||
const String _han = r'㐀-䶿一-鿿豈-';
|
||
// 西文边界字符(字母/数字);西文侧在右边界额外允许 % ‰ 收尾
|
||
const String _west = r'A-Za-z0-9';
|
||
|
||
final RegExp _hanWest = RegExp('([$_han])([$_west])');
|
||
final RegExp _westHan = RegExp('([$_west%‰])([$_han])');
|
||
// 强调/行内码标记夹在边界中间:汉**西 / 西**汉
|
||
final RegExp _hanMarkWest = RegExp('([$_han])(\\*{1,3}|`)([$_west])');
|
||
final RegExp _westMarkHan = RegExp('([$_west%‰])(\\*{1,3}|`)([$_han])');
|
||
|
||
/// 纯文本版:用于标题、副题、简介等 Text 控件。
|
||
String cjkAutoSpace(String text) {
|
||
var s = text;
|
||
s = s.replaceAllMapped(_hanWest, (m) => '${m[1]}$_thin${m[2]}');
|
||
s = s.replaceAllMapped(_westHan, (m) => '${m[1]}$_thin${m[2]}');
|
||
return s;
|
||
}
|
||
|
||
/// markdown 版:跳过 fenced code block,并处理强调/行内码边界。
|
||
String cjkMarkdownAutoSpace(String md) {
|
||
final lines = md.split('\n');
|
||
var inFence = false;
|
||
for (var i = 0; i < lines.length; i++) {
|
||
final t = lines[i].trimLeft();
|
||
if (t.startsWith('```') || t.startsWith('~~~')) {
|
||
inFence = !inFence;
|
||
continue;
|
||
}
|
||
if (inFence) continue;
|
||
var s = lines[i];
|
||
s = s.replaceAllMapped(_hanMarkWest, (m) => '${m[1]}$_thin${m[2]}${m[3]}');
|
||
s = s.replaceAllMapped(_westMarkHan, (m) => '${m[1]}${m[2]}$_thin${m[3]}');
|
||
lines[i] = cjkAutoSpace(s);
|
||
}
|
||
return lines.join('\n');
|
||
}
|