""" Simple Markdown Parser for Forum ================================ Converts basic markdown to safe HTML. Supports: bold, italic, code, links, auto-links, lists, quotes, @mentions """ import re from markupsafe import Markup, escape 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'{m.group(0)}', text ) def parse_forum_markdown(text): """ Convert markdown text to safe HTML. Supported syntax: - **bold** or __bold__ - *italic* or _italic_ - `inline code` - [link text](url) - bare https://... URLs (auto-linked) - - list items - > quotes - @mentions (highlighted) """ if not text: return Markup('') # Normalize line endings (Windows \r\n -> \n) text = text.replace('\r\n', '\n').replace('\r', '\n') # Escape HTML first for security text = str(escape(text)) # Apply inline formatting BEFORE block structure # This ensures URLs inside list items get linked # Code blocks (``` ... ```) text = re.sub( r'```(.*?)```', r'
\1',
text,
flags=re.DOTALL
)
# Inline code (`code`)
text = re.sub(r'`([^`]+)`', r'\1', text)
# Bold (**text** or __text__)
text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text)
text = re.sub(r'__([^_]+)__', r'\1', text)
# Italic (*text* or _text_) - careful not to match bold
text = re.sub(r'(?\1', text)
text = re.sub(r'(?\1', 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'{link_text}'
return match.group(0)
text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', safe_link, text)
# Auto-link bare URLs (after [text](url) to avoid doubling)
text = re.sub(
r'(?)https?://[^\s<]+',
lambda m: f'{m.group(0)}',
text
)
# @mentions - highlight them
text = re.sub(
r'@([\w.\-]+)',
r'@\1',
text
)
# Now process block structure (lists, quotes, paragraphs)
lines = text.split('\n')
result_lines = []
in_list = False
in_quote = False
for line in lines:
stripped = line.strip()
# Empty line = paragraph break
if not stripped:
if in_list:
result_lines.append('')
in_list = False
if in_quote:
result_lines.append('')
in_quote = False
result_lines.append('') in_quote = True result_lines.append(stripped[5:]) continue elif in_quote: result_lines.append('') in_quote = False # List items (- text) if stripped.startswith('- '): if not in_list: result_lines.append('
', '', '
']): output.append(line) else: # Regular text — join with previous regular text using space if output and output[-1] and not any(output[-1].strip().startswith(t) for t in ['', '
- ', '
', '', '
']): output[-1] = output[-1] + ' ' + line else: output.append(line) return Markup('\n'.join(output)) def register_markdown_filter(app): """Register the markdown filter with Flask app.""" app.jinja_env.filters['forum_markdown'] = parse_forum_markdown