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() | |