from flask import Flask, render_template, request, jsonify, send_file import markdown from markdown.extensions import fenced_code, tables, codehilite import re from weasyprint import HTML, CSS from io import BytesIO app = Flask(__name__) # Configure markdown with extensions md = markdown.Markdown(extensions=[ 'fenced_code', 'tables', 'codehilite', 'extra', 'nl2br' ]) def protect_math(text): """Protect LaTeX math expressions from markdown processing""" # Store math expressions math_blocks = [] # Protect display math ($$...$$) def save_display_math(match): math_blocks.append(('display', match.group(1))) return f'MATHBLOCK{len(math_blocks)-1}MATHBLOCK' # Protect inline math ($...$) def save_inline_math(match): math_blocks.append(('inline', match.group(1))) return f'MATHBLOCK{len(math_blocks)-1}MATHBLOCK' # Replace display math first (greedy, multiline) text = re.sub(r'\$\$(.*?)\$\$', save_display_math, text, flags=re.DOTALL) # Then inline math (don't allow $ inside, don't cross newlines) text = re.sub(r'\$([^\$\n]+)\$', save_inline_math, text) return text, math_blocks def restore_math(text, math_blocks): """Restore LaTeX math expressions after markdown processing""" for i, (math_type, content) in enumerate(math_blocks): placeholder = f'MATHBLOCK{i}MATHBLOCK' if math_type == 'display': replacement = f'$$\n{content}\n$$' else: # Use \( \) delimiters for inline math as they're more robust replacement = f'\\({content}\\)' # Replace both the placeholder and any HTML-escaped version text = text.replace(placeholder, replacement) return text @app.route('/') def index(): return render_template('index.html') @app.route('/render', methods=['POST']) def render_markdown(): """Convert markdown to HTML""" data = request.get_json() markdown_text = data.get('markdown', '') # Protect math expressions before markdown processing protected_text, math_blocks = protect_math(markdown_text) # Reset the markdown instance to clear state md.reset() # Convert markdown to HTML html = md.convert(protected_text) # Restore math expressions after markdown processing html = restore_math(html, math_blocks) return jsonify({'html': html}) def convert_latex_to_mathml(html_content): """Convert LaTeX expressions to MathML for PDF rendering""" from latex2mathml.converter import convert as latex_to_mathml # Convert display math $$...$$ to MathML def replace_display(match): latex = match.group(1).strip() try: mathml = latex_to_mathml(latex) return f'
{mathml}
' except Exception as e: print(f"Error converting display LaTeX: {latex[:50]}... Error: {e}") return f'
{latex}
' # Convert inline math \(...\) to MathML def replace_inline(match): latex = match.group(1).strip() try: mathml = latex_to_mathml(latex) return mathml except Exception as e: print(f"Error converting inline LaTeX: {latex[:50]}... Error: {e}") return f'{latex}' # Process display math html_content = re.sub(r'\$\$(.*?)\$\$', replace_display, html_content, flags=re.DOTALL) # Process inline math html_content = re.sub(r'\\[(](.*?)\\[)]', replace_inline, html_content, flags=re.DOTALL) return html_content @app.route('/export-pdf', methods=['POST']) def export_pdf(): """Generate PDF from rendered HTML with embedded equation images""" data = request.get_json() html_content = data.get('html', '') # Create a complete HTML document with styles full_html = f''' {html_content} ''' # Generate PDF pdf_file = BytesIO() HTML(string=full_html).write_pdf(pdf_file) pdf_file.seek(0) return send_file( pdf_file, mimetype='application/pdf', as_attachment=True, download_name='markdown-export.pdf' ) @app.route('/export-latex', methods=['POST']) def export_latex(): """Convert markdown to LaTeX document""" data = request.get_json() markdown_text = data.get('markdown', '') # Convert markdown elements to LaTeX latex_content = markdown_to_latex(markdown_text) # Create response with LaTeX file return jsonify({'latex': latex_content}) def markdown_to_latex(markdown_text): """Convert markdown syntax to LaTeX""" # Start with document structure latex = r'''\documentclass{article} \usepackage{amsmath} \usepackage{amssymb} \usepackage{graphicx} \usepackage{hyperref} \usepackage{listings} \usepackage{xcolor} \lstset{ basicstyle=\ttfamily, breaklines=true, frame=single, backgroundcolor=\color{gray!10} } \begin{document} ''' lines = markdown_text.split('\n') i = 0 in_code_block = False code_lang = '' while i < len(lines): line = lines[i] # Code blocks if line.startswith('```'): if not in_code_block: in_code_block = True code_lang = line[3:].strip() latex += r'\begin{lstlisting}' if code_lang: latex += f'[language={code_lang}]' latex += '\n' else: in_code_block = False latex += r'\end{lstlisting}' + '\n' i += 1 continue if in_code_block: latex += line + '\n' i += 1 continue # Headers if line.startswith('# '): latex += r'\section{' + line[2:] + '}\n' elif line.startswith('## '): latex += r'\subsection{' + line[3:] + '}\n' elif line.startswith('### '): latex += r'\subsubsection{' + line[4:] + '}\n' # Horizontal rule elif line.strip() == '---' or line.strip() == '***': latex += r'\hrulefill' + '\n\n' # Empty lines elif line.strip() == '': latex += '\n' # Regular paragraphs else: # Convert inline markdown processed_line = line # Bold processed_line = re.sub(r'\*\*(.+?)\*\*', r'\\textbf{\1}', processed_line) # Italic processed_line = re.sub(r'\*(.+?)\*', r'\\textit{\1}', processed_line) # Inline code processed_line = re.sub(r'`(.+?)`', r'\\texttt{\1}', processed_line) latex += processed_line + '\n' i += 1 latex += r''' \end{document} ''' return latex if __name__ == '__main__': app.run(debug=True, port=8080)