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.
Files changed (46) hide show
  1. cv_tools/__init__.py +7 -0
  2. cv_tools/__main__.py +5 -0
  3. cv_tools/api.py +123 -0
  4. cv_tools/bib.py +122 -0
  5. cv_tools/charset.py +142 -0
  6. cv_tools/cli.py +384 -0
  7. cv_tools/doctor.py +185 -0
  8. cv_tools/engine.py +320 -0
  9. cv_tools/loader.py +424 -0
  10. cv_tools/markup.py +614 -0
  11. cv_tools/migrate.py +222 -0
  12. cv_tools/model.py +192 -0
  13. cv_tools/render/__init__.py +1 -0
  14. cv_tools/render/html.py +581 -0
  15. cv_tools/render/latex.py +154 -0
  16. cv_tools/schema/cv-v2.schema.json +707 -0
  17. cv_tools/starter/cv.yaml +129 -0
  18. cv_tools/starter/publications.bib +55 -0
  19. cv_tools/templates/html/cv.css +252 -0
  20. cv_tools/templates/html/cv.html.jinja +19 -0
  21. cv_tools/templates/html/icons/github.svg +1 -0
  22. cv_tools/templates/html/icons/linkedin.svg +1 -0
  23. cv_tools/templates/html/icons/orcid.svg +1 -0
  24. cv_tools/templates/html/icons/twitter.svg +1 -0
  25. cv_tools/templates/html/macros.jinja +89 -0
  26. cv_tools/templates/latex/cv.tex.jinja +45 -0
  27. cv_tools/templates/latex/layouts/dated.jinja +12 -0
  28. cv_tools/templates/latex/layouts/entries-inline.jinja +23 -0
  29. cv_tools/templates/latex/layouts/entries.jinja +29 -0
  30. cv_tools/templates/latex/layouts/list.jinja +24 -0
  31. cv_tools/templates/latex/layouts/pairs.jinja +14 -0
  32. cv_tools/templates/latex/layouts/publications.jinja +14 -0
  33. cv_tools/templates/latex/layouts/text.jinja +10 -0
  34. cv_tools/templates/latex/orcid.pdf +0 -0
  35. cv_tools/templates/latex/packages.txt +47 -0
  36. cv_tools/templates/latex/resume.cls +265 -0
  37. cv_tools/templates/latex/unicode.txt +398 -0
  38. cv_tools/v1.py +484 -0
  39. cv_tools/validate.py +165 -0
  40. cv_tools/watch.py +76 -0
  41. cv_tools_cli-2.0.0.dist-info/METADATA +498 -0
  42. cv_tools_cli-2.0.0.dist-info/RECORD +46 -0
  43. cv_tools_cli-2.0.0.dist-info/WHEEL +5 -0
  44. cv_tools_cli-2.0.0.dist-info/entry_points.txt +2 -0
  45. cv_tools_cli-2.0.0.dist-info/licenses/LICENSE +21 -0
  46. cv_tools_cli-2.0.0.dist-info/top_level.txt +1 -0
@@ -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 + "."