import streamlit as st
import subprocess
import tempfile
import base64
from pathlib import Path
import os
import shutil
import io
from PIL import Image
import fitz # PyMuPDF
import time
import re
# Try to import streamlit-monaco
try:
from streamlit_monaco import st_monaco
MONACO_AVAILABLE = True
except ImportError:
MONACO_AVAILABLE = False
# Set page configuration
st.set_page_config(
page_title="Professional LaTeX Editor",
page_icon="📝",
layout="wide",
initial_sidebar_state="collapsed"
)
# Check if pdflatex is available
def is_pdflatex_installed():
return shutil.which("pdflatex") is not None
# Function to convert LaTeX to PDF
def latex_to_pdf(latex_code):
# Check if pdflatex is installed
if not is_pdflatex_installed():
st.error("pdflatex not found. Debug info:")
st.code(f"PATH: {os.environ.get('PATH')}")
result = subprocess.run(["which", "pdflatex"], capture_output=True, text=True)
st.code(f"which pdflatex: {result.stdout} {result.stderr}")
return None, "", "Error: pdflatex is not installed or not in PATH."
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
tex_file = temp_path / "document.tex"
pdf_file = temp_path / "document.pdf"
# Write LaTeX code to file
with open(tex_file, "w") as f:
f.write(latex_code)
try:
# Run pdflatex to compile the LaTeX file
process = subprocess.run(
["pdflatex", "-interaction=nonstopmode", "-output-directory", temp_dir, str(tex_file)],
capture_output=True,
text=True
)
# Check if PDF was created
if pdf_file.exists():
with open(pdf_file, "rb") as file:
pdf_data = file.read()
return pdf_data, process.stdout, process.stderr
else:
return None, process.stdout, process.stderr
except Exception as e:
return None, "", str(e)
# Function to create download link for PDF
def get_download_link(pdf_data, filename="document.pdf"):
b64_pdf = base64.b64encode(pdf_data).decode()
return f'Download PDF'
# Convert PDF to image for preview
def render_pdf_preview(pdf_data):
if not pdf_data:
return None
try:
# Create a file-like object from the PDF data
pdf_stream = io.BytesIO(pdf_data)
# Open PDF with PyMuPDF (fitz)
pdf_document = fitz.open(stream=pdf_stream, filetype="pdf")
# Render pages as images
images = []
for page_num in range(min(3, len(pdf_document))): # Preview first 3 pages max
page = pdf_document.load_page(page_num)
pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) # Zoom factor 2 for better resolution
img_data = pix.tobytes("png")
img = Image.open(io.BytesIO(img_data))
images.append(img)
pdf_document.close()
return images
except Exception as e:
st.error(f"Error rendering PDF preview: {str(e)}")
return None
# Function to parse LaTeX errors
def parse_latex_errors(output):
errors = []
warnings = []
# Match errors
error_matches = re.finditer(r'! (.+?)\.[\r\n]', output)
for match in error_matches:
errors.append(match.group(1))
# Match warnings
warning_matches = re.finditer(r'LaTeX Warning: (.+?)[\r\n]', output)
for match in warning_matches:
warnings.append(match.group(1))
return errors, warnings
# Function to extract document structure
def extract_document_structure(latex_code):
structure = []
# Ensure latex_code is a string
if latex_code is None:
return structure
if not isinstance(latex_code, str):
try:
latex_code = str(latex_code)
except:
return structure
# Find sections, subsections, etc.
section_pattern = r'\\(section|subsection|subsubsection|chapter|part)\{([^}]+)\}'
matches = re.finditer(section_pattern, latex_code)
for match in matches:
section_type = match.group(1)
section_title = match.group(2)
# Calculate indentation level based on section type
level = {
'part': 0,
'chapter': 1,
'section': 2,
'subsection': 3,
'subsubsection': 4
}.get(section_type, 0)
structure.append({
'type': section_type,
'title': section_title,
'level': level,
'position': match.start() # Position in document for navigation
})
return structure
# Function to analyze LaTeX packages
def analyze_packages(latex_code):
if not isinstance(latex_code, str):
try:
latex_code = str(latex_code)
except:
return []
# Find all package imports
package_pattern = r'\\usepackage(?:\[.*?\])?\{([^}]+)\}'
matches = re.finditer(package_pattern, latex_code)
packages = []
for match in matches:
package_name = match.group(1)
packages.append(package_name)
return packages
# Function to find all environments in the document
def find_environments(latex_code):
if not isinstance(latex_code, str):
try:
latex_code = str(latex_code)
except:
return []
# Find all environments (begin-end pairs)
env_pattern = r'\\begin\{([^}]+)\}(.*?)\\end\{\1\}'
matches = re.finditer(env_pattern, latex_code, re.DOTALL)
environments = []
for match in matches:
env_name = match.group(1)
environments.append({
'name': env_name,
'position': match.start(),
'content': match.group(2)
})
return environments
# Default LaTeX template
default_template = r"""\documentclass{article}
\usepackage[utf8]{inputenc}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{graphicx}
\usepackage{hyperref}
\usepackage{xcolor}
\title{Professional \LaTeX{} Document}
\author{Your Name}
\date{\today}
\begin{document}
\maketitle
\section{Introduction}
This is the introduction to your document. You can write your content here.
\section{Mathematical Expressions}
\subsection{Equations}
The famous Einstein's equation:
\begin{equation}
E = mc^2
\end{equation}
The quadratic formula:
\begin{equation}
x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
\end{equation}
\subsection{Calculus}
An integral example:
\begin{equation}
\int_{0}^{\pi} \sin(x) \, dx = 2
\end{equation}
\section{Lists and Items}
\subsection{Bullet Points}
\begin{itemize}
\item First item with \textbf{bold text}
\item Second item with \textit{italic text}
\item Third item with \textcolor{blue}{colored text}
\end{itemize}
\subsection{Numbered List}
\begin{enumerate}
\item First step
\item Second step
\item Third step
\end{enumerate}
\section{Tables}
\begin{table}[h]
\centering
\begin{tabular}{|c|c|c|}
\hline
Cell 1 & Cell 2 & Cell 3 \\
\hline
Data 1 & Data 2 & Data 3 \\
\hline
\end{tabular}
\caption{A simple table}
\label{tab:simple}
\end{table}
\section{Figures}
You can include figures using the following syntax:
% \begin{figure}[h]
% \centering
% \includegraphics[width=0.7\textwidth]{example-image}
% \caption{Example figure}
% \label{fig:example}
% \end{figure}
\section{Citations}
You can cite references using the \verb|\cite{}| command \cite{example}.
\section{Cross-References}
You can reference sections, figures, and tables using the \verb|\ref{}| command.
For example, see Table~\ref{tab:simple}.
\section{Conclusion}
Your conclusion here.
% Sample bibliography entry
\begin{thebibliography}{9}
\bibitem{example} Author, A. (2023). \textit{Title of the Work}. Publisher.
\end{thebibliography}
\end{document}
"""
# Enhanced VSCode-like styling
st.markdown("""
""", unsafe_allow_html=True)
# VS Code-style editor tabs
def render_editor_tabs(active_tab="document.tex"):
tab_html = f"""
"""
st.markdown(tab_html, unsafe_allow_html=True)
# VS Code-style status bar
def render_status_bar():
status_html = """
LaTeX
Line: 1, Col: 1
UTF-8
"""
st.markdown(status_html, unsafe_allow_html=True)
# Render a document outline based on section hierarchy
def render_document_outline(structure):
if not structure:
return
st.markdown('', unsafe_allow_html=True)
for item in structure:
level = item['level']
title = item['title']
type_icon = {
'part': '📑',
'chapter': '📕',
'section': '📌',
'subsection': '📎',
'subsubsection': '📍'
}.get(item['type'], '📋')
# Indent based on level
indent = " " * (level * 4)
st.markdown(
f'
'
f'{type_icon}'
f'{indent}{title}'
f'
',
unsafe_allow_html=True
)
st.markdown('
', unsafe_allow_html=True)
# Render errors and warnings in a VS Code style
def render_problems(errors, warnings):
if not errors and not warnings:
return
st.markdown('', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
# Show errors
for error in errors:
st.markdown(
f'
'
f'
❌
'
f'
{error}'
f'
document.tex
'
f'
',
unsafe_allow_html=True
)
# Show warnings
for warning in warnings:
st.markdown(
f'
'
f'
⚠️
'
f'
{warning}'
f'
document.tex
'
f'
',
unsafe_allow_html=True
)
st.markdown('
', unsafe_allow_html=True)
# Render document information panel
def render_document_info(latex_code):
# Ensure latex_code is a string
if not isinstance(latex_code, str):
try:
latex_code = str(latex_code)
except:
latex_code = ""
# Calculate basic document stats
word_count = len(re.findall(r'\b\w+\b', latex_code))
char_count = len(latex_code)
line_count = len(latex_code.split('\n'))
# Find document class
doc_class_match = re.search(r'\\documentclass(?:\[.*?\])?\{(.*?)\}', latex_code)
doc_class = doc_class_match.group(1) if doc_class_match else "unknown"
# Count equations
equation_count = len(re.findall(r'\\begin\{equation', latex_code))
align_count = len(re.findall(r'\\begin\{align', latex_code))
# Count figures
figure_count = len(re.findall(r'\\begin\{figure', latex_code))
# Count tables
table_count = len(re.findall(r'\\begin\{table', latex_code))
# Count custom command definitions
command_count = len(re.findall(r'\\newcommand', latex_code))
# Count references
ref_count = len(re.findall(r'\\ref\{', latex_code))
cite_count = len(re.findall(r'\\cite\{', latex_code))
# Render the info panel
st.markdown('', unsafe_allow_html=True)
info_rows = [
("Document Class", doc_class),
("Word Count", word_count),
("Character Count", char_count),
("Line Count", line_count),
("Equations", equation_count),
("Align Environments", align_count),
("Figures", figure_count),
("Tables", table_count),
("Custom Commands", command_count),
("References", ref_count),
("Citations", cite_count)
]
for label, value in info_rows:
st.markdown(
f'
'
f'{label}:'
f'{value}'
f'
',
unsafe_allow_html=True
)
st.markdown('
', unsafe_allow_html=True)
# Display the packages used in the document
def render_packages_panel(packages):
if not packages:
return
st.markdown('', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
for package in packages:
st.markdown(
f'
'
f'\\usepackage{{{package}}}'
f'
',
unsafe_allow_html=True
)
st.markdown('
', unsafe_allow_html=True)
# Display environments in the document
def render_environments_panel(environments):
if not environments:
return
st.markdown('', unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
# Count environment types
env_counts = {}
for env in environments:
env_name = env['name']
if env_name in env_counts:
env_counts[env_name] += 1
else:
env_counts[env_name] = 1
# Display counts
for env_name, count in env_counts.items():
st.markdown(
f'
'
f'\\begin{{{env_name}}} - {count} instances'
f'
',
unsafe_allow_html=True
)
st.markdown('
', unsafe_allow_html=True)
# Display a simple F5 shortcut reminder
def render_f5_shortcut():
st.markdown(
''
'F5'
'Press F5 to compile the document'
'
',
unsafe_allow_html=True
)
# Main application
def main():
# Initialize session state
if 'latex_code' not in st.session_state:
st.session_state.latex_code = default_template
if 'show_preview' not in st.session_state:
st.session_state.show_preview = False
if 'last_compiled' not in st.session_state:
st.session_state.last_compiled = None
if 'errors' not in st.session_state:
st.session_state.errors = []
if 'warnings' not in st.session_state:
st.session_state.warnings = []
# Check installation status
if not is_pdflatex_installed():
st.warning("⚠️ LaTeX is not installed. The compilation feature will not work.")
# Create main layout
col1, col2 = st.columns([3, 2])
with col1:
# Create tabs for main editing area with VS Code style
editor_tabs = st.tabs(["Editor", "Settings"])
with editor_tabs[0]:
# VS Code-like editor interface
st.markdown('', unsafe_allow_html=True)
# Tab bar
render_editor_tabs()
# Editor with Monaco or fallback to text area
st.markdown('
', unsafe_allow_html=True)
if MONACO_AVAILABLE:
try:
# Try to use Monaco editor with minimal parameters
latex_code = st_monaco(st.session_state.latex_code, height=500)
if latex_code is not None:
st.session_state.latex_code = latex_code
except Exception as e:
# Fallback to text area if Monaco fails
st.warning(f"Monaco editor unavailable: {str(e)}")
latex_code = st.text_area("", value=st.session_state.latex_code, height=500, key="latex_editor", label_visibility="collapsed")
st.session_state.latex_code = latex_code
else:
# Fallback to regular text area
latex_code = st.text_area("", value=st.session_state.latex_code, height=500, key="latex_editor", label_visibility="collapsed")
st.session_state.latex_code = latex_code
st.markdown('
', unsafe_allow_html=True)
# Status bar
render_status_bar()
st.markdown('
', unsafe_allow_html=True)
# F5 shortcut reminder
render_f5_shortcut()
# Control buttons with VS Code styling
st.markdown('', unsafe_allow_html=True)
compile_btn = st.button("Compile", help="Compile LaTeX to PDF (F5)")
load_template_btn = st.button("Load Template", help="Load default template")
clear_btn = st.button("Clear", help="Clear editor content")
st.markdown('
', unsafe_allow_html=True)
# Handle button actions
if compile_btn:
st.session_state.compile_clicked = True
st.session_state.last_compiled = time.time()
if load_template_btn:
st.session_state.latex_code = default_template
st.rerun()
if clear_btn:
st.session_state.latex_code = ""
st.rerun()
with editor_tabs[1]:
st.markdown("LaTeX Settings
", unsafe_allow_html=True)
st.markdown('', unsafe_allow_html=True)
col_a, col_b = st.columns(2)
with col_a:
st.checkbox("Auto-compile on save", value=False, key="auto_compile")
st.checkbox("Use pdflatex", value=True, key="use_pdflatex")
st.checkbox("Enable BibTeX", value=False, key="use_bibtex")
st.checkbox("Show line numbers", value=True, key="show_line_numbers")
with col_b:
st.selectbox("Document Class",
["article", "report", "book", "letter", "beamer"],
index=0, key="doc_class")
st.selectbox("PDF Engine",
["pdflatex", "xelatex", "lualatex"],
index=0, key="pdf_engine")
st.selectbox("Editor Theme",
["Dark", "Light", "High Contrast"],
index=0, key="editor_theme")
st.markdown('
', unsafe_allow_html=True)
with col2:
# Output tabs with VS Code style
output_tabs = st.tabs(["Output", "Outline", "Analysis", "Problems"])
with output_tabs[0]:
# PDF compilation and output
if 'compile_clicked' in st.session_state and st.session_state.compile_clicked:
with st.spinner("Compiling..."):
pdf_data, stdout, stderr = latex_to_pdf(st.session_state.latex_code)
# Parse errors and warnings
errors, warnings = parse_latex_errors(stdout + stderr)
st.session_state.errors = errors
st.session_state.warnings = warnings
if pdf_data:
st.session_state.pdf_data = pdf_data
st.success("Compilation successful")
# Toggle button for preview
if st.button("Toggle Preview", help="Show or hide the PDF preview"):
st.session_state.show_preview = not st.session_state.show_preview
# Download button
st.markdown(get_download_link(pdf_data), unsafe_allow_html=True)
# Display compilation info
if st.session_state.last_compiled:
time_str = time.strftime("%H:%M:%S", time.localtime(st.session_state.last_compiled))
st.info(f"Last compiled: {time_str}")
# Optional preview
if st.session_state.show_preview:
st.markdown('', unsafe_allow_html=True)
preview_images = render_pdf_preview(pdf_data)
if preview_images:
for i, img in enumerate(preview_images):
st.image(img, caption=f"Page {i+1}", use_container_width=True,
output_format="PNG")
st.markdown('
', unsafe_allow_html=True)
# Terminal output in collapsible section
with st.expander("Terminal Output"):
st.markdown('', unsafe_allow_html=True)
st.text(stdout)
st.markdown('
', unsafe_allow_html=True)
st.session_state.compile_clicked = False
else:
st.error("Compilation failed")
st.markdown('', unsafe_allow_html=True)
# Highlight errors in output
for line in stderr.split('\n'):
if "error" in line.lower():
st.markdown(f'{line}', unsafe_allow_html=True)
elif "warning" in line.lower():
st.markdown(f'{line}', unsafe_allow_html=True)
else:
st.write(line)
st.markdown('
', unsafe_allow_html=True)
st.session_state.compile_clicked = False
# Display previous PDF if available
elif 'pdf_data' in st.session_state and st.session_state.pdf_data:
# Toggle button for preview
if st.button("Toggle Preview", help="Show or hide the PDF preview"):
st.session_state.show_preview = not st.session_state.show_preview
# Download button
st.markdown(get_download_link(st.session_state.pdf_data), unsafe_allow_html=True)
# Display compilation info
if st.session_state.last_compiled:
time_str = time.strftime("%H:%M:%S", time.localtime(st.session_state.last_compiled))
st.info(f"Last compiled: {time_str}")
# Optional preview
if st.session_state.show_preview:
st.markdown('', unsafe_allow_html=True)
preview_images = render_pdf_preview(st.session_state.pdf_data)
if preview_images:
for i, img in enumerate(preview_images):
st.image(img, caption=f"Page {i+1}", use_container_width=True,
output_format="PNG")
st.markdown('
', unsafe_allow_html=True)
else:
st.info("Click 'Compile' to generate PDF output or press F5")
with output_tabs[1]:
# Document structure/outline view (with type check)
if isinstance(st.session_state.latex_code, str):
structure = extract_document_structure(st.session_state.latex_code)
else:
structure = []
if structure:
render_document_outline(structure)
else:
st.info("No document structure detected. Add sections using the toolbar.")
with output_tabs[2]:
# Document analysis features
# Document info
render_document_info(st.session_state.latex_code)
# Package analysis
packages = analyze_packages(st.session_state.latex_code)
if packages:
render_packages_panel(packages)
else:
st.info("No packages detected. Add packages using \\usepackage{}.")
# Environment analysis
environments = find_environments(st.session_state.latex_code)
if environments:
render_environments_panel(environments)
with output_tabs[3]:
# Problems panel (errors & warnings)
if st.session_state.errors or st.session_state.warnings:
render_problems(st.session_state.errors, st.session_state.warnings)
else:
st.info("No problems detected in the document.")
if __name__ == "__main__":
main()