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"""
📄 {active_tab}
""" 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('
Problems
', 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('
Imported Packages
', 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('
Environments
', 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()