mirror of https://github.com/eyhc1/rendercv.git
simplify folder structure
This commit is contained in:
parent
93e39686d3
commit
d464dc4c42
|
@ -170,8 +170,9 @@ cython_debug/
|
||||||
*.pdf
|
*.pdf
|
||||||
|
|
||||||
# TinyTeX binaries
|
# TinyTeX binaries
|
||||||
rendercv/tinytex/vendor/TinyTeX/
|
rendercv/vendor/TinyTeX/
|
||||||
|
|
||||||
# RenderCV related
|
# RenderCV related
|
||||||
tests/outputs/
|
tests/outputs/
|
||||||
tests/inputs/personal.json
|
tests/inputs/personal.json
|
||||||
|
tests/inputs/personal.yaml
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Python: Current File",
|
||||||
|
"type": "python",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${file}",
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"justMyCode": true,
|
||||||
|
"env": {
|
||||||
|
"PYTHONPATH": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
|
from rendercv.data_model import RenderCVDataModel
|
||||||
|
from rendercv.tinytex import run_latex
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# logging config:
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.DEBUG,
|
||||||
|
format="%(name)s - %(levelname)s - %(message)s",
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
templateName = "classic"
|
||||||
|
templatePath = os.path.join(workspace, "rendercv", "templates", templateName)
|
||||||
|
environment = Environment(
|
||||||
|
loader=FileSystemLoader(templatePath),
|
||||||
|
trim_blocks=True,
|
||||||
|
lstrip_blocks=True,
|
||||||
|
)
|
||||||
|
environment.globals.update(str=str)
|
||||||
|
|
||||||
|
def markdown_to_latex(value: str) -> str:
|
||||||
|
"""
|
||||||
|
To be continued...
|
||||||
|
"""
|
||||||
|
# convert links
|
||||||
|
link = re.search("\[(.*)\]\((.*?)\)", value)
|
||||||
|
if link is not None:
|
||||||
|
link = link.groups()
|
||||||
|
oldLinkString = "[" + link[0] + "](" + link[1] + ")"
|
||||||
|
newLinkString = "\hrefExternal{" + link[1] + "}{" + link[0] + "}"
|
||||||
|
|
||||||
|
value = value.replace(oldLinkString, newLinkString)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
environment.filters["markdown_to_latex"] = markdown_to_latex
|
||||||
|
|
||||||
|
environment.block_start_string = "((*"
|
||||||
|
environment.block_end_string = "*))"
|
||||||
|
environment.variable_start_string = "<<"
|
||||||
|
environment.variable_end_string = ">>"
|
||||||
|
environment.comment_start_string = "((#"
|
||||||
|
environment.comment_end_string = "#))"
|
||||||
|
|
||||||
|
template = environment.get_template(f"{templateName}.tex.j2")
|
||||||
|
|
||||||
|
inpur_name = "personal"
|
||||||
|
|
||||||
|
input_file_path = os.path.join(workspace, "tests", "inputs", f"{inpur_name}.yaml")
|
||||||
|
with open(input_file_path) as file:
|
||||||
|
yaml = YAML()
|
||||||
|
raw_json = yaml.load(file)
|
||||||
|
|
||||||
|
data = RenderCVDataModel(**raw_json)
|
||||||
|
|
||||||
|
output_latex_file = template.render(design=data.design.options, cv=data.cv)
|
||||||
|
|
||||||
|
# Create an output file and write the rendered LaTeX code to it:
|
||||||
|
output_file_path = os.path.join(workspace, "tests", "outputs", f"{inpur_name}.tex")
|
||||||
|
os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
|
||||||
|
with open(output_file_path, "w") as file:
|
||||||
|
file.write(output_latex_file)
|
||||||
|
|
||||||
|
run_latex(output_file_path)
|
|
@ -0,0 +1,409 @@
|
||||||
|
from datetime import date as Date
|
||||||
|
from typing import Literal
|
||||||
|
from typing_extensions import Annotated
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
|
from pydantic import BaseModel, HttpUrl, Field, model_validator, computed_field
|
||||||
|
from pydantic.functional_validators import AfterValidator
|
||||||
|
from pydantic_extra_types.phone_numbers import PhoneNumber
|
||||||
|
from pydantic_extra_types.color import Color
|
||||||
|
|
||||||
|
from spellchecker import SpellChecker
|
||||||
|
|
||||||
|
# ======================================================================================
|
||||||
|
# HELPERS ==============================================================================
|
||||||
|
# ======================================================================================
|
||||||
|
|
||||||
|
spell = SpellChecker()
|
||||||
|
|
||||||
|
# don't give spelling warnings for these words:
|
||||||
|
dictionary = [
|
||||||
|
"aerostructures",
|
||||||
|
"sportsperson",
|
||||||
|
"cern",
|
||||||
|
"mechatronics",
|
||||||
|
"calculix",
|
||||||
|
"microcontroller",
|
||||||
|
"ansys",
|
||||||
|
"nx",
|
||||||
|
"aselsan",
|
||||||
|
"hrjet",
|
||||||
|
"simularge",
|
||||||
|
"siemens",
|
||||||
|
"dynamometer",
|
||||||
|
"dc",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def check_spelling(sentence: str) -> str:
|
||||||
|
"""
|
||||||
|
To be continued...
|
||||||
|
"""
|
||||||
|
modifiedSentence = sentence.lower() # convert to lower case
|
||||||
|
modifiedSentence = re.sub(
|
||||||
|
r"\-+", " ", modifiedSentence
|
||||||
|
) # replace hyphens with spaces
|
||||||
|
modifiedSentence = re.sub(
|
||||||
|
"[^a-z\s\-']", "", modifiedSentence
|
||||||
|
) # remove all the special characters
|
||||||
|
words = modifiedSentence.split() # split sentence into a list of words
|
||||||
|
misspelled = spell.unknown(words) # find misspelled words
|
||||||
|
|
||||||
|
if len(misspelled) > 0:
|
||||||
|
for word in misspelled:
|
||||||
|
# for each misspelled word, check if it is in the dictionary and otherwise
|
||||||
|
# give a warning
|
||||||
|
if word in dictionary:
|
||||||
|
continue
|
||||||
|
|
||||||
|
logging.warning(
|
||||||
|
f'The word "{word}" might be misspelled according to the'
|
||||||
|
" pyspellchecker."
|
||||||
|
)
|
||||||
|
|
||||||
|
return sentence
|
||||||
|
|
||||||
|
|
||||||
|
SpellCheckedString = Annotated[str, AfterValidator(check_spelling)]
|
||||||
|
|
||||||
|
|
||||||
|
def compute_time_span_string(start_date: Date, end_date: Date) -> str:
|
||||||
|
"""
|
||||||
|
To be continued...
|
||||||
|
"""
|
||||||
|
# calculate the number of days between start_date and end_date:
|
||||||
|
timeSpanInDays = (end_date - start_date).days
|
||||||
|
|
||||||
|
# calculate the number of years between start_date and end_date:
|
||||||
|
howManyYears = timeSpanInDays // 365
|
||||||
|
if howManyYears == 0:
|
||||||
|
howManyYearsString = None
|
||||||
|
elif howManyYears == 1:
|
||||||
|
howManyYearsString = "1 year"
|
||||||
|
else:
|
||||||
|
howManyYearsString = f"{howManyYears} years"
|
||||||
|
|
||||||
|
# calculate the number of months between start_date and end_date:
|
||||||
|
howManyMonths = round((timeSpanInDays % 365) / 30)
|
||||||
|
if howManyMonths == 0:
|
||||||
|
howManyMonths = 1
|
||||||
|
|
||||||
|
if howManyMonths == 0:
|
||||||
|
howManyMonthsString = None
|
||||||
|
elif howManyMonths == 1:
|
||||||
|
howManyMonthsString = "1 month"
|
||||||
|
else:
|
||||||
|
howManyMonthsString = f"{howManyMonths} months"
|
||||||
|
|
||||||
|
# combine howManyYearsString and howManyMonthsString:
|
||||||
|
if howManyYearsString is None:
|
||||||
|
timeSpanString = howManyMonthsString
|
||||||
|
elif howManyMonthsString is None:
|
||||||
|
timeSpanString = howManyYearsString
|
||||||
|
else:
|
||||||
|
timeSpanString = f"{howManyYearsString} {howManyMonthsString}"
|
||||||
|
|
||||||
|
return timeSpanString
|
||||||
|
|
||||||
|
|
||||||
|
def format_date(date: Date) -> str:
|
||||||
|
"""
|
||||||
|
To be continued...
|
||||||
|
"""
|
||||||
|
# Month abbreviations,
|
||||||
|
# taken from: https://web.library.yale.edu/cataloging/months
|
||||||
|
abbreviations_of_months = [
|
||||||
|
"Jan.",
|
||||||
|
"Feb.",
|
||||||
|
"Mar.",
|
||||||
|
"Apr.",
|
||||||
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"Aug.",
|
||||||
|
"Sept.",
|
||||||
|
"Oct.",
|
||||||
|
"Nov.",
|
||||||
|
"Dec.",
|
||||||
|
]
|
||||||
|
|
||||||
|
month = int(date.strftime("%m"))
|
||||||
|
monthAbbreviation = abbreviations_of_months[month - 1]
|
||||||
|
year = date.strftime("%Y")
|
||||||
|
date_string = f"{monthAbbreviation} {year}"
|
||||||
|
|
||||||
|
return date_string
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================================
|
||||||
|
# ======================================================================================
|
||||||
|
# ======================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================================
|
||||||
|
# DESIGN MODELS ========================================================================
|
||||||
|
# ======================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class ClassicThemeOptions(BaseModel):
|
||||||
|
"""
|
||||||
|
In RenderCV, each theme has its own ThemeNameThemeOptions class so that new themes
|
||||||
|
can be implemented easily.
|
||||||
|
"""
|
||||||
|
|
||||||
|
primary_color: Color = Field(default="blue")
|
||||||
|
|
||||||
|
page_top_margin: str = Field(default="1.35cm")
|
||||||
|
page_bottom_margin: str = Field(default="1.35cm")
|
||||||
|
page_left_margin: str = Field(default="1.35cm")
|
||||||
|
page_right_margin: str = Field(default="1.35cm")
|
||||||
|
|
||||||
|
section_title_top_margin: str = Field(default="0.13cm")
|
||||||
|
section_title_bottom_margin: str = Field(default="0.13cm")
|
||||||
|
|
||||||
|
vertical_margin_between_bullet_points: str = Field(default="0.07cm")
|
||||||
|
bullet_point_left_margin: str = Field(default="0.7cm")
|
||||||
|
|
||||||
|
vertical_margin_between_entries: str = Field(default="0.12cm")
|
||||||
|
|
||||||
|
vertical_margin_between_entries_and_highlights: str = Field(default="0.12cm")
|
||||||
|
|
||||||
|
date_and_location_width: str = Field(default="3.7cm")
|
||||||
|
|
||||||
|
|
||||||
|
class Design(BaseModel):
|
||||||
|
theme: Literal["classic"] = "classic"
|
||||||
|
options: ClassicThemeOptions
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================================
|
||||||
|
# ======================================================================================
|
||||||
|
# ======================================================================================
|
||||||
|
|
||||||
|
# ======================================================================================
|
||||||
|
# CONTENT MODELS =======================================================================
|
||||||
|
# ======================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class Skill(BaseModel):
|
||||||
|
# 1) Mandotory user inputs:
|
||||||
|
name: str
|
||||||
|
# 2) Optional user inputs:
|
||||||
|
details: str = None
|
||||||
|
|
||||||
|
|
||||||
|
class Event(BaseModel):
|
||||||
|
start_date: Date = None
|
||||||
|
end_date: Date | Literal["present"] = None
|
||||||
|
date: str | Date = None
|
||||||
|
location: str = None
|
||||||
|
highlights: list[SpellCheckedString] = None
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
@classmethod
|
||||||
|
def check_dates(cls, model):
|
||||||
|
"""
|
||||||
|
To be continued...
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
model.start_date is not None
|
||||||
|
and model.end_date is not None
|
||||||
|
and model.date is not None
|
||||||
|
):
|
||||||
|
logging.warning(
|
||||||
|
"start_date, end_date and date are all provided. Therefore, date will"
|
||||||
|
" be ignored."
|
||||||
|
)
|
||||||
|
model.date = None
|
||||||
|
elif model.date is not None and (
|
||||||
|
model.start_date is not None or model.end_date is not None
|
||||||
|
):
|
||||||
|
logging.warning(
|
||||||
|
"date is provided. Therefore, start_date and end_date will be ignored."
|
||||||
|
)
|
||||||
|
model.start_date = None
|
||||||
|
model.end_date = None
|
||||||
|
|
||||||
|
return model
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@cached_property
|
||||||
|
def date_and_location_strings(self) -> list[str]:
|
||||||
|
date_and_location_strings = []
|
||||||
|
|
||||||
|
if self.location is not None:
|
||||||
|
date_and_location_strings.append(self.location)
|
||||||
|
|
||||||
|
if self.date is not None:
|
||||||
|
# Then it means start_date and end_date are not provided.
|
||||||
|
date_and_location_strings.append(self.date)
|
||||||
|
else:
|
||||||
|
# Then it means start_date and end_date are provided.
|
||||||
|
|
||||||
|
start_date = format_date(self.start_date)
|
||||||
|
|
||||||
|
if self.end_date == "present":
|
||||||
|
end_date = "present"
|
||||||
|
|
||||||
|
time_span_string = compute_time_span_string(
|
||||||
|
self.start_date, Date.today()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
end_date = format_date(self.end_date)
|
||||||
|
|
||||||
|
time_span_string = compute_time_span_string(
|
||||||
|
self.start_date, self.end_date
|
||||||
|
)
|
||||||
|
|
||||||
|
date_and_location_strings.append(f"{start_date} to {end_date}")
|
||||||
|
|
||||||
|
list_of_no_time_span_string_classes = [
|
||||||
|
"Education",
|
||||||
|
]
|
||||||
|
if not self.__class__.__name__ in list_of_no_time_span_string_classes:
|
||||||
|
date_and_location_strings.append(f"{time_span_string}")
|
||||||
|
|
||||||
|
return date_and_location_strings
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@cached_property
|
||||||
|
def highlight_strings(self) -> list[SpellCheckedString]:
|
||||||
|
"""
|
||||||
|
To be continued...
|
||||||
|
"""
|
||||||
|
highlight_strings = []
|
||||||
|
|
||||||
|
highlight_strings.extend(self.highlights)
|
||||||
|
|
||||||
|
return highlight_strings
|
||||||
|
|
||||||
|
|
||||||
|
class TestScore(Event):
|
||||||
|
# 1) Mandotory user inputs:
|
||||||
|
name: str
|
||||||
|
score: str
|
||||||
|
# 2) Optional user inputs:
|
||||||
|
url: HttpUrl = None
|
||||||
|
|
||||||
|
|
||||||
|
class NormalEntry(Event):
|
||||||
|
# 1) Mandotory user inputs:
|
||||||
|
name: str
|
||||||
|
# 2) Optional user inputs:
|
||||||
|
url: HttpUrl = None
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@cached_property
|
||||||
|
def highlight_strings(self) -> list[SpellCheckedString]:
|
||||||
|
"""
|
||||||
|
To be continued...
|
||||||
|
"""
|
||||||
|
highlight_strings = []
|
||||||
|
|
||||||
|
highlight_strings.extend(self.highlights)
|
||||||
|
|
||||||
|
if self.url is not None:
|
||||||
|
# remove "https://" from the url for a cleaner look
|
||||||
|
textUrl = str(self.url).replace("https://", "")
|
||||||
|
linkString = f"Course certificate: [{textUrl}]({self.transcript_url}))"
|
||||||
|
highlight_strings.append(linkString)
|
||||||
|
|
||||||
|
return highlight_strings
|
||||||
|
|
||||||
|
|
||||||
|
class ExperienceEntry(Event):
|
||||||
|
# 1) Mandotory user inputs:
|
||||||
|
company: str
|
||||||
|
position: str
|
||||||
|
# 2) Optional user inputs:
|
||||||
|
|
||||||
|
|
||||||
|
class EducationEntry(Event):
|
||||||
|
# 1) Mandotory user inputs:
|
||||||
|
institution: str
|
||||||
|
area: str
|
||||||
|
# 2) Optional user inputs:
|
||||||
|
study_type: str = None
|
||||||
|
gpa: str = None
|
||||||
|
transcript_url: HttpUrl = None
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@cached_property
|
||||||
|
def highlight_strings(self) -> list[SpellCheckedString]:
|
||||||
|
"""
|
||||||
|
To be continued...
|
||||||
|
"""
|
||||||
|
highlight_strings = []
|
||||||
|
|
||||||
|
if self.gpa is not None:
|
||||||
|
gpaString = f"GPA: {self.gpa}"
|
||||||
|
if self.transcript_url is not None:
|
||||||
|
gpaString += f" ([Transcript]({self.transcript_url}))"
|
||||||
|
highlight_strings.append(gpaString)
|
||||||
|
|
||||||
|
highlight_strings.extend(self.highlights)
|
||||||
|
|
||||||
|
return highlight_strings
|
||||||
|
|
||||||
|
|
||||||
|
class SocialNetwork(BaseModel):
|
||||||
|
# 1) Mandotory user inputs:
|
||||||
|
network: Literal["LinkedIn", "GitHub", "Instagram"]
|
||||||
|
username: str
|
||||||
|
|
||||||
|
|
||||||
|
class Connection(BaseModel):
|
||||||
|
# 3) Derived fields (not user inputs):
|
||||||
|
name: Literal["LinkedIn", "GitHub", "Instagram", "phone", "email", "website"]
|
||||||
|
value: str
|
||||||
|
|
||||||
|
|
||||||
|
class CurriculumVitae(BaseModel):
|
||||||
|
# 1) Mandotory user inputs:
|
||||||
|
name: str
|
||||||
|
# 2) Optional user inputs:
|
||||||
|
email: str = None
|
||||||
|
phone: PhoneNumber = None
|
||||||
|
website: HttpUrl = None
|
||||||
|
location: str = None
|
||||||
|
social_networks: list[SocialNetwork] = None
|
||||||
|
education: list[EducationEntry] = None
|
||||||
|
work_experience: list[ExperienceEntry] = None
|
||||||
|
academic_projects: list[NormalEntry] = None
|
||||||
|
certificates: list[NormalEntry] = None
|
||||||
|
extracurricular_activities: list[ExperienceEntry] = None
|
||||||
|
test_scores: list[TestScore] = None
|
||||||
|
skills: list[Skill] = None
|
||||||
|
|
||||||
|
@computed_field
|
||||||
|
@cached_property
|
||||||
|
def connections(self) -> list[str]:
|
||||||
|
connections = []
|
||||||
|
if self.phone is not None:
|
||||||
|
connections.append(Connection(name="phone", value=self.phone))
|
||||||
|
if self.email is not None:
|
||||||
|
connections.append(Connection(name="email", value=self.email))
|
||||||
|
if self.website is not None:
|
||||||
|
connections.append(Connection(name="website", value=str(self.website)))
|
||||||
|
if self.social_networks is not None:
|
||||||
|
for social_network in self.social_networks:
|
||||||
|
connections.append(
|
||||||
|
Connection(
|
||||||
|
name=social_network.network, value=social_network.username
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return connections
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================================
|
||||||
|
# ======================================================================================
|
||||||
|
# ======================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class RenderCVDataModel(BaseModel):
|
||||||
|
design: Design
|
||||||
|
cv: CurriculumVitae
|
|
@ -1,6 +1,7 @@
|
||||||
((# IMPORT MACROS #))
|
((# IMPORT MACROS #))
|
||||||
((* from "components/education.tex.j2" import education with context *))
|
((* from "components/education_entry.tex.j2" import education_entry with context *))
|
||||||
((* from "components/experience.tex.j2" import experience with context *))
|
((* from "components/experience_entry.tex.j2" import experience_entry with context *))
|
||||||
|
((* from "components/normal_entry.tex.j2" import normal_entry with context *))
|
||||||
((* from "components/header.tex.j2" import header with context *))
|
((* from "components/header.tex.j2" import header with context *))
|
||||||
|
|
||||||
\documentclass[10pt, a4paper]{memoir}
|
\documentclass[10pt, a4paper]{memoir}
|
||||||
|
@ -94,27 +95,65 @@
|
||||||
\newcommand{\hrefExternal}[2]{\href{#1}{#2\, \raisebox{.1ex}{\footnotesize \faExternalLink*}}} % new command for external links
|
\newcommand{\hrefExternal}[2]{\href{#1}{#2\, \raisebox{.1ex}{\footnotesize \faExternalLink*}}} % new command for external links
|
||||||
|
|
||||||
\begin{document}
|
\begin{document}
|
||||||
% Test out the 4 main entry types with the commands below:
|
|
||||||
<<header(name=cv.name, connections=cv.connections)|indent(4)>>
|
<<header(name=cv.name, connections=cv.connections)|indent(4)>>
|
||||||
|
|
||||||
\section{Education}
|
\section{Education}
|
||||||
((* for edu in cv.education *))
|
|
||||||
<<education(
|
((* for edu in cv.education *))
|
||||||
|
<<education_entry(
|
||||||
study_type=edu.study_type,
|
study_type=edu.study_type,
|
||||||
institution=edu.institution,
|
institution=edu.institution,
|
||||||
area=edu.area,
|
area=edu.area,
|
||||||
highlights=edu.highlight_strings,
|
highlights=edu.highlight_strings,
|
||||||
date_and_location_strings=edu.date_and_location_strings
|
date_and_location_strings=edu.date_and_location_strings
|
||||||
)|indent(4)>>
|
)|indent(4)>>
|
||||||
((* endfor *))
|
((* if not loop.last *))
|
||||||
|
\vspace*{<<design.vertical_margin_between_entries>>}
|
||||||
|
|
||||||
|
((* endif *))
|
||||||
|
((* endfor *))
|
||||||
|
|
||||||
\section{Work Experience}
|
\section{Work Experience}
|
||||||
((* for work in cv.work_experience *))
|
|
||||||
<<experience(
|
((* for work in cv.work_experience *))
|
||||||
|
<<experience_entry(
|
||||||
company=work.company,
|
company=work.company,
|
||||||
position=work.position,
|
position=work.position,
|
||||||
highlights=work.highlights,
|
highlights=work.highlight_strings,
|
||||||
date_and_location_strings=work.date_and_location_strings
|
date_and_location_strings=work.date_and_location_strings
|
||||||
)|indent(4)>>
|
)|indent(4)>>
|
||||||
((* endfor *))
|
((* if not loop.last *))
|
||||||
|
\vspace*{<<design.vertical_margin_between_entries>>}
|
||||||
|
|
||||||
|
((* endif *))
|
||||||
|
((* endfor *))
|
||||||
|
|
||||||
|
\section{Academic Projects}
|
||||||
|
((* for academic_project in cv.academic_projects *))
|
||||||
|
<<normal_entry(
|
||||||
|
name=academic_project.name,
|
||||||
|
highlights=academic_project.highlight_strings,
|
||||||
|
date_and_location_strings=academic_project.date_and_location_strings,
|
||||||
|
url=academic_project.url
|
||||||
|
urlText=""
|
||||||
|
)|indent(4)>>
|
||||||
|
((* if not loop.last *))
|
||||||
|
\vspace*{<<design.vertical_margin_between_entries>>}
|
||||||
|
|
||||||
|
((* endif *))
|
||||||
|
((* endfor *))
|
||||||
|
|
||||||
|
\section{Certificates}
|
||||||
|
((* for certificate in cv.certificates *))
|
||||||
|
<<normal_entry(
|
||||||
|
name=certificate.name,
|
||||||
|
highlights=certificate.highlight_strings,
|
||||||
|
date_and_location_strings=certificate.date_and_location_strings
|
||||||
|
)|indent(4)>>
|
||||||
|
((* if not loop.last *))
|
||||||
|
\vspace*{<<design.vertical_margin_between_entries>>}
|
||||||
|
|
||||||
|
((* endif *))
|
||||||
|
((* endfor *))
|
||||||
|
|
||||||
\end{document}
|
\end{document}
|
|
@ -0,0 +1,19 @@
|
||||||
|
((* from "components/highlights.tex.j2" import highlights as print_higlights with context *))
|
||||||
|
((* from "components/date_and_location_strings.tex.j2" import date_and_location_strings as print_date_and_locations *))
|
||||||
|
|
||||||
|
((* macro education_entry(study_type, institution, area, highlights, date_and_location_strings)*))
|
||||||
|
((# \begin{tabularx}{⟨width⟩}[⟨pos⟩]{⟨preamble⟩} #))
|
||||||
|
((# width: \textwidth #))
|
||||||
|
((# preamble: first column, second column, third column #))
|
||||||
|
((# first column: p{0.55cm}; constant width, ragged left column #))
|
||||||
|
((# second column: X; variable width, ragged left column #))
|
||||||
|
((# third column: R{<<design.date_and_location_width>>}; constant widthm ragged right column #))
|
||||||
|
\begin{tabularx}{\textwidth}{p{0.55cm} X R{<<design.date_and_location_width>>}}
|
||||||
|
\textbf{<<study_type if study_type is not none>>}
|
||||||
|
&
|
||||||
|
\textbf{<<institution>>}, <<area>>
|
||||||
|
<<print_higlights(highlights)|indent(4)->>
|
||||||
|
&
|
||||||
|
<<print_date_and_locations(date_and_location_strings)|indent(4)->>
|
||||||
|
\end{tabularx}
|
||||||
|
((* endmacro *))
|
|
@ -0,0 +1,16 @@
|
||||||
|
((* from "components/highlights.tex.j2" import highlights as print_higlights with context *))
|
||||||
|
((* from "components/date_and_location_strings.tex.j2" import date_and_location_strings as print_date_and_locations *))
|
||||||
|
|
||||||
|
((* macro experience_entry(company, position, highlights, date_and_location_strings)*))
|
||||||
|
((# \begin{tabularx}{⟨width⟩}[⟨pos⟩]{⟨preamble⟩} #))
|
||||||
|
((# width: \textwidth #))
|
||||||
|
((# preamble: first column, second column #))
|
||||||
|
((# first column:: X; variable width, ragged left column #))
|
||||||
|
((# second column: R{<<design.date_and_location_width>>}; constant width ragged right column #))
|
||||||
|
\begin{tabularx}{\textwidth}{X R{<<design.date_and_location_width>>}}
|
||||||
|
\textbf{<<company>>}, <<position>>
|
||||||
|
<<print_higlights(highlights)|indent(4)->>
|
||||||
|
&
|
||||||
|
<<print_date_and_locations(date_and_location_strings)|indent(4)->>
|
||||||
|
\end{tabularx}
|
||||||
|
((* endmacro *))
|
|
@ -11,6 +11,5 @@
|
||||||
\hspace{0.5cm}
|
\hspace{0.5cm}
|
||||||
((* endif *))
|
((* endif *))
|
||||||
((* endfor *))
|
((* endfor *))
|
||||||
|
|
||||||
\end{header}
|
\end{header}
|
||||||
((* endmacro *))
|
((* endmacro *))
|
|
@ -4,5 +4,5 @@
|
||||||
((* for item in highlights *))
|
((* for item in highlights *))
|
||||||
\item <<item|markdown_to_latex>>
|
\item <<item|markdown_to_latex>>
|
||||||
((* endfor *))
|
((* endfor *))
|
||||||
\end{highlights}
|
\end{highlights}
|
||||||
((* endmacro *))
|
((* endmacro *))
|
|
@ -0,0 +1,21 @@
|
||||||
|
((* from "components/highlights.tex.j2" import highlights as print_higlights with context *))
|
||||||
|
((* from "components/date_and_location_strings.tex.j2" import date_and_location_strings as print_date_and_locations *))
|
||||||
|
|
||||||
|
((* macro normal_entry(name, highlights, date_and_location_strings, url, urlText)*))
|
||||||
|
((# \begin{tabularx}{⟨width⟩}[⟨pos⟩]{⟨preamble⟩} #))
|
||||||
|
((# width: \textwidth #))
|
||||||
|
((# preamble: first column, second column #))
|
||||||
|
((# first column:: X; variable width, ragged left column #))
|
||||||
|
((# second column: R{<<design.date_and_location_width>>}; constant width ragged right column #))
|
||||||
|
\begin{tabularx}{\textwidth}{X R{<<design.date_and_location_width>>}}
|
||||||
|
((* if url is not none *))
|
||||||
|
((* set markdownUrl = "["+urlText+"]("+ str(url) +")" *))
|
||||||
|
\textbf{<<name>>}, <<markdownUrl|markdown_to_latex>>
|
||||||
|
((* else *))
|
||||||
|
\textbf{<<name>>}
|
||||||
|
((* endif *))
|
||||||
|
<<print_higlights(highlights)|indent(4)->>
|
||||||
|
&
|
||||||
|
<<print_date_and_locations(date_and_location_strings)|indent(4)->>
|
||||||
|
\end{tabularx}
|
||||||
|
((* endmacro *))
|
|
@ -0,0 +1,35 @@
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def run_latex(latexFilePath):
|
||||||
|
latexFilePath = os.path.normpath(latexFilePath)
|
||||||
|
latexFile = os.path.basename(latexFilePath)
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
# remove all files except the .tex file
|
||||||
|
for file in os.listdir(os.path.dirname(latexFilePath)):
|
||||||
|
if file.endswith(".tex"):
|
||||||
|
continue
|
||||||
|
os.remove(os.path.join(os.path.dirname(latexFilePath), file))
|
||||||
|
|
||||||
|
tinytexPath = os.path.join(
|
||||||
|
os.path.dirname(__file__),
|
||||||
|
"vendor",
|
||||||
|
"TinyTeX",
|
||||||
|
"bin",
|
||||||
|
"windows",
|
||||||
|
)
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
f"{tinytexPath}\\latexmk.exe",
|
||||||
|
"-lualatex",
|
||||||
|
# "-c",
|
||||||
|
f"{latexFile}",
|
||||||
|
"-synctex=1",
|
||||||
|
"-interaction=nonstopmode",
|
||||||
|
"-file-line-error",
|
||||||
|
],
|
||||||
|
cwd=os.path.dirname(latexFilePath),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("Only Windows is supported for now.")
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue