
Paper Compilation
- 1 installs
- 255 repo stars
- Updated February 27, 2026
- lingzhi227/claude-research-skills
This is a copy of paper-compilation by lingzhi227 - installs and ranking accrue to the original listing.
Helps with ai & agent building tasks.
About
paper-compilation is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- paper-compilation
- AI & Agent Building
- AI-coding skill
Paper Compilation by the numbers
- 1 all-time installs (skills.sh)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/lingzhi227/claude-research-skills --skill paper-compilationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 255 |
| Last updated | February 27, 2026 |
| Repository | lingzhi227/claude-research-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Paper Compilation
Compile a LaTeX paper to PDF with error detection and correction.
Input
$ARGUMENTS— Path to the main.texfile
Scripts
Compile paper
python ~/.claude/skills/paper-compilation/scripts/compile_paper.py paper/main.tex
python ~/.claude/skills/paper-compilation/scripts/compile_paper.py paper/main.tex --check-style
python ~/.claude/skills/paper-compilation/scripts/compile_paper.py paper/main.tex --output paper/output.pdfReports: compilation status, page count, warnings, citation/reference stats, style issues.
Validate citations before compiling
python ~/.claude/skills/citation-management/scripts/validate_citations.py \
--tex paper/main.tex --bib paper/references.bib --check-figures --figures-dir paper/figures/Auto-fix LaTeX errors
python ~/.claude/skills/paper-compilation/scripts/fix_latex_errors.py \
--tex paper/main.tex --log compile.log --output paper/main_fixed.texFixes: HTML tags in LaTeX, mismatched environments, missing figures. Key flags: --dry-run, --auto-detect
Compile with auto-fix retry
python ~/.claude/skills/paper-compilation/scripts/compile_paper.py paper/main.tex --auto-fixRuns fix_latex_errors.py + recompile up to 3 rounds until compilation succeeds.
Workflow
Step 1: Pre-Compilation Validation
Run validate_citations.py to catch issues before compiling:
- Every
\cite{key}has a matching.bibentry - Every
\includegraphics{file}exists - No duplicate labels or sections
Step 2: Compile
Run compile_paper.py which executes: pdflatex → bibtex → pdflatex → pdflatex
Step 3: Error Correction Loop (up to 5 rounds)
If compilation fails, read the error output and fix:
! Undefined control sequence→ Add missing package or fix typo! Missing $ inserted→ Wrap math in$...$! Missing } inserted→ Fix unmatched braceCitation 'key' undefined→ Add to .bib or fix\cite</end{figure}>→ Replace with\end{figure}(HTML syntax in LaTeX)
Apply minimal fixes. Do not remove packages unnecessarily. Recompile after each fix.
Step 4: Post-Compilation Report
Check: page count vs venue limit, remaining warnings, chktex style issues.
Troubleshooting
pdflatex not found
# macOS
brew install --cask mactex-no-gui
# Ubuntu
sudo apt install texlive-fullAlternative: latexmk (auto-handles multiple passes)
latexmk -pdf -interaction=nonstopmode main.texRelated Skills
- Upstream: latex-formatting, citation-management, figure-generation, table-generation
- Downstream: self-review
- See also: paper-assembly
#!/usr/bin/env python3
"""Compile a LaTeX paper to PDF with error detection.
Runs the full pdflatex → bibtex → pdflatex → pdflatex pipeline,
reports errors, and optionally runs chktex for style checking.
Self-contained: uses only stdlib.
Adapted from AI-Scientist (compile_latex) and data-to-paper (save_latex_and_compile_to_pdf).
Usage:
python compile_paper.py paper/main.tex
python compile_paper.py paper/main.tex --check-style
python compile_paper.py paper/main.tex --output paper/output.pdf
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
def run_command(cmd: list[str], cwd: str, timeout: int = 60) -> tuple[int, str, str]:
"""Run a command and return (returncode, stdout, stderr)."""
try:
result = subprocess.run(
cmd, cwd=cwd,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, timeout=timeout,
)
return result.returncode, result.stdout, result.stderr
except subprocess.TimeoutExpired:
return -1, "", f"Command timed out after {timeout}s: {' '.join(cmd)}"
except FileNotFoundError:
return -2, "", f"Command not found: {cmd[0]}"
def check_bib_exists(tex_content: str) -> bool:
"""Check if the tex file references a bibliography."""
return bool(re.search(r"\\bibliography\{", tex_content) or
re.search(r"\\begin\{filecontents\}\{.*\.bib\}", tex_content) or
re.search(r"\\addbibresource\{", tex_content))
def extract_errors(log_content: str) -> list[str]:
"""Extract LaTeX errors from log file."""
errors = []
lines = log_content.split("\n")
for i, line in enumerate(lines):
if line.startswith("! "):
# Grab the error line and a few lines of context
context = lines[i:i+5]
errors.append("\n".join(context))
return errors
def extract_warnings(log_content: str) -> list[str]:
"""Extract significant warnings from log file."""
warnings = []
for line in log_content.split("\n"):
line = line.strip()
if "Overfull \\hbox" in line:
warnings.append(line)
elif "Underfull \\vbox" in line:
warnings.append(line)
elif "Citation" in line and "undefined" in line:
warnings.append(line)
elif "Reference" in line and "undefined" in line:
warnings.append(line)
elif "LaTeX Warning: There were undefined references" in line:
warnings.append(line)
return warnings
def count_pages(pdf_path: str) -> int | None:
"""Try to count PDF pages using python."""
try:
with open(pdf_path, "rb") as f:
content = f.read()
# Simple heuristic: count /Type /Page entries
count = len(re.findall(rb"/Type\s*/Page[^s]", content))
return count if count > 0 else None
except Exception:
return None
def compile_latex(tex_file: str, output_pdf: str | None = None,
check_style: bool = False, timeout: int = 60) -> bool:
"""Compile LaTeX to PDF. Returns True on success."""
tex_file = os.path.abspath(tex_file)
if not os.path.exists(tex_file):
print(f"Error: {tex_file} not found", file=sys.stderr)
return False
cwd = os.path.dirname(tex_file)
basename = os.path.splitext(os.path.basename(tex_file))[0]
with open(tex_file, encoding="utf-8", errors="replace") as f:
tex_content = f.read()
has_bib = check_bib_exists(tex_content)
# Build compilation sequence
commands = [
["pdflatex", "-interaction=nonstopmode", "-halt-on-error", os.path.basename(tex_file)],
]
if has_bib:
commands.append(["bibtex", basename])
commands.append(["pdflatex", "-interaction=nonstopmode", os.path.basename(tex_file)])
commands.append(["pdflatex", "-interaction=nonstopmode", os.path.basename(tex_file)])
print(f"Compiling {tex_file}")
print(f" Working directory: {cwd}")
print(f" Bibliography: {'yes' if has_bib else 'no'}")
print(f" Passes: {len(commands)}")
print()
all_stdout = ""
success = True
for i, cmd in enumerate(commands):
step_name = cmd[0] + (" (pass 1)" if i == 0 else f" (pass {i+1})" if cmd[0] == "pdflatex" else "")
print(f" [{i+1}/{len(commands)}] {step_name}...", end=" ")
rc, stdout, stderr = run_command(cmd, cwd, timeout)
all_stdout += stdout
if rc == -2:
print(f"FAILED - {cmd[0]} not installed")
print(f"\n Install LaTeX:")
print(f" macOS: brew install --cask mactex-no-gui")
print(f" Ubuntu: sudo apt install texlive-full")
return False
elif rc == -1:
print(f"TIMEOUT ({timeout}s)")
success = False
elif rc != 0 and cmd[0] == "pdflatex" and i == 0:
# First pass failure is critical
print("FAILED")
errors = extract_errors(stdout)
if errors:
print("\n Errors found:")
for err in errors[:5]:
for line in err.split("\n"):
print(f" {line}")
success = False
break
else:
print("OK")
# Check for output PDF
pdf_path = os.path.join(cwd, f"{basename}.pdf")
if os.path.exists(pdf_path):
if output_pdf:
shutil.copy2(pdf_path, output_pdf)
pdf_path = output_pdf
pages = count_pages(pdf_path)
size_kb = os.path.getsize(pdf_path) / 1024
print(f"\n Output: {pdf_path}")
print(f" Size: {size_kb:.0f} KB")
if pages:
print(f" Pages: {pages}")
else:
print(f"\n ERROR: No PDF produced")
success = False
# Warnings
warnings = extract_warnings(all_stdout)
if warnings:
print(f"\n Warnings ({len(warnings)}):")
for w in warnings[:10]:
print(f" {w}")
if len(warnings) > 10:
print(f" ... and {len(warnings) - 10} more")
# Errors in final pass
errors = extract_errors(all_stdout)
if errors and success:
print(f"\n Non-fatal errors ({len(errors)}):")
for err in errors[:3]:
first_line = err.split("\n")[0]
print(f" {first_line}")
# Style check with chktex
if check_style:
print(f"\n Running chktex...")
rc, stdout, stderr = run_command(
["chktex", "-q", "-n2", "-n24", "-n13", "-n1", os.path.basename(tex_file)],
cwd, timeout=30
)
if rc == -2:
print(" chktex not installed (optional)")
elif stdout.strip():
issues = [l for l in stdout.strip().split("\n") if l.strip()]
print(f" Style issues ({len(issues)}):")
for issue in issues[:10]:
print(f" {issue}")
else:
print(" No style issues found")
# Citation/reference stats
cite_count = len(re.findall(r"\\cite[a-z]*\{", tex_content))
ref_count = len(re.findall(r"\\ref\{", tex_content))
fig_count = len(re.findall(r"\\includegraphics", tex_content))
table_count = len(re.findall(r"\\begin\{table", tex_content))
undef_cites = len([w for w in warnings if "Citation" in w and "undefined" in w])
undef_refs = len([w for w in warnings if "Reference" in w and "undefined" in w])
print(f"\n Stats:")
print(f" Citations: {cite_count} used, {undef_cites} undefined")
print(f" References: {ref_count} used, {undef_refs} undefined")
print(f" Figures: {fig_count}")
print(f" Tables: {table_count}")
status = "SUCCESS" if success else "FAILED"
print(f"\n Result: {status}")
return success
def main():
parser = argparse.ArgumentParser(description="Compile LaTeX paper to PDF")
parser.add_argument("tex_file", help="Main .tex file")
parser.add_argument("--output", "-o", help="Output PDF path")
parser.add_argument("--check-style", action="store_true", help="Run chktex style check")
parser.add_argument("--timeout", type=int, default=60, help="Timeout per command (seconds)")
parser.add_argument("--auto-fix", action="store_true", help="Auto-fix errors and retry (up to 3 rounds)")
args = parser.parse_args()
success = compile_latex(
args.tex_file,
output_pdf=args.output,
check_style=args.check_style,
timeout=args.timeout,
)
if not success and args.auto_fix:
fix_script = os.path.join(os.path.dirname(__file__), "fix_latex_errors.py")
if os.path.exists(fix_script):
for attempt in range(1, 4):
print(f"\n--- Auto-fix attempt {attempt}/3 ---")
fix_cmd = [sys.executable, fix_script, "--tex", args.tex_file, "--auto-detect",
"--output", args.tex_file]
import subprocess as _subprocess
result = _subprocess.run(fix_cmd, capture_output=True, text=True)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
success = compile_latex(
args.tex_file,
output_pdf=args.output,
check_style=args.check_style,
timeout=args.timeout,
)
if success:
print(f"\nAuto-fix succeeded on attempt {attempt}!")
break
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Automated LaTeX error fixer.
Reads a pdflatex error log and the corresponding .tex file, identifies
common errors, and applies automated fixes.
Self-contained: uses only stdlib.
Usage:
python fix_latex_errors.py --tex main.tex --log compile.log --output fixed.tex
python fix_latex_errors.py --tex main.tex --log compile.log --dry-run
python fix_latex_errors.py --tex main.tex --auto-detect --output fixed.tex
"""
import argparse
import os
import re
import sys
class LatexFix:
"""A fix to apply to LaTeX content."""
def __init__(self, name: str, description: str):
self.name = name
self.description = description
self.applied = False
def __repr__(self):
return f"Fix({self.name})"
def parse_errors(log_content: str) -> list[dict]:
"""Parse pdflatex log file and extract errors with context."""
errors = []
lines = log_content.split("\n")
for i, line in enumerate(lines):
if line.startswith("! "):
error = {"message": line[2:], "context": [], "line_num": None, "type": ""}
# Get context lines
for j in range(i + 1, min(i + 6, len(lines))):
error["context"].append(lines[j])
# Try to extract line number
for ctx in error["context"]:
m = re.search(r"l\.(\d+)", ctx)
if m:
error["line_num"] = int(m.group(1))
break
# Classify error
msg = error["message"]
if "Undefined control sequence" in msg:
error["type"] = "undefined_command"
elif "Missing $ inserted" in msg:
error["type"] = "missing_math"
elif "Missing } inserted" in msg or "Missing { inserted" in msg:
error["type"] = "missing_brace"
elif "Environment" in msg and "undefined" in msg:
error["type"] = "undefined_env"
elif "File" in msg and "not found" in msg:
error["type"] = "missing_file"
elif "Misplaced alignment tab" in msg:
error["type"] = "misplaced_tab"
else:
error["type"] = "other"
errors.append(error)
# Also extract warnings about undefined citations/references
for line in lines:
if "Citation" in line and "undefined" in line:
m = re.search(r"`([^']+)'", line)
key = m.group(1) if m else ""
errors.append({"message": line.strip(), "type": "undefined_citation",
"context": [], "line_num": None, "key": key})
elif "Reference" in line and "undefined" in line:
m = re.search(r"`([^']+)'", line)
key = m.group(1) if m else ""
errors.append({"message": line.strip(), "type": "undefined_reference",
"context": [], "line_num": None, "key": key})
return errors
def fix_unescaped_chars(content: str) -> tuple[str, list[LatexFix]]:
"""Fix unescaped special characters in non-math text."""
fixes = []
# Build pattern to skip math regions
math_regions = []
for m in re.finditer(r'\$\$.*?\$\$|\$.*?\$|\\\(.*?\\\)|\\\[.*?\\\]', content, re.DOTALL):
math_regions.append((m.start(), m.end()))
def in_math(pos):
return any(s <= pos < e for s, e in math_regions)
# Fix unescaped & outside tabular
result = list(content)
char_fixes = [
('&', r'\&', 'unescaped_ampersand'),
('%', r'\%', 'unescaped_percent'),
('#', r'\#', 'unescaped_hash'),
]
for char, replacement, fix_name in char_fixes:
new_content = content
offset = 0
for m in re.finditer(re.escape(char), content):
pos = m.start()
if in_math(pos):
continue
# Check if already escaped
if pos > 0 and content[pos - 1] == '\\':
continue
# Don't fix in tabular environments
before = content[:pos]
if before.count(r'\begin{tabular}') > before.count(r'\end{tabular}'):
continue
fix = LatexFix(fix_name, f"Escaped {char} at position {pos}")
fixes.append(fix)
# Apply fixes via regex (simpler approach)
for char, replacement, _ in char_fixes:
# Only fix in non-math, non-tabular, non-already-escaped contexts
new = []
last = 0
in_tab = 0
for m in re.finditer(r'\\begin\{tabular\}|\\end\{tabular\}|' + re.escape(char), content[last:]):
pass # Complex tracking, use simpler approach
return content, fixes
def fix_html_tags(content: str) -> tuple[str, list[LatexFix]]:
"""Replace HTML-like tags that end up in LaTeX."""
fixes = []
replacements = [
(r'<b>(.*?)</b>', r'\\textbf{\1}', 'html_bold'),
(r'<i>(.*?)</i>', r'\\textit{\1}', 'html_italic'),
(r'<em>(.*?)</em>', r'\\emph{\1}', 'html_emphasis'),
(r'<br\s*/?>', r'\\\\', 'html_br'),
(r'<p>', r'\n\n', 'html_p_open'),
(r'</p>', '', 'html_p_close'),
(r'<code>(.*?)</code>', r'\\texttt{\1}', 'html_code'),
(r'<sub>(.*?)</sub>', r'$_{\1}$', 'html_subscript'),
(r'<sup>(.*?)</sup>', r'$^{\1}$', 'html_superscript'),
(r'</?(div|span|section|h[1-6])[^>]*>', '', 'html_block'),
]
for pattern, repl, fix_name in replacements:
matches = list(re.finditer(pattern, content, re.IGNORECASE))
if matches:
fixes.append(LatexFix(fix_name, f"Replaced {len(matches)} HTML {fix_name} tags"))
content = re.sub(pattern, repl, content, flags=re.IGNORECASE)
return content, fixes
def fix_mismatched_environments(content: str) -> tuple[str, list[LatexFix]]:
"""Detect and report mismatched begin/end environments."""
fixes = []
begins = re.findall(r'\\begin\{(\w+)\}', content)
ends = re.findall(r'\\end\{(\w+)\}', content)
begin_counts = {}
end_counts = {}
for b in begins:
begin_counts[b] = begin_counts.get(b, 0) + 1
for e in ends:
end_counts[e] = end_counts.get(e, 0) + 1
for env in set(list(begin_counts.keys()) + list(end_counts.keys())):
bc = begin_counts.get(env, 0)
ec = end_counts.get(env, 0)
if bc > ec:
# Missing \end — add at end of document
for _ in range(bc - ec):
content = content.rstrip() + f"\n\\end{{{env}}}\n"
fixes.append(LatexFix(f"add_end_{env}", f"Added missing \\end{{{env}}}"))
elif ec > bc:
# Extra \end — remove last occurrence
for _ in range(ec - bc):
idx = content.rfind(f"\\end{{{env}}}")
if idx >= 0:
content = content[:idx] + content[idx + len(f"\\end{{{env}}}"):]
fixes.append(LatexFix(f"remove_end_{env}", f"Removed extra \\end{{{env}}}"))
return content, fixes
def fix_missing_math_mode(content: str) -> tuple[str, list[LatexFix]]:
"""Fix common math-mode issues like unescaped underscores in text."""
fixes = []
# Find _ outside math mode that's part of a variable name pattern
# e.g., "model_name" should become "model\_name" or "$model\_name$"
# This is a simplified heuristic
return content, fixes
def fix_missing_figures(content: str, tex_dir: str) -> tuple[str, list[LatexFix]]:
"""Comment out includegraphics for missing figure files."""
fixes = []
for m in re.finditer(r'\\includegraphics(?:\[.*?\])?\{([^}]+)\}', content):
fig_path = m.group(1)
full_path = os.path.join(tex_dir, fig_path)
# Check with and without common extensions
found = os.path.exists(full_path)
if not found:
for ext in ['.png', '.pdf', '.jpg', '.jpeg', '.eps']:
if os.path.exists(full_path + ext):
found = True
break
if not found:
# Comment out the line containing this includegraphics
line_start = content.rfind('\n', 0, m.start()) + 1
line_end = content.find('\n', m.end())
if line_end == -1:
line_end = len(content)
original_line = content[line_start:line_end]
if not original_line.strip().startswith('%'):
commented = '% FIXME: missing file - ' + original_line
content = content[:line_start] + commented + content[line_end:]
fixes.append(LatexFix("comment_missing_figure",
f"Commented out missing figure: {fig_path}"))
return content, fixes
def auto_detect_log(tex_file: str) -> str | None:
"""Try to find the .log file for a .tex file."""
base = os.path.splitext(tex_file)[0]
log_file = base + ".log"
if os.path.exists(log_file):
return log_file
return None
def main():
parser = argparse.ArgumentParser(description="Automated LaTeX error fixer")
parser.add_argument("--tex", required=True, help="Main .tex file")
parser.add_argument("--log", help="pdflatex .log file")
parser.add_argument("--auto-detect", action="store_true", help="Auto-detect .log file")
parser.add_argument("--output", "-o", help="Output .tex file")
parser.add_argument("--dry-run", action="store_true", help="Show fixes without applying")
args = parser.parse_args()
if not os.path.exists(args.tex):
print(f"Error: {args.tex} not found", file=sys.stderr)
sys.exit(1)
with open(args.tex, encoding="utf-8", errors="replace") as f:
content = f.read()
tex_dir = os.path.dirname(os.path.abspath(args.tex))
# Load error log if available
log_content = ""
log_file = args.log
if not log_file and args.auto_detect:
log_file = auto_detect_log(args.tex)
if log_file and os.path.exists(log_file):
with open(log_file, encoding="utf-8", errors="replace") as f:
log_content = f.read()
errors = parse_errors(log_content) if log_content else []
if errors:
print(f"Found {len(errors)} errors/warnings in log", file=sys.stderr)
for e in errors[:10]:
print(f" [{e['type']}] {e['message'][:80]}", file=sys.stderr)
# Apply fixes
all_fixes = []
content, fixes = fix_html_tags(content)
all_fixes.extend(fixes)
content, fixes = fix_mismatched_environments(content)
all_fixes.extend(fixes)
content, fixes = fix_missing_figures(content, tex_dir)
all_fixes.extend(fixes)
content, fixes = fix_missing_math_mode(content)
all_fixes.extend(fixes)
if args.dry_run:
print(f"\n## Fixes that would be applied ({len(all_fixes)}):")
for fix in all_fixes:
print(f" - [{fix.name}] {fix.description}")
if not all_fixes:
print(" No fixes needed.")
sys.exit(0)
if not all_fixes:
print("No fixes needed.", file=sys.stderr)
sys.exit(0)
print(f"\nApplied {len(all_fixes)} fixes:", file=sys.stderr)
for fix in all_fixes:
print(f" - [{fix.name}] {fix.description}", file=sys.stderr)
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
f.write(content)
print(f"Fixed file written to {args.output}", file=sys.stderr)
else:
sys.stdout.write(content)
if __name__ == "__main__":
main()