Spaces:
Running
Running
File size: 12,010 Bytes
21cde0c d55a0af 21cde0c 6c3a31d 21cde0c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 | """Turning the formatting a person puts on a caption into the HTML that is stored, and showing it back."""
import random
import pytest
from telethon.extensions import html as telegram_html
from telethon.helpers import add_surrogate, del_surrogate
from telethon.tl import types
from bot.richtext import from_entities, preview
from .tgfakes import telegram_text
def html_of(markup, start=0):
text, entities = telegram_text(markup)
return from_entities(text, entities, start)
def formats_of(html_text):
"""What Telegram would show for stored HTML: ``{(kind, offset, length, url)}``, from Telethon's own parser."""
_text, entities = telegram_html.parse(html_text)
return {(type(e).__name__, e.offset, e.length, getattr(e, "url", None)) for e in entities}
# ---------------------------------------------------------------------------
# the formatting that is kept
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("markup, expected", [
("[b]Tokyo[/b] Revengers", "<b>Tokyo</b> Revengers"),
("a [i]b[/i] c", "a <i>b</i> c"),
("a [u]b[/u] c", "a <u>b</u> c"),
("a [s]b[/s] c", "a <s>b</s> c"),
("a [code]b[/code] c", "a <code>b</code> c"),
("a [a=https://t.me/x]b[/a] c", 'a <a href="https://t.me/x">b</a> c'),
("a [quote]b[/quote]", "a <blockquote>b</blockquote>"),
("[pre=python]print(1)[/pre]", '<pre><code class="language-python">print(1)</code></pre>'),
("[pre]x = 1[/pre]", "<pre>x = 1</pre>"),
])
def test_each_kind_of_formatting_becomes_its_tag(markup, expected):
assert html_of(markup) == expected
def test_a_collapsed_quote_stays_collapsed():
entity = types.MessageEntityBlockquote(0, 3, collapsed=True)
assert from_entities("one", [entity]) == "<blockquote expandable>one</blockquote>"
def test_a_mention_of_a_person_keeps_its_link():
assert from_entities("hi Sam", [types.MessageEntityMentionName(3, 3, 12345)]) == 'hi <a href="tg://user?id=12345">Sam</a>'
def test_bold_italic_and_a_link_in_one_caption():
markup = "[b]Watch[/b] {title} [i]episode {episode}[/i] on [a=https://t.me/Anime_X]our channel[/a]"
assert html_of(markup) == '<b>Watch</b> {title} <i>episode {episode}</i> on <a href="https://t.me/Anime_X">our channel</a>'
def test_formatting_that_is_not_the_persons_own_is_left_out():
"""What Telegram finds by itself comes back by itself when the caption is sent; spoilers and custom emoji are not kept."""
assert html_of("[tag]#anime[/tag] [at]@channel[/at] [spoiler]secret[/spoiler] /start") is None
plain, entities = telegram_text("link https://example.com mail a@b.co")
entities += [types.MessageEntityUrl(5, 19), types.MessageEntityEmail(30, 6), types.MessageEntityCustomEmoji(0, 1, 5)]
assert from_entities(plain, entities) is None
assert html_of("[spoiler]a[/spoiler] [b]b[/b] [tag]#c[/tag]") == "a <b>b</b> #c"
@pytest.mark.parametrize("text, entities", [("just words", None), ("just words", []), ("", [types.MessageEntityBold(0, 1)])])
def test_no_formatting_means_nothing_to_convert(text, entities):
assert from_entities(text, entities) is None
def test_text_that_looks_like_html_is_escaped_once_formatting_is_used():
assert html_of("[b]Tom & Jerry[/b] <3 > <i>not a tag</i>") == "<b>Tom & Jerry</b> <3 > <i>not a tag</i>"
def test_a_link_address_survives_the_html():
url = 'https://example.com/?a=1©=2&q="z"'
stored = html_of(f"see [a={url}]this[/a]")
assert "&copy=2" in stored # written out, not read as the Β© character
assert formats_of(stored) == {("MessageEntityTextUrl", 4, 4, url)}
def test_emoji_do_not_move_the_formatting():
"""Telegram counts an emoji outside the basic plane (and each part of a family emoji) as two units."""
assert html_of("π₯π₯ [b]Hello[/b] π¨βπ©βπ§ [i]x[/i]") == "π₯π₯ <b>Hello</b> π¨βπ©βπ§ <i>x</i>"
def test_a_prefix_in_front_is_skipped():
text, entities = telegram_text("/words [b]Hi[/b] there")
assert from_entities(text, entities, start=len("/words ")) == "<b>Hi</b> there"
whole = types.MessageEntityBold(0, len("/words Hi")) # formatting that began inside the prefix is cut where the caption starts
assert from_entities("/words Hi", [whole], start=len("/words ")) == "<b>Hi</b>"
def test_start_counts_characters_not_utf16_units():
text, entities = telegram_text("π₯ hi [b]x[/b]")
assert from_entities(text, entities, start=2) == "hi <b>x</b>"
def test_spaces_around_the_caption_are_dropped_with_their_formatting():
assert html_of(" [b]Hi[/b] ") == "<b>Hi</b>"
assert html_of("[b]Hi [/b]") == "<b>Hi</b>"
assert html_of("[b] [/b]") is None
# ---------------------------------------------------------------------------
# formats that meet
# ---------------------------------------------------------------------------
def test_nested_formatting_stays_nested():
assert html_of("[b]a [i]b[/i] c[/b]") == "<b>a <i>b</i> c</b>"
def test_formats_that_only_partly_overlap_are_split_so_the_tags_nest():
stored = html_of("[b]Hel[i]lo[/b] wo[/i]rld")
assert stored == "<b>Hel<i>lo</i></b><i> wo</i>rld"
# the same words are bold and italic as before
text, entities = telegram_html.parse(stored)
units = add_surrogate(text)
bold = "".join(units[e.offset:e.offset + e.length] for e in entities if isinstance(e, types.MessageEntityBold))
italic = "".join(units[e.offset:e.offset + e.length] for e in entities if isinstance(e, types.MessageEntityItalic))
assert (bold, italic) == ("Hello", "lo wo")
def test_two_kinds_on_the_same_words():
stored = html_of("[b][i]x[/i][/b] y")
assert formats_of(stored) == {("MessageEntityBold", 0, 1, None), ("MessageEntityItalic", 0, 1, None)}
def test_the_same_kind_twice_over_the_same_words_is_one_tag():
"""The HTML parser can't nest a tag in itself, so ``<b>a<b>b</b>c</b>`` would lose its outer bold."""
text, entities = "abcde", [types.MessageEntityBold(0, 3), types.MessageEntityBold(1, 3), types.MessageEntityBold(4, 1)]
stored = from_entities(text, entities)
assert stored == "<b>abcde</b>"
assert formats_of(stored) == {("MessageEntityBold", 0, 5, None)}
def test_of_two_overlapping_links_the_later_one_gets_what_is_left():
entities = [types.MessageEntityTextUrl(0, 5, "https://a.io"), types.MessageEntityTextUrl(3, 5, "https://b.io")]
assert from_entities("abcdefgh", entities) == '<a href="https://a.io">abcde</a><a href="https://b.io">fgh</a>'
# ---------------------------------------------------------------------------
# {variables} are replaced as a whole, so a format must not cut one in two
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("markup, expected", [
("Episode {[b]episode[/b]} here", "Episode <b>{episode}</b> here"), # double-tapping the word leaves the braces out
("[b]{epis[/b]ode} here", "<b>{episode}</b> here"),
("Episode [b]{epis[/b]ode} here", "Episode <b>{episode}</b> here"),
("Ep {epis[b]ode} here[/b]", "Ep <b>{episode} here</b>"),
("[b]{title} {epis[/b]ode}", "<b>{title} {episode}</b>"),
("[a=https://x.io]{ti[/a]tle}", '<a href="https://x.io">{title}</a>'),
("[b]Ep[/b] {episode} [i]x[/i]", "<b>Ep</b> {episode} <i>x</i>"), # a format that does not touch a variable is left alone
("{[b]nonsense[/b]}", "{<b>nonsense</b>}"), # only the variables the caption knows are held together
("Ep {[b]2episode[/b]} here", "Ep <b>{2episode}</b> here"), # the padded episode variables are held together too
("[b]{3epis[/b]ode}", "<b>{3episode}</b>"),
])
def test_a_format_never_cuts_a_variable_in_two(markup, expected):
assert html_of(markup) == expected
# ---------------------------------------------------------------------------
# it always says the same as the message did
# ---------------------------------------------------------------------------
def _coverage(text, entities):
units = add_surrogate(text)
covered = [set() for _ in units]
for e in entities:
for i in range(max(e.offset, 0), min(e.offset + e.length, len(units))):
covered[i].add((type(e).__name__, getattr(e, "url", None)))
return covered
def test_random_messages_read_back_exactly_the_way_they_were_formatted():
"""Whatever mix of overlapping formats, emoji and characters that need escaping: Telethon's own parser (which sends the
caption) reads back the same text with every character formatted the way it was."""
rng = random.Random(11)
alphabet = list("abc def") + ["π₯", "&", "<", ">", "\n", "Γ©", "π¨βπ©βπ§", '"', "'"]
kinds = [types.MessageEntityBold, types.MessageEntityItalic, types.MessageEntityUnderline, types.MessageEntityStrike, types.MessageEntityCode]
def off_the_middle_of_an_emoji(units, at):
while at < len(units) and "\udc00" <= units[at] <= "\udfff":
at += 1
return at
checked = 0
for _ in range(800):
text = "".join(rng.choice(alphabet) for _ in range(rng.randint(1, 16)))
units = add_surrogate(text)
entities = []
for _ in range(rng.randint(1, 5)):
a = rng.randrange(len(units))
a, b = off_the_middle_of_an_emoji(units, a), off_the_middle_of_an_emoji(units, rng.randint(a + 1, len(units)))
if b > a:
entities.append(rng.choice(kinds)(a, b - a))
cuts = sorted(rng.sample(range(len(units) + 1), min(len(units) + 1, 4))) # links that do not overlap one another
for n in range(0, len(cuts) - 1, 2):
a, b = off_the_middle_of_an_emoji(units, cuts[n]), off_the_middle_of_an_emoji(units, cuts[n + 1])
if b > a and rng.random() < 0.5:
entities.append(types.MessageEntityTextUrl(a, b - a, f'https://x.io/?n={n}&q="a"'))
stored = from_entities(text, entities)
if stored is None:
continue
checked += 1
lead = len(units) - len(units.lstrip())
moved = [type(e)(e.offset - lead, e.length, **({"url": e.url} if hasattr(e, "url") else {})) for e in entities]
read_text, read_entities = telegram_html.parse(stored)
assert read_text == text.strip(), stored
assert _coverage(read_text, read_entities) == _coverage(text.strip(), moved), (text, stored)
assert checked > 300
# ---------------------------------------------------------------------------
# showing a stored caption
# ---------------------------------------------------------------------------
def test_a_stored_caption_is_shown_with_its_formatting():
assert preview("<b>{filename}</b> Β· {size}", 60) == "<b>{filename}</b> Β· {size}"
stored = html_of("[b]Watch[/b] {title} [i]episode {episode}[/i] on [a=https://t.me/Anime_X]our channel[/a]")
assert preview(stored, 1000) == stored
def test_typed_html_is_shown_safely_whatever_was_typed():
for typed in ("</b>stray <i>open", "<b>x</b></b><u>", "<i><b>a</i></b>", "<<<>>>", "<a href=>x</a>", "<b>"):
shown = preview(typed, 60)
assert telegram_html.parse(shown)[0] == (telegram_html.parse(typed)[0] or "β") # the same words
assert preview(shown, 60) == shown # and what is shown is already well-formed: showing it again changes nothing
assert preview("</b>stray <i>open", 60) == "stray open"
def test_special_characters_are_escaped_when_shown():
assert preview("Tom & Jerry <3 >", 60) == "Tom & Jerry <3 >"
def test_a_long_caption_is_cut_and_the_tags_still_balance():
shown = preview("<b>" + "x" * 100 + "</b>", 20)
assert shown == "<b>" + "x" * 19 + "</b>β¦"
assert preview("short", 20) == "short"
def test_a_cut_never_splits_an_emoji():
shown = preview("π₯" * 30, 11)
assert shown == "π₯π₯π₯π₯π₯β¦"
assert del_surrogate(add_surrogate(shown)) == shown
def test_an_empty_caption_is_a_dash():
assert preview("", 20) == "β"
|