#!/usr/bin/env python3 """ Compile results.tex using online LaTeX compilation API No local LaTeX installation required. Usage: python compile_latex_online.py [options] [file.tex] Options: -v, --verbose Show full error logs -s, --strict Strict mode: fail on any errors or warnings --dry-run Only run pre-flight checks, don't compile API Usage Limits: - latex.ytotech.com: No documented rate limits (experimental service) - latexonline.cc: No documented limits, but may throttle heavy usage """ import requests import json import os import sys import re def safe_print(text): """Print text, handling encoding issues on Windows.""" try: print(text) except UnicodeEncodeError: print(text.encode('utf-8', errors='replace').decode('utf-8')) def parse_error_logs(response_data): """Extract and format error logs from API response.""" logs = [] if isinstance(response_data, dict): if 'error' in response_data: logs.append(f"Error: {response_data['error']}") if 'logs' in response_data: for log in response_data['logs']: if isinstance(log, dict): logs.append(f"[{log.get('type', 'log')}] {log.get('content', str(log))}") else: logs.append(str(log)) if 'log_files' in response_data: for name, content in response_data['log_files'].items(): logs.append(f"\n=== {name} ===") logs.append(content[:10000] if len(content) > 10000 else content) if 'stdout' in response_data: logs.append(f"\n=== STDOUT ===\n{response_data['stdout']}") if 'stderr' in response_data: logs.append(f"\n=== STDERR ===\n{response_data['stderr']}") return logs def extract_latex_errors(log_text): """Extract LaTeX error messages from log text.""" errors = [] warnings = [] lines = log_text.split('\n') if isinstance(log_text, str) else [] i = 0 while i < len(lines): line = lines[i] # LaTeX errors start with ! if line.startswith('!'): error_block = [line] for j in range(i + 1, min(i + 6, len(lines))): error_block.append(lines[j]) if lines[j].startswith('l.'): # Line number indicator break errors.append('\n'.join(error_block)) # LaTeX warnings elif 'Warning:' in line or 'warning:' in line: warnings.append(line.strip()) # Overfull/Underfull box warnings elif line.startswith('Overfull') or line.startswith('Underfull'): warnings.append(line.strip()) i += 1 return errors, warnings def check_latex_syntax(tex_content): """ Pre-flight check for common LaTeX syntax errors. Returns: (errors: list, warnings: list) """ errors = [] warnings = [] lines = tex_content.split('\n') # Track brace balance brace_count = 0 # Track environments env_stack = [] for i, line in enumerate(lines, 1): # Skip comments comment_pos = line.find('%') if comment_pos == 0: continue elif comment_pos > 0 and line[comment_pos-1] != '\\': line = line[:comment_pos] # Only check non-comment part # Count braces for j, char in enumerate(line): if char == '{' and (j == 0 or line[j-1] != '\\'): brace_count += 1 elif char == '}' and (j == 0 or line[j-1] != '\\'): brace_count -= 1 # Check for command without backslash (common copy-paste error) unescaped_cmds = re.findall(r'(? {line.strip()[:80]}") # Check for malformed commands (e.g., \textbf{text textbf{more}) if '\\textbf{' in line: # Count \textbf{ vs textbf{ (without backslash) proper = len(re.findall(r'\\textbf\{', line)) improper = len(re.findall(r'(? 0: errors.append(f"Line {i}: Malformed \\textbf command (missing backslash)") safe_print(f" -> {line.strip()[:80]}") # Track environments for match in re.finditer(r'\\begin\{(\w+)\}', line): env_stack.append((match.group(1), i)) for match in re.finditer(r'\\end\{(\w+)\}', line): env_name = match.group(1) if env_stack and env_stack[-1][0] == env_name: env_stack.pop() elif env_stack: errors.append(f"Line {i}: \\end{{{env_name}}} but expected \\end{{{env_stack[-1][0]}}} (opened at line {env_stack[-1][1]})") else: errors.append(f"Line {i}: \\end{{{env_name}}} without matching \\begin") # Check for quadruple backslashes if '\\\\\\\\' in line: errors.append(f"Line {i}: Quadruple backslash (should be \\\\)") # Check for unbalanced $ signs (math mode) dollar_count = len(re.findall(r'(?