nordabiz/utils/markdown.py
Maciej Pienczyn c5f724f954 feat: Add forum search, markdown, user stats, and admin bulk actions
New features implemented:
- Forum search with title/content filtering
- Solution filter (topics with marked solutions)
- Quote reply functionality with @mention
- @mentions parsing and notifications
- Simple markdown formatting (bold, italic, code, quotes, lists)
- User forum statistics tooltip (topics, replies, solutions, reactions)
- Admin bulk actions (pin/unpin, lock/unlock, status change, delete)

Files changed:
- blueprints/forum/routes.py: user_forum_stats, admin_forum_bulk_action endpoints
- templates/forum/topic.html: user stats tooltips, markdown CSS
- templates/forum/index.html: search box, solution filter
- templates/admin/forum.html: bulk selection checkboxes and action bar
- utils/markdown.py: simple forum markdown parser
- utils/notifications.py: @mention notification parsing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:11:29 +01:00

127 lines
3.4 KiB
Python

"""
Simple Markdown Parser for Forum
================================
Converts basic markdown to safe HTML.
Supports: bold, italic, code, links, lists, quotes, @mentions
"""
import re
from markupsafe import Markup, escape
def parse_forum_markdown(text):
"""
Convert markdown text to safe HTML.
Supported syntax:
- **bold** or __bold__
- *italic* or _italic_
- `inline code`
- [link text](url)
- - list items
- > quotes
- @mentions (highlighted)
Args:
text: Raw markdown text
Returns:
Markup object with safe HTML
"""
if not text:
return Markup('')
# Escape HTML first for security
text = str(escape(text))
# Process line by line for block elements
lines = text.split('\n')
result_lines = []
in_list = False
in_quote = False
for line in lines:
stripped = line.strip()
# Quote blocks (> text)
if stripped.startswith('&gt; '): # Escaped >
if not in_quote:
result_lines.append('<blockquote class="forum-quote">')
in_quote = True
result_lines.append(stripped[5:]) # Remove &gt; prefix
continue
elif in_quote:
result_lines.append('</blockquote>')
in_quote = False
# List items (- text)
if stripped.startswith('- '):
if not in_list:
result_lines.append('<ul class="forum-list">')
in_list = True
result_lines.append(f'<li>{stripped[2:]}</li>')
continue
elif in_list:
result_lines.append('</ul>')
in_list = False
result_lines.append(line)
# Close open blocks
if in_list:
result_lines.append('</ul>')
if in_quote:
result_lines.append('</blockquote>')
text = '\n'.join(result_lines)
# Inline formatting (order matters!)
# Code blocks (``` ... ```)
text = re.sub(
r'```(.*?)```',
r'<pre class="forum-code-block"><code>\1</code></pre>',
text,
flags=re.DOTALL
)
# Inline code (`code`)
text = re.sub(r'`([^`]+)`', r'<code class="forum-code">\1</code>', text)
# Bold (**text** or __text__)
text = re.sub(r'\*\*([^*]+)\*\*', r'<strong>\1</strong>', text)
text = re.sub(r'__([^_]+)__', r'<strong>\1</strong>', text)
# Italic (*text* or _text_) - careful not to match bold
text = re.sub(r'(?<!\*)\*([^*]+)\*(?!\*)', r'<em>\1</em>', text)
text = re.sub(r'(?<!_)_([^_]+)_(?!_)', r'<em>\1</em>', text)
# Links [text](url) - only allow http/https
def safe_link(match):
link_text = match.group(1)
url = match.group(2)
if url.startswith(('http://', 'https://', '/')):
return f'<a href="{url}" target="_blank" rel="noopener noreferrer" class="forum-link">{link_text}</a>'
return match.group(0) # Return original if not safe
text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', safe_link, text)
# @mentions - highlight them
text = re.sub(
r'@([\w.\-]+)',
r'<span class="forum-mention">@\1</span>',
text
)
# Convert newlines to <br> (but not inside pre/blockquote)
# Simple approach: just convert \n to <br>
text = text.replace('\n', '<br>\n')
return Markup(text)
def register_markdown_filter(app):
"""Register the markdown filter with Flask app."""
app.jinja_env.filters['forum_markdown'] = parse_forum_markdown