cv-tools-cli 2.0.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.
- cv_tools/__init__.py +7 -0
- cv_tools/__main__.py +5 -0
- cv_tools/api.py +123 -0
- cv_tools/bib.py +122 -0
- cv_tools/charset.py +142 -0
- cv_tools/cli.py +384 -0
- cv_tools/doctor.py +185 -0
- cv_tools/engine.py +320 -0
- cv_tools/loader.py +424 -0
- cv_tools/markup.py +614 -0
- cv_tools/migrate.py +222 -0
- cv_tools/model.py +192 -0
- cv_tools/render/__init__.py +1 -0
- cv_tools/render/html.py +581 -0
- cv_tools/render/latex.py +154 -0
- cv_tools/schema/cv-v2.schema.json +707 -0
- cv_tools/starter/cv.yaml +129 -0
- cv_tools/starter/publications.bib +55 -0
- cv_tools/templates/html/cv.css +252 -0
- cv_tools/templates/html/cv.html.jinja +19 -0
- cv_tools/templates/html/icons/github.svg +1 -0
- cv_tools/templates/html/icons/linkedin.svg +1 -0
- cv_tools/templates/html/icons/orcid.svg +1 -0
- cv_tools/templates/html/icons/twitter.svg +1 -0
- cv_tools/templates/html/macros.jinja +89 -0
- cv_tools/templates/latex/cv.tex.jinja +45 -0
- cv_tools/templates/latex/layouts/dated.jinja +12 -0
- cv_tools/templates/latex/layouts/entries-inline.jinja +23 -0
- cv_tools/templates/latex/layouts/entries.jinja +29 -0
- cv_tools/templates/latex/layouts/list.jinja +24 -0
- cv_tools/templates/latex/layouts/pairs.jinja +14 -0
- cv_tools/templates/latex/layouts/publications.jinja +14 -0
- cv_tools/templates/latex/layouts/text.jinja +10 -0
- cv_tools/templates/latex/orcid.pdf +0 -0
- cv_tools/templates/latex/packages.txt +47 -0
- cv_tools/templates/latex/resume.cls +265 -0
- cv_tools/templates/latex/unicode.txt +398 -0
- cv_tools/v1.py +484 -0
- cv_tools/validate.py +165 -0
- cv_tools/watch.py +76 -0
- cv_tools_cli-2.0.0.dist-info/METADATA +498 -0
- cv_tools_cli-2.0.0.dist-info/RECORD +46 -0
- cv_tools_cli-2.0.0.dist-info/WHEEL +5 -0
- cv_tools_cli-2.0.0.dist-info/entry_points.txt +2 -0
- cv_tools_cli-2.0.0.dist-info/licenses/LICENSE +21 -0
- cv_tools_cli-2.0.0.dist-info/top_level.txt +1 -0
cv_tools/render/latex.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""LaTeX renderer: turns the normalized v2 model into a .tex file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import jinja2
|
|
10
|
+
|
|
11
|
+
from .. import __version__
|
|
12
|
+
from ..bib import Publication
|
|
13
|
+
from ..markup import CELL_NEWLINE, Text, escape_url, split_date, to_latex
|
|
14
|
+
from ..model import DEFAULT_SEPARATOR, ICON_KINDS, ContactItem, ContactLine, Document, Section
|
|
15
|
+
|
|
16
|
+
TEMPLATE_DIR = resources.files("cv_tools") / "templates" / "latex"
|
|
17
|
+
SUPPORT_FILES = ("resume.cls", "orcid.pdf")
|
|
18
|
+
AUTO_DATE = r"\customdate\today" # the {date} insertion appends {} (see markup.to_latex)
|
|
19
|
+
|
|
20
|
+
_ICON_MACROS = {
|
|
21
|
+
"linkedin": r"\colorfaLinkedinSquare",
|
|
22
|
+
"github": r"\colorfaGithub",
|
|
23
|
+
"twitter": r"\colorfaTwitter",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class RenderResult:
|
|
29
|
+
tex: str
|
|
30
|
+
files: dict[str, bytes] = field(default_factory=dict) # support files next to the .tex
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _environment() -> jinja2.Environment:
|
|
34
|
+
return jinja2.Environment(
|
|
35
|
+
loader=jinja2.PackageLoader("cv_tools", "templates/latex"),
|
|
36
|
+
autoescape=False,
|
|
37
|
+
undefined=jinja2.StrictUndefined,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def render(doc: Document) -> RenderResult:
|
|
42
|
+
date = date_latex(doc.date)
|
|
43
|
+
tex = _environment().get_template("cv.tex.jinja").render(
|
|
44
|
+
version=__version__,
|
|
45
|
+
name=to_latex(doc.name),
|
|
46
|
+
addresses=_address_order([contact_line(line, date) for line in doc.contact]),
|
|
47
|
+
sections=[_section(s) for s in doc.sections],
|
|
48
|
+
)
|
|
49
|
+
files = {name: (TEMPLATE_DIR / name).read_bytes() for name in SUPPORT_FILES}
|
|
50
|
+
return RenderResult(tex, files)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def date_latex(date: Text) -> str:
|
|
54
|
+
return AUTO_DATE if date == "auto" else to_latex(date)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _address_order(lines: list[str]) -> list[str]:
|
|
58
|
+
# resume.cls prints a third \address line first; emit it last so the
|
|
59
|
+
# lines appear in the order they were written.
|
|
60
|
+
if len(lines) == 3:
|
|
61
|
+
return [lines[1], lines[2], lines[0]]
|
|
62
|
+
return lines
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def contact_line(line: ContactLine, date: str) -> str:
|
|
66
|
+
"""One contact line; icons attach to the previous item with a space."""
|
|
67
|
+
if line.separator == DEFAULT_SEPARATOR:
|
|
68
|
+
separator = r" \\ " # \\ prints the class address separator inside \address
|
|
69
|
+
else:
|
|
70
|
+
separator = to_latex(line.separator)
|
|
71
|
+
out = ""
|
|
72
|
+
for i, item in enumerate(line.items):
|
|
73
|
+
if i:
|
|
74
|
+
out += " " if item.kind in ICON_KINDS else separator
|
|
75
|
+
out += contact_item(item, date)
|
|
76
|
+
return out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def contact_item(item: ContactItem, date: str) -> str:
|
|
80
|
+
if item.kind == "text":
|
|
81
|
+
return to_latex(item.value, date=date)
|
|
82
|
+
url = _url(item.value)
|
|
83
|
+
if item.kind == "url":
|
|
84
|
+
return rf"\url{{{url}}}"
|
|
85
|
+
if item.kind == "orcid":
|
|
86
|
+
return rf"\orcidicon{{{url}}}"
|
|
87
|
+
return rf"\href{{{url}}}{{{_ICON_MACROS[item.kind]}}}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _url(value: Text) -> str:
|
|
91
|
+
return to_latex(value) if not isinstance(value, str) else escape_url(value)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _opt(value: Optional[Text]) -> Optional[str]:
|
|
95
|
+
return None if value is None else to_latex(value)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _req(value: Optional[Text]) -> str:
|
|
99
|
+
return "" if value is None else to_latex(value)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _section(section: Section) -> dict:
|
|
103
|
+
if section.layout == "newpage":
|
|
104
|
+
return {"layout": "newpage"}
|
|
105
|
+
title = _req(section.title)
|
|
106
|
+
view = {
|
|
107
|
+
"layout": section.layout,
|
|
108
|
+
"title": title,
|
|
109
|
+
"comment": " ".join(title.split()), # a comment must stay on one line
|
|
110
|
+
"description": _opt(section.description),
|
|
111
|
+
"entries": [],
|
|
112
|
+
}
|
|
113
|
+
for e in section.entries:
|
|
114
|
+
entry = {
|
|
115
|
+
"title": _req(e.title),
|
|
116
|
+
"date": _req(e.date),
|
|
117
|
+
"subtitle": _req(e.subtitle),
|
|
118
|
+
"note": _req(e.note),
|
|
119
|
+
"location": _opt(e.location),
|
|
120
|
+
"text": _req(e.text),
|
|
121
|
+
"details": [to_latex(d) for d in e.details],
|
|
122
|
+
"bullets": [to_latex(b) for b in e.bullets],
|
|
123
|
+
}
|
|
124
|
+
if section.layout == "dated":
|
|
125
|
+
entry["month"], entry["year"] = dated_date(e.date)
|
|
126
|
+
entry["text"] = dated_text(e.text)
|
|
127
|
+
view["entries"].append(entry)
|
|
128
|
+
if section.layout == "publications":
|
|
129
|
+
view["citations"] = [citation(p) for p in section.publications]
|
|
130
|
+
return view
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def dated_date(value: Optional[Text]) -> tuple[str, str]:
|
|
134
|
+
return split_date(value, newline=CELL_NEWLINE)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def dated_text(value: Optional[Text]) -> str:
|
|
138
|
+
return "" if value is None else to_latex(value, newline=CELL_NEWLINE)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def citation(pub: Publication) -> str:
|
|
142
|
+
"""One publication in the v1 layout, its BibTeX fields inserted as written."""
|
|
143
|
+
names = [rf"\textbf{{{a.name}}}" if a.highlighted else a.name for a in pub.authors]
|
|
144
|
+
authors = ", ".join(names[:-1]) + " and " + names[-1] if len(names) > 1 else "".join(names)
|
|
145
|
+
parts = [
|
|
146
|
+
authors,
|
|
147
|
+
rf"\emph{{{pub.title}}}" if pub.title is not None else "",
|
|
148
|
+
pub.venue or "",
|
|
149
|
+
pub.year or "",
|
|
150
|
+
]
|
|
151
|
+
text = ", ".join(p for p in parts if p)
|
|
152
|
+
if pub.url:
|
|
153
|
+
text += rf" \href{{{escape_url(pub.url)}}}{{[Link]}}"
|
|
154
|
+
return text + "."
|