Fix Matrix formatting: build HTML directly instead of markdown conversion

The markdown library's conversion collapsed blockquote lines into a
single paragraph, causing all deal info to render on one line in Matrix
clients. Build HTML directly with <br> tags for reliable line breaks.
Also adds html.escape() for proper escaping of titles and prices, and
removes the unused markdown dependency.

https://claude.ai/code/session_01B7YPGrE3NatkwadCXVjv2j
This commit is contained in:
Claude
2026-02-28 05:23:37 +00:00
parent 949eceb983
commit c5ae8543e1
2 changed files with 35 additions and 55 deletions

View File

@@ -1,25 +1,18 @@
"""Message formatting for Matrix deal posts. """Message formatting for Matrix deal posts.
Produces both a plain-text fallback and HTML formatted body for Matrix messages. Produces both a plain-text fallback and HTML formatted body for Matrix messages.
HTML is built directly using the Matrix-supported HTML subset rather than going
through a Markdown-to-HTML converter, which collapses line breaks.
""" """
from datetime import datetime, timezone from datetime import datetime, timezone
from html import escape
import markdown
from .cheapshark import CheapSharkDeal from .cheapshark import CheapSharkDeal
from .currency import format_price from .currency import format_price
from .epic import EpicFreeGame from .epic import EpicFreeGame
from .itad_deals import ITADDeal from .itad_deals import ITADDeal
_md = markdown.Markdown()
def _render_html(md_text: str) -> str:
"""Convert Markdown to HTML, resetting the parser between calls."""
_md.reset()
return _md.convert(md_text)
def format_deal(deal: CheapSharkDeal, is_historical_low: bool = False) -> tuple[str, str]: def format_deal(deal: CheapSharkDeal, is_historical_low: bool = False) -> tuple[str, str]:
"""Format a CheapShark deal into (plain_text, html) for Matrix. """Format a CheapShark deal into (plain_text, html) for Matrix.
@@ -32,22 +25,18 @@ def format_deal(deal: CheapSharkDeal, is_historical_low: bool = False) -> tuple[
sale_multi = format_price(sale_price) sale_multi = format_price(sale_price)
normal_display = format_price(normal_price) normal_display = format_price(normal_price)
title = escape(deal.title)
lines = [ html_lines = [
f"**🎮 [DEAL] {deal.title}**", f"<strong>🎮 [DEAL] {title}</strong>",
f"> {discount}% off on {deal.store_name} ~~{normal_display}~~", f"{discount}% off on {escape(deal.store_name)} <del>{escape(normal_display)}</del>",
f"> 💰 **{sale_multi}**", f"💰 <strong>{escape(sale_multi)}</strong>",
] ]
if is_historical_low: if is_historical_low:
lines.append("> 🏆 _All-time low!_") html_lines.append("🏆 <em>All-time low!</em>")
html_lines.append(f'🔗 <a href="{escape(deal.deal_url)}">View Deal</a>')
html = "<br>\n".join(html_lines)
lines.append(f"> 🔗 [View Deal]({deal.deal_url})")
md_text = "\n".join(lines)
html = _render_html(md_text)
# Plain text fallback — strip markdown syntax
plain_lines = [ plain_lines = [
f"🎮 [DEAL] {deal.title}", f"🎮 [DEAL] {deal.title}",
f" {discount}% off on {deal.store_name} (was {normal_display})", f" {discount}% off on {deal.store_name} (was {normal_display})",
@@ -56,7 +45,6 @@ def format_deal(deal: CheapSharkDeal, is_historical_low: bool = False) -> tuple[
if is_historical_low: if is_historical_low:
plain_lines.append(" 🏆 All-time low!") plain_lines.append(" 🏆 All-time low!")
plain_lines.append(f" 🔗 {deal.deal_url}") plain_lines.append(f" 🔗 {deal.deal_url}")
plain_text = "\n".join(plain_lines) plain_text = "\n".join(plain_lines)
return plain_text, html return plain_text, html
@@ -69,20 +57,17 @@ def format_itad_deal(deal: ITADDeal) -> tuple[str, str]:
""" """
sale_multi = format_price(deal.sale_price) sale_multi = format_price(deal.sale_price)
normal_display = format_price(deal.normal_price) normal_display = format_price(deal.normal_price)
title = escape(deal.title)
lines = [ html_lines = [
f"**🎮 [DEAL] {deal.title}**", f"<strong>🎮 [DEAL] {title}</strong>",
f"> {deal.discount}% off on {deal.shop_name} ~~{normal_display}~~", f"{deal.discount}% off on {escape(deal.shop_name)} <del>{escape(normal_display)}</del>",
f"> 💰 **{sale_multi}**", f"💰 <strong>{escape(sale_multi)}</strong>",
] ]
if deal.is_historical_low: if deal.is_historical_low:
lines.append("> 🏆 _All-time low!_") html_lines.append("🏆 <em>All-time low!</em>")
html_lines.append(f'🔗 <a href="{escape(deal.url)}">View Deal</a>')
lines.append(f"> 🔗 [View Deal]({deal.url})") html = "<br>\n".join(html_lines)
md_text = "\n".join(lines)
html = _render_html(md_text)
plain_lines = [ plain_lines = [
f"🎮 [DEAL] {deal.title}", f"🎮 [DEAL] {deal.title}",
@@ -92,7 +77,6 @@ def format_itad_deal(deal: ITADDeal) -> tuple[str, str]:
if deal.is_historical_low: if deal.is_historical_low:
plain_lines.append(" 🏆 All-time low!") plain_lines.append(" 🏆 All-time low!")
plain_lines.append(f" 🔗 {deal.url}") plain_lines.append(f" 🔗 {deal.url}")
plain_text = "\n".join(plain_lines) plain_text = "\n".join(plain_lines)
return plain_text, html return plain_text, html
@@ -103,26 +87,25 @@ def format_epic_free(game: EpicFreeGame) -> tuple[str, str]:
Returns (body, formatted_body). Returns (body, formatted_body).
""" """
lines = [ title = escape(game.title)
f"**🆓 [FREE] {game.title}**",
"> Free on Epic Games Store", html_lines = [
f"<strong>🆓 [FREE] {title}</strong>",
"Free on Epic Games Store",
] ]
if game.end_date: if game.end_date:
try: try:
end_dt = datetime.fromisoformat(game.end_date.replace("Z", "+00:00")) end_dt = datetime.fromisoformat(game.end_date.replace("Z", "+00:00"))
date_str = end_dt.strftime("%B %-d") date_str = end_dt.strftime("%B %-d")
lines.append(f"> 📅 _Free until {date_str}_") html_lines.append(f"📅 <em>Free until {date_str}</em>")
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
if game.url: if game.url:
lines.append(f"> 🔗 [Claim Now]({game.url})") html_lines.append(f'🔗 <a href="{escape(game.url)}">Claim Now</a>')
html = "<br>\n".join(html_lines)
md_text = "\n".join(lines)
html = _render_html(md_text)
# Plain text fallback
plain_lines = [ plain_lines = [
f"🆓 [FREE] {game.title}", f"🆓 [FREE] {game.title}",
" Free on Epic Games Store", " Free on Epic Games Store",
@@ -136,7 +119,6 @@ def format_epic_free(game: EpicFreeGame) -> tuple[str, str]:
pass pass
if game.url: if game.url:
plain_lines.append(f" 🔗 {game.url}") plain_lines.append(f" 🔗 {game.url}")
plain_text = "\n".join(plain_lines) plain_text = "\n".join(plain_lines)
return plain_text, html return plain_text, html
@@ -144,24 +126,24 @@ def format_epic_free(game: EpicFreeGame) -> tuple[str, str]:
def format_epic_upcoming(game: EpicFreeGame) -> tuple[str, str]: def format_epic_upcoming(game: EpicFreeGame) -> tuple[str, str]:
"""Format an upcoming Epic free game into (plain_text, html) for Matrix.""" """Format an upcoming Epic free game into (plain_text, html) for Matrix."""
lines = [ title = escape(game.title)
f"**📢 [UPCOMING FREE] {game.title}**",
"> Coming soon — Free on Epic Games Store", html_lines = [
f"<strong>📢 [UPCOMING FREE] {title}</strong>",
"Coming soon — Free on Epic Games Store",
] ]
if game.end_date: if game.end_date:
try: try:
end_dt = datetime.fromisoformat(game.end_date.replace("Z", "+00:00")) end_dt = datetime.fromisoformat(game.end_date.replace("Z", "+00:00"))
date_str = end_dt.strftime("%B %-d") date_str = end_dt.strftime("%B %-d")
lines.append(f"> 📅 _Free until {date_str}_") html_lines.append(f"📅 <em>Free until {date_str}</em>")
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
if game.url: if game.url:
lines.append(f"> 🔗 [Store Page]({game.url})") html_lines.append(f'🔗 <a href="{escape(game.url)}">Store Page</a>')
html = "<br>\n".join(html_lines)
md_text = "\n".join(lines)
html = _render_html(md_text)
plain_lines = [ plain_lines = [
f"📢 [UPCOMING FREE] {game.title}", f"📢 [UPCOMING FREE] {game.title}",
@@ -169,7 +151,6 @@ def format_epic_upcoming(game: EpicFreeGame) -> tuple[str, str]:
] ]
if game.url: if game.url:
plain_lines.append(f" 🔗 {game.url}") plain_lines.append(f" 🔗 {game.url}")
plain_text = "\n".join(plain_lines) plain_text = "\n".join(plain_lines)
return plain_text, html return plain_text, html

View File

@@ -2,5 +2,4 @@ matrix-nio>=0.21.0
httpx>=0.25.0 httpx>=0.25.0
aiosqlite>=0.19.0 aiosqlite>=0.19.0
apscheduler>=3.10.0 apscheduler>=3.10.0
markdown>=3.5.0
python-dotenv>=1.0.0 python-dotenv>=1.0.0