Skip to content

Instantly share code, notes, and snippets.

@stevecooperorg
Created April 26, 2025 15:43
Show Gist options
  • Save stevecooperorg/db397f4ace04fef60e31967a7a5ebe1e to your computer and use it in GitHub Desktop.
Save stevecooperorg/db397f4ace04fef60e31967a7a5ebe1e to your computer and use it in GitHub Desktop.

md2pdf

A tool to convert Markdown files to PDF using Pandoc and Typst.

Requirements

These three programs should be in your path;

  • Python 3
  • Pandoc
  • Typst

Installation

  1. Make the script executable:
chmod +x md2pdf
  1. Optionally, add the bin directory to your PATH:
export PATH="$PATH:/path/to/bin"

Usage

./md2pdf <root_dir> <input_markdown> <output_pdf> <template>

Arguments

  • root_dir: The project root directory (passed to typst's --root)
  • input_markdown: Path to markdown file (relative to root)
  • output_pdf: Path to output PDF (relative to root)
  • template: Path to template file (relative to root)

Example

./md2pdf . summaries/glossary.md out/summaries/glossary.pdf my-template.typ

Markdown Metadata

The tool supports Pandoc-style metadata at the top of your markdown files:

% title: My Document Title
% author: John Doe

Content here...

Metadata Rules

  • If neither title nor author exists: header is omitted
  • If only title exists: current user is used as author
  • If only author exists: script exits with error
  • If any other metadata line exists: script exits with error

How It Works

  1. Extracts metadata from the markdown file
  2. Converts markdown to typst using pandoc
  3. Adds template import and metadata to the typst file
  4. Compiles to PDF using typst
  5. Cleans up temporary files

Notes

  • All paths are relative to the root directory
  • The template file must be accessible from the root directory
  • The output directory will be created if it doesn't exist
#!/usr/bin/env python
import os
import sys
import re
import subprocess
from pathlib import Path
def parse_metadata(file_path):
title = None
author = None
with open(file_path, 'r') as f:
# Read first few lines looking for metadata
for line in f:
line = line.strip()
if not line: # Empty line ends metadata
break
if not line.startswith('%'): # Non-metadata line ends metadata
break
# Remove the % and any leading whitespace
line = line.lstrip('%').strip()
# Parse title and author
if line.startswith('title:'):
title = line[6:].strip()
elif line.startswith('author:'):
author = line[7:].strip()
else:
print(f"Error: Unknown metadata line: {line}")
sys.exit(1)
# Handle missing metadata
if not title and not author:
return None, None
if not author:
# Get current user as author
author = subprocess.run(['whoami'], capture_output=True, text=True, check=True).stdout.strip()
if not title:
print("Error: Title is required if author is specified")
sys.exit(1)
return title, author
def main():
if len(sys.argv) < 5:
print(f"Usage: {sys.argv[0]} <root_dir> <input_markdown> <output_pdf> <template>")
print(f"Example: {sys.argv[0]} . summaries/glossary.md out/glossary.pdf ludosport.typ")
sys.exit(1)
root = Path(sys.argv[1]).resolve()
input_file = root / sys.argv[2]
output_file = root / sys.argv[3]
template_file = root / sys.argv[4]
# Check if input file exists
if not input_file.exists():
print(f"Error: Input file '{input_file}' does not exist")
sys.exit(1)
# Create output directory
output_file.parent.mkdir(parents=True, exist_ok=True)
# Create a temporary typst file next to the output
temp_typst = output_file.with_suffix('.typ')
# Get metadata from markdown file
title, author = parse_metadata(input_file)
# Convert markdown to typst using pandoc
subprocess.run([
"pandoc",
str(input_file),
"--from", "markdown",
"--to", "typst",
"--output", str(temp_typst)
], check=True)
# Calculate relative path to template from the temp file location
rel_template = os.path.relpath(template_file, temp_typst.parent)
# Add the template import and show command at the top of the typst file
with open(temp_typst, 'r') as f:
content = f.read()
with open(temp_typst, 'w') as f:
if title and author:
f.write(f'''#import "{rel_template}": *
#show: book.with(
title: [{title}],
author: "{author}",
)
{content}''')
else:
f.write(f'''#import "{rel_template}": *
{content}''')
# Convert typst to PDF
try:
result = subprocess.run([
"typst",
"compile",
"--root", str(root),
str(temp_typst),
str(output_file)
], capture_output=True, text=True, check=True)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
except subprocess.CalledProcessError as e:
print(e.stdout)
print(e.stderr, file=sys.stderr)
sys.exit(1)
finally:
# Clean up temporary typst file
temp_typst.unlink()
print(f"Conversion complete: {output_file}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment