算法遇记 | 字符串段拆插问题 - 富文本
1. 场景说明
最近遇到一个小问题,这里把问题模型简化,记录一下处理方式,也算是一个小纪念。先说一下场景,如下所示:
已知字符串 src
匹配段列表:matches
这样,在 Flutter 中可以通过对 matches
的遍历,形成富文本段,进行展示,效果如下:
TextSpan formSpan() {
List<List<int>> matches = [
[1, 2], [5, 8], [14, 15]
];
String src = "toly 1994,hello!";
List<InlineSpan> span = [];
int cursor = 0;
for (int i = 0; i < matches.length; i++) {
List<int> match = matches[i];
// 非匹配段
String noMatchStr = src.substring(cursor, match[0]);
span.add(TextSpan(text:noMatchStr , style: style0));
// 匹配段
String matchStr = src.substring(match[0], match[1]+1);
span.add(TextSpan(text: matchStr, style: style1,));
cursor = match[1]+1;
}
if (cursor != src.length - 1) {
span.add(TextSpan(text: src.substring(cursor), style: style0));
}
return TextSpan(children: span);
}
2. 要解决的需求
现在有个需求,给定槽点列表 slots
,在 保持原有匹配效果 的前提下,在每个槽点对应的索引处,插入该槽点的索引值,如下所示:
如下,是插入后的效果,其中原来的高亮样式保持不变,且在指定位置处额外插入了文字。这时候 有用怪 难抑心中疑问,发出灵魂呼喊:这有什么用呢?
如下所示,如果在定点插入的东西不是文字,而是其他组件,比如 FlutterLogo
。就完成了在不影响原有高亮匹配情况下,在指定槽位插入其他组件的能力:
说一个最直接的应用场景,如下代码高亮行号的插入,就是使用这种手段。不影响原有富文本,在定点插入指定组件。
代码高亮 + 行号 | 代码高亮 + 行号 |
---|---|
![]() | ![]() |
3. 实现思路
这个问题的本质是根据 slots
点,对已字符段进行分割。就像一个拼接手术:首先找到位置,然后剪开,把插入段放在两片之间,再黏在一起:
由于槽点可以在任意位置,所以对于每段来说,操作都是一致的。这样对于每段字符,可以封装一个通用方法来处理。如下,定义 insertSlotWithBoundary
方法,传入每段的起止索引。第一步,应该校验当前段中是否存在槽点。如下左图所示,该段无槽点,就不需要进行什么处理:
这里定义 slotCursor
记录槽点数组的游标,它会随着每次槽点被处理,而自加。所以某段在处理时,通过 slots[slotCursor]
可以得到当前待入槽点位置。如下所示,当 slotCursor
长度大于大于总槽位时,说明已经插入完毕,不需要关注槽点了;或者 待入槽点位置
要比 end
还大,说明当前段没有槽点:
int slotCursor = 0;
insertSlotWithBoundary(int start, int end, TextStyle style) {
if (slotCursor >= slots.length || slots[slotCursor] > end) {
// 说明当前段没有槽点,无需处理
span.add(TextSpan(
text: src.substring(start, end),
style: style,
));
return;
}
// TODO 槽点处理
}
在某段中,可能存在 n
个槽点,把段分割为 n+1
段。结合 slotCursor
游标和 end
值,可以通过 while
循环进行遍历处理:
在进入循环时,将 slotCursor++
,需要注意截取的终点需要额外处理一下。若干槽位已经结束,或下一槽位大于 end
,说明 下一槽点不再当前段
。 将截取的终点设为 end
:
insertSlotWithBoundary(int start, int end, TextStyle style) {
// 同上,略...
// 有槽点,分割插槽
String matchStr = src.substring(start, slots[slotCursor]);
span.add(TextSpan(text: matchStr, style: style));
while (slots[slotCursor] < end) {
int slotPosition = slots[slotCursor];
slotCursor++;
int currentEndPosition = 0;
if (slotCursor == slots.length || slots[slotCursor] > end) {
// 说明插槽结束
// 说明下一槽点不再当前段
currentEndPosition = end;
} else {
currentEndPosition = slots[slotCursor];
}
// 插入槽点组件:
span.add(const WidgetSpan(child: FlutterLogo()));
String matchStr = src.substring(slotPosition, currentEndPosition);
span.add(TextSpan(
text: matchStr,
style: style,
));
if (slotCursor >= slots.length) break;
}
}
到这里,处理就完成了,虽然代码量比较少,但是其中需要考虑的点挺多的。包括校验条件、循环流程、游标处理等。在实现期间也走了不少弯路,试错花了不少时间,在调试中逐步解决问题。本以为我完成不了代码高亮的行号显示的,但在耐心和分析中还是写出来了,过程可谓是痛快的。
现在终于可以在 Flutter 中代码展示或者文本展示时加上行号了,仅以此文纪念这份自主解决问题的的愉悦感。下面是完整的 formSpan
方法,感兴趣的可以自己试一下:
TextSpan formSpan() {
List<List<int>> matches = [[1, 2], [5, 8], [14, 15]];
List<int> slots = [0, 2, 6, 8, 11, 13];
String src = "toly 1994,hello!";
List<InlineSpan> span = [];
int cursor = 0;
int slotCursor = 0;
insertSlotWithBoundary(int start, int end, TextStyle style) {
if (slotCursor>=slots.length||slots[slotCursor] > end) {
// 说明当前段没有槽点,无需处理
span.add(TextSpan(
text: src.substring(start, end),
style: style,
));
return;
}
// 有槽点,分割插槽
String matchStr = src.substring(start, slots[slotCursor]);
span.add(TextSpan(text: matchStr, style: style));
while (slots[slotCursor] < end) {
int slotPosition = slots[slotCursor];
slotCursor++;
int currentEndPosition = 0;
if (slotCursor == slots.length || slots[slotCursor] > end) {
// 说明插槽结束
// 说明下一槽点不再当前段
currentEndPosition = end;
} else {
currentEndPosition = slots[slotCursor];
}
span.add(const WidgetSpan(child: FlutterLogo()));
String matchStr2 = src.substring(slotPosition, currentEndPosition);
span.add(TextSpan(
text: matchStr2,
style: style,
));
if (slotCursor >= slots.length) break;
}
}
for (int i = 0; i < matches.length; i++) {
List<int> match = matches[i];
insertSlotWithBoundary(cursor, match[0], style0);
insertSlotWithBoundary(match[0], match[1] + 1, style1);
cursor = match[1] + 1;
}
if (cursor != src.length - 1) {
insertSlotWithBoundary(cursor,src.length, style0);
}
return TextSpan(children: span);
}
转载自:https://juejin.cn/post/7176805642303176762