Skip to content

Commit 1d9436c

Browse files
authored
moderner-cv:0.1.2 (#1609)
1 parent 20a816b commit 1d9436c

File tree

6 files changed

+417
-0
lines changed

6 files changed

+417
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Pavel Zwerschke
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.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# moderner-cv
2+
3+
[![License][license-badge]][license]
4+
[![CI][ci-badge]][ci]
5+
[![Latest release][latest-release-badge]][typst-universe]
6+
7+
[license-badge]: https://img.shields.io/github/license/pavelzw/moderner-cv?style=flat-square
8+
[license]: ./LICENSE
9+
[ci-badge]: https://img.shields.io/github/actions/workflow/status/pavelzw/moderner-cv/ci.yml?style=flat-square
10+
[ci]: https://github.com/pavelzw/moderner-cv/actions/
11+
[latest-release-badge]: https://img.shields.io/github/v/tag/pavelzw/moderner-cv?style=flat-square&label=latest&sort=semver
12+
[typst-universe]: https://typst.app/universe/package/moderner-cv
13+
14+
This is a typst adaptation of LaTeX's [moderncv](https://github.com/moderncv/moderncv), a modern curriculum vitae class.
15+
16+
## Requirements
17+
18+
This template uses FontAwesome icons via the [fontawesome typst package](https://typst.app/universe/package/fontawesome).
19+
In order to properly use it, you need to have fontawesome installed on your system or have typst configured (via `--font-path`) to use the fontawesome font files.
20+
You can download fontawesome [here](https://fontawesome.com/download).
21+
22+
> [!TIP]
23+
> You can use typst in combination with [pixi](https://pixi.sh) to easily add fontawesome to your project and run it reproducibly anywhere.
24+
>
25+
> ```toml
26+
> [dependencies]
27+
> typst = ">=0.12.0"
28+
> typstyle = ">=0.12"
29+
> font-otf-fontawesome = "*"
30+
> ```
31+
32+
## Usage
33+
34+
```typst
35+
#import "@preview/moderner-cv:0.1.2": *
36+
37+
#show: moderner-cv.with(
38+
name: "Jane Doe",
39+
lang: "en",
40+
social: (
41+
42+
github: "jane-doe",
43+
linkedin: "jane-doe",
44+
// custom socials: (icon, link, body)
45+
// any fontawesome icon can be used: https://fontawesome.com/search
46+
website: ("link", "https://example.me", "example.me"),
47+
image-path: "/my-image.png",
48+
),
49+
)
50+
51+
// ...
52+
```
53+
54+
### Image
55+
56+
To add an image to your curriculum vitae, you can a path to that image to the `image-path` parameter (the path should be from the root of your project and start with a `/`). Here are the additional parameters:
57+
58+
- `image-height`: size of the image
59+
- `image-frame-stroke`: stroke of the frame. By default is 1pt + the main of the file. Can be any stroke value. Set to `none` to remove the frame.
60+
61+
## Examples
62+
63+
![Jane Doe's CV](assets/thumbnail.png)
64+
65+
## Building and Testing Locally
66+
67+
To build and test the template locally, you can run `pixi run watch` in the root of this repository.
68+
Please ensure to use the version of moderner-cv that is in this repository instead of the one on the typst universe by temporarily changing the import in [`cv.typ`](./template/cv.typ) to `#import "../lib.typ": *`.
Loading
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
#import "@preview/fontawesome:0.5.0": *
2+
3+
#let _cv-line(left, right, ..args) = {
4+
set block(below: 0pt)
5+
table(
6+
columns: (1fr, 5fr),
7+
stroke: none,
8+
..args.named(),
9+
left,
10+
right,
11+
)
12+
}
13+
#let moderncv-blue = rgb("#3973AF")
14+
15+
#let _header(
16+
title: [],
17+
subtitle: [],
18+
image-path: none,
19+
image-height: none,
20+
image-frame-stroke: auto,
21+
color: moderncv-blue,
22+
socials: (:),
23+
) = {
24+
let titleStack = stack(
25+
dir: ttb,
26+
spacing: 1em,
27+
text(size: 30pt, title),
28+
text(size: 20pt, subtitle),
29+
)
30+
31+
let social(icon, link_prefix, username) = [
32+
#fa-icon(icon) #link(link_prefix + username)[#username]
33+
]
34+
35+
let custom-social(icon, dest, body) = [
36+
#fa-icon(icon) #link(dest, body)
37+
]
38+
39+
let socialsDict = (
40+
// key: (faIcon, linkPrefix)
41+
phone: ("phone", "tel:"),
42+
email: ("envelope", "mailto:"),
43+
github: ("github", "https://github.com/"),
44+
linkedin: ("linkedin", "https://linkedin.com/in/"),
45+
x: ("x-twitter", "https://twitter.com/"),
46+
bluesky: ("bluesky", "https://bsky.app/profile/"),
47+
)
48+
49+
let socialsList = ()
50+
for entry in socials {
51+
assert(type(entry) == array, message: "Invalid social entry type.")
52+
assert(entry.len() == 2, message: "Invalid social entry length.")
53+
let (key, value) = entry
54+
if type(value) == str {
55+
if key not in socialsDict {
56+
panic("Unknown social key: " + key)
57+
}
58+
let (icon, linkPrefix) = socialsDict.at(key)
59+
socialsList.push(social(icon, linkPrefix, value))
60+
} else if type(value) == array {
61+
assert(value.len() == 3, message: "Invalid social entry: " + key)
62+
let (icon, dest, body) = value
63+
socialsList.push(custom-social(icon, dest, body))
64+
} else {
65+
panic("Invalid social entry: " + entry)
66+
}
67+
}
68+
69+
let socialStack = stack(
70+
dir: ttb,
71+
spacing: 0.5em,
72+
..socialsList,
73+
)
74+
75+
let imageStack = []
76+
77+
if image-path != none {
78+
let image = image(image-path, height: image-height)
79+
80+
let imageFramed = []
81+
82+
if image-frame-stroke == none {
83+
// no frame
84+
imageFramed = image
85+
} else {
86+
if image-frame-stroke == auto {
87+
// default stroke
88+
image-frame-stroke = 1pt + color
89+
} else {
90+
image-frame-stroke = stroke(image-frame-stroke)
91+
if image-frame-stroke.paint == auto {
92+
// use the main color by default
93+
// fields on stroke are not yet mutable
94+
image-frame-stroke = stroke((
95+
paint: color,
96+
thickness: image-frame-stroke.thickness,
97+
cap: image-frame-stroke.cap,
98+
join: image-frame-stroke.join,
99+
dash: image-frame-stroke.dash,
100+
miter-limit: image-frame-stroke.miter-limit,
101+
))
102+
}
103+
}
104+
imageFramed = rect(image, stroke: image-frame-stroke)
105+
}
106+
107+
imageStack = stack(
108+
dir: ltr,
109+
h(1em),
110+
imageFramed,
111+
)
112+
}
113+
114+
stack(
115+
dir: ltr,
116+
titleStack,
117+
align(
118+
right + top,
119+
socialStack,
120+
),
121+
imageStack,
122+
)
123+
}
124+
125+
#let moderner-cv(
126+
name: [],
127+
subtitle: [CV],
128+
social: (:),
129+
color: moderncv-blue,
130+
lang: "en",
131+
font: "New Computer Modern",
132+
image-path: none,
133+
image-height: 8em,
134+
image-frame-stroke: auto,
135+
paper: "a4",
136+
margin: (
137+
top: 10mm,
138+
bottom: 15mm,
139+
left: 15mm,
140+
right: 15mm,
141+
),
142+
show-footer: true,
143+
body,
144+
) = [
145+
#set page(
146+
paper: paper,
147+
margin: margin,
148+
)
149+
#set text(
150+
font: font,
151+
lang: lang,
152+
)
153+
154+
#show heading: it => {
155+
set text(weight: "regular")
156+
set text(color)
157+
set block(above: 0pt)
158+
_cv-line(
159+
[],
160+
[#it.body],
161+
)
162+
}
163+
#show heading.where(level: 1): it => {
164+
set text(weight: "regular")
165+
set text(color)
166+
_cv-line(
167+
align: horizon,
168+
[#box(fill: color, width: 28mm, height: 0.25em)],
169+
[#it.body],
170+
)
171+
}
172+
173+
#_header(
174+
title: name,
175+
subtitle: subtitle,
176+
image-path: image-path,
177+
image-height: image-height,
178+
image-frame-stroke: image-frame-stroke,
179+
color: color,
180+
socials: social,
181+
)
182+
183+
#body
184+
185+
#if show-footer [
186+
#v(1fr, weak: false)
187+
#name\
188+
#datetime.today().display("[month repr:long] [day], [year]")
189+
]
190+
]
191+
192+
#let cv-line(left-side, right-side) = {
193+
_cv-line(
194+
align(right, left-side),
195+
par(right-side, justify: true),
196+
)
197+
}
198+
199+
#let cv-entry(
200+
date: [],
201+
title: [],
202+
employer: [],
203+
..description,
204+
) = {
205+
let elements = (
206+
strong(title),
207+
emph(employer),
208+
..description.pos(),
209+
)
210+
cv-line(
211+
date,
212+
elements.join(", "),
213+
)
214+
}
215+
216+
#let cv-language(name: [], level: [], comment: []) = {
217+
_cv-line(
218+
align(right, name),
219+
stack(dir: ltr, level, align(right, emph(comment))),
220+
)
221+
}
222+
223+
#let cv-double-item(left-1, right-1, left-2, right-2) = {
224+
set block(below: 0pt)
225+
table(
226+
columns: (1fr, 2fr, 1fr, 2fr),
227+
stroke: none,
228+
align(right, left-1), right-1, align(right, left-2), right-2,
229+
)
230+
}
231+
232+
#let cv-list-item(item) = {
233+
_cv-line(
234+
[],
235+
list(item),
236+
)
237+
}
238+
239+
#let cv-list-double-item(item1, item2) = {
240+
set block(below: 0pt)
241+
table(
242+
columns: (1fr, 2.5fr, 2.5fr),
243+
stroke: none,
244+
[], list(item1), list(item2),
245+
)
246+
}

0 commit comments

Comments
 (0)