cover

Telegram のリッチテキストレンダリングメカニズムについての簡単な分析

sorcererxw

最近、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
    }
  ]
}

実際には、以下のリッチテキストに対応しています:

🕶️
It a test sentenceです。

生成方法

Telegramのリッチテキストは、プレーンテキスト(content)を基にして、その上に次々とレイヤー(entity)を重ねて実現されています。各エンティティには、フォーマットの種類、テキストの開始位置と長さ、および現在のフォーマットに関するいくつかの追加情報が含まれています。

  • ユーザーがテキストを編集する際に、テキストの一部を選択し、適切なフォーマットを適用すると、eneities に対応する entity が追加されます。
  • ユーザーがあるテキストのフォーマットを解除した場合、すべてのエンティティを走査して、該当するフォーマットのエンティティを探し出し、そのエンティティが変更された範囲をちょうど含んでいる場合は、現在のエンティティを一つまたは二つの新しいエンティティに分割して、重複している中間の領域を取り除く必要があります。

レンダリング方法

このようなパターンを見ると、最初に思いつくのは、レイヤーごとにエンティティを replace を使って元のテキストに戻す方法です。しかし、すぐにこの方法ではうまくいかないことがわかります。テキストの置換を行うと、元の offset+length の位置関係が破壊されてしまい、複数のエンティティを処理する際にずれが生じます。

それでは、考え方を変えてみましょう。一つの段落と一連のエンティティを組み合わせることで、異なるタイプのエンティティが重なり合い、様々なフォーマットが生まれます。例えば、あるテキストは bold+italic で、別のテキストは underline+bold+url などです。しかし、どのような場合でも、必ず同じフォーマットを持つ部分文字列が存在します。この前提に基づいて、段落全体を分解し、各部分文字列が同じフォーマットを持つ一連の原子部分文字列を生成することができます。

それについての計画方法は、すべてのエンティティの両端を取り、分割点として使用するだけです:

points = entities
   .map(e=>[e.offset,e.offset+e.length])
   .flatten()
   .uniq()
   .sort()

たとえば上記の例のように、この方法で一連のスタイルポイントを得ることができます: <code>[0,3,5,7,10,13,15,21]</code>

これらの分割点を使用して元のテキストを分割すると、以下のような一連のサブストリングが得られます:["it ","is"," a"," te","st ","se","ntence","."]

次に、各サブストリングを個別に処理し、順番に対応するエンティティを検索して、そのフォーマットを上書きすればよいです。

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])
   }
}