initial commit

This commit is contained in:
2026-03-01 11:35:14 -06:00
commit a1428df440
10 changed files with 1557 additions and 0 deletions

339
app.py Normal file
View 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)