"""Telegram-style HTML -> (text, entities), for responses of commands that run on the device (server responses arrive parsed). Offsets are UTF-16 code units, as Telegram counts them. Pure Python: tested offline. Tags: b strong i em u ins s strike del code pre (`` inside), a href (tg://user?id=N mentions), blockquote [expandable], tg-spoiler / span class="tg-spoiler", tg-emoji emoji-id, br. """ from html.parser import HTMLParser _SIMPLE = { "b": "bold", "strong": "bold", "i": "italic", "em": "italic", "u": "underline", "ins": "underline", "s": "strike", "strike": "strike", "del": "strike", "code": "code", "pre": "pre", "blockquote": "blockquote", "tg-spoiler": "spoiler", } def utf16_len(text): return len(text.encode("utf-16-le")) // 2 class _Parser(HTMLParser): def __init__(self): super().__init__(convert_charrefs=True) self.parts = [] self.length = 0 # UTF-16 units so far self.open = [] # [(tag, entity or None)] self.entities = [] def _entity(self, tag, attrs): attrs = dict(attrs) if tag == "code" and any(t == "pre" for t, _ in self.open): pre = next(e for t, e in reversed(self.open) if t == "pre") language = (attrs.get("class") or "").removeprefix("language-") if pre is not None and language: pre["language"] = language return None if tag == "span" and attrs.get("class") == "tg-spoiler": return {"type": "spoiler"} if tag in _SIMPLE: entity = {"type": _SIMPLE[tag]} if tag == "blockquote" and "expandable" in attrs: entity["collapsed"] = True return entity if tag == "a" and attrs.get("href"): href = attrs["href"] if href.startswith("tg://user?id="): try: return {"type": "mention_name", "user_id": int(href.split("=", 1)[1])} except ValueError: pass return {"type": "text_link", "url": href} if tag == "tg-emoji": try: return {"type": "custom_emoji", "document_id": int(attrs.get("emoji-id"))} except (TypeError, ValueError): return None return None def handle_starttag(self, tag, attrs): if tag == "br": self.handle_data("\n") return entity = self._entity(tag, attrs) if entity is not None: entity["offset"] = self.length self.open.append((tag, entity)) def handle_endtag(self, tag): for index in range(len(self.open) - 1, -1, -1): if self.open[index][0] == tag: _, entity = self.open.pop(index) if entity is not None: entity["length"] = self.length - entity["offset"] if entity["length"] > 0: self.entities.append(entity) return def handle_data(self, data): self.parts.append(data) self.length += utf16_len(data) def parse(html): """(plain text, entities) of `html`; entities ordered by offset.""" parser = _Parser() parser.feed(html or "") parser.close() entities = sorted(parser.entities, key=lambda e: (e["offset"], -e["length"])) return "".join(parser.parts), entities def escape(text): """`text` made safe to put into a response.""" return str(text).replace("&", "&").replace("<", "<").replace(">", ">")