Spaces:
Running
Running
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'<a href="data:application/pdf;base64,{b64_pdf}" download="{filename}" class="download-button">Download PDF</a>' | |
# 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(""" | |
<style> | |
/* Base theming - VS Code inspired */ | |
html, body, .stApp { | |
background-color: #1e1e1e; | |
color: #cccccc; | |
} | |
/* Streamlit component overrides */ | |
.stButton button { | |
background-color: #0e639c; | |
color: white; | |
border: none; | |
padding: 0.4rem 1rem; | |
font-size: 0.8rem; | |
border-radius: 2px; | |
} | |
.stButton button:hover { | |
background-color: #1177bb; | |
} | |
/* VS Code-like editor styling */ | |
.stTextArea textarea { | |
font-family: 'Consolas', 'Monaco', 'Courier New', monospace !important; | |
font-size: 14px !important; | |
line-height: 1.5 !important; | |
background-color: #1e1e1e !important; | |
color: #d4d4d4 !important; | |
padding: 10px !important; | |
border-radius: 4px !important; | |
border: 1px solid #252526 !important; | |
} | |
/* Editor tab bar */ | |
.tab-bar { | |
display: flex; | |
background-color: #252526; | |
border-bottom: 1px solid #2d2d2d; | |
padding: 0; | |
height: 36px; | |
} | |
.tab { | |
padding: 0 15px; | |
height: 36px; | |
line-height: 36px; | |
background-color: #2d2d2d; | |
color: white; | |
border-right: 1px solid #252526; | |
font-size: 13px; | |
display: flex; | |
align-items: center; | |
} | |
.tab.active { | |
background-color: #1e1e1e; | |
border-top: 1px solid #0e639c; | |
} | |
.tab-icon { | |
margin-right: 6px; | |
opacity: 0.8; | |
} | |
/* Status bar */ | |
.status-bar { | |
display: flex; | |
justify-content: space-between; | |
background-color: #007acc; | |
color: white; | |
padding: 3px 10px; | |
font-size: 12px; | |
} | |
/* Terminal/console styling */ | |
.terminal { | |
background-color: #1e1e1e; | |
color: #cccccc; | |
padding: 10px; | |
font-family: 'Cascadia Code', 'Consolas', monospace; | |
font-size: 13px; | |
border-top: 1px solid #2d2d2d; | |
overflow-y: auto; | |
max-height: 200px; | |
} | |
.terminal-error { | |
color: #f48771; | |
} | |
.terminal-warning { | |
color: #cca700; | |
} | |
/* Outline view */ | |
.outline-view { | |
background-color: #252526; | |
border: 1px solid #2d2d2d; | |
border-radius: 4px; | |
margin-top: 10px; | |
max-height: 400px; | |
overflow-y: auto; | |
} | |
.outline-item { | |
padding: 5px 10px; | |
cursor: pointer; | |
display: flex; | |
align-items: center; | |
} | |
.outline-item:hover { | |
background-color: #2a2d2e; | |
} | |
.outline-item-text { | |
margin-left: 5px; | |
} | |
/* Download button styling */ | |
.download-button { | |
display: inline-block; | |
padding: 8px 16px; | |
background-color: #3d995e; | |
color: white !important; | |
text-align: center; | |
text-decoration: none; | |
font-size: 13px; | |
border-radius: 2px; | |
transition: background-color 0.3s; | |
margin-top: 10px; | |
font-weight: normal; | |
} | |
.download-button:hover { | |
background-color: #4eb772; | |
} | |
/* PDF preview area */ | |
.preview-container { | |
background-color: #252526; | |
border: 1px solid #2d2d2d; | |
border-radius: 4px; | |
padding: 15px; | |
margin-top: 10px; | |
} | |
/* Control panel */ | |
.control-panel { | |
background-color: #252526; | |
border: 1px solid #2d2d2d; | |
border-radius: 4px; | |
padding: 10px; | |
margin-bottom: 10px; | |
} | |
.button-group { | |
display: flex; | |
gap: 5px; | |
} | |
/* Messages */ | |
.stInfo { | |
background-color: #063b49; | |
color: #bbbbbb; | |
border: 1px solid #145b6c; | |
} | |
.stError { | |
background-color: #5a1d1d; | |
color: #bbbbbb; | |
border: 1px solid #6c2b2b; | |
} | |
.stSuccess { | |
background-color: #143d27; | |
color: #bbbbbb; | |
border: 1px solid #1e5a3a; | |
} | |
/* Hide Streamlit footer and menu */ | |
footer, header {display: none !important;} | |
#MainMenu {visibility: hidden;} | |
/* Custom tabs */ | |
.stTabs [data-baseweb="tab-list"] { | |
background-color: #2d2d2d; | |
gap: 0px !important; | |
} | |
.stTabs [data-baseweb="tab"] { | |
background-color: #252526; | |
color: #cccccc; | |
border-radius: 0; | |
border-right: 1px solid #1e1e1e; | |
padding: 0px 16px; | |
height: 36px; | |
} | |
.stTabs [aria-selected="true"] { | |
background-color: #1e1e1e; | |
border-top: 2px solid #007acc !important; | |
color: white; | |
} | |
/* Problems panel */ | |
.problems-panel { | |
background-color: #252526; | |
border: 1px solid #2d2d2d; | |
border-radius: 4px; | |
margin-top: 10px; | |
padding: 0; | |
} | |
.problems-header { | |
background-color: #2d2d2d; | |
padding: 5px 10px; | |
font-weight: bold; | |
font-size: 13px; | |
} | |
.problem-item { | |
padding: 8px 10px; | |
border-bottom: 1px solid #2d2d2d; | |
display: flex; | |
align-items: flex-start; | |
} | |
.problem-icon { | |
margin-right: 8px; | |
flex-shrink: 0; | |
} | |
.problem-message { | |
flex-grow: 1; | |
} | |
.problem-location { | |
font-size: 12px; | |
color: #8a8a8a; | |
margin-top: 2px; | |
} | |
/* Document info panel */ | |
.document-info { | |
background-color: #252526; | |
border: 1px solid #2d2d2d; | |
border-radius: 4px; | |
padding: 10px; | |
margin-top: 10px; | |
font-size: 13px; | |
} | |
.info-row { | |
display: flex; | |
justify-content: space-between; | |
margin-bottom: 5px; | |
border-bottom: 1px solid #333; | |
padding-bottom: 5px; | |
} | |
.info-label { | |
font-weight: bold; | |
color: #8a8a8a; | |
} | |
.info-value { | |
text-align: right; | |
} | |
/* Packages panel */ | |
.packages-panel { | |
background-color: #252526; | |
border: 1px solid #2d2d2d; | |
border-radius: 4px; | |
margin-top: 10px; | |
padding: 0; | |
} | |
.packages-header { | |
background-color: #2d2d2d; | |
padding: 5px 10px; | |
font-weight: bold; | |
font-size: 13px; | |
} | |
.package-item { | |
padding: 5px 10px; | |
border-bottom: 1px solid #2d2d2d; | |
display: flex; | |
align-items: center; | |
} | |
.package-name { | |
color: #dcdcaa; | |
margin-right: 10px; | |
font-family: monospace; | |
} | |
/* Environment analyzer */ | |
.environments-panel { | |
background-color: #252526; | |
border: 1px solid #2d2d2d; | |
border-radius: 4px; | |
margin-top: 10px; | |
padding: 0; | |
} | |
.environments-header { | |
background-color: #2d2d2d; | |
padding: 5px 10px; | |
font-weight: bold; | |
font-size: 13px; | |
} | |
.environment-item { | |
padding: 5px 10px; | |
border-bottom: 1px solid #2d2d2d; | |
} | |
.environment-name { | |
color: #4ec9b0; | |
font-family: monospace; | |
font-weight: bold; | |
} | |
/* Simple keyboard shortcut */ | |
.shortcut-item { | |
background-color: #252526; | |
border: 1px solid #2d2d2d; | |
border-radius: 4px; | |
padding: 5px 10px; | |
margin-top: 10px; | |
font-size: 13px; | |
display: flex; | |
align-items: center; | |
} | |
.shortcut-key { | |
background-color: #3c3c3c; | |
padding: 2px 8px; | |
border-radius: 3px; | |
font-family: 'Consolas', 'Monaco', 'Courier New', monospace; | |
font-size: 12px; | |
margin-right: 10px; | |
} | |
/* Monaco editor container */ | |
.monaco-editor-container { | |
border: 1px solid #2d2d2d; | |
border-radius: 4px; | |
overflow: hidden; | |
margin-bottom: 10px; | |
} | |
/* Make sure Monaco editor has correct background */ | |
.monaco-editor, .monaco-editor-background, .monaco-editor .margin { | |
background-color: #1e1e1e !important; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
# VS Code-style editor tabs | |
def render_editor_tabs(active_tab="document.tex"): | |
tab_html = f""" | |
<div class="tab-bar"> | |
<div class="tab active"> | |
<span class="tab-icon">π</span> {active_tab} | |
</div> | |
</div> | |
""" | |
st.markdown(tab_html, unsafe_allow_html=True) | |
# VS Code-style status bar | |
def render_status_bar(): | |
status_html = """ | |
<div class="status-bar"> | |
<span>LaTeX</span> | |
<span>Line: 1, Col: 1</span> | |
<span>UTF-8</span> | |
</div> | |
""" | |
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('<div class="outline-view">', 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'<div class="outline-item">' | |
f'<span>{type_icon}</span>' | |
f'<span class="outline-item-text">{indent}{title}</span>' | |
f'</div>', | |
unsafe_allow_html=True | |
) | |
st.markdown('</div>', 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('<div class="problems-panel">', unsafe_allow_html=True) | |
st.markdown('<div class="problems-header">Problems</div>', unsafe_allow_html=True) | |
# Show errors | |
for error in errors: | |
st.markdown( | |
f'<div class="problem-item">' | |
f'<div class="problem-icon">β</div>' | |
f'<div class="problem-message">{error}' | |
f'<div class="problem-location">document.tex</div>' | |
f'</div></div>', | |
unsafe_allow_html=True | |
) | |
# Show warnings | |
for warning in warnings: | |
st.markdown( | |
f'<div class="problem-item">' | |
f'<div class="problem-icon">β οΈ</div>' | |
f'<div class="problem-message">{warning}' | |
f'<div class="problem-location">document.tex</div>' | |
f'</div></div>', | |
unsafe_allow_html=True | |
) | |
st.markdown('</div>', 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('<div class="document-info">', 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'<div class="info-row">' | |
f'<span class="info-label">{label}:</span>' | |
f'<span class="info-value">{value}</span>' | |
f'</div>', | |
unsafe_allow_html=True | |
) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Display the packages used in the document | |
def render_packages_panel(packages): | |
if not packages: | |
return | |
st.markdown('<div class="packages-panel">', unsafe_allow_html=True) | |
st.markdown('<div class="packages-header">Imported Packages</div>', unsafe_allow_html=True) | |
for package in packages: | |
st.markdown( | |
f'<div class="package-item">' | |
f'<span class="package-name">\\usepackage{{{package}}}</span>' | |
f'</div>', | |
unsafe_allow_html=True | |
) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Display environments in the document | |
def render_environments_panel(environments): | |
if not environments: | |
return | |
st.markdown('<div class="environments-panel">', unsafe_allow_html=True) | |
st.markdown('<div class="environments-header">Environments</div>', 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'<div class="environment-item">' | |
f'<span class="environment-name">\\begin{{{env_name}}}</span> - {count} instances' | |
f'</div>', | |
unsafe_allow_html=True | |
) | |
st.markdown('</div>', unsafe_allow_html=True) | |
# Display a simple F5 shortcut reminder | |
def render_f5_shortcut(): | |
st.markdown( | |
'<div class="shortcut-item">' | |
'<span class="shortcut-key">F5</span>' | |
'<span>Press F5 to compile the document</span>' | |
'</div>', | |
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('<div class="editor-container">', unsafe_allow_html=True) | |
# Tab bar | |
render_editor_tabs() | |
# Editor with Monaco or fallback to text area | |
st.markdown('<div class="monaco-editor-container">', 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('</div>', unsafe_allow_html=True) | |
# Status bar | |
render_status_bar() | |
st.markdown('</div>', unsafe_allow_html=True) | |
# F5 shortcut reminder | |
render_f5_shortcut() | |
# Control buttons with VS Code styling | |
st.markdown('<div class="button-group">', 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('</div>', 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("<h3>LaTeX Settings</h3>", unsafe_allow_html=True) | |
st.markdown('<div class="control-panel">', 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('</div>', 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('<div class="preview-container">', 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('</div>', unsafe_allow_html=True) | |
# Terminal output in collapsible section | |
with st.expander("Terminal Output"): | |
st.markdown('<div class="terminal">', unsafe_allow_html=True) | |
st.text(stdout) | |
st.markdown('</div>', unsafe_allow_html=True) | |
st.session_state.compile_clicked = False | |
else: | |
st.error("Compilation failed") | |
st.markdown('<div class="terminal">', unsafe_allow_html=True) | |
# Highlight errors in output | |
for line in stderr.split('\n'): | |
if "error" in line.lower(): | |
st.markdown(f'<span class="terminal-error">{line}</span>', unsafe_allow_html=True) | |
elif "warning" in line.lower(): | |
st.markdown(f'<span class="terminal-warning">{line}</span>', unsafe_allow_html=True) | |
else: | |
st.write(line) | |
st.markdown('</div>', 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('<div class="preview-container">', 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('</div>', 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() |