sync-cv-formatter 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sync_cv_formatter/__init__.py +13 -0
- sync_cv_formatter/renderers/__init__.py +4 -0
- sync_cv_formatter/renderers/html_renderer.py +211 -0
- sync_cv_formatter/renderers/pdf_renderer.py +42 -0
- sync_cv_formatter/schemas/__init__.py +27 -0
- sync_cv_formatter/schemas/resume_document.py +100 -0
- sync_cv_formatter/templates/html/_base_head.html.j2 +10 -0
- sync_cv_formatter/templates/html/_macros.html.j2 +43 -0
- sync_cv_formatter/templates/html/_sections.html.j2 +57 -0
- sync_cv_formatter/templates/html/_sections_experience_first.html.j2 +57 -0
- sync_cv_formatter/templates/html/_sections_tail.html.j2 +95 -0
- sync_cv_formatter/templates/html/classic/styles.css +217 -0
- sync_cv_formatter/templates/html/classic/template.html.j2 +16 -0
- sync_cv_formatter/templates/html/compact/styles.css +245 -0
- sync_cv_formatter/templates/html/compact/template.html.j2 +21 -0
- sync_cv_formatter/templates/html/executive/styles.css +229 -0
- sync_cv_formatter/templates/html/executive/template.html.j2 +19 -0
- sync_cv_formatter/templates/html/modern/styles.css +253 -0
- sync_cv_formatter/templates/html/modern/template.html.j2 +22 -0
- sync_cv_formatter/templates/html/professional/styles.css +258 -0
- sync_cv_formatter/templates/html/professional/template.html.j2 +19 -0
- sync_cv_formatter-1.2.0.dist-info/METADATA +90 -0
- sync_cv_formatter-1.2.0.dist-info/RECORD +25 -0
- sync_cv_formatter-1.2.0.dist-info/WHEEL +5 -0
- sync_cv_formatter-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from sync_cv_formatter.renderers.html_renderer import (
|
|
2
|
+
populate_html_template,
|
|
3
|
+
save_html_file,
|
|
4
|
+
)
|
|
5
|
+
from sync_cv_formatter.renderers.pdf_renderer import render_html_to_pdf
|
|
6
|
+
from sync_cv_formatter.schemas.resume_document import ResumeDocument
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ResumeDocument",
|
|
10
|
+
"populate_html_template",
|
|
11
|
+
"render_html_to_pdf",
|
|
12
|
+
"save_html_file",
|
|
13
|
+
]
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
6
|
+
|
|
7
|
+
from sync_cv_formatter.schemas.resume_document import ResumeDocument, ResumeTemplateId
|
|
8
|
+
|
|
9
|
+
_PKG_DIR = Path(__file__).resolve().parent.parent
|
|
10
|
+
HTML_TEMPLATES_DIR = _PKG_DIR / "templates" / "html"
|
|
11
|
+
|
|
12
|
+
_MONTHS = (
|
|
13
|
+
"Jan",
|
|
14
|
+
"Feb",
|
|
15
|
+
"Mar",
|
|
16
|
+
"Apr",
|
|
17
|
+
"May",
|
|
18
|
+
"Jun",
|
|
19
|
+
"Jul",
|
|
20
|
+
"Aug",
|
|
21
|
+
"Sep",
|
|
22
|
+
"Oct",
|
|
23
|
+
"Nov",
|
|
24
|
+
"Dec",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _format_resume_date(value: str | None) -> str:
|
|
29
|
+
if not value:
|
|
30
|
+
return "Present"
|
|
31
|
+
|
|
32
|
+
normalized = str(value).strip()
|
|
33
|
+
if normalized.lower() == "present":
|
|
34
|
+
return "Present"
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
parsed = datetime.fromisoformat(normalized.replace("Z", "+00:00"))
|
|
38
|
+
return f"{_MONTHS[parsed.month - 1]} {parsed.year}"
|
|
39
|
+
except ValueError:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
if len(normalized) >= 7 and normalized[4] == "-":
|
|
43
|
+
try:
|
|
44
|
+
year = int(normalized[:4])
|
|
45
|
+
month = int(normalized[5:7])
|
|
46
|
+
if 1 <= month <= 12:
|
|
47
|
+
return f"{_MONTHS[month - 1]} {year}"
|
|
48
|
+
except ValueError:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
return normalized
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _format_external_url(url: str | None) -> str:
|
|
55
|
+
if not url:
|
|
56
|
+
return ""
|
|
57
|
+
normalized = str(url).strip()
|
|
58
|
+
return normalized if normalized.startswith("http") else f"https://{normalized}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _resume_link_domain(url: str | None) -> str | None:
|
|
62
|
+
href = _format_external_url(url)
|
|
63
|
+
if not href:
|
|
64
|
+
return None
|
|
65
|
+
try:
|
|
66
|
+
hostname = urlparse(href).hostname or ""
|
|
67
|
+
return hostname.removeprefix("www.") or None
|
|
68
|
+
except ValueError:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _resume_favicon_url(url: str | None) -> str:
|
|
73
|
+
domain = _resume_link_domain(url)
|
|
74
|
+
if not domain:
|
|
75
|
+
return ""
|
|
76
|
+
return f"https://www.google.com/s2/favicons?domain={domain}&sz=32"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_PLATFORM_LABELS = {
|
|
80
|
+
"linkedin": "LinkedIn",
|
|
81
|
+
"github": "GitHub",
|
|
82
|
+
"gitlab": "GitLab",
|
|
83
|
+
"twitter": "Twitter",
|
|
84
|
+
"x": "X",
|
|
85
|
+
"youtube": "YouTube",
|
|
86
|
+
"instagram": "Instagram",
|
|
87
|
+
"facebook": "Facebook",
|
|
88
|
+
"portfolio": "Portfolio",
|
|
89
|
+
"website": "Website",
|
|
90
|
+
"syncques": "SyncQues",
|
|
91
|
+
"leetcode": "LeetCode",
|
|
92
|
+
"stackoverflow": "Stack Overflow",
|
|
93
|
+
"medium": "Medium",
|
|
94
|
+
"behance": "Behance",
|
|
95
|
+
"dribbble": "Dribbble",
|
|
96
|
+
"credential": "View Credential",
|
|
97
|
+
"live": "Live",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_DOMAIN_LABEL_PATTERNS: list[tuple[str, str]] = [
|
|
101
|
+
("linkedin.com", "LinkedIn"),
|
|
102
|
+
("github.com", "GitHub"),
|
|
103
|
+
("gitlab.com", "GitLab"),
|
|
104
|
+
("twitter.com", "X"),
|
|
105
|
+
("x.com", "X"),
|
|
106
|
+
("youtube.com", "YouTube"),
|
|
107
|
+
("youtu.be", "YouTube"),
|
|
108
|
+
("instagram.com", "Instagram"),
|
|
109
|
+
("facebook.com", "Facebook"),
|
|
110
|
+
("fb.com", "Facebook"),
|
|
111
|
+
("syncques.com", "SyncQues"),
|
|
112
|
+
("leetcode.com", "LeetCode"),
|
|
113
|
+
("stackoverflow.com", "Stack Overflow"),
|
|
114
|
+
("medium.com", "Medium"),
|
|
115
|
+
("behance.net", "Behance"),
|
|
116
|
+
("dribbble.com", "Dribbble"),
|
|
117
|
+
("credly.com", "Credly"),
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _resume_link_label(
|
|
122
|
+
platform_or_kind: str, url: str | None = None, fallback: str | None = None
|
|
123
|
+
) -> str:
|
|
124
|
+
platform_key = platform_or_kind.lower().replace(" ", "_")
|
|
125
|
+
if platform_key in _PLATFORM_LABELS:
|
|
126
|
+
return _PLATFORM_LABELS[platform_key]
|
|
127
|
+
|
|
128
|
+
href = _format_external_url(url)
|
|
129
|
+
if href:
|
|
130
|
+
lower_href = href.lower()
|
|
131
|
+
for domain, label in _DOMAIN_LABEL_PATTERNS:
|
|
132
|
+
if domain in lower_href:
|
|
133
|
+
return label
|
|
134
|
+
|
|
135
|
+
domain = _resume_link_domain(href)
|
|
136
|
+
if domain:
|
|
137
|
+
base = domain.split(".")[0]
|
|
138
|
+
return base[:1].upper() + base[1:]
|
|
139
|
+
|
|
140
|
+
if fallback:
|
|
141
|
+
return fallback
|
|
142
|
+
|
|
143
|
+
return platform_or_kind.replace("_", " ").title()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
TEMPLATE_DIRS: dict[ResumeTemplateId, str] = {
|
|
147
|
+
"professional": "professional",
|
|
148
|
+
"executive": "executive",
|
|
149
|
+
"modern": "modern",
|
|
150
|
+
"classic": "classic",
|
|
151
|
+
"compact": "compact",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_LEGACY_TEMPLATE_ALIASES: dict[str, ResumeTemplateId] = {
|
|
155
|
+
"creative": "modern",
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _get_template_env(template_id: ResumeTemplateId) -> Environment:
|
|
160
|
+
template_dir = HTML_TEMPLATES_DIR / TEMPLATE_DIRS[template_id]
|
|
161
|
+
env = Environment(
|
|
162
|
+
loader=FileSystemLoader(
|
|
163
|
+
[str(template_dir), str(HTML_TEMPLATES_DIR)],
|
|
164
|
+
),
|
|
165
|
+
autoescape=select_autoescape(["html", "xml"]),
|
|
166
|
+
)
|
|
167
|
+
env.filters["format_resume_date"] = _format_resume_date
|
|
168
|
+
env.filters["format_external_url"] = _format_external_url
|
|
169
|
+
env.filters["resume_favicon_url"] = _resume_favicon_url
|
|
170
|
+
env.globals["resume_link_label"] = _resume_link_label
|
|
171
|
+
return env
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _load_css(template_id: ResumeTemplateId) -> str:
|
|
175
|
+
css_path = HTML_TEMPLATES_DIR / TEMPLATE_DIRS[template_id] / "styles.css"
|
|
176
|
+
return css_path.read_text(encoding="utf-8")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _resolve_template_id(
|
|
180
|
+
template_id: str | ResumeTemplateId | None,
|
|
181
|
+
*,
|
|
182
|
+
fallback: ResumeTemplateId = "professional",
|
|
183
|
+
) -> ResumeTemplateId:
|
|
184
|
+
normalized = str(template_id or fallback).strip().lower()
|
|
185
|
+
if normalized in _LEGACY_TEMPLATE_ALIASES:
|
|
186
|
+
return _LEGACY_TEMPLATE_ALIASES[normalized]
|
|
187
|
+
if normalized in TEMPLATE_DIRS:
|
|
188
|
+
return normalized # type: ignore[return-value]
|
|
189
|
+
return fallback
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def populate_html_template(
|
|
193
|
+
document: ResumeDocument,
|
|
194
|
+
template_id: ResumeTemplateId | None = None,
|
|
195
|
+
*,
|
|
196
|
+
interactive: bool = False,
|
|
197
|
+
) -> str:
|
|
198
|
+
selected_template = _resolve_template_id(template_id or document.template_id)
|
|
199
|
+
env = _get_template_env(selected_template)
|
|
200
|
+
template = env.get_template("template.html.j2")
|
|
201
|
+
return template.render(
|
|
202
|
+
document=document,
|
|
203
|
+
css_content=_load_css(selected_template),
|
|
204
|
+
interactive=interactive,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def save_html_file(content: str, output_path: str) -> str:
|
|
209
|
+
html_path = output_path.replace(".pdf", ".html")
|
|
210
|
+
Path(html_path).write_text(content, encoding="utf-8")
|
|
211
|
+
return html_path
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def render_html_to_pdf(html_file: str, output_dir: str | None = None) -> tuple[bool, str]:
|
|
5
|
+
html_path = Path(html_file)
|
|
6
|
+
|
|
7
|
+
if not html_path.exists():
|
|
8
|
+
return False, f"HTML file not found: {html_file}"
|
|
9
|
+
|
|
10
|
+
if output_dir is None:
|
|
11
|
+
output_dir = html_path.parent
|
|
12
|
+
|
|
13
|
+
output_dir = Path(output_dir)
|
|
14
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
15
|
+
pdf_path = output_dir / html_path.with_suffix(".pdf").name
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from playwright.sync_api import sync_playwright
|
|
19
|
+
except ImportError:
|
|
20
|
+
return False, "Playwright is not installed. Run: playwright install chromium"
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
file_url = html_path.resolve().as_uri()
|
|
24
|
+
with sync_playwright() as playwright:
|
|
25
|
+
browser = playwright.chromium.launch(headless=True)
|
|
26
|
+
page = browser.new_page()
|
|
27
|
+
page.goto(file_url, wait_until="networkidle")
|
|
28
|
+
page.pdf(
|
|
29
|
+
path=str(pdf_path),
|
|
30
|
+
format="A4",
|
|
31
|
+
print_background=True,
|
|
32
|
+
prefer_css_page_size=True,
|
|
33
|
+
margin={"top": "0", "right": "0", "bottom": "0", "left": "0"},
|
|
34
|
+
)
|
|
35
|
+
browser.close()
|
|
36
|
+
|
|
37
|
+
if pdf_path.exists():
|
|
38
|
+
return True, f"PDF created successfully: {pdf_path}"
|
|
39
|
+
|
|
40
|
+
return False, "PDF file was not created by Playwright"
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
return False, f"HTML to PDF rendering failed: {exc}"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from sync_cv_formatter.schemas.resume_document import (
|
|
2
|
+
AchievementItem,
|
|
3
|
+
BasicsSection,
|
|
4
|
+
EducationItem,
|
|
5
|
+
ExperienceItem,
|
|
6
|
+
ProjectItem,
|
|
7
|
+
ResumeDocument,
|
|
8
|
+
ResumeDocumentMetadata,
|
|
9
|
+
ResumeSections,
|
|
10
|
+
ResumeTemplateId,
|
|
11
|
+
ResumeTypeValue,
|
|
12
|
+
SkillsSection,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AchievementItem",
|
|
17
|
+
"BasicsSection",
|
|
18
|
+
"EducationItem",
|
|
19
|
+
"ExperienceItem",
|
|
20
|
+
"ProjectItem",
|
|
21
|
+
"ResumeDocument",
|
|
22
|
+
"ResumeDocumentMetadata",
|
|
23
|
+
"ResumeSections",
|
|
24
|
+
"ResumeTemplateId",
|
|
25
|
+
"ResumeTypeValue",
|
|
26
|
+
"SkillsSection",
|
|
27
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
ResumeTemplateId = Literal["professional", "executive", "modern", "classic", "compact"]
|
|
7
|
+
ResumeTypeValue = Literal["standard", "ats_optimized"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BasicsSection(BaseModel):
|
|
11
|
+
full_name: str = ""
|
|
12
|
+
email: str = ""
|
|
13
|
+
phone: str | None = None
|
|
14
|
+
location: str | None = None
|
|
15
|
+
summary: str | None = None
|
|
16
|
+
syncques_url: str | None = None
|
|
17
|
+
social_links: dict[str, str] = Field(default_factory=dict)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SkillsSection(BaseModel):
|
|
21
|
+
flat: list[str] = Field(default_factory=list)
|
|
22
|
+
categorized: dict[str, list[str]] = Field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ExperienceItem(BaseModel):
|
|
26
|
+
title: str = ""
|
|
27
|
+
company: str = ""
|
|
28
|
+
location: str | None = None
|
|
29
|
+
start_date: str | None = None
|
|
30
|
+
end_date: str | None = "Present"
|
|
31
|
+
description: str = ""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EducationItem(BaseModel):
|
|
35
|
+
institution: str = ""
|
|
36
|
+
degree: str = ""
|
|
37
|
+
field_of_study: str | None = None
|
|
38
|
+
location: str | None = None
|
|
39
|
+
start_date: str | None = None
|
|
40
|
+
end_date: str | None = "Present"
|
|
41
|
+
gpa: str | None = None
|
|
42
|
+
description: str | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ProjectItem(BaseModel):
|
|
46
|
+
title: str = ""
|
|
47
|
+
description: str = ""
|
|
48
|
+
technologies: list[str] = Field(default_factory=list)
|
|
49
|
+
project_url: str | None = None
|
|
50
|
+
repository_url: str | None = None
|
|
51
|
+
start_date: str | None = None
|
|
52
|
+
end_date: str | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AchievementItem(BaseModel):
|
|
56
|
+
title: str = ""
|
|
57
|
+
organization: str | None = None
|
|
58
|
+
description: str | None = None
|
|
59
|
+
date_earned: str | None = None
|
|
60
|
+
credential_url: str | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ResumeSections(BaseModel):
|
|
64
|
+
skills: SkillsSection = Field(default_factory=SkillsSection)
|
|
65
|
+
experience: list[ExperienceItem] = Field(default_factory=list)
|
|
66
|
+
education: list[EducationItem] = Field(default_factory=list)
|
|
67
|
+
projects: list[ProjectItem] = Field(default_factory=list)
|
|
68
|
+
achievements: list[AchievementItem] = Field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ResumeDocumentMetadata(BaseModel):
|
|
72
|
+
generated_at: str
|
|
73
|
+
resume_type: ResumeTypeValue = "standard"
|
|
74
|
+
job_title: str | None = None
|
|
75
|
+
job_description: str | None = None
|
|
76
|
+
last_edited_at: str | None = None
|
|
77
|
+
content_version: int = 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ResumeDocument(BaseModel):
|
|
81
|
+
schema_version: str = "1.0"
|
|
82
|
+
template_id: ResumeTemplateId = "professional"
|
|
83
|
+
basics: BasicsSection = Field(default_factory=BasicsSection)
|
|
84
|
+
sections: ResumeSections = Field(default_factory=ResumeSections)
|
|
85
|
+
metadata: ResumeDocumentMetadata
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def default_metadata(
|
|
89
|
+
cls,
|
|
90
|
+
resume_type: ResumeTypeValue = "standard",
|
|
91
|
+
job_title: str | None = None,
|
|
92
|
+
job_description: str | None = None,
|
|
93
|
+
) -> ResumeDocumentMetadata:
|
|
94
|
+
return ResumeDocumentMetadata(
|
|
95
|
+
generated_at=datetime.now(timezone.utc).isoformat(),
|
|
96
|
+
resume_type=resume_type,
|
|
97
|
+
job_title=job_title,
|
|
98
|
+
job_description=job_description,
|
|
99
|
+
content_version=1,
|
|
100
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<meta charset="UTF-8" />
|
|
2
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
3
|
+
<title>{{ document.basics.full_name }} — Resume</title>
|
|
4
|
+
<style>{{ css_content }}</style>
|
|
5
|
+
{% if interactive %}
|
|
6
|
+
<style>
|
|
7
|
+
[data-resume-section] { cursor: pointer; border-radius: 2px; }
|
|
8
|
+
[data-resume-section]:hover { background: rgba(0, 51, 153, 0.05); }
|
|
9
|
+
</style>
|
|
10
|
+
{% endif %}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{% macro resume_contact(document) %}
|
|
2
|
+
{% set ns = namespace(first=true) %}
|
|
3
|
+
{% if document.basics.email %}
|
|
4
|
+
{% if not ns.first %}<span class="contact-sep">·</span>{% endif %}
|
|
5
|
+
<a href="mailto:{{ document.basics.email }}">{{ document.basics.email }}</a>
|
|
6
|
+
{% set ns.first = false %}
|
|
7
|
+
{% endif %}
|
|
8
|
+
{% if document.basics.phone %}
|
|
9
|
+
{% if not ns.first %}<span class="contact-sep">·</span>{% endif %}
|
|
10
|
+
<a href="tel:{{ document.basics.phone }}">{{ document.basics.phone }}</a>
|
|
11
|
+
{% set ns.first = false %}
|
|
12
|
+
{% endif %}
|
|
13
|
+
{% if document.basics.location %}
|
|
14
|
+
{% if not ns.first %}<span class="contact-sep">·</span>{% endif %}
|
|
15
|
+
<span>{{ document.basics.location }}</span>
|
|
16
|
+
{% set ns.first = false %}
|
|
17
|
+
{% endif %}
|
|
18
|
+
{% if document.basics.syncques_url %}
|
|
19
|
+
{% if not ns.first %}<span class="contact-sep">·</span>{% endif %}
|
|
20
|
+
{{ external_link('syncques', document.basics.syncques_url) }}
|
|
21
|
+
{% set ns.first = false %}
|
|
22
|
+
{% endif %}
|
|
23
|
+
{% for platform, url in document.basics.social_links.items() %}
|
|
24
|
+
{% if url %}
|
|
25
|
+
{% if not ns.first %}<span class="contact-sep">·</span>{% endif %}
|
|
26
|
+
{{ external_link(platform, url) }}
|
|
27
|
+
{% set ns.first = false %}
|
|
28
|
+
{% endif %}
|
|
29
|
+
{% endfor %}
|
|
30
|
+
{% endmacro %}
|
|
31
|
+
|
|
32
|
+
{% macro external_link(platform, url, fallback='') %}
|
|
33
|
+
{% set href = url | format_external_url %}
|
|
34
|
+
{% if href %}
|
|
35
|
+
<a href="{{ href }}" class="resume-external-link">
|
|
36
|
+
{% set favicon = url | resume_favicon_url %}
|
|
37
|
+
{% if favicon %}
|
|
38
|
+
<img src="{{ favicon }}" alt="" class="resume-link-icon" width="12" height="12" />
|
|
39
|
+
{% endif %}
|
|
40
|
+
<span>{{ resume_link_label(platform, url, fallback) }}</span>
|
|
41
|
+
</a>
|
|
42
|
+
{% endif %}
|
|
43
|
+
{% endmacro %}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{% if document.basics.summary %}
|
|
2
|
+
<section class="resume-section" data-resume-section="basics">
|
|
3
|
+
<h2 class="resume-section-title">Professional Summary</h2>
|
|
4
|
+
<p class="resume-summary">{{ document.basics.summary }}</p>
|
|
5
|
+
</section>
|
|
6
|
+
{% endif %}
|
|
7
|
+
|
|
8
|
+
{% if document.sections.skills.categorized or document.sections.skills.flat %}
|
|
9
|
+
<section class="resume-section" data-resume-section="skills">
|
|
10
|
+
<h2 class="resume-section-title">Technical Skills</h2>
|
|
11
|
+
{% if document.sections.skills.categorized %}
|
|
12
|
+
<div class="skills-block">
|
|
13
|
+
{% for category, skills in document.sections.skills.categorized.items() %}
|
|
14
|
+
{% if skills %}
|
|
15
|
+
<p class="skill-line"><strong>{{ category }}:</strong> {{ skills | join(', ') }}</p>
|
|
16
|
+
{% endif %}
|
|
17
|
+
{% endfor %}
|
|
18
|
+
</div>
|
|
19
|
+
{% elif document.sections.skills.flat %}
|
|
20
|
+
<p class="skill-inline">{{ document.sections.skills.flat | join(' · ') }}</p>
|
|
21
|
+
{% endif %}
|
|
22
|
+
</section>
|
|
23
|
+
{% endif %}
|
|
24
|
+
|
|
25
|
+
{% if document.sections.experience %}
|
|
26
|
+
<section class="resume-section" data-resume-section="experience">
|
|
27
|
+
<h2 class="resume-section-title">Work Experience</h2>
|
|
28
|
+
{% for exp in document.sections.experience %}
|
|
29
|
+
<article class="entry">
|
|
30
|
+
<div class="entry-header">
|
|
31
|
+
<div class="entry-heading">
|
|
32
|
+
<h3 class="entry-title">
|
|
33
|
+
{{ exp.title }}{% if exp.company %} at {{ exp.company }}{% endif %}{% if exp.location %}, {{ exp.location }}{% endif %}
|
|
34
|
+
</h3>
|
|
35
|
+
</div>
|
|
36
|
+
{% if exp.start_date or exp.end_date %}
|
|
37
|
+
<div class="entry-date">
|
|
38
|
+
{{ exp.start_date | format_resume_date }}{% if exp.start_date or exp.end_date %} – {% endif %}{{ exp.end_date | format_resume_date }}
|
|
39
|
+
</div>
|
|
40
|
+
{% endif %}
|
|
41
|
+
</div>
|
|
42
|
+
{% if exp.description %}
|
|
43
|
+
<ul class="entry-bullets">
|
|
44
|
+
{% for line in exp.description.replace('\\n', '\n').split('\n') %}
|
|
45
|
+
{% set bullet = line.strip().lstrip('•').lstrip('-').lstrip('–').strip() %}
|
|
46
|
+
{% if bullet %}
|
|
47
|
+
<li>{{ bullet }}</li>
|
|
48
|
+
{% endif %}
|
|
49
|
+
{% endfor %}
|
|
50
|
+
</ul>
|
|
51
|
+
{% endif %}
|
|
52
|
+
</article>
|
|
53
|
+
{% endfor %}
|
|
54
|
+
</section>
|
|
55
|
+
{% endif %}
|
|
56
|
+
|
|
57
|
+
{% include '_sections_tail.html.j2' %}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{% if document.basics.summary %}
|
|
2
|
+
<section class="resume-section" data-resume-section="basics">
|
|
3
|
+
<h2 class="resume-section-title">Professional Summary</h2>
|
|
4
|
+
<p class="resume-summary">{{ document.basics.summary }}</p>
|
|
5
|
+
</section>
|
|
6
|
+
{% endif %}
|
|
7
|
+
|
|
8
|
+
{% if document.sections.experience %}
|
|
9
|
+
<section class="resume-section" data-resume-section="experience">
|
|
10
|
+
<h2 class="resume-section-title">Work Experience</h2>
|
|
11
|
+
{% for exp in document.sections.experience %}
|
|
12
|
+
<article class="entry">
|
|
13
|
+
<div class="entry-header">
|
|
14
|
+
<div class="entry-heading">
|
|
15
|
+
<h3 class="entry-title">
|
|
16
|
+
{{ exp.title }}{% if exp.company %} at {{ exp.company }}{% endif %}{% if exp.location %}, {{ exp.location }}{% endif %}
|
|
17
|
+
</h3>
|
|
18
|
+
</div>
|
|
19
|
+
{% if exp.start_date or exp.end_date %}
|
|
20
|
+
<div class="entry-date">
|
|
21
|
+
{{ exp.start_date | format_resume_date }}{% if exp.start_date or exp.end_date %} – {% endif %}{{ exp.end_date | format_resume_date }}
|
|
22
|
+
</div>
|
|
23
|
+
{% endif %}
|
|
24
|
+
</div>
|
|
25
|
+
{% if exp.description %}
|
|
26
|
+
<ul class="entry-bullets">
|
|
27
|
+
{% for line in exp.description.replace('\\n', '\n').split('\n') %}
|
|
28
|
+
{% set bullet = line.strip().lstrip('•').lstrip('-').lstrip('–').strip() %}
|
|
29
|
+
{% if bullet %}
|
|
30
|
+
<li>{{ bullet }}</li>
|
|
31
|
+
{% endif %}
|
|
32
|
+
{% endfor %}
|
|
33
|
+
</ul>
|
|
34
|
+
{% endif %}
|
|
35
|
+
</article>
|
|
36
|
+
{% endfor %}
|
|
37
|
+
</section>
|
|
38
|
+
{% endif %}
|
|
39
|
+
|
|
40
|
+
{% if document.sections.skills.categorized or document.sections.skills.flat %}
|
|
41
|
+
<section class="resume-section" data-resume-section="skills">
|
|
42
|
+
<h2 class="resume-section-title">Technical Skills</h2>
|
|
43
|
+
{% if document.sections.skills.categorized %}
|
|
44
|
+
<div class="skills-block">
|
|
45
|
+
{% for category, skills in document.sections.skills.categorized.items() %}
|
|
46
|
+
{% if skills %}
|
|
47
|
+
<p class="skill-line"><strong>{{ category }}:</strong> {{ skills | join(', ') }}</p>
|
|
48
|
+
{% endif %}
|
|
49
|
+
{% endfor %}
|
|
50
|
+
</div>
|
|
51
|
+
{% elif document.sections.skills.flat %}
|
|
52
|
+
<p class="skill-inline">{{ document.sections.skills.flat | join(' · ') }}</p>
|
|
53
|
+
{% endif %}
|
|
54
|
+
</section>
|
|
55
|
+
{% endif %}
|
|
56
|
+
|
|
57
|
+
{% include '_sections_tail.html.j2' %}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{% if document.sections.education %}
|
|
2
|
+
<section class="resume-section" data-resume-section="education">
|
|
3
|
+
<h2 class="resume-section-title">Education</h2>
|
|
4
|
+
{% for edu in document.sections.education %}
|
|
5
|
+
<article class="entry">
|
|
6
|
+
<div class="entry-header">
|
|
7
|
+
<div class="entry-heading">
|
|
8
|
+
<h3 class="entry-title">{{ edu.institution }}</h3>
|
|
9
|
+
<p class="entry-subtitle">
|
|
10
|
+
{{ edu.degree }}{% if edu.field_of_study %} in {{ edu.field_of_study }}{% endif %}
|
|
11
|
+
{%- if edu.location %} · {{ edu.location }}{% endif -%}
|
|
12
|
+
</p>
|
|
13
|
+
</div>
|
|
14
|
+
{% if edu.start_date or edu.end_date %}
|
|
15
|
+
<div class="entry-date">
|
|
16
|
+
{{ edu.start_date | format_resume_date }}{% if edu.start_date or edu.end_date %} – {% endif %}{{ edu.end_date | format_resume_date }}
|
|
17
|
+
</div>
|
|
18
|
+
{% endif %}
|
|
19
|
+
</div>
|
|
20
|
+
{% if edu.gpa or edu.description %}
|
|
21
|
+
<p class="entry-detail">
|
|
22
|
+
{% if edu.gpa %}<em>GPA:</em> {{ edu.gpa }}{% endif %}
|
|
23
|
+
{% if edu.gpa and edu.description %} · {% endif %}
|
|
24
|
+
{% if edu.description %}{{ edu.description }}{% endif %}
|
|
25
|
+
</p>
|
|
26
|
+
{% endif %}
|
|
27
|
+
</article>
|
|
28
|
+
{% endfor %}
|
|
29
|
+
</section>
|
|
30
|
+
{% endif %}
|
|
31
|
+
|
|
32
|
+
{% if document.sections.projects %}
|
|
33
|
+
<section class="resume-section" data-resume-section="projects">
|
|
34
|
+
<h2 class="resume-section-title">Projects</h2>
|
|
35
|
+
{% for proj in document.sections.projects %}
|
|
36
|
+
<article class="entry">
|
|
37
|
+
<div class="entry-header">
|
|
38
|
+
<h3 class="entry-title">{{ proj.title }}</h3>
|
|
39
|
+
{% if proj.repository_url or proj.project_url %}
|
|
40
|
+
<div class="entry-links">
|
|
41
|
+
{% if proj.repository_url %}
|
|
42
|
+
{{ external_link('github', proj.repository_url, 'Repository') }}
|
|
43
|
+
{% endif %}
|
|
44
|
+
{% if proj.project_url %}
|
|
45
|
+
{{ external_link('live', proj.project_url, 'Live') }}
|
|
46
|
+
{% endif %}
|
|
47
|
+
</div>
|
|
48
|
+
{% endif %}
|
|
49
|
+
</div>
|
|
50
|
+
{% if proj.description %}
|
|
51
|
+
<ul class="entry-bullets">
|
|
52
|
+
{% for line in proj.description.replace('\\n', '\n').split('\n') %}
|
|
53
|
+
{% set bullet = line.strip().lstrip('•').lstrip('-').lstrip('–').strip() %}
|
|
54
|
+
{% if bullet %}
|
|
55
|
+
<li>{{ bullet }}</li>
|
|
56
|
+
{% endif %}
|
|
57
|
+
{% endfor %}
|
|
58
|
+
</ul>
|
|
59
|
+
{% endif %}
|
|
60
|
+
{% if proj.technologies %}
|
|
61
|
+
<p class="entry-detail"><em>Technologies:</em> {{ proj.technologies | join(', ') }}</p>
|
|
62
|
+
{% endif %}
|
|
63
|
+
</article>
|
|
64
|
+
{% endfor %}
|
|
65
|
+
</section>
|
|
66
|
+
{% endif %}
|
|
67
|
+
|
|
68
|
+
{% if document.sections.achievements %}
|
|
69
|
+
<section class="resume-section" data-resume-section="achievements">
|
|
70
|
+
<h2 class="resume-section-title">Achievements & Certifications</h2>
|
|
71
|
+
{% for ach in document.sections.achievements %}
|
|
72
|
+
<article class="entry">
|
|
73
|
+
<div class="entry-header">
|
|
74
|
+
<div class="entry-heading">
|
|
75
|
+
<h3 class="entry-title">{{ ach.title }}</h3>
|
|
76
|
+
{% if ach.organization %}
|
|
77
|
+
<p class="entry-meta">{{ ach.organization }}</p>
|
|
78
|
+
{% endif %}
|
|
79
|
+
</div>
|
|
80
|
+
{% if ach.date_earned %}
|
|
81
|
+
<div class="entry-date">{{ ach.date_earned | format_resume_date }}</div>
|
|
82
|
+
{% endif %}
|
|
83
|
+
</div>
|
|
84
|
+
{% if ach.description %}
|
|
85
|
+
<p class="entry-detail">{{ ach.description }}</p>
|
|
86
|
+
{% endif %}
|
|
87
|
+
{% if ach.credential_url %}
|
|
88
|
+
<div class="entry-links">
|
|
89
|
+
{{ external_link('credential', ach.credential_url, 'View Credential') }}
|
|
90
|
+
</div>
|
|
91
|
+
{% endif %}
|
|
92
|
+
</article>
|
|
93
|
+
{% endfor %}
|
|
94
|
+
</section>
|
|
95
|
+
{% endif %}
|