340 lines
9.7 KiB
Python
340 lines
9.7 KiB
Python
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'<div style="text-align: center; margin: 1em 0;">{mathml}</div>'
|
|
except Exception as e:
|
|
print(f"Error converting display LaTeX: {latex[:50]}... Error: {e}")
|
|
return f'<div style="text-align: center; margin: 1em 0; font-family: monospace;">{latex}</div>'
|
|
|
|
# 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'<span style="font-family: monospace;">{latex}</span>'
|
|
|
|
# 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'''
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<style>
|
|
@page {{
|
|
size: A4;
|
|
margin: 2cm;
|
|
}}
|
|
body {{
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
line-height: 1.8;
|
|
color: #333;
|
|
font-size: 11pt;
|
|
}}
|
|
h1 {{
|
|
font-size: 2em;
|
|
margin: 0.67em 0;
|
|
border-bottom: 2px solid #eee;
|
|
padding-bottom: 0.3em;
|
|
}}
|
|
h2 {{
|
|
font-size: 1.5em;
|
|
margin: 0.75em 0;
|
|
border-bottom: 1px solid #eee;
|
|
padding-bottom: 0.3em;
|
|
}}
|
|
h3 {{
|
|
font-size: 1.25em;
|
|
margin: 0.83em 0;
|
|
}}
|
|
p {{
|
|
margin: 1em 0;
|
|
}}
|
|
code {{
|
|
background-color: #f5f5f5;
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
font-family: 'Monaco', 'Courier New', monospace;
|
|
font-size: 0.9em;
|
|
}}
|
|
pre {{
|
|
background-color: #f6f8fa;
|
|
padding: 16px;
|
|
border-radius: 6px;
|
|
overflow-x: auto;
|
|
margin: 1em 0;
|
|
}}
|
|
pre code {{
|
|
background-color: transparent;
|
|
padding: 0;
|
|
}}
|
|
blockquote {{
|
|
border-left: 4px solid #ddd;
|
|
padding-left: 16px;
|
|
margin: 1em 0;
|
|
color: #666;
|
|
}}
|
|
ul, ol {{
|
|
margin: 1em 0;
|
|
padding-left: 2em;
|
|
}}
|
|
li {{
|
|
margin: 0.5em 0;
|
|
}}
|
|
table {{
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
margin: 1em 0;
|
|
}}
|
|
th, td {{
|
|
border: 1px solid #ddd;
|
|
padding: 8px 12px;
|
|
text-align: left;
|
|
}}
|
|
th {{
|
|
background-color: #f5f5f5;
|
|
font-weight: bold;
|
|
}}
|
|
tr:nth-child(even) {{
|
|
background-color: #f9f9f9;
|
|
}}
|
|
a {{
|
|
color: #0366d6;
|
|
text-decoration: none;
|
|
}}
|
|
img {{
|
|
max-width: 100%;
|
|
height: auto;
|
|
display: inline-block;
|
|
vertical-align: middle;
|
|
}}
|
|
img[style*="display: block"] {{
|
|
display: block !important;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
}}
|
|
hr {{
|
|
border: none;
|
|
border-top: 2px solid #eee;
|
|
margin: 2em 0;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
{html_content}
|
|
</body>
|
|
</html>
|
|
'''
|
|
|
|
# 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)
|