
Latex Formatting
- 12 installs
- 255 repo stars
- Updated February 27, 2026
- lingzhi227/claude-skills
This is a copy of latex-formatting by lingzhi227 - installs and ranking accrue to the original listing.
Helps with ai & agent building tasks.
About
latex-formatting is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- latex-formatting
- AI & Agent Building
- AI-coding skill
Latex Formatting by the numbers
- 12 all-time installs (skills.sh)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/lingzhi227/claude-skills --skill latex-formattingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12 |
|---|---|
| repo stars | ★ 255 |
| Last updated | February 27, 2026 |
| Repository | lingzhi227/claude-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
LaTeX Formatting
Set up and manage LaTeX formatting for academic papers.
Input
$0— Action:setup,fix,check$1— Venue name (forsetup) or.texfile path (forfix/check)
Scripts
Pre-submission format checker
python ~/.claude/skills/latex-formatting/scripts/latex_checker.py paper/main.tex --venue neurips --check-anonChecks: word count, required sections, TODO markers, anonymization, mismatched environments, content stats.
Validate citations and references
python ~/.claude/skills/citation-management/scripts/validate_citations.py \
--tex paper/main.tex --bib paper/references.bib --check-figuresClean LaTeX text (fix special characters)
python ~/.claude/skills/latex-formatting/scripts/clean_latex.py \
--input paper/main.tex --output paper/main_cleaned.texReplaces special/non-UTF8 characters with LaTeX equivalents, skips math environments. Key flags: --dry-run, --tables-only
Auto-fix after checking
python ~/.claude/skills/latex-formatting/scripts/latex_checker.py paper/main.tex --venue neurips --fixRuns checks then applies clean_latex.py fixes, writing to main_fixed.tex.
References
- Venue specs, project structure, packages, commands:
~/.claude/skills/latex-formatting/references/venue-templates.md
Action: setup
Create the project directory structure and main.tex for the specified venue. Use the template from references/venue-templates.md.
Action: fix
Fix common LaTeX issues: unescaped special chars, math mode errors, float placement, overfull/underfull boxes, cross-reference issues.
Action: check
Run latex_checker.py for pre-submission validation. Check page count, anonymization, required sections, TODO markers.
Related Skills
- Upstream: paper-writing-section, table-generation
- Downstream: paper-compilation
- See also: citation-management
Conference Venue Templates Reference
Venue Specifications
| Venue | Pages | Columns | Style | Anonymous | Refs Extra |
|---|---|---|---|---|---|
| NeurIPS | 9 | 1 | neurips_2025.sty | Yes | Yes |
| ICML | 8 | 2 | icml2025.sty | Yes | Yes |
| ICLR | 9 | 1 | iclr2025_conference.sty | Yes | Yes |
| AAAI | 7+1 | 2 | aaai25.sty | Yes | 1 extra |
| ACL/EMNLP | 8 | 2 | acl.sty | Yes | Yes |
| CVPR/ICCV | 8 | 2 | cvpr.sty | Yes | Yes |
| ICBINB | 4 | 2 | icbinb.sty | Yes | Yes |
| arXiv | ∞ | 1 | article.cls | No | N/A |
Standard Project Structure
paper/
├── main.tex
├── references.bib
├── sections/
│ ├── abstract.tex
│ ├── introduction.tex
│ ├── related_work.tex
│ ├── background.tex
│ ├── methods.tex
│ ├── experiments.tex
│ ├── results.tex
│ └── conclusion.tex
├── figures/
├── tables/
└── appendix/
└── appendix.texEssential Packages (always include)
\usepackage{amsmath,amssymb,amsthm} % Math
\usepackage{graphicx} % Figures
\usepackage{booktabs} % Professional tables
\usepackage{hyperref} % Clickable references
\usepackage{algorithm,algpseudocode} % Algorithms
\usepackage{subcaption} % Subfigures
\usepackage{xcolor} % Colors
\usepackage{enumitem} % List customization
\usepackage{multirow} % Table multi-row
\usepackage{cleveref} % Smart references
\usepackage{microtype} % Better typographyCommon Custom Commands
\DeclareMathOperator*{\argmin}{arg\,min}
\DeclareMathOperator*{\argmax}{arg\,max}
\newcommand{\norm}[1]{\left\| #1 \right\|}
\newcommand{\abs}[1]{\left| #1 \right|}
\newcommand{\ie}{\textit{i.e.}}
\newcommand{\eg}{\textit{e.g.}}
\newcommand{\etal}{\textit{et al.}}
% Remove before submission:
\newcommand{\todo}[1]{\textcolor{red}{[TODO: #1]}}Anonymization Checklist
- [ ] No author names in
\author{} - [ ] No "our previous work [X]" or "we previously showed"
- [ ] No GitHub/institutional URLs
- [ ] No acknowledgments section
- [ ] No grant numbers or funding info
- [ ] Supplementary material is anonymous too
#!/usr/bin/env python3
"""Clean and sanitize LaTeX text for compilation.
Replaces special characters with LaTeX equivalents, handles math/text
mode separation, fixes non-UTF8 characters, and cleans table content.
Extracted from data-to-paper. Stdlib-only (uses re instead of regex).
Usage:
python clean_latex.py --input draft.tex --output cleaned.tex
python clean_latex.py --input draft.tex --dry-run
python clean_latex.py --input draft.tex --output cleaned.tex --tables-only
"""
import argparse
import os
import re
import sys
# Special characters to LaTeX escape sequences
CHARS = {
'&': r'\&',
'%': r'\%',
'#': r'\#',
'_': r'\_',
'^': r'\textasciicircum{}',
'<': r'$<$',
'>': r'$>$',
'\u2264': r'$\leq$', # ≤
'\u2265': r'$\geq$', # ≥
'\u2260': r'$\neq$', # ≠
'\u00b1': r'$\pm$', # ±
'\u00d7': r'$\times$', # ×
'\u00f7': r'$\div$', # ÷
'\u00b0': r'$^{\circ}$', # °
'\u221e': r'$\infty$', # ∞
'\u221a': r'$\sqrt{}$', # √
'\u2211': r'$\sum$', # ∑
'\u220f': r'$\prod$', # ∏
'|': r'\textbar{}',
'\u2208': r'$\in$', # ∈
'\u2209': r'$\notin$', # ∉
'\u2200': r'$\forall$', # ∀
'\u2203': r'$\exists$', # ∃
'\u2205': r'$\emptyset$', # ∅
'\u200b': '', # zero width space
'\u202f': ' ', # narrow no-break space
}
# Non-UTF8 characters to LaTeX-safe replacements
NON_UTF8_CHARS = {
'\u2013': '--', # –
'\u2019': "'", # '
'\u2018': "`", # '
'\u201c': "``", # "
'\u201d': "''", # "
'\u00b2': r'$^2$', # ²
'\u00b3': r'$^3$', # ³
'\u00bc': r'$\frac{1}{4}$', # ¼
'\u00bd': r'$\frac{1}{2}$', # ½
'\u00be': r'$\frac{3}{4}$', # ¾
'\u2206': r'$\Delta$', # ∆
'\u2207': r'$\nabla$', # ∇
'\u2202': r'$\partial$', # ∂
'\u2014': '---', # —
'\u2026': r'\ldots{}', # …
'\u00e9': r"\'e", # é
'\u00e8': r"\`e", # è
'\u00fc': r'\"u', # ü
'\u00f6': r'\"o', # ö
'\u00e4': r'\"a', # ä
}
TABLE_CHARS = {
'>': r'$>$',
'<': r'$<$',
'=': r'$=$',
'|': r'\textbar{}',
}
# Simplified math pattern (stdlib re, no recursion)
# Matches: $...$, $$...$$, \(...\), \[...\], \begin{equation}...\end{equation},
# \begin{align}...\end{align}, \begin{figure}...\end{figure}
MATH_ENVS = [
(r'\$\$', r'\$\$'), # $$...$$
(r'(?<!\$)\$(?!\$)', r'(?<!\$)\$(?!\$)'), # $...$
(r'\\\(', r'\\\)'), # \(...\)
(r'\\\[', r'\\\]'), # \[...\]
]
LATEX_ENV_NAMES = [
'equation', 'equation*', 'align', 'align*', 'gather', 'gather*',
'math', 'displaymath', 'figure', 'figure*', 'lstlisting',
'tabular', 'tabular*', 'array',
]
SKIP_COMMANDS = [r'\\ref\{[^}]*\}', r'\\label\{[^}]*\}', r'\\autoref\{[^}]*\}',
r'\\cite[a-z]*\{[^}]*\}', r'\\url\{[^}]*\}', r'\\href\{[^}]*\}\{[^}]*\}']
def build_skip_pattern() -> re.Pattern:
"""Build a combined regex pattern for all math/skip regions."""
patterns = []
# Comments (% to end of line, unless escaped)
patterns.append(r'(?<!\\)%[^\n]*')
# Command definitions (contain # for parameters)
patterns.append(r'\\(?:(?:re)?newcommand|providecommand|DeclareMathOperator)\*?'
r'(?:\{[^}]*\}|\[[^\]]*\])*\{[^}]*\}')
patterns.append(r'\\def\\[a-zA-Z@]+[^{]*\{[^}]*\}')
# Dollar signs
patterns.append(r'\$\$.*?\$\$')
patterns.append(r'(?<!\$)\$(?!\$).*?(?<!\$)\$(?!\$)')
# Escaped delimiters
patterns.append(r'\\\(.*?\\\)')
patterns.append(r'\\\[.*?\\\]')
# Named environments
for env in LATEX_ENV_NAMES:
esc = re.escape(env)
patterns.append(rf'\\begin\{{{esc}\}}.*?\\end\{{{esc}\}}')
# Skip commands
patterns.extend(SKIP_COMMANDS)
return re.compile('|'.join(patterns), re.DOTALL)
SKIP_RE = build_skip_pattern()
def replace_special_latex_chars(text: str) -> str:
"""Replace special characters in text (non-math) with LaTeX equivalents."""
chars_pattern = '|'.join(re.escape(c) for c in CHARS.keys())
pattern = re.compile(rf'(?<!\\)({chars_pattern})')
return pattern.sub(lambda m: CHARS[m.group(1)], text)
def replace_non_utf8_chars(text: str) -> str:
"""Replace non-UTF8 characters with LaTeX-safe equivalents."""
for char, replacement in NON_UTF8_CHARS.items():
text = text.replace(char, replacement)
return text
def process_latex_text_and_math(text: str, process_text=None, process_math=None) -> str:
"""Process text and math regions separately.
Applies process_text to non-math regions and process_math to math regions.
"""
if process_text is None:
process_text = replace_special_latex_chars
if process_math is None:
process_math = lambda x: x
result = []
last_end = 0
for match in SKIP_RE.finditer(text):
# Process non-math text before this match
non_math = text[last_end:match.start()]
result.append(process_text(non_math))
# Keep math region as-is (or apply process_math)
result.append(process_math(match.group()))
last_end = match.end()
# Process remaining text after last match
result.append(process_text(text[last_end:]))
return "".join(result)
def escape_table_chars(text: str) -> str:
"""Escape special characters in table cells."""
pattern = re.compile('|'.join(re.escape(k) for k in TABLE_CHARS.keys()))
return pattern.sub(lambda m: TABLE_CHARS[m.group()], text)
def escape_special_chars_in_table(table: str,
begin: str = r'\begin{tabular}',
end: str = r'\end{tabular}') -> str:
"""Apply character escaping to the tabular content of a LaTeX table."""
if begin not in table:
return table
if end not in table:
return table
before, rest = table.split(begin, 1)
tabular, after = rest.split(end, 1)
tabular = process_latex_text_and_math(tabular, escape_table_chars)
return before + begin + tabular + end + after
def clean_latex_file(content: str, tables_only: bool = False) -> str:
"""Clean a full LaTeX file content."""
if tables_only:
# Only clean table environments
parts = re.split(r'(\\begin\{tabular\}.*?\\end\{tabular\})', content, flags=re.DOTALL)
result = []
for part in parts:
if part.startswith(r'\begin{tabular}'):
result.append(escape_special_chars_in_table(part))
else:
result.append(part)
return ''.join(result)
# Full cleaning
content = replace_non_utf8_chars(content)
content = process_latex_text_and_math(content)
return content
def main():
parser = argparse.ArgumentParser(description="Clean and sanitize LaTeX text")
parser.add_argument("--input", required=True, help="Input .tex file")
parser.add_argument("--output", "-o", help="Output .tex file (default: stdout)")
parser.add_argument("--dry-run", action="store_true", help="Show changes without writing")
parser.add_argument("--tables-only", action="store_true", help="Only clean table environments")
args = parser.parse_args()
with open(args.input, encoding="utf-8", errors="replace") as f:
content = f.read()
cleaned = clean_latex_file(content, tables_only=args.tables_only)
if args.dry_run:
# Show diff summary
orig_lines = content.splitlines()
new_lines = cleaned.splitlines()
changes = 0
for i, (old, new) in enumerate(zip(orig_lines, new_lines)):
if old != new:
changes += 1
if changes <= 20:
print(f"Line {i+1}:")
print(f" - {old[:100]}")
print(f" + {new[:100]}")
if changes > 20:
print(f"... and {changes - 20} more changes")
print(f"\nTotal lines changed: {changes}")
return
if args.output:
with open(args.output, "w", encoding="utf-8") as f:
f.write(cleaned)
print(f"Cleaned file written to {args.output}", file=sys.stderr)
else:
sys.stdout.write(cleaned)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Pre-submission LaTeX format checker.
Validates a LaTeX paper against common venue requirements:
page count, anonymization, required sections, formatting issues.
Self-contained: uses only stdlib.
Usage:
python latex_checker.py paper/main.tex
python latex_checker.py paper/main.tex --venue neurips --page-limit 9
python latex_checker.py paper/main.tex --check-anon
"""
import argparse
import os
import re
import sys
VENUE_LIMITS = {
"neurips": {"pages": 9, "columns": 1, "anonymous": True},
"icml": {"pages": 8, "columns": 2, "anonymous": True},
"iclr": {"pages": 9, "columns": 1, "anonymous": True},
"aaai": {"pages": 7, "columns": 2, "anonymous": True},
"acl": {"pages": 8, "columns": 2, "anonymous": True},
"emnlp": {"pages": 8, "columns": 2, "anonymous": True},
"cvpr": {"pages": 8, "columns": 2, "anonymous": True},
"icbinb": {"pages": 4, "columns": 2, "anonymous": True},
"arxiv": {"pages": None, "columns": None, "anonymous": False},
}
REQUIRED_SECTIONS = [
"abstract", "introduction",
]
EXPECTED_SECTIONS = [
"abstract", "introduction", "related work",
"method", "experiment", "result", "conclusion",
]
def load_tex(path: str) -> str:
"""Load tex file, following \\input{} directives."""
with open(path, encoding="utf-8", errors="replace") as f:
content = f.read()
# Resolve \input{file} directives
base_dir = os.path.dirname(path)
def resolve_input(match):
fname = match.group(1)
if not fname.endswith(".tex"):
fname += ".tex"
fpath = os.path.join(base_dir, fname)
if os.path.exists(fpath):
with open(fpath, encoding="utf-8", errors="replace") as f:
return f.read()
return match.group(0)
content = re.sub(r"\\input\{([^}]+)\}", resolve_input, content)
return content
def estimate_word_count(tex_content: str) -> int:
"""Rough word count excluding LaTeX commands, math, and comments."""
# Remove comments
text = re.sub(r"%.*$", "", tex_content, flags=re.MULTILINE)
# Remove math environments
text = re.sub(r"\$\$.*?\$\$", "", text, flags=re.DOTALL)
text = re.sub(r"\$.*?\$", "", text)
text = re.sub(r"\\begin\{(?:equation|align|gather|math).*?\}.*?\\end\{(?:equation|align|gather|math).*?\}", "", text, flags=re.DOTALL)
# Remove LaTeX commands
text = re.sub(r"\\[a-zA-Z]+(?:\[.*?\])?\{([^}]*)\}", r"\1", text)
text = re.sub(r"\\[a-zA-Z]+", "", text)
# Remove braces and special chars
text = re.sub(r"[{}\\]", "", text)
words = text.split()
return len(words)
def check_anonymization(tex_content: str) -> list[str]:
"""Check for anonymization violations."""
issues = []
# Check for author names in common locations
author_match = re.search(r"\\author\{([^}]+)\}", tex_content)
if author_match:
author_text = author_match.group(1)
if "anonymous" not in author_text.lower() and "author" not in author_text.lower():
issues.append(f"Author field contains names: {author_text[:50]}...")
# Check for self-citations like "our previous work [1]"
self_cite_patterns = [
r"our (?:previous|prior|earlier|recent) (?:work|paper|study)",
r"we (?:previously|earlier|recently) (?:proposed|showed|demonstrated)",
r"in our (?:previous|prior|earlier) (?:work|paper)",
]
for pat in self_cite_patterns:
if re.search(pat, tex_content, re.IGNORECASE):
issues.append(f"Possible self-citation: pattern '{pat}' found")
# Check for GitHub/institutional links
url_patterns = [
(r"github\.com/[a-zA-Z0-9_-]+/", "GitHub link found"),
(r"gitlab\.com/[a-zA-Z0-9_-]+/", "GitLab link found"),
(r"\\url\{https?://(?!arxiv|doi|paperswithcode)[^}]+\}", "Non-anonymous URL found"),
]
for pat, msg in url_patterns:
match = re.search(pat, tex_content)
if match:
issues.append(f"{msg}: {match.group(0)[:60]}")
# Check for acknowledgments (should be removed for review)
if re.search(r"\\section\*?\{Acknowledgment", tex_content, re.IGNORECASE):
issues.append("Acknowledgments section present (remove for anonymous submission)")
return issues
def check_required_sections(tex_content: str) -> tuple[list[str], list[str]]:
"""Check for required and expected sections."""
sections = re.findall(r"\\section\*?\{([^}]+)\}", tex_content)
section_names_lower = [s.lower().strip() for s in sections]
# Check abstract
has_abstract = bool(re.search(r"\\begin\{abstract\}", tex_content))
missing_required = []
if not has_abstract and "abstract" not in section_names_lower:
missing_required.append("Abstract")
if not any("introduction" in s for s in section_names_lower):
missing_required.append("Introduction")
missing_expected = []
for expected in EXPECTED_SECTIONS:
if expected == "abstract":
if not has_abstract:
missing_expected.append("Abstract")
elif not any(expected in s for s in section_names_lower):
missing_expected.append(expected.title())
return missing_required, missing_expected
def check_todos(tex_content: str) -> list[str]:
"""Find remaining TODO/TBD/FIXME markers."""
issues = []
patterns = [r"TODO", r"TBD", r"FIXME", r"XXX", r"HACK"]
for pat in patterns:
matches = list(re.finditer(pat, tex_content, re.IGNORECASE))
for m in matches:
# Get surrounding context
start = max(0, m.start() - 30)
end = min(len(tex_content), m.end() + 30)
context = tex_content[start:end].replace("\n", " ").strip()
# Skip if in a comment
line_start = tex_content.rfind("\n", 0, m.start()) + 1
line = tex_content[line_start:m.end()]
if "%" in line[:line.index(pat) if pat in line else 0]:
continue
issues.append(f"{pat} found: ...{context}...")
return issues
def main():
parser = argparse.ArgumentParser(description="Pre-submission LaTeX checker")
parser.add_argument("tex_file", help="Main .tex file")
parser.add_argument("--venue", choices=list(VENUE_LIMITS.keys()), help="Target venue")
parser.add_argument("--page-limit", type=int, help="Override page limit")
parser.add_argument("--check-anon", action="store_true", help="Check anonymization")
parser.add_argument("--fix", action="store_true", help="Apply fixes using clean_latex.py")
args = parser.parse_args()
tex_content = load_tex(args.tex_file)
issues_total = 0
print(f"Checking: {args.tex_file}")
if args.venue:
print(f"Venue: {args.venue.upper()}")
print()
# Word count
word_count = estimate_word_count(tex_content)
print(f"Estimated word count: {word_count}")
# Section count
sections = re.findall(r"\\section\*?\{([^}]+)\}", tex_content)
print(f"Sections: {len(sections)}")
for s in sections:
print(f" - {s}")
print()
# Required sections
missing_req, missing_exp = check_required_sections(tex_content)
if missing_req:
print(f"MISSING REQUIRED SECTIONS:")
for s in missing_req:
print(f" - {s}")
issues_total += len(missing_req)
if missing_exp:
print(f"MISSING EXPECTED SECTIONS:")
for s in missing_exp:
print(f" - {s}")
print()
# TODOs
todos = check_todos(tex_content)
if todos:
print(f"REMAINING MARKERS ({len(todos)}):")
for t in todos[:10]:
print(f" - {t}")
issues_total += len(todos)
print()
# Anonymization
if args.check_anon or (args.venue and VENUE_LIMITS.get(args.venue, {}).get("anonymous")):
anon_issues = check_anonymization(tex_content)
if anon_issues:
print(f"ANONYMIZATION ISSUES ({len(anon_issues)}):")
for issue in anon_issues:
print(f" - {issue}")
issues_total += len(anon_issues)
else:
print("Anonymization: OK")
print()
# Common formatting issues
fmt_issues = []
# Check for unescaped special chars (outside math mode, simplified)
for char, escaped in [("_", r"\_"), ("%", r"\%"), ("&", r"\&")]:
# This is a simplified check
pass
# Check for \begin without \end
begins = re.findall(r"\\begin\{(\w+)\}", tex_content)
ends = re.findall(r"\\end\{(\w+)\}", tex_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:
fmt_issues.append(f"Mismatched environment: \\begin{{{env}}} ({bc}x) vs \\end{{{env}}} ({ec}x)")
if fmt_issues:
print(f"FORMATTING ISSUES ({len(fmt_issues)}):")
for issue in fmt_issues:
print(f" - {issue}")
issues_total += len(fmt_issues)
print()
# Stats
cite_count = len(re.findall(r"\\cite[a-z]*\{", tex_content))
fig_count = len(re.findall(r"\\begin\{figure", tex_content))
table_count = len(re.findall(r"\\begin\{table", tex_content))
eq_count = len(re.findall(r"\\begin\{(?:equation|align)", tex_content))
print(f"Content stats:")
print(f" Citations: {cite_count}")
print(f" Figures: {fig_count}")
print(f" Tables: {table_count}")
print(f" Equations: {eq_count}")
print()
# Summary
if issues_total == 0:
print("All checks passed!")
else:
print(f"Total issues: {issues_total}")
if args.fix and issues_total > 0:
clean_script = os.path.join(os.path.dirname(__file__), "clean_latex.py")
if os.path.exists(clean_script):
import subprocess
fixed_path = args.tex_file.replace(".tex", "_fixed.tex")
fix_cmd = [sys.executable, clean_script, "--input", args.tex_file, "--output", fixed_path]
result = subprocess.run(fix_cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f"\nFixed file written to: {fixed_path}")
if result.stderr:
print(result.stderr)
else:
print(f"\nFix failed: {result.stderr}", file=sys.stderr)
sys.exit(1 if issues_total > 0 else 0)
if __name__ == "__main__":
main()