""" 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, current_user_name=None): """ 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; mark self-mentions with extra class self_variants = set() if current_user_name: norm = current_user_name.strip().lower() self_variants = {norm.replace(' ', '.'), norm.replace(' ', '_'), norm.replace(' ', '')} def _render_mention(m): handle = m.group(1).lower() cls = 'forum-mention forum-mention-self' if handle in self_variants else 'forum-mention' return f'@{m.group(1)}' text = re.sub(r'@([\w.\-]+)', _render_mention, 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('
') continue # Quote blocks (> text) — > because already escaped if stripped.startswith('> '): if not in_quote: 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('') in_list = False result_lines.append(stripped) # Close open blocks if in_list: result_lines.append('') if in_quote: result_lines.append('') # Join with spaces — no extra
between lines within same paragraph # Consecutive non-block lines are part of the same paragraph output = [] for i, line in enumerate(result_lines): s = line.strip() # Block elements get their own line, no extra spacing if any(s.startswith(t) for t in ['', '', '', '', '
']): 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