initial commit
This commit is contained in:
339
app.py
Normal file
339
app.py
Normal file
@@ -0,0 +1,339 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user