Skill Tracer
Skills › Scientific Agent Skills › literature-review › generate_pdf.py

generate_pdf.py

107 lines · skills/literature-review/scripts/generate_pdf.py at 5f2bc03

1#!/usr/bin/env python3
2"""Render reviewed Markdown with Pandoc/XeLaTeX and optional local CSL bibliography."""
3
4import argparse
5import os
6import subprocess
7from pathlib import Path
8
9
10def check_dependencies():
11 """Check both executables; TeX package availability still needs a real render."""
12 available = True
13 for name in ('pandoc', 'xelatex'):
14 try:
15 subprocess.run([name, '--version'], capture_output=True, check=True)
16 print(f'[OK] {name} is installed')
17 except (subprocess.CalledProcessError, OSError):
18 print(f'[FAIL] {name} is NOT installed')
19 available = False
20 return available
23def generate_pdf(markdown_file: str, output_pdf: str = None,
24 citation_style: str = None, template: str = None,
25 toc: bool = True, number_sections: bool = True,
26 bibliography: str = None) -> bool:
27 """Render PDF. Style is a local CSL path/name; omitted uses Pandoc's default.
29 A sibling .bib is detected automatically. Pandoc citation syntax [@key] is
30 required for citeproc; manually typed reference text is not reformatted.
31 """
32 source = Path(markdown_file).resolve()
33 destination = Path(output_pdf).resolve() if output_pdf else source.with_suffix('.pdf')
34 if not source.is_file() or source == destination:
35 print('[FAIL] Input must exist and output must differ from input')
36 return False
37 if not check_dependencies():
38 return False
39 cmd = ['pandoc', str(source), '-o', str(destination), '--pdf-engine=xelatex',
40 '--resource-path', os.pathsep.join([str(source.parent), str(Path.cwd())]),
41 '-V', 'geometry:margin=1in', '-V', 'fontsize=11pt',
42 '-V', 'colorlinks=true', '-V', 'linkcolor=blue', '-V', 'urlcolor=blue',
43 '-V', 'citecolor=blue', '--citeproc']
44 if toc:
45 cmd.extend(['--toc', '--toc-depth=3'])
46 if number_sections:
47 cmd.append('--number-sections')
48 bib = Path(bibliography).resolve() if bibliography else source.with_suffix('.bib')
49 if bibliography and not bib.is_file():
50 print(f'[FAIL] Bibliography not found: {bib}')
51 return False
52 if bib.is_file():
53 cmd.extend(['--bibliography', str(bib)])
54 if citation_style:
55 style = Path(citation_style if citation_style.endswith('.csl') else citation_style + '.csl')
56 if not style.is_file():
57 style = source.parent / style
58 if not style.is_file():
59 print('[FAIL] Supply an existing CSL file with --csl; style names are not downloaded automatically')
60 return False
61 cmd.extend(['--csl', str(style.resolve())])
62 if template:
63 if not Path(template).is_file():
64 print(f'[FAIL] Template not found: {template}')
65 return False
66 cmd.extend(['--template', str(Path(template).resolve())])
67 try:
68 result = subprocess.run(cmd, capture_output=True, text=True, check=True)
69 if result.stderr:
70 print(result.stderr.strip()) # Surface missing citation and rendering warnings.
71 if not destination.is_file() or not destination.read_bytes().startswith(b'%PDF-'):
72 print('[FAIL] Pandoc returned without a valid PDF output')
73 return False
74 print(f'[OK] PDF generated: {destination}')
75 return True
76 except (subprocess.CalledProcessError, OSError) as exc:
77 print(f'[FAIL] PDF generation: {getattr(exc, "stderr", None) or exc}')
78 return False
81def main():
82 parser = argparse.ArgumentParser(description=__doc__)
83 parser.add_argument('markdown_file', nargs='?')
84 parser.add_argument('output_pdf', nargs='?', help='Legacy positional output path')
85 parser.add_argument('-o', '--output')
86 parser.add_argument('--csl', '--citation-style', dest='citation_style',
87 help='Local CSL path, or local style name such as apa (requires apa.csl)')
88 parser.add_argument('--bibliography', help='Local .bib, .json or other Pandoc bibliography file')
89 parser.add_argument('--template')
90 parser.add_argument('--no-toc', action='store_true')
91 parser.add_argument('--no-numbers', action='store_true')
92 parser.add_argument('--check-deps', action='store_true')
93 args = parser.parse_args()
94 if args.check_deps:
95 raise SystemExit(0 if check_dependencies() else 1)
96 if not args.markdown_file:
97 parser.error('markdown_file is required unless --check-deps is used')
98 if args.output and args.output_pdf:
99 parser.error('choose either positional output or --output')
100 raise SystemExit(0 if generate_pdf(args.markdown_file, args.output or args.output_pdf,
101 citation_style=args.citation_style, template=args.template,
102 toc=not args.no_toc, number_sections=not args.no_numbers,
103 bibliography=args.bibliography) else 1)
106if __name__ == '__main__':
107 main()