"""Plex text fields as plain text.
Plex hands a summary over as the agent wrote it, and some agents (a Hungarian TV listing, for one)
write HTML: `
…
` paragraphs and `á`-style entities. The app renders summaries as text,
so the tags showed literally on the info page and the hover card, and a card's 300-character cut
could land inside one. Every summary and tagline is stored through `plain_text`, and the migration
that cleaned the existing rows calls it too, so the two cannot disagree.
"""
import html
import re
_BREAK = re.compile(r"
", re.IGNORECASE)
# A paragraph or block boundary: a blank line between the two. Only a tag name ends the match, so a
# bare "<" in prose ("a < b", "<3") is left alone.
_BLOCK = re.compile(r"?(?:p|div|li|ul|ol|h[1-6]|blockquote)\b[^>]*>", re.IGNORECASE)
_TAG = re.compile(r"?[a-zA-Z][^>]*>")
_BLANKS = re.compile(r"[ \t]*\n[ \t]*\n\s*")
def plain_text(value: str | None) -> str | None:
"""`value` with its markup gone: line breaks kept as newlines, paragraphs as a blank line,
every other tag dropped and entities decoded. None or an all-blank result stays None."""
if value is None:
return None
if "<" in value or "&" in value:
value = _BREAK.sub("\n", value)
value = _BLOCK.sub("\n\n", value)
value = _TAG.sub("", value)
value = html.unescape(value)
value = _BLANKS.sub("\n\n", value)
return value.strip() or None