Spaces:
Running
Running
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) | |
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'<i class="{icon}"></i>' | |
# Otherwise, treat as emoji | |
return f'<span class="emoji">{icon}</span>' | |
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'''<a href="{data['url']}" class="social-link" target="_blank"> | |
{icon_html} | |
<span>{data['text']}</span> | |
</a>''' | |
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_LINKS -->', 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() | |