Spaces:
Build error
Build error
| 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 with Chinese Support", | |
| page_icon="📝", | |
| layout="wide", | |
| initial_sidebar_state="collapsed" | |
| ) | |
| # Check if LaTeX engines are available | |
| def check_latex_engines(): | |
| engines = { | |
| 'pdflatex': shutil.which("pdflatex") is not None, | |
| 'xelatex': shutil.which("xelatex") is not None, | |
| 'lualatex': shutil.which("lualatex") is not None | |
| } | |
| return engines | |
| # Function to convert LaTeX to PDF with engine selection | |
| def latex_to_pdf(latex_code, engine='xelatex'): | |
| # Check if the specified engine is installed | |
| if not shutil.which(engine): | |
| st.error(f"{engine} not found. Debug info:") | |
| st.code(f"PATH: {os.environ.get('PATH')}") | |
| result = subprocess.run(["which", engine], capture_output=True, text=True) | |
| st.code(f"which {engine}: {result.stdout} {result.stderr}") | |
| return None, "", f"Error: {engine} 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", encoding='utf-8') as f: | |
| f.write(latex_code) | |
| try: | |
| # Different compilation arguments for different engines | |
| if engine == 'xelatex': | |
| cmd = ["xelatex", "-interaction=nonstopmode", "-output-directory", temp_dir, str(tex_file)] | |
| elif engine == 'lualatex': | |
| cmd = ["lualatex", "-interaction=nonstopmode", "-output-directory", temp_dir, str(tex_file)] | |
| else: # pdflatex | |
| cmd = ["pdflatex", "-interaction=nonstopmode", "-output-directory", temp_dir, str(tex_file)] | |
| # Run LaTeX engine to compile the file | |
| process = subprocess.run(cmd, 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 with Chinese support | |
| default_template_chinese = r"""\documentclass{article} | |
| \usepackage{xeCJK} | |
| \usepackage{amsmath} | |
| \usepackage{amssymb} | |
| \usepackage{graphicx} | |
| \usepackage{hyperref} | |
| \usepackage{xcolor} | |
| \usepackage{geometry} | |
| % Set Chinese fonts (using Noto fonts available in the container) | |
| \setCJKmainfont{Noto Serif CJK SC} | |
| \setCJKsansfont{Noto Sans CJK SC} | |
| \setCJKmonofont{Noto Sans Mono CJK SC} | |
| % Page geometry | |
| \geometry{a4paper, margin=1in} | |
| \title{中文LaTeX文档示例} | |
| \author{作者姓名} | |
| \date{\today} | |
| \begin{document} | |
| \maketitle | |
| \section{简介} | |
| 这是一个支持中文的LaTeX文档示例。您可以在这里编写中文内容。 | |
| This document also supports English text seamlessly mixed with Chinese characters. | |
| \section{数学表达式} | |
| \subsection{方程式} | |
| 著名的爱因斯坦质能方程: | |
| \begin{equation} | |
| E = mc^2 | |
| \end{equation} | |
| 二次方程求解公式: | |
| \begin{equation} | |
| x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} | |
| \end{equation} | |
| \subsection{微积分} | |
| 积分示例: | |
| \begin{equation} | |
| \int_{0}^{\pi} \sin(x) \, dx = 2 | |
| \end{equation} | |
| \section{列表和项目} | |
| \subsection{无序列表} | |
| \begin{itemize} | |
| \item 第一项包含\textbf{粗体文字} | |
| \item 第二项包含\textit{斜体文字} | |
| \item 第三项包含\textcolor{blue}{彩色文字} | |
| \end{itemize} | |
| \subsection{有序列表} | |
| \begin{enumerate} | |
| \item 第一步 | |
| \item 第二步 | |
| \item 第三步 | |
| \end{enumerate} | |
| \section{表格} | |
| \begin{table}[h] | |
| \centering | |
| \begin{tabular}{|c|c|c|} | |
| \hline | |
| 姓名 & 年龄 & 城市 \\ | |
| \hline | |
| 张三 & 25 & 北京 \\ | |
| \hline | |
| 李四 & 30 & 上海 \\ | |
| \hline | |
| \end{tabular} | |
| \caption{简单表格示例} | |
| \label{tab:simple} | |
| \end{table} | |
| \section{图形} | |
| 您可以使用以下语法包含图形: | |
| % \begin{figure}[h] | |
| % \centering | |
| % \includegraphics[width=0.7\textwidth]{example-image} | |
| % \caption{示例图形} | |
| % \label{fig:example} | |
| % \end{figure} | |
| \section{引用文献} | |
| 您可以使用 \verb|\cite{}| 命令引用参考文献 \cite{example}。 | |
| \section{交叉引用} | |
| 您可以使用 \verb|\ref{}| 命令引用章节、图形和表格。 | |
| 例如,参见表格~\ref{tab:simple}。 | |
| \section{结论} | |
| 这里是您的结论。 | |
| % 示例参考文献条目 | |
| \begin{thebibliography}{9} | |
| \bibitem{example} 作者姓名 (2023). \textit{书籍标题}. 出版社. | |
| \end{thebibliography} | |
| \end{document} | |
| """ | |
| # Default LaTeX template (English) | |
| default_template_english = 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{Conclusion} | |
| Your conclusion here. | |
| \begin{thebibliography}{9} | |
| \bibitem{example} Author, A. (2023). \textit{Title of the Work}. Publisher. | |
| \end{thebibliography} | |
| \end{document} | |
| """ | |
| # Enhanced VSCode-like styling (keeping the original 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; | |
| } | |
| /* Engine status indicators */ | |
| .engine-status { | |
| display: flex; | |
| gap: 10px; | |
| margin: 10px 0; | |
| padding: 10px; | |
| background-color: #252526; | |
| border-radius: 4px; | |
| } | |
| .engine-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| padding: 5px 10px; | |
| border-radius: 3px; | |
| font-size: 12px; | |
| } | |
| .engine-available { | |
| background-color: #1e3a1e; | |
| color: #4caf50; | |
| } | |
| .engine-unavailable { | |
| background-color: #3a1e1e; | |
| color: #f44336; | |
| } | |
| /* All other existing styles remain the same */ | |
| .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 { | |
| 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; | |
| } | |
| .preview-container { | |
| background-color: #252526; | |
| border: 1px solid #2d2d2d; | |
| border-radius: 4px; | |
| padding: 15px; | |
| margin-top: 10px; | |
| } | |
| .control-panel { | |
| background-color: #252526; | |
| border: 1px solid #2d2d2d; | |
| border-radius: 4px; | |
| padding: 10px; | |
| margin-bottom: 10px; | |
| } | |
| .button-group { | |
| display: flex; | |
| gap: 5px; | |
| } | |
| .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; | |
| } | |
| footer, header {display: none !important;} | |
| #MainMenu {visibility: hidden;} | |
| .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 { | |
| 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 { | |
| 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 { | |
| 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; | |
| } | |
| .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; | |
| } | |
| .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 { | |
| border: 1px solid #2d2d2d; | |
| border-radius: 4px; | |
| overflow: hidden; | |
| margin-bottom: 10px; | |
| } | |
| .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 with engine info | |
| def render_status_bar(engine="XeLaTeX"): | |
| status_html = f""" | |
| <div class="status-bar"> | |
| <span>{engine}</span> | |
| <span>Line: 1, Col: 1</span> | |
| <span>UTF-8</span> | |
| </div> | |
| """ | |
| st.markdown(status_html, unsafe_allow_html=True) | |
| # Render engine status | |
| def render_engine_status(): | |
| engines = check_latex_engines() | |
| engine_html = '<div class="engine-status">' | |
| for engine_name, available in engines.items(): | |
| status_class = "engine-available" if available else "engine-unavailable" | |
| status_icon = "✅" if available else "❌" | |
| engine_html += f'<div class="engine-item {status_class}">{status_icon} {engine_name}</div>' | |
| engine_html += '</div>' | |
| st.markdown(engine_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, engine): | |
| # 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)) | |
| # Check for Chinese characters | |
| chinese_chars = len(re.findall(r'[\u4e00-\u9fff]', latex_code)) | |
| # Render the info panel | |
| st.markdown('<div class="document-info">', unsafe_allow_html=True) | |
| info_rows = [ | |
| ("LaTeX Engine", engine), | |
| ("Document Class", doc_class), | |
| ("Word Count", word_count), | |
| ("Character Count", char_count), | |
| ("Line Count", line_count), | |
| ("Chinese Characters", chinese_chars), | |
| ("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_chinese | |
| 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 = [] | |
| if 'selected_engine' not in st.session_state: | |
| st.session_state.selected_engine = 'xelatex' | |
| # Check available engines | |
| engines = check_latex_engines() | |
| available_engines = [engine for engine, available in engines.items() if available] | |
| if not available_engines: | |
| st.error("⚠️ No LaTeX engines are installed. The compilation feature will not work.") | |
| return | |
| # 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, language="latex") | |
| 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 with engine info | |
| render_status_bar(st.session_state.selected_engine.upper()) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # Engine status display | |
| render_engine_status() | |
| # 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_chinese_btn = st.button("Load Chinese Template", help="Load Chinese template") | |
| load_english_btn = st.button("Load English Template", help="Load English 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_chinese_btn: | |
| st.session_state.latex_code = default_template_chinese | |
| st.rerun() | |
| if load_english_btn: | |
| st.session_state.latex_code = default_template_english | |
| 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: | |
| # Engine selection | |
| if available_engines: | |
| selected_engine = st.selectbox( | |
| "LaTeX Engine", | |
| available_engines, | |
| index=available_engines.index(st.session_state.selected_engine) if st.session_state.selected_engine in available_engines else 0, | |
| key="engine_select" | |
| ) | |
| st.session_state.selected_engine = selected_engine | |
| st.checkbox("Auto-compile on save", value=False, key="auto_compile") | |
| 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("Font Selection", | |
| ["Noto CJK (Chinese)", "WQY (Chinese)", "Latin Modern (English)", "Times (English)"], | |
| index=0, key="font_selection") | |
| st.selectbox("Editor Theme", | |
| ["Dark", "Light", "High Contrast"], | |
| index=0, key="editor_theme") | |
| # Engine information | |
| st.markdown("### Engine Information") | |
| engine_info = { | |
| 'pdflatex': 'Traditional LaTeX engine, good for English documents', | |
| 'xelatex': 'Modern engine with Unicode support, excellent for Chinese and mixed languages', | |
| 'lualatex': 'Advanced engine with Lua scripting support, good for complex typography' | |
| } | |
| for engine, description in engine_info.items(): | |
| status = "✅ Available" if engines.get(engine, False) else "❌ Not Available" | |
| st.write(f"**{engine.upper()}**: {description} - {status}") | |
| 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, st.session_state.selected_engine) | |
| # 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(f"Compilation successful using {st.session_state.selected_engine.upper()}") | |
| # 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} with {st.session_state.selected_engine.upper()}") | |
| # 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(f"Compilation failed with {st.session_state.selected_engine.upper()}") | |
| 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} with {st.session_state.selected_engine.upper()}") | |
| # 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 \\section{}, \\subsection{}, etc.") | |
| with output_tabs[2]: | |
| # Document analysis features | |
| # Document info | |
| render_document_info(st.session_state.latex_code, st.session_state.selected_engine.upper()) | |
| # 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() |