Skip to content

Commit 81fadd5

Browse files
committed
feat(manuscript): add LaTeX submission packaging script
1 parent 720786d commit 81fadd5

1 file changed

Lines changed: 188 additions & 0 deletions

File tree

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
#!/usr/bin/env python3
2+
"""Package a flat, submission-ready zip from the multi-venue LaTeX setup."""
3+
4+
import re
5+
import shutil
6+
import subprocess
7+
import sys
8+
import tempfile
9+
from pathlib import Path
10+
11+
REPO_ROOT = Path(__file__).resolve().parent.parent
12+
MANUSCRIPT = REPO_ROOT / "manuscript"
13+
SHARED = MANUSCRIPT / "shared"
14+
15+
VENUES = {
16+
"oxford": {
17+
"support_globs": ["*.cls", "*.bst"],
18+
},
19+
"biorxiv": {
20+
"support_globs": ["*.cls", "*.bst", "*.sty"],
21+
},
22+
}
23+
24+
# Paths in main.tex that reference the shared directory
25+
SHARED_PATH_PATTERNS = [
26+
(re.compile(r"\{\.\.\/shared\/Fig\/\}"), "{./}"),
27+
(re.compile(r"\{\.\.\/shared\/([^}]+)\}"), r"{\1}"),
28+
(re.compile(r"\\input\{\.\.\/shared\/([^}]+)\}"), r"\\input{\1}"),
29+
]
30+
31+
32+
# ── helpers ──────────────────────────────────────────────────────────────────
33+
34+
def die(msg: str) -> None:
35+
print(f"Error: {msg}", file=sys.stderr)
36+
sys.exit(1)
37+
38+
39+
def pick_venue() -> str:
40+
"""Let the user choose a venue or accept a CLI argument."""
41+
available = [v for v in VENUES if (MANUSCRIPT / v).is_dir()]
42+
if not available:
43+
die("No venue directories found in manuscript/.")
44+
45+
if len(sys.argv) > 1:
46+
venue = sys.argv[1].strip().lower()
47+
if venue not in available:
48+
die(f"Unknown venue '{venue}'. Available: {', '.join(available)}")
49+
return venue
50+
51+
if len(available) == 1:
52+
return available[0]
53+
54+
print("Available venues:")
55+
for i, v in enumerate(available, 1):
56+
print(f" {i}) {v}")
57+
choice = input(f"Select venue [1-{len(available)}]: ").strip()
58+
try:
59+
return available[int(choice) - 1]
60+
except (ValueError, IndexError):
61+
die("Invalid selection.")
62+
return "" # unreachable
63+
64+
65+
def collect_files(venue: str) -> list[tuple[Path, str]]:
66+
"""Return (source_path, flat_name) pairs for all files to include."""
67+
venue_dir = MANUSCRIPT / venue
68+
files: list[tuple[Path, str]] = []
69+
70+
# main.tex and pre-compiled bibliography
71+
files.append((venue_dir / "main.tex", "main.tex"))
72+
bbl = venue_dir / "main.bbl"
73+
if bbl.exists():
74+
files.append((bbl, "main.bbl"))
75+
76+
# venue-specific support files (cls, bst, sty)
77+
for glob in VENUES[venue]["support_globs"]:
78+
for p in sorted(venue_dir.glob(glob)):
79+
files.append((p, p.name))
80+
81+
# shared content and bibliography
82+
for name in ["content.tex", "abbreviations.tex", "reference.bib"]:
83+
p = SHARED / name
84+
if p.exists():
85+
files.append((p, name))
86+
87+
# figures
88+
fig_dir = SHARED / "Fig"
89+
if fig_dir.is_dir():
90+
for p in sorted(fig_dir.iterdir()):
91+
if p.is_file():
92+
files.append((p, p.name))
93+
94+
return files
95+
96+
97+
def flatten_main_tex(path: Path) -> None:
98+
"""Rewrite main.tex so all paths point to the flat directory."""
99+
text = path.read_text()
100+
for pattern, replacement in SHARED_PATH_PATTERNS:
101+
text = pattern.sub(replacement, text)
102+
path.write_text(text)
103+
104+
105+
def compile_pdf(work_dir: Path) -> Path:
106+
"""Run pdflatex and return the path to the resulting PDF."""
107+
pdf = work_dir / "main.pdf"
108+
cmd = ["pdflatex", "-interaction=nonstopmode", "main.tex"]
109+
result = subprocess.run(cmd, cwd=work_dir, capture_output=True)
110+
if not pdf.exists():
111+
output = result.stdout.decode("utf-8", errors="replace")
112+
print(output[-2000:] if len(output) > 2000 else output)
113+
die("pdflatex failed — see output above.")
114+
return pdf
115+
116+
117+
def open_pdf(pdf: Path) -> None:
118+
"""Open the PDF with the system viewer."""
119+
opener = shutil.which("xdg-open") or shutil.which("open")
120+
if opener:
121+
subprocess.Popen([opener, str(pdf)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
122+
else:
123+
print(f" Open manually: {pdf}")
124+
125+
126+
def clean_build_artifacts(work_dir: Path) -> None:
127+
"""Remove LaTeX build artifacts, keeping only source files."""
128+
extensions = {".aux", ".log", ".out", ".fls", ".fdb_latexmk", ".blg", ".pdf", ".synctex.gz"}
129+
for p in work_dir.iterdir():
130+
if p.suffix in extensions:
131+
p.unlink()
132+
133+
134+
# ── main ─────────────────────────────────────────────────────────────────────
135+
136+
def main() -> None:
137+
venue = pick_venue()
138+
venue_dir = MANUSCRIPT / venue
139+
print(f"Packaging '{venue}' submission …\n")
140+
141+
files = collect_files(venue)
142+
missing = [src for src, _ in files if not src.exists()]
143+
if missing:
144+
die(f"Missing files:\n " + "\n ".join(str(p) for p in missing))
145+
146+
# 1. Create temp directory and copy files
147+
tmp = Path(tempfile.mkdtemp(prefix="gll-submission-"))
148+
print(f"Working in {tmp}\n")
149+
for src, name in files:
150+
shutil.copy2(src, tmp / name)
151+
print(f" {name:<40s}{src.relative_to(MANUSCRIPT)}")
152+
153+
# 2. Flatten paths in main.tex
154+
flatten_main_tex(tmp / "main.tex")
155+
print(f"\n Flattened paths in main.tex")
156+
157+
# 3. Compile test PDF
158+
print(f"\n Compiling test PDF …")
159+
pdf = compile_pdf(tmp)
160+
print(f" OK — {pdf.stat().st_size / 1024:.0f} KB\n")
161+
162+
# 4. Let user verify
163+
open_pdf(pdf)
164+
answer = input("Verify the PDF. Package into zip? [Y/n] ").strip().lower()
165+
if answer not in ("", "y", "yes"):
166+
shutil.rmtree(tmp)
167+
print("Aborted — temp directory removed.")
168+
return
169+
170+
# 5. Clean build artifacts
171+
clean_build_artifacts(tmp)
172+
173+
# 6. Create zip
174+
zip_name = f"{venue}-latex-submission"
175+
zip_path = MANUSCRIPT / zip_name
176+
shutil.make_archive(str(zip_path), "zip", tmp)
177+
final_zip = zip_path.with_suffix(".zip")
178+
size_mb = final_zip.stat().st_size / (1024 * 1024)
179+
180+
# 7. Clean up temp directory
181+
shutil.rmtree(tmp)
182+
183+
print(f"\n Created {final_zip.relative_to(REPO_ROOT)} ({size_mb:.1f} MB)")
184+
print(f" Temp directory removed.")
185+
186+
187+
if __name__ == "__main__":
188+
main()

0 commit comments

Comments
 (0)