├── photo.jpg ├── make_cv.ps1 ├── .gitignore ├── cv_template.tex ├── LICENSE.txt ├── cv_en_john_doe.md ├── README.md └── md_to_tex.py /photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucafrance/luca-cv/HEAD/photo.jpg -------------------------------------------------------------------------------- /make_cv.ps1: -------------------------------------------------------------------------------- 1 | pandoc -s cv_en_john_doe.md -o cv_en_john_doe.docx 2 | python md_to_tex.py cv_en_john_doe.md english "scale=0.85" 3 | pdflatex cv_en_john_doe.tex 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.docx 2 | 3 | *.4tc 4 | *.aux 5 | *.bak 6 | *.gz 7 | *.log 8 | *.out 9 | *.pdf 10 | *.tex 11 | *.tmp 12 | *.xref 13 | 14 | !cv_template.tex 15 | -------------------------------------------------------------------------------- /cv_template.tex: -------------------------------------------------------------------------------- 1 | \documentclass[10pt,a4paper,sans]{moderncv} 2 | 3 | % moderncv themes 4 | \moderncvstyle{classic} % style options are 'casual' (default), 'classic', 'banking', 'oldstyle' and 'fancy' 5 | \moderncvcolor{black} % color options 'black', 'blue' (default), 'burgundy', 'green', 'grey', 'orange', 'purple' and 'red' 6 | \nopagenumbers{} % uncomment to suppress automatic page numbering for CVs longer than one page 7 | \usepackage[$language]{babel} 8 | \usepackage[none]{hyphenat} 9 | 10 | % adjust the page margins 11 | \usepackage[$geometry_options]{geometry} 12 | \setlength{\hintscolumnwidth}{3.15cm} % if you want to change the width of the column with the dates 13 | 14 | % personal data 15 | $personal_data 16 | 17 | \begin{document} 18 | \makecvtitle 19 | 20 | $content 21 | 22 | \end{document} 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Luca Franceschini (lucaf.eu) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cv_en_john_doe.md: -------------------------------------------------------------------------------- 1 | # John Doe 2 | 3 | Curriculum Vitae 4 | 5 | ![](photo.jpg) 6 | 7 | `email` [john.doe@example.com](mailto:john.doe@example.com) | 8 | `homepage` [example.com](https://example.com) | 9 | `linkedin` [your-linkedin-url](https://www.linkedin.com/in/your-linkedin-url/) | 10 | `github` [your-github-username](https://github.com/your-github-username) 11 | 12 | ## Work Experience 13 | 14 | *Since Jan 2022* Lorem ipsum Inc. -- Assistant to the Assistant of the Senior President 15 | 16 | Lorem ipsum dolor sit amet 17 | 18 | - consectetur adipiscing elit 19 | - sed do eiusmod tempor incididunt 20 | - ut labore et dolore magna aliqua 21 | 22 | Ut enim ad minim veniam 23 | 24 | - quis nostrud exercitation ullamco 25 | - laboris nisi ut aliquip ex ea commodo consequat 26 | - duis aute irure dolor in reprehenderit in voluptate 27 | 28 | *Jan 2021 -- Dec 2021* Dolor sit amet Corp. -- Junior Vice President Assistant 29 | 30 | Lorem ipsum dolor sit amet 31 | 32 | - consectetur adipiscing elit 33 | - sed do eiusmod tempor incididunt 34 | - ut labore et dolore magna aliqua 35 | 36 | Ut enim ad minim veniam 37 | 38 | - quis nostrud exercitation ullamco 39 | - laboris nisi ut aliquip ex ea commodo consequat 40 | - duis aute irure dolor in reprehenderit in voluptate 41 | 42 | ## Education 43 | 44 | *Jan 2020 -- Dec 2020* MBA Business Waterfall Transformation -- Lorem Ipsum Business School 45 | 46 | *Sep 2019 -- Sep 2019* B.A. Studies of Medieval Logistics 47 | 48 | ## Skills 49 | 50 | *IT* PowerPoint, Excel, VisiCalc, Lotus 1-2-3 51 | 52 | *Languages* English (native), German (fluent), French (fluent) 53 | 54 | ## Certifications 55 | 56 | *Jun 2022* [Certified Scrum Storyteller -- Flaccid Scrum School](https://example.com) 57 | 58 | *Jun 2021* Class Participation non-fungible Token -- DeFi Centralized Coalition 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Luca-CV 2 | 3 | ## Use 4 | 5 | - Replace `photo.jpg` with your photo. 6 | - Update `cv_en_john_doe.md`. 7 | - Run `make_cv.ps1` to generate `cv_en_john_doe.docx`,`cv_en_john_doe.tex`, `cv_en_john_doe.pdf`. 8 | 9 | ## Requirements 10 | 11 | - [Python](https://www.python.org/) 12 | - [Pandoc](https://pandoc.org/) 13 | - pdflatex (included in [MiKTeX](https://miktex.org)) 14 | 15 | ## Version History 16 | 17 | ### v2.0 18 | 19 | Add optional argument for LaTeX's `gemoetry` options to `md_to_tex.py`. 20 | ``` 21 | python md_to_tex.py cv_en_john_doe.md english "scale=0.85" 22 | ``` 23 | 24 | The variable `new_page_sections` defines the name of sections which get a `\newpage` in the LaTeX. 25 | Leave the variable emty to not add any `\newpage`. 26 | ``` 27 | new_page_sections = [ 28 | "Project Experience", 29 | "Keywords", 30 | "Projekterfahrung", 31 | "Stichworte", 32 | ] 33 | ``` 34 | 35 | Add support for markdown links. 36 | ``` 37 | *Jun 2022* [Certified Scrum Storyteller -- Flaccid Scrum School](https://example.com) 38 | ``` 39 | 40 | Properly convert markdown lists to `itemize` environments in LaTeX. 41 | ``` 42 | - consectetur adipiscing elit 43 | - sed do eiusmod tempor incididunt 44 | - ut labore et dolore magna aliqua 45 | ``` 46 | ``` 47 | \cvitem{}{\begin{itemize} 48 | \item consectetur adipiscing elit 49 | \item sed do eiusmod tempor incididunt 50 | \item ut labore et dolore magna aliqua 51 | \end{itemize}} 52 | ``` 53 | 54 | Add support for html comments. 55 | ``` 56 | 57 | ``` 58 | 59 | Minor fixes and adjustments. 60 | 61 | ### v1.0 62 | 63 | [How I manage my CV with Markdown, Pandoc, Python, and LaTeX](https://lucaf.eu/2022/08/18/cv-markdown-pandoc-python-latex.html) 64 | 65 | ## Acknowledgment 66 | 67 | [Template photo](https://unsplash.com/photos/dLij9K4ObYY) by [Joe Shields](https://unsplash.com/@fortyozsteak) 68 | -------------------------------------------------------------------------------- /md_to_tex.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import re 3 | import sys 4 | 5 | new_page_sections = [] 6 | 7 | def personal_info_from_md_line(txt): 8 | """Return personal info for lines with links 9 | 10 | e.g. 11 | input: "`email` [abc@example.com](mailto:abc@example.com)" 12 | output: "abc@example.com" 13 | """ 14 | 15 | txt = txt.split("[")[1] 16 | txt = txt.split("]")[0] 17 | return txt 18 | 19 | def split_cv_line(txt): 20 | """Return a tuple of the side bar text and title 21 | 22 | e.g. 23 | input: "*Gen 2000 -- Dec 2020* Example GmbH -- Intern" 24 | output: ("Gen 2000 -- Dec 2020", "Example GmbH -- Intern") 25 | """ 26 | 27 | if not txt.startswith("*"): 28 | return "", txt 29 | 30 | txt = txt.split("*", 1)[1] 31 | txt = txt.split("*", 1) 32 | side_text = txt[0].strip() 33 | title = txt[1].strip() 34 | return side_text, title 35 | 36 | def md_to_tex(markdown_text): 37 | """Convert links in markdown texts to LaTex links""" 38 | 39 | # Regular expression to find markdown links 40 | markdown_link_pattern = re.compile(r'\[([^\]]+)\]\(([^)]+)\)') 41 | 42 | # Function to replace markdown links with LaTeX links 43 | def replace_link(match): 44 | text = match.group(1) 45 | url = match.group(2) 46 | return f'\\href{{{url}}}{{{text}}}' 47 | 48 | # Replace all markdown links in the text 49 | latex_text = markdown_link_pattern.sub(replace_link, markdown_text) 50 | return latex_text 51 | 52 | class CvSection(): 53 | 54 | def __init__(self, title): 55 | self.title = title 56 | 57 | def to_tex(self): 58 | tex_code = "\\section{{{}}}".format(self.title) 59 | if self.title in new_page_sections: 60 | tex_code = "\\newpage\n\n" + tex_code 61 | return tex_code 62 | 63 | class CvSubSection(): 64 | 65 | def __init__(self, title): 66 | self.title = title 67 | 68 | def to_tex(self): 69 | tex_code = "\\subsection{{{}}}".format(self.title) 70 | return tex_code 71 | 72 | class CvEntryOrItem(): 73 | 74 | def __init__(self, side_txt, title): 75 | self.side_txt = side_txt 76 | self.title = title 77 | 78 | def to_tex(self): 79 | title = self.title 80 | bold_cventry_field = None 81 | if title.startswith("-"): 82 | # For each list item an individual `itemize` environment is created. 83 | # The redundant begins and ends of `itemize` environments are later removed 84 | # in the `content_to_tex` method of the `CurriculumVitae` class. 85 | # \cvitem{}{\begin{itemize}\item ... \end{itemize}} 86 | # \cvitem{}{\begin{itemize}\item ... \end{itemize}} 87 | # \cvitem{}{\begin{itemize}\item ... \end{itemize}} 88 | title = "\\begin{itemize}\\item " + title[1:] + "\\end{itemize}" 89 | elif title.startswith(" -"): 90 | title ="\\quad -" + title[5:] 91 | elif "**" in title: 92 | title_split = title.split(sep="**", maxsplit=2) 93 | bold_cventry_field = title_split[1].strip() 94 | title = title_split[2].strip() 95 | while "*" in title: 96 | title_split = title.split(sep="*", maxsplit=2) 97 | title = title_split[0] + "\\emph{" + title_split[1] + "}" + title_split[2] 98 | if bold_cventry_field is None: 99 | return f"\\cvitem{{{self.side_txt}}}{{{title}}}" 100 | else: 101 | return f"\\cventry{{{self.side_txt}}}{{{bold_cventry_field}}}{{{title}}}{{}}{{}}{{}}" 102 | 103 | class CurriculumVitae(): 104 | 105 | def __init__(self, language=None, geometry_options="scale=0.80"): 106 | if language is None: 107 | language = "english" 108 | self.language = language 109 | self.name = ("John", "Doe") 110 | self.title = None 111 | self.content = None 112 | self.photo = None 113 | self.geometry_options = geometry_options # options for the `geometry` Tex package 114 | 115 | def from_markdown(self, markdown_src): 116 | self.content = [] 117 | # Remove html comments from the markdown source 118 | markdown_src = re.sub(r'', '', markdown_src, flags=re.DOTALL) 119 | lines = markdown_src.splitlines() 120 | i = 0 121 | while i < len(lines): 122 | line = lines[i] 123 | if line.startswith("# "): 124 | line = line.removeprefix("# ") 125 | self.name = line.split(" ", 1) 126 | i += 1 127 | # The first non-empty line after the name is the title 128 | while lines[i] == "": 129 | i += 1 130 | self.title = lines[i] 131 | i += 1 132 | continue 133 | 134 | if line.startswith("![]"): 135 | self.photo = line.split("(")[1].split(")")[0] 136 | i += 1 137 | continue 138 | 139 | if line.startswith("`email`"): 140 | self.email = personal_info_from_md_line(line) 141 | i += 1 142 | continue 143 | if line.startswith("`homepage`"): 144 | self.homepage = personal_info_from_md_line(line) 145 | i += 1 146 | continue 147 | if line.startswith("`linkedin`"): 148 | self.linkedin = personal_info_from_md_line(line) 149 | i += 1 150 | continue 151 | if line.startswith("`github`"): 152 | self.github = personal_info_from_md_line(line) 153 | i += 1 154 | continue 155 | 156 | if line.startswith("## "): 157 | self.content.append(CvSection(line.removeprefix("## "))) 158 | i += 1 159 | continue 160 | 161 | if line.startswith("### "): 162 | self.content.append(CvSubSection(line.removeprefix("### "))) 163 | i += 1 164 | continue 165 | 166 | if line.strip() != "": 167 | side_text, title = split_cv_line(line) 168 | self.content.append(CvEntryOrItem(side_text, title)) 169 | i += 1 170 | continue 171 | 172 | i += 1 173 | 174 | def personal_data_to_tex(self): 175 | tex_out = [] 176 | tex_out.append("\\name {{{}}}{{{}}}".format(self.name[0], self.name[1])) 177 | if self.title is not None: 178 | tex_out.append("\\title{{{}}}".format(self.title)) 179 | if self.photo is not None: 180 | tex_out.append("\\photo[80pt][0pt]{{{}}}".format(self.photo)) 181 | if self.email is not None: 182 | tex_out.append("\\email{{{}}}".format(self.email)) 183 | if self.homepage is not None: 184 | tex_out.append("\\homepage{{{}}}".format(self.homepage)) 185 | if self.linkedin is not None: 186 | tex_out.append("\\social[linkedin]{{{}}}".format(self.linkedin)) 187 | if self.github is not None: 188 | tex_out.append("\\social[github]{{{}}}".format(self.github)) 189 | 190 | tex_out = "\n".join(tex_out) 191 | return tex_out 192 | 193 | def remove_redundant_itemize(tex_code): 194 | """Remove redundant `itemize` environment statements from Tex code generated by the `CvEntryOrItem` class.""" 195 | 196 | # \cvitem{}{\begin{itemize}\item ... \end{itemize}} 197 | # \cvitem{}{\begin{itemize}\item ... \end{itemize}} 198 | # \cvitem{}{\begin{itemize}\item ... \end{itemize}} 199 | tex_code = tex_code.replace("\\end{itemize}}\n\\cvitem{}{\\begin{itemize}", "\n") 200 | # \cvitem{}{\begin{itemize}\item ... 201 | # \item ... 202 | # \item ... \end{itemize}} 203 | tex_code = tex_code.replace("\\begin{itemize}", "\\begin{itemize}\n") 204 | # \cvitem{}{\begin{itemize} 205 | # \item ... 206 | # \item ... 207 | # \item ... \end{itemize}} 208 | tex_code = tex_code.replace("\\end{itemize}}", "\n \\end{itemize}}") 209 | # \cvitem{}{\begin{itemize} 210 | # \item ... 211 | # \item ... 212 | # \item ... 213 | # \end{itemize}} 214 | tex_code = tex_code.replace("\\item", " \\item") 215 | # \cvitem{}{\begin{itemize} 216 | # \item ... 217 | # \item ... 218 | # \item ... 219 | # \end{itemize}} 220 | return tex_code 221 | 222 | def content_to_tex(self): 223 | content_lines = [item.to_tex() for item in self.content] 224 | tex_code = "\n".join(content_lines) 225 | tex_code = CurriculumVitae.remove_redundant_itemize(tex_code) 226 | return tex_code 227 | 228 | def to_tex(self): 229 | tex_template = open("cv_template.tex", "rt").read() 230 | tex_template = tex_template.replace("$geometry_options", self.geometry_options) 231 | tex_template = tex_template.replace("$language", self.language) 232 | tex_template = tex_template.replace("$personal_data", self.personal_data_to_tex()) 233 | tex_template = tex_template.replace("$content", self.content_to_tex()) 234 | tex_template = tex_template.replace("<", "\\textless") 235 | tex_template = tex_template.replace(">", "\\textgreater") 236 | tex_template = md_to_tex(tex_template) 237 | return tex_template 238 | 239 | 240 | if __name__ == "__main__": 241 | src_filename = sys.argv[1] 242 | if len(sys.argv) > 2: 243 | language = sys.argv[2] 244 | else: 245 | language = None 246 | if len(sys.argv) > 3: 247 | geometry_options = sys.argv[3] 248 | cv = CurriculumVitae(language, geometry_options) 249 | else: 250 | cv = CurriculumVitae(language) 251 | 252 | cv.from_markdown(open(src_filename, "rt").read()) 253 | open(os.path.splitext(src_filename)[0] + ".tex", "wt").write(cv.to_tex()) 254 | --------------------------------------------------------------------------------