3939# Upstream: /`+/g -- runs of one or more backticks.
4040BACKTICKS = re .compile (r"`+" )
4141
42+ # The exact set of characters JS ``String.prototype.trim``/``trimEnd`` removes:
43+ # the ECMAScript WhiteSpace set plus the LineTerminator set. Python's bare
44+ # ``str.strip()``/``str.rstrip()`` strip a broader Unicode-whitespace set (e.g.
45+ # the C0 separators ``\x1c``-``\x1f`` and NEL ``\x85``, which JS keeps; and they
46+ # do NOT strip the BOM ````, which JS removes). Passing this explicit
47+ # string to ``strip``/``rstrip`` matches JS character-for-character.
48+ JS_WHITESPACE = (
49+ "\t \n \v \f \r " # \t \n \v \f \r and SPACE
50+ " " # NO-BREAK SPACE
51+ " " # OGHAM SPACE MARK
52+ " " # EN QUAD..HAIR SPACE
53+ "
" # LINE SEPARATOR
54+ "
" # PARAGRAPH SEPARATOR
55+ " " # NARROW NO-BREAK SPACE
56+ " " # MEDIUM MATHEMATICAL SPACE
57+ " " # IDEOGRAPHIC SPACE
58+ "" # ZERO WIDTH NO-BREAK SPACE (BOM)
59+ )
60+
61+
62+ def _present (value : TelegramRichText | None ) -> bool :
63+ """Match JS truthiness for an optional ``TelegramRichText`` field.
64+
65+ Upstream gates these fields with ``value.x ? ... : ""``. The only values
66+ a ``TelegramRichText`` (``str | list | object``) can take that differ from
67+ Python truthiness is the empty list ``[]``: JS arrays are ALWAYS truthy, so
68+ ``credit: []`` renders upstream but a bare ``if value.get("credit")`` would
69+ skip it in Python. We therefore render whenever the value is present and is
70+ not the empty string ``""`` (the only falsy ``TelegramRichText`` in JS):
71+
72+ - ``None`` / absent -> skip (JS ``undefined`` is falsy)
73+ - ``""`` -> skip (JS ``""`` is falsy)
74+ - ``[]`` -> render (JS arrays are truthy)
75+ - ``[span]`` / ``"text"`` / object -> render (truthy)
76+ """
77+ return value is not None and value != ""
78+
4279
4380def truncate_rich_markdown (markdown : str ) -> str :
4481 """Truncate *markdown* to the Telegram rich-message character limit.
@@ -81,7 +118,7 @@ def _inline_code(value: str) -> str:
81118 runs = BACKTICKS .findall (value )
82119 size = max (1 , * (len (run ) + 1 for run in runs )) if runs else 1
83120 marker = "`" * size
84- has_boundary_space = value .startswith (" " ) and value .endswith (" " ) and len (value .strip ()) > 0
121+ has_boundary_space = value .startswith (" " ) and value .endswith (" " ) and len (value .strip (JS_WHITESPACE )) > 0
85122 padding = " " if value .startswith ("`" ) or value .endswith ("`" ) or has_boundary_space else ""
86123 return f"{ marker } { padding } { value } { padding } { marker } "
87124
@@ -196,46 +233,46 @@ def _plain(markdown: TelegramRichText) -> str:
196233def _caption (value : TelegramRichCaption | None = None ) -> str :
197234 if not value :
198235 return ""
199- credit = f"\n { _text (value ['credit' ])} " if value .get ("credit" ) else ""
236+ credit = f"\n { _text (value ['credit' ])} " if _present ( value .get ("credit" ) ) else ""
200237 return f"{ _text (value ['text' ])} { credit } "
201238
202239
203240def _plain_caption (value : TelegramRichCaption | None = None ) -> str :
204241 if not value :
205242 return ""
206- credit = f"\n { _plain (value ['credit' ])} " if value .get ("credit" ) else ""
243+ credit = f"\n { _plain (value ['credit' ])} " if _present ( value .get ("credit" ) ) else ""
207244 return f"{ _plain (value ['text' ])} { credit } "
208245
209246
210247def _cell (value : TelegramRichCell ) -> str :
211- return _text (value ["text" ]) if value .get ("text" ) else ""
248+ return _text (value ["text" ]) if _present ( value .get ("text" ) ) else ""
212249
213250
214251def _item (value : TelegramRichItem ) -> str :
215252 checked = ""
216253 if value .get ("has_checkbox" ):
217254 checked = "[x] " if value .get ("is_checked" ) else "[ ] "
218255 content = "\n \n " .join (_block (b ) for b in value ["blocks" ]).replace ("\n " , "\n " )
219- return f"{ value ['label' ]} { checked } { content } " .rstrip ()
256+ return f"{ value ['label' ]} { checked } { content } " .rstrip (JS_WHITESPACE )
220257
221258
222259def _plain_item (value : TelegramRichItem ) -> str :
223260 checked = ""
224261 if value .get ("has_checkbox" ):
225262 checked = "[x] " if value .get ("is_checked" ) else "[ ] "
226263 content = "\n " .join (b for b in (_plain_block (blk ) for blk in value ["blocks" ]) if b ).replace ("\n " , "\n " )
227- return f"{ value ['label' ]} { checked } { content } " .rstrip ()
264+ return f"{ value ['label' ]} { checked } { content } " .rstrip (JS_WHITESPACE )
228265
229266
230267def _table (value : TelegramRichBlockTable ) -> str :
231268 rows = [f"| { ' | ' .join (_cell (c ) for c in row )} |" for row in value ["cells" ]]
232269 if len (rows ) == 0 :
233- return _text (value ["caption" ]) if value .get ("caption" ) else ""
270+ return _text (value ["caption" ]) if _present ( value .get ("caption" ) ) else ""
234271
235272 columns = max (len (row ) for row in value ["cells" ])
236273 separator = f"| { ' | ' .join ('---' for _ in range (columns ))} |"
237274 content = "\n " .join ([rows [0 ], separator , * rows [1 :]])
238- return f"{ _text (value ['caption' ])} \n \n { content } " if value .get ("caption" ) else content
275+ return f"{ _text (value ['caption' ])} \n \n { content } " if _present ( value .get ("caption" ) ) else content
239276
240277
241278def _quote (value : str ) -> str :
@@ -261,10 +298,10 @@ def _block(value: TelegramRichBlock) -> str:
261298 return "\n " .join (_item (i ) for i in value ["items" ])
262299 case "blockquote" :
263300 content = "\n \n " .join (_block (b ) for b in value ["blocks" ])
264- credit = f"\n \n { _text (value ['credit' ])} " if value .get ("credit" ) else ""
301+ credit = f"\n \n { _text (value ['credit' ])} " if _present ( value .get ("credit" ) ) else ""
265302 return _quote (f"{ content } { credit } " )
266303 case "pullquote" :
267- credit = f"\n \n { _text (value ['credit' ])} " if value .get ("credit" ) else ""
304+ credit = f"\n \n { _text (value ['credit' ])} " if _present ( value .get ("credit" ) ) else ""
268305 return _quote (f"{ _text (value ['text' ])} { credit } " )
269306 case "collage" | "slideshow" :
270307 content = "\n \n " .join (b for b in (_block (blk ) for blk in value ["blocks" ]) if b )
@@ -295,21 +332,22 @@ def _plain_block(value: TelegramRichBlock) -> str:
295332 return "\n " .join (_plain_item (i ) for i in value ["items" ])
296333 case "blockquote" :
297334 content = "\n \n " .join (b for b in (_plain_block (blk ) for blk in value ["blocks" ]) if b )
298- credit = f"\n \n { _plain (value ['credit' ])} " if value .get ("credit" ) else ""
335+ credit = f"\n \n { _plain (value ['credit' ])} " if _present ( value .get ("credit" ) ) else ""
299336 return f"{ content } { credit } "
300337 case "pullquote" :
301- credit = f"\n \n { _plain (value ['credit' ])} " if value .get ("credit" ) else ""
338+ credit = f"\n \n { _plain (value ['credit' ])} " if _present ( value .get ("credit" ) ) else ""
302339 return f"{ _plain (value ['text' ])} { credit } "
303340 case "collage" | "slideshow" :
304341 content = "\n \n " .join (b for b in (_plain_block (blk ) for blk in value ["blocks" ]) if b )
305342 description = _plain_caption (value .get ("caption" ))
306343 return "\n \n " .join (part for part in (content , description ) if part )
307344 case "table" :
308345 rows = [
309- "\t " .join (_plain (entry ["text" ]) if entry .get ("text" ) else "" for entry in row ) for row in value ["cells" ]
346+ "\t " .join (_plain (entry ["text" ]) if _present (entry .get ("text" )) else "" for entry in row )
347+ for row in value ["cells" ]
310348 ]
311349 content = "\n " .join (rows )
312- return f"{ _plain (value ['caption' ])} \n \n { content } " if value .get ("caption" ) else content
350+ return f"{ _plain (value ['caption' ])} \n \n { content } " if _present ( value .get ("caption" ) ) else content
313351 case "details" :
314352 body = "\n \n " .join (b for b in (_plain_block (blk ) for blk in value ["blocks" ]) if b )
315353 return f"{ _plain (value ['summary' ])} \n \n { body } "
@@ -403,12 +441,12 @@ def _media(blocks: list[TelegramRichBlock], result: list[RichMedia]) -> None:
403441
404442def rich_message_to_markdown (message : TelegramRichMessage ) -> str :
405443 """Render a Telegram rich message to Markdown."""
406- return "\n \n " .join (b for b in (_block (blk ) for blk in message ["blocks" ]) if b ).strip ()
444+ return "\n \n " .join (b for b in (_block (blk ) for blk in message ["blocks" ]) if b ).strip (JS_WHITESPACE )
407445
408446
409447def rich_message_to_text (message : TelegramRichMessage ) -> str :
410448 """Render a Telegram rich message to plain text."""
411- return "\n \n " .join (b for b in (_plain_block (blk ) for blk in message ["blocks" ]) if b ).strip ()
449+ return "\n \n " .join (b for b in (_plain_block (blk ) for blk in message ["blocks" ]) if b ).strip (JS_WHITESPACE )
412450
413451
414452def rich_message_media (message : TelegramRichMessage ) -> list [RichMedia ]:
0 commit comments