import argparse
import markdown
import yaml
from weasyprint import HTML
from pathlib import Path
from typing import Dict, Tuple, Any
class ResumeConverter:
def __init__(self, input_path: Path, template_path: Path):
self.input_path = input_path
self.template_path = template_path
self.markdown_content = self._read_file(input_path)
self.template_content = self._read_file(template_path)
@staticmethod
def _read_file(filepath: Path) -> str:
"""Read content from a file."""
with open(filepath, 'r', encoding='utf-8') as f:
return f.read()
def _parse_markdown(self) -> Tuple[Dict[str, Any], str]:
"""Parse markdown content with YAML frontmatter."""
# Split content into lines
lines = self.markdown_content.splitlines()
# Get name from first line
name = lines[0].lstrip('# ').strip()
# Find YAML content
yaml_lines = []
content_lines = []
in_yaml = False
for line in lines[1:]:
if line.strip() == 'header:' or line.strip() == 'social:':
in_yaml = True
yaml_lines.append(line)
elif in_yaml:
if line and (line.startswith(' ') or line.startswith('\t')):
yaml_lines.append(line)
else:
in_yaml = False
content_lines.append(line)
else:
content_lines.append(line)
# Parse YAML
yaml_content = '\n'.join(yaml_lines)
try:
metadata = yaml.safe_load(yaml_content)
except yaml.YAMLError as e:
print(f"Error parsing YAML: {e}")
metadata = {}
metadata['name'] = name
content = '\n'.join(content_lines)
return metadata, content
def _generate_icon(self, icon: str) -> str:
"""Generate icon HTML from either Font Awesome class or emoji."""
if not icon:
return ''
# If icon starts with 'fa' or contains 'fa-', treat as Font Awesome
if icon.startswith('fa') or 'fa-' in icon:
return f''
# Otherwise, treat as emoji
return f'{icon}'
def _generate_social_links_html(self, social_data: Dict[str, Dict[str, str]]) -> str:
"""Generate HTML for social links section."""
social_items = []
for platform, data in social_data.items():
icon = data['icon']
# For Font Awesome icons, add fa-brands class to enable brand colors
if icon.startswith('fa') or 'fa-' in icon:
icon = f"fa-brands {icon}" if 'fa-brands' not in icon else icon
icon_html = self._generate_icon(icon)
item = f'''
{icon_html}
{data['text']}
'''
social_items.append(item)
return '\n'.join(social_items)
def convert_to_html(self) -> str:
"""Convert markdown to HTML using template."""
# Parse markdown and YAML
metadata, content = self._parse_markdown()
# Convert markdown content
html_content = markdown.markdown(content, extensions=['extra'])
# Generate social links section
if 'social' in metadata:
social_html = self._generate_social_links_html(metadata['social'])
else:
social_html = ''
# Replace template placeholders
html = self.template_content.replace('{{name}}', metadata['name'])
html = html.replace('{{title}}', f"{metadata['name']}'s Resume")
html = html.replace('{{content}}', html_content)
html = html.replace('', social_html)
# Replace header information
if 'header' in metadata:
header = metadata['header']
html = html.replace('{{header_title}}', header.get('title', ''))
html = html.replace('{{header_email}}', header.get('email', ''))
html = html.replace('{{header_phone}}', header.get('phone', ''))
html = html.replace('{{header_location}}', header.get('location', ''))
return html
def save_html(self, output_path: Path, html_content: str) -> None:
"""Save HTML content to file."""
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"Created HTML file: {output_path}")
def save_pdf(self, output_path: Path, html_content: str) -> None:
"""Convert HTML to PDF and save."""
try:
HTML(string=html_content).write_pdf(output_path)
print(f"Created PDF file: {output_path}")
except Exception as e:
print(f"Error converting to PDF: {e}")
def main():
parser = argparse.ArgumentParser(description='Convert markdown resume to HTML/PDF')
parser.add_argument('input', nargs='?', default='resume.md',
help='Input markdown file (default: resume.md)')
parser.add_argument('--template', default='template.html',
help='HTML template file (default: template.html)')
parser.add_argument('--output-html', help='Output HTML file')
parser.add_argument('--output-pdf', help='Output PDF file')
args = parser.parse_args()
# Process paths
input_path = Path(args.input)
template_path = Path(args.template)
output_html = Path(args.output_html) if args.output_html else input_path.with_suffix('.html')
output_pdf = Path(args.output_pdf) if args.output_pdf else input_path.with_suffix('.pdf')
# Create converter and process files
converter = ResumeConverter(input_path, template_path)
html_content = converter.convert_to_html()
# Save output files
converter.save_html(output_html, html_content)
converter.save_pdf(output_pdf, html_content)
if __name__ == '__main__':
main()