feat(forum): beautify Google Maps URLs + fix edit/quote raw content
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

Google Maps URLs can be 800+ chars of tracking data that poisoned the
forum UI. Extract the place name from /maps/place/NAME/ (or fall back
to coordinates) and render as '📍 Name'. Full URL remains in the href.

Two secondary fixes:

- Edit/quote modals were reading .innerText of the rendered reply,
  which baked the current render (including any stale/broken HTML from
  older bad renders) back into the textarea. Switched to emitting the
  raw DB content via {{ content|tojson }} so what you edit is what you
  wrote.

- @mention regex was matching '@54.1234' inside Maps URLs and similar.
  Tightened to require a letter start and non-slash/non-word lookbehind
  so coords and email-style strings pass through untouched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-04-14 14:38:59 +02:00
parent dcdaa8d338
commit 958b967df2
2 changed files with 42 additions and 16 deletions

View File

@ -1154,7 +1154,7 @@
{% if not topic.is_locked %}
<div class="user-actions">
{% if topic.author_id == current_user.id or current_user.can_moderate_forum() %}
<button type="button" class="action-btn" onclick="openEditModal('topic', {{ topic.id }}, document.getElementById('topicContent').innerText)">
<button type="button" class="action-btn" onclick="openEditModal('topic', {{ topic.id }}, {{ topic.content|tojson }})">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
Edytuj
</button>
@ -1223,7 +1223,7 @@
<button type="button" class="admin-btn admin-btn-sm" onclick="toggleSolution({{ reply.id }})" title="{% if reply.is_solution %}Usuń oznaczenie{% else %}Oznacz jako rozwiązanie{% endif %}">
</button>
<button type="button" class="admin-btn admin-btn-sm" onclick="openEditModal('reply', {{ reply.id }}, document.querySelector('#reply-{{ reply.id }} .reply-content').innerText)">
<button type="button" class="admin-btn admin-btn-sm" onclick="openEditModal('reply', {{ reply.id }}, {{ reply.content|tojson }})">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="14" height="14"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
{% if reply.is_deleted %}
@ -1301,7 +1301,7 @@
{% if not topic.is_locked %}
<div class="user-actions">
{% if reply.author_id == current_user.id %}
<button type="button" class="action-btn" onclick="openEditModal('reply', {{ reply.id }}, document.querySelector('#reply-{{ reply.id }} .reply-content').innerText)">
<button type="button" class="action-btn" onclick="openEditModal('reply', {{ reply.id }}, {{ reply.content|tojson }})">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="14" height="14"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
Edytuj
</button>
@ -1310,7 +1310,7 @@
Usuń
</button>
{% endif %}
<button type="button" class="action-btn" onclick="quoteReply('{{ reply.author.name or reply.author.email.split('@')[0] }}', document.querySelector('#reply-{{ reply.id }} .reply-content').innerText)">
<button type="button" class="action-btn" onclick="quoteReply('{{ reply.author.name or reply.author.email.split('@')[0] }}', {{ reply.content|tojson }})">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" width="14" height="14"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>
Cytuj
</button>

View File

@ -7,16 +7,37 @@ Supports: bold, italic, code, links, auto-links, lists, quotes, @mentions
"""
import re
import urllib.parse
from markupsafe import Markup, escape
def _link_display(url):
"""Shorten a URL to a human-friendly label while keeping the full href.
Google Maps URLs are especially unreadable a single place link can be
800 characters of tracking data. Extract the place name (or coordinates)
and render it as `📍 Name` instead.
"""
if 'google.' in url and '/maps/' in url:
m = re.search(r'/maps/place/([^/@?]+)', url)
if m:
name = urllib.parse.unquote(m.group(1)).replace('+', ' ').strip()
if name:
return f'📍 {name}'
m = re.search(r'@(-?\d+\.\d+),(-?\d+\.\d+)', url)
if m:
return f'📍 Mapa ({m.group(1)}, {m.group(2)})'
return '📍 Google Maps'
return url
def _autolink(text):
"""Convert bare URLs to clickable links. Works on escaped text before HTML wrapping."""
return re.sub(
r'https?://[^\s<]+',
lambda m: f'<a href="{m.group(0)}" target="_blank" rel="noopener noreferrer" class="forum-link">{m.group(0)}</a>',
text
)
def wrap(m):
url = m.group(0)
display = _link_display(url)
return f'<a href="{url}" target="_blank" rel="noopener noreferrer" class="forum-link">{display}</a>'
return re.sub(r'https?://[^\s<]+', wrap, text)
def parse_forum_markdown(text, current_user_name=None):
@ -77,12 +98,14 @@ def parse_forum_markdown(text, current_user_name=None):
text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', safe_link, text)
# Auto-link bare URLs (after [text](url) to avoid doubling)
text = re.sub(
r'(?<!href=")(?<!">)https?://[^\s<]+',
lambda m: f'<a href="{m.group(0)}" target="_blank" rel="noopener noreferrer" class="forum-link">{m.group(0)}</a>',
text
)
# Auto-link bare URLs (after [text](url) to avoid doubling).
# Beautify Google Maps URLs via _link_display so the visible label is
# a pin + place name instead of an 800-char tracking URL.
def _wrap_autolink(m):
url = m.group(0)
display = _link_display(url)
return f'<a href="{url}" target="_blank" rel="noopener noreferrer" class="forum-link">{display}</a>'
text = re.sub(r'(?<!href=")(?<!">)https?://[^\s<]+', _wrap_autolink, text)
# @mentions - highlight them; mark self-mentions with extra class
self_variants = set()
@ -95,7 +118,10 @@ def parse_forum_markdown(text, current_user_name=None):
cls = 'forum-mention forum-mention-self' if handle in self_variants else 'forum-mention'
return f'<span class="{cls}">@{m.group(1)}</span>'
text = re.sub(r'@([\w.\-]+)', _render_mention, text)
# Mentions must start with a letter and not be preceded by `/` or another
# word char — this prevents matching `@54.123` from Google Maps URLs or
# `email@host` style strings that happen to land in plaintext.
text = re.sub(r'(?<![/\w])@([a-zA-Z][\w.\-]*)', _render_mention, text)
# Now process block structure (lists, quotes, paragraphs)
lines = text.split('\n')