最近在试着调用 Telegram 的 TDLib API 拉取数据,然后在网页上渲染展示。分析下 Telegram 富文本数据的数据结构与渲染方案。
通过 API 可以获取如下的数据结构:
{
"content": "It is a test sentence.",
"entities": [
{
"type": "bold",
"offset": 3,
"length": 2
},
{
"type": "italic",
"offset": 0,
"length": 7
},
{
"type": "url",
"offset": 13,
"length": 8,
"url": "https://www.google.com"
},
{
"type": "strikethrough",
"offset": 10,
"length": 5
}
]
}
其实就是对应下面这一段富文本:
如何生成¶
可以看到,telegram 的富文本就是基于一段 plain text(content),然后在上面不断叠加一层一层 layer(entity)来实现的。每一个 entity 当中包含了格式类型,文本开始位置和长度,以及当前格式的一些附加信息。
- 在用户编辑文本的时候,选中一段文本,并施加相应的格式,就往 eneities 当中增加相应的 entity。
- 如果用户取消某一段文本的格式,就需要遍历所有 entities,找到符合对应格式的 entity,如果恰好包含所修改的区间,则需要将当前 entity 拆分为一个或两个只 entity,以剔除中间重复的区域。
如何渲染¶
看到这种模式,最自觉想到的方案就是,一层一层 entity 通过 replace 去还远到原始文本中。但很快就会发现这样行不通:通过文本替换,势必破坏原来的 offset+length 的位置关系,当要处理多个 entity 的时候出现偏移。
那么不如换个思路,一整段话 + 一组 entity,不同类型 entity 互相叠加排列组合,会出现非常多的格式,比如某些文本是 bold+italic,有些是 underline+bold+url 等等。但是无论如何,必然存在一个子串内,所有文本的格式是一样。那么,基于这个设定,我们可以将整个段落打散,生成一组原子子串,每一个子串内所有文本拥有相同的格式。
至于如何打算,只需要取所有 entity 两侧的端点,作为分割点即可:
points = entities
.map(e=>[e.offset,e.offset+e.length])
.flatten()
.uniq()
.sort()
比如上文的例子,这样一来我们就能得到一组风格点: [0,3,5,7,10,13,15,21]
使用这些分割点对原始文本进行分割,就可以得到一组子串:["it ","is"," a"," te","st ","se","ntence","."]
然后我们单独处理每一个子串,依次查找与子匹配的 entity 并覆盖其格式即可。
currentPos = 0
for (let i=0; i<segments.length(); i++) {
for (e of entities) {
if (e.offset>currentPos) continue
if (e.offset+e.length<=currentPos) continue
segments[i] = e.applyFormat(segments[i])
}
}