Compare commits
No commits in common. "1e6c9059e8bb9e0fad6e21f5d05315e9478ddd19" and "72d40e509447c8ebd8f8db5859b5b10221cf9e2a" have entirely different histories.
1e6c9059e8
...
72d40e5094
7 changed files with 327 additions and 529 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -51,6 +51,7 @@ dist/
|
||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
|
lib/
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
|
|
@ -61,7 +62,6 @@ share/python-wheels/
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
MANIFEST
|
MANIFEST
|
||||||
# lib/
|
|
||||||
|
|
||||||
# PyInstaller
|
# PyInstaller
|
||||||
# Usually these files are written by a python script from a template
|
# Usually these files are written by a python script from a template
|
||||||
|
|
|
||||||
21
content.yml
21
content.yml
|
|
@ -62,23 +62,6 @@ experience:
|
||||||
en: "Implementation of a 'scoping review': Comprehensive source research to the extent of 2000 candidates"
|
en: "Implementation of a 'scoping review': Comprehensive source research to the extent of 2000 candidates"
|
||||||
- de: Editorielle Vorbereitung eines Arbeitspapiers auf eine Veröffentlichung durch wissenschaftlichen Verlag
|
- de: Editorielle Vorbereitung eines Arbeitspapiers auf eine Veröffentlichung durch wissenschaftlichen Verlag
|
||||||
en: Editorial adaptation from a working paper towards a journal article ready for publishing
|
en: Editorial adaptation from a working paper towards a journal article ready for publishing
|
||||||
- date:
|
|
||||||
de: 2023--2024
|
|
||||||
en: 2023--2024
|
|
||||||
typeid: 2
|
|
||||||
title:
|
|
||||||
de: Eventlogistik, Eventverwaltung und Logistikkoordination
|
|
||||||
en: Event logistics, event management and logistics coordination
|
|
||||||
place:
|
|
||||||
de: Belantis & EmiR Entertainment
|
|
||||||
en: Belantis & EmiR Entertainment
|
|
||||||
bullets:
|
|
||||||
- de: Betreuung des Aufbaus von Publikumsevents zwischen 100 und 1000 Gästen
|
|
||||||
en: Event setup for public events accommodating between 100 and 1000 guests
|
|
||||||
- de: Durchführung von Logistikarbeiten zur Vorbereitung der Eventflächen
|
|
||||||
en: Execution of logistics tasks in preparation for the respective event areas
|
|
||||||
- de: Verlagerung und Platzierung von Dekorations- und funktionellen Elementen an verschiedenen Standorten
|
|
||||||
en: Relocation and placement of decorative and functinoal elements at various locations
|
|
||||||
- date:
|
- date:
|
||||||
de: 2023--2024
|
de: 2023--2024
|
||||||
en: 2023--2024
|
en: 2023--2024
|
||||||
|
|
@ -160,7 +143,7 @@ experience:
|
||||||
typeid: 1
|
typeid: 1
|
||||||
title:
|
title:
|
||||||
de: Redaktionsarbeit, Soziale Absicherung und Widerstandsfähigkeit
|
de: Redaktionsarbeit, Soziale Absicherung und Widerstandsfähigkeit
|
||||||
en: Editorial work, Social Protection and Resilience
|
en: Editorial work, Social Protection and Resilience, Roskilde University
|
||||||
place:
|
place:
|
||||||
de: Universität Roskilde
|
de: Universität Roskilde
|
||||||
en: Roskilde University
|
en: Roskilde University
|
||||||
|
|
@ -372,7 +355,7 @@ education:
|
||||||
en: HTWK Leipzig, Germany
|
en: HTWK Leipzig, Germany
|
||||||
title:
|
title:
|
||||||
de: Medieninformatik, BSc (nicht abg.)
|
de: Medieninformatik, BSc (nicht abg.)
|
||||||
en: Media Computer Science, BSc (incompl.)
|
en: Media Computer Science, BSc (not compl.)
|
||||||
date:
|
date:
|
||||||
de: 2015
|
de: 2015
|
||||||
en: 2015
|
en: 2015
|
||||||
|
|
|
||||||
160
cv.typ
160
cv.typ
|
|
@ -1,7 +1,157 @@
|
||||||
#import "lib/resume.typ": resume
|
#import "lib.typ": *
|
||||||
|
|
||||||
#resume.with(
|
#let cv(contents, use_sidebar: false) = {
|
||||||
main: ("experience", "education", "volunteering", "skills", "languages"),
|
show: style
|
||||||
sidebar:()
|
show: smartypants
|
||||||
)(yaml("content.yml"))
|
set text(lang: lang)
|
||||||
|
|
||||||
|
let date_formatting = {
|
||||||
|
if lang == "de" {
|
||||||
|
"[day]. [month repr:long] [year]"
|
||||||
|
} else {
|
||||||
|
"[month repr:long] [day], [year]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set page(
|
||||||
|
paper: "a4",
|
||||||
|
margin: (x: 0.9cm, y: 1.3cm),
|
||||||
|
footer: [
|
||||||
|
#set text(
|
||||||
|
fill: luma(200),
|
||||||
|
size: 8pt,
|
||||||
|
)
|
||||||
|
#_columns_3[
|
||||||
|
#smallcaps[#datetime.today().display(date_formatting)]
|
||||||
|
][
|
||||||
|
#smallcaps[#contents.about.fullname]
|
||||||
|
][
|
||||||
|
#context counter(page).display()
|
||||||
|
]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
set par(justify: true)
|
||||||
|
|
||||||
|
header(contents.about)
|
||||||
|
|
||||||
|
let body = {
|
||||||
|
if "summary" in contents {
|
||||||
|
section(
|
||||||
|
title: "",
|
||||||
|
{
|
||||||
|
contents.summary.at(lang)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if "experience" in contents {
|
||||||
|
let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang)
|
||||||
|
section(title: title, entries: contents.experience)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
if "education" in contents {
|
||||||
|
let title = (en: "Education", de: "Ausbildung").at(lang)
|
||||||
|
section(title: title, entries: contents.thesis + contents.education)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
if not use_sidebar {
|
||||||
|
if "volunteering" in contents {
|
||||||
|
let title = (en: "Volunteer Work", de: "Ehrenamt").at(lang)
|
||||||
|
section(title: title, entries: contents.volunteering)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
if "skills" in contents {
|
||||||
|
let title = (en: "Qualifications", de: "Qualifikationen").at(lang)
|
||||||
|
section(
|
||||||
|
title: title,
|
||||||
|
{
|
||||||
|
sidebar_entry(item: contents.skills)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if "languages" in contents {
|
||||||
|
let title = (en: "Languages", de: "Sprachen").at(lang)
|
||||||
|
section(
|
||||||
|
title: title,
|
||||||
|
{
|
||||||
|
sidebar_entry(item: contents.languages)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sidebar = {
|
||||||
|
if "volunteering" in contents {
|
||||||
|
let title = (en: "Volunteer Work", de: "Ehrenamt").at(lang)
|
||||||
|
[== #title]
|
||||||
|
for e in contents.volunteering {
|
||||||
|
[
|
||||||
|
- *#e.title.at(lang)* (#e.date.at(lang))
|
||||||
|
#par(e.bullets.at(0).at(lang)) \
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if "languages" in contents {
|
||||||
|
let title = (en: "Languages", de: "Sprachen").at(lang)
|
||||||
|
[== #title]
|
||||||
|
sidebar_entry(item: contents.languages, is_sidebar: true)
|
||||||
|
[\ ]
|
||||||
|
}
|
||||||
|
|
||||||
|
if "skills" in contents {
|
||||||
|
let title = (en: "Qualifications", de: "Kenntnisse").at(lang)
|
||||||
|
[== #title]
|
||||||
|
sidebar_entry(item: contents.skills, is_sidebar: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if not use_sidebar {
|
||||||
|
body
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let margin = 1pt
|
||||||
|
grid(
|
||||||
|
columns: (2fr, 1fr),
|
||||||
|
block(
|
||||||
|
outset: 0pt,
|
||||||
|
inset: (top: 0.4 * margin, right: 0pt, rest: margin),
|
||||||
|
stroke: none,
|
||||||
|
width: 100%,
|
||||||
|
{
|
||||||
|
set block(above: 10pt)
|
||||||
|
show heading.where(level: 1): it => style(s => {
|
||||||
|
let h = text(size: 18pt, upper(it))
|
||||||
|
let dim = measure(h, s)
|
||||||
|
stack(
|
||||||
|
dir: ltr,
|
||||||
|
h,
|
||||||
|
place(
|
||||||
|
dy: 7pt,
|
||||||
|
dx: 10pt,
|
||||||
|
horizon + left,
|
||||||
|
line(stroke: accent-color, length: 100% - dim.width - 10pt),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
body
|
||||||
|
},
|
||||||
|
),
|
||||||
|
{
|
||||||
|
v(20pt)
|
||||||
|
set block(inset: (left: 20 * margin, right: 20 * margin))
|
||||||
|
show heading: it => align(right, upper(it))
|
||||||
|
set list(marker: "")
|
||||||
|
show list: it => {
|
||||||
|
set par(justify: false)
|
||||||
|
align(right, block(it))
|
||||||
|
}
|
||||||
|
sidebar
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#cv.with(use_sidebar: false)(yaml("content.yml"))
|
||||||
|
|
||||||
|
|
|
||||||
162
lib/resume.typ
162
lib/resume.typ
|
|
@ -1,162 +0,0 @@
|
||||||
#import "lib.typ": *
|
|
||||||
#import "wrapit.typ": *
|
|
||||||
|
|
||||||
#let create_body(main: (), contents: (:)) = {
|
|
||||||
for item in main {
|
|
||||||
if item == "summary" and "summary" in contents {
|
|
||||||
section(
|
|
||||||
title: "",
|
|
||||||
{
|
|
||||||
contents.summary.at(lang)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if item == "experience_by_type" and "experience" in contents {
|
|
||||||
let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang)
|
|
||||||
section(title: title)[]
|
|
||||||
by_experience_type(experience: contents.experience, type: contents.experience_types)
|
|
||||||
}
|
|
||||||
if item == "experience_by_client" and "experience" in contents {
|
|
||||||
let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang)
|
|
||||||
section(title: title)[]
|
|
||||||
by_client(experience: contents.experience)
|
|
||||||
}
|
|
||||||
if item == "experience" and "experience" in contents {
|
|
||||||
let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang)
|
|
||||||
section(title: title, entries: contents.experience)[]
|
|
||||||
}
|
|
||||||
|
|
||||||
if item == "education" and "education" in contents {
|
|
||||||
let title = (en: "Education", de: "Ausbildung").at(lang)
|
|
||||||
section(title: title, entries: contents.thesis + contents.education)[]
|
|
||||||
}
|
|
||||||
|
|
||||||
if item == "volunteering" and "volunteering" in contents {
|
|
||||||
let title = (en: "Volunteer Work", de: "Ehrenamt").at(lang)
|
|
||||||
section(title: title, entries: contents.volunteering)[]
|
|
||||||
}
|
|
||||||
|
|
||||||
if item == "skills" and "skills" in contents {
|
|
||||||
let title = (en: "Qualifications", de: "Qualifikationen").at(lang)
|
|
||||||
section(
|
|
||||||
title: title,
|
|
||||||
{
|
|
||||||
sidebar_entry(item: contents.skills)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if item == "languages" and "languages" in contents {
|
|
||||||
let title = (en: "Languages", de: "Sprachen").at(lang)
|
|
||||||
section(
|
|
||||||
title: title,
|
|
||||||
{
|
|
||||||
sidebar_entry(item: contents.languages)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#let create_sidebar(sidebar: (), contents: (:)) = {
|
|
||||||
for item in sidebar {
|
|
||||||
if item == "volunteering" and "volunteering" in contents {
|
|
||||||
let title = (en: "Volunteer Work", de: "Ehrenamt").at(lang)
|
|
||||||
[== #title]
|
|
||||||
for e in contents.at("volunteering") {
|
|
||||||
[
|
|
||||||
- *#e.title.at(lang)* (#e.date.at(lang))
|
|
||||||
#par(e.bullets.at(0).at(lang)) \
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if item == "languages" and "languages" in contents {
|
|
||||||
let title = (en: "Languages", de: "Sprachen").at(lang)
|
|
||||||
[== #title]
|
|
||||||
sidebar_entry(item: contents.languages, is_sidebar: true)
|
|
||||||
[\ ]
|
|
||||||
}
|
|
||||||
|
|
||||||
if item == "skills" and "skills" in contents {
|
|
||||||
let title = (en: "Qualifications", de: "Kenntnisse").at(lang)
|
|
||||||
[== #title]
|
|
||||||
sidebar_entry(item: contents.skills, is_sidebar: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#let resume(contents, main: ("experience_by_type", "education"), sidebar: ("volunteering", "languages", "skills")) = {
|
|
||||||
show: style
|
|
||||||
set text(lang: lang)
|
|
||||||
|
|
||||||
let date_formatting = {
|
|
||||||
if lang == "de" {
|
|
||||||
"[day]. [month repr:long] [year]"
|
|
||||||
} else {
|
|
||||||
"[month repr:long] [day], [year]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set page(
|
|
||||||
paper: "a4",
|
|
||||||
margin: (x: 0.9cm, y: 1.3cm),
|
|
||||||
footer: [
|
|
||||||
#set text(
|
|
||||||
fill: luma(200),
|
|
||||||
size: 8pt,
|
|
||||||
)
|
|
||||||
#_columns_3[
|
|
||||||
#smallcaps[#datetime.today().display(date_formatting)]
|
|
||||||
][
|
|
||||||
#smallcaps[#contents.about.fullname]
|
|
||||||
][
|
|
||||||
#context counter(page).display()
|
|
||||||
]
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
set par(justify: true)
|
|
||||||
|
|
||||||
header(contents.about)
|
|
||||||
|
|
||||||
set block(above: 10pt)
|
|
||||||
show heading.where(level: 1): it => style(s => {
|
|
||||||
let h = text(size: 18pt, upper(it))
|
|
||||||
let dim = measure(h, s)
|
|
||||||
stack(
|
|
||||||
dir: ltr,
|
|
||||||
h,
|
|
||||||
place(
|
|
||||||
dy: 7pt,
|
|
||||||
dx: 10pt,
|
|
||||||
horizon + left,
|
|
||||||
line(stroke: accent-color, length: 100% - dim.width - 10pt),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
let margin = 1pt
|
|
||||||
let sb = if sidebar.len() > 0 {
|
|
||||||
block(
|
|
||||||
fill: luma(230),
|
|
||||||
inset: (top: 15 * margin, left: 10 * margin, right: 15 * margin, bottom: 15 * margin),
|
|
||||||
{
|
|
||||||
show heading: it => align(right, upper(it))
|
|
||||||
set list(marker: "")
|
|
||||||
show list: it => {
|
|
||||||
set par(justify: false)
|
|
||||||
align(right, block(it))
|
|
||||||
}
|
|
||||||
create_sidebar(sidebar: sidebar, contents: contents)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else { [] }
|
|
||||||
wrap-content(
|
|
||||||
sb,
|
|
||||||
create_body(main: main, contents: contents),
|
|
||||||
align: top + right,
|
|
||||||
columns: (auto, 30%),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
340
lib/wrapit.typ
340
lib/wrapit.typ
|
|
@ -1,340 +0,0 @@
|
||||||
// https://github.com/ntjess/wrap-it?tab=readme-ov-file
|
|
||||||
#let styled = text(red)[lorem].func()
|
|
||||||
|
|
||||||
#let _gridded(dir, fixed, to-wrap, ..kwargs) = {
|
|
||||||
let dir-kwargs = (:)
|
|
||||||
if dir not in (ltr, rtl) {
|
|
||||||
panic("Specify either `rtl` or `ltr` as the wrap direction")
|
|
||||||
}
|
|
||||||
let args = if dir == rtl {
|
|
||||||
(to-wrap, fixed)
|
|
||||||
} else {
|
|
||||||
(fixed, to-wrap)
|
|
||||||
}
|
|
||||||
grid(..args, columns: 2, rows: 2, column-gutter: 1em, ..kwargs)
|
|
||||||
}
|
|
||||||
|
|
||||||
#let _grid-height(content, container-size) = {
|
|
||||||
measure(box(width: container-size.width, content)).height
|
|
||||||
}
|
|
||||||
|
|
||||||
#let _get-chunk(words, end, reverse, start: 0) = {
|
|
||||||
if end < 0 {
|
|
||||||
return words.join(" ")
|
|
||||||
}
|
|
||||||
if reverse {
|
|
||||||
words = words.rev()
|
|
||||||
}
|
|
||||||
let subset = words.slice(start, end)
|
|
||||||
if reverse {
|
|
||||||
subset = subset.rev()
|
|
||||||
}
|
|
||||||
subset.join(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
#let _get-wrap-index(height-func, words, goal-height, reverse) = {
|
|
||||||
for index in range(1, words.len(), step: 1) {
|
|
||||||
let cur-height = height-func(_get-chunk(words, index, reverse))
|
|
||||||
if cur-height > goal-height {
|
|
||||||
return index - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
#let _rewrap(element, new-content) = {
|
|
||||||
let fields = element.fields()
|
|
||||||
for key in ("body", "text", "children", "child") {
|
|
||||||
if key in fields {
|
|
||||||
let _ = fields.remove(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let positional = (new-content,)
|
|
||||||
if "styles" in fields {
|
|
||||||
positional.push(fields.remove("styles"))
|
|
||||||
}
|
|
||||||
element.func()(..fields, ..positional)
|
|
||||||
}
|
|
||||||
|
|
||||||
#let split-other(body, height-func, goal-height, align, splitter-func) = {
|
|
||||||
(wrapped: none, rest: body)
|
|
||||||
}
|
|
||||||
|
|
||||||
#let split-has-text(body, height-func, goal-height, align, splitter-func) = {
|
|
||||||
let words = body.text.split(" ")
|
|
||||||
let reverse = align.y == bottom
|
|
||||||
let wrap-index = _get-wrap-index(height-func, words, goal-height, reverse)
|
|
||||||
let _rewrap = _rewrap.with(body)
|
|
||||||
if wrap-index > 0 {
|
|
||||||
let chunk = _rewrap(_get-chunk(words, wrap-index, reverse))
|
|
||||||
let end-chunk = _rewrap(_get-chunk(words, words.len(), reverse, start: wrap-index))
|
|
||||||
(
|
|
||||||
wrapped: context {
|
|
||||||
chunk
|
|
||||||
linebreak(justify: par.justify)
|
|
||||||
},
|
|
||||||
rest: end-chunk,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(wrapped: none, rest: body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#let split-has-children(body, height-func, goal-height, align, splitter-func) = {
|
|
||||||
let reverse = align.y == bottom
|
|
||||||
let children = if reverse {
|
|
||||||
body.children.rev()
|
|
||||||
} else {
|
|
||||||
body.children
|
|
||||||
}
|
|
||||||
for (ii, child) in children.enumerate() {
|
|
||||||
let prev-children = children.slice(0, ii).join()
|
|
||||||
let new-height-func(child) = {
|
|
||||||
height-func((prev-children, child).join())
|
|
||||||
}
|
|
||||||
let height = new-height-func(child)
|
|
||||||
if height <= goal-height {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// height func calculator should now account for prior children
|
|
||||||
let split = splitter-func(child, new-height-func, goal-height, align)
|
|
||||||
let new-children = (..children.slice(0, ii), split.wrapped)
|
|
||||||
let new-rest = children.slice(ii + 1)
|
|
||||||
if split.rest != none {
|
|
||||||
new-rest.insert(0, split.rest)
|
|
||||||
}
|
|
||||||
if reverse {
|
|
||||||
new-children = new-children.rev()
|
|
||||||
new-rest = new-rest.rev()
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
wrapped: _rewrap(body, new-children),
|
|
||||||
rest: _rewrap(body, new-rest),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
panic("This function should only be called if the seq child should be split")
|
|
||||||
}
|
|
||||||
|
|
||||||
#let split-has-body(body, height-func, goal-height, align, splitter-func) = {
|
|
||||||
// Elements that can be split and have a 'body' field.
|
|
||||||
let splittable = (strong, emph, underline, stroke, overline, highlight, list.item, styled)
|
|
||||||
|
|
||||||
let new-height-func(content) = {
|
|
||||||
height-func(_rewrap(body, content))
|
|
||||||
}
|
|
||||||
let args = (new-height-func, goal-height, align, splitter-func)
|
|
||||||
let body-text = body.at("body", default: body.at("child", default: none))
|
|
||||||
if body.func() in splittable {
|
|
||||||
let result = splitter-func(body-text, new-height-func, goal-height, align)
|
|
||||||
if result.wrapped != none {
|
|
||||||
return (wrapped: _rewrap(body, result.wrapped), rest: _rewrap(body, result.rest))
|
|
||||||
} else {
|
|
||||||
return split-other(body, ..args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Shape doesn't split nicely, so treat it as unwrappable
|
|
||||||
return split-other(body, ..args)
|
|
||||||
}
|
|
||||||
|
|
||||||
#let splitter(body, height-func, goal-height, align) = {
|
|
||||||
let self-height = height-func(body)
|
|
||||||
if self-height <= goal-height {
|
|
||||||
return (wrapped: body, rest: none)
|
|
||||||
}
|
|
||||||
if type(body) == str {
|
|
||||||
body = text(body)
|
|
||||||
}
|
|
||||||
let body-splitter = if body.has("text") {
|
|
||||||
split-has-text
|
|
||||||
} else if body.has("body") or body.has("child") {
|
|
||||||
split-has-body
|
|
||||||
} else if body.has("children") {
|
|
||||||
split-has-children
|
|
||||||
} else {
|
|
||||||
split-other
|
|
||||||
}
|
|
||||||
return body-splitter(body, height-func, goal-height, align, splitter)
|
|
||||||
}
|
|
||||||
|
|
||||||
#let _inner-wrap-content(to-wrap, y-align, grid-func, container-size, ..grid-kwargs) = {
|
|
||||||
let height-func(txt) = _grid-height(grid-func(txt), container-size)
|
|
||||||
let goal-height = height-func([])
|
|
||||||
if y-align == top {
|
|
||||||
goal-height += measure(v(1em)).height
|
|
||||||
}
|
|
||||||
let result = splitter(to-wrap, height-func, goal-height, y-align)
|
|
||||||
if y-align == top {
|
|
||||||
grid-func(result.wrapped)
|
|
||||||
result.rest
|
|
||||||
} else {
|
|
||||||
result.rest
|
|
||||||
grid-func(result.wrapped)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Places `to-wrap` next to `fixed`, wrapping `to-wrap` as its height overflows `fixed`.
|
|
||||||
///
|
|
||||||
/// *Basic Use:*
|
|
||||||
/// ```typ
|
|
||||||
/// #let body = lorem(40)
|
|
||||||
/// #wrap-content(rect(fill: teal), body)
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// *Something More Fun:*
|
|
||||||
/// ```typ
|
|
||||||
/// #set par(justify: true)
|
|
||||||
/// // Helpers; not required
|
|
||||||
/// #let grad(map) = {
|
|
||||||
/// gradient.linear(
|
|
||||||
/// ..eval("color.map." + map)
|
|
||||||
/// )
|
|
||||||
/// }
|
|
||||||
/// #let make-fig(fill) = {
|
|
||||||
/// set figure.caption(separator: "")
|
|
||||||
/// fill = grad(fill)
|
|
||||||
/// figure(
|
|
||||||
/// rect(fill: fill, radius: 0.5em),
|
|
||||||
/// caption: [],
|
|
||||||
/// )
|
|
||||||
/// }
|
|
||||||
/// #let (fig1, fig2) = {
|
|
||||||
/// ("viridis", "plasma").map(make-fig)
|
|
||||||
/// }
|
|
||||||
/// #wrap-content(fig1, body, align: right)
|
|
||||||
/// #wrap-content(fig2, [#body #body], align: bottom)
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Note that you can increase the distance between a figure's bottom and the wrapped
|
|
||||||
/// text by boxing it with an inset:
|
|
||||||
/// ```typ
|
|
||||||
/// #let spaced = box(
|
|
||||||
/// make-fig("rocket"),
|
|
||||||
/// inset: (bottom: 0.3em)
|
|
||||||
/// )
|
|
||||||
/// #wrap-content(spaced, body)
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// - fixed (content): Content that will not be wrapped, (i.e., a figure).
|
|
||||||
///
|
|
||||||
/// - to-wrap (content): Content that will be wrapped, (i.e., text). Currently, logic
|
|
||||||
/// works best with pure-text content, but hypothetically will work with any `content`.
|
|
||||||
///
|
|
||||||
/// - align (alignment): Alignment of `fixed` relative to `to-wrap`. `top` will align
|
|
||||||
/// the top of `fixed` with the top of `to-wrap`, and `bottom` will align the bottom of
|
|
||||||
/// `fixed` with the bottom of `to-wrap`. `left` and `right` alignments determine
|
|
||||||
/// horizontal alignment of `fixed` relative to `to-wrap`. Alignments can be combined,
|
|
||||||
/// i.e., `bottom + right` will align the bottom-right corner of `fixed` with the
|
|
||||||
/// bottom-right corner of `to-wrap`.
|
|
||||||
/// ```typ
|
|
||||||
/// #wrap-content(
|
|
||||||
/// make-fig("turbo"),
|
|
||||||
/// body,
|
|
||||||
/// align: bottom + right
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// - size (size, auto): Size of the wrapping container. If `auto`, this will be set to
|
|
||||||
/// the current container size. Otherwise, wrapping logic will attempt to stay within
|
|
||||||
/// the provided constraints.
|
|
||||||
///
|
|
||||||
/// - ..grid-kwargs (any): Keyword arguments to pass to the underlying `grid` function.
|
|
||||||
/// Of note:
|
|
||||||
/// - `column-gutter` controls horizontal margin between `fixed` and `to-wrap`. Or,
|
|
||||||
/// you can surround the fixed content in a box with `(inset: ...)` for more
|
|
||||||
/// fine-grained control.
|
|
||||||
/// - `columns` can be set to force sizing of `fixed` and `to-wrap`. For instance,
|
|
||||||
/// `columns: (50%, 50%)` will force `fixed` and `to-wrap` to each take up half
|
|
||||||
/// of the available space. If content isn't this big, the fill will be blank
|
|
||||||
/// margin.
|
|
||||||
/// ```typ
|
|
||||||
/// #let spaced = box(
|
|
||||||
/// make-fig("mako"), inset: 0.5em
|
|
||||||
/// )
|
|
||||||
/// #wrap-content(spaced, body)
|
|
||||||
/// ```
|
|
||||||
/// ```typ
|
|
||||||
/// #wrap-content(
|
|
||||||
/// make-fig("spectral"),
|
|
||||||
/// body,
|
|
||||||
/// align: bottom,
|
|
||||||
/// columns: (50%, 50%),
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
#let wrap-content(
|
|
||||||
fixed,
|
|
||||||
to-wrap,
|
|
||||||
align: top + left,
|
|
||||||
size: auto,
|
|
||||||
..grid-kwargs,
|
|
||||||
) = {
|
|
||||||
if center in (align.x, align.y) {
|
|
||||||
panic("Center alignment is not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
// "none" x alignment defaults to left
|
|
||||||
let dir = if align.x == right {
|
|
||||||
rtl
|
|
||||||
} else {
|
|
||||||
ltr
|
|
||||||
}
|
|
||||||
let gridded(..args) = box(_gridded(dir, fixed, ..grid-kwargs, ..args))
|
|
||||||
// "none" y alignment defaults to top
|
|
||||||
let y-align = if align.y == bottom {
|
|
||||||
bottom
|
|
||||||
} else {
|
|
||||||
top
|
|
||||||
}
|
|
||||||
|
|
||||||
if size != auto {
|
|
||||||
_inner-wrap-content(to-wrap, y-align, gridded, size, ..grid-kwargs)
|
|
||||||
} else {
|
|
||||||
layout(container-size => {
|
|
||||||
_inner-wrap-content(to-wrap, y-align, gridded, container-size, ..grid-kwargs)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrap a body of text around two pieces of content. The logic only works if enough text
|
|
||||||
/// exists to overflow both the top and bottom content. Use this instead of 2 separate
|
|
||||||
/// `wrap-content` calls if you want to avoid a paragraph break between the top and bottom
|
|
||||||
/// content.
|
|
||||||
///
|
|
||||||
/// *Example:*
|
|
||||||
/// ```typ
|
|
||||||
/// #let fig1 = make-fig("inferno")
|
|
||||||
/// #let fig2 = make-fig("rainbow")
|
|
||||||
/// #wrap-top-bottom(fig1, fig2, lorem(60))
|
|
||||||
/// ```
|
|
||||||
/// - top-fixed (content): Content that will not be wrapped, (i.e., a figure).
|
|
||||||
/// - bottom-fixed (content): Content that will not be wrapped, (i.e., a figure).
|
|
||||||
/// - body (content): Content that will be wrapped, (i.e., text)
|
|
||||||
/// - top-kwargs (any): Keyword arguments to pass to the underlying `wrap-content` function
|
|
||||||
/// for the top content. `x` alignment is kept (left/right), but `y` alignment is
|
|
||||||
/// overridden to `top`.
|
|
||||||
/// - bottom-kwargs (any): Keyword arguments to pass to the underlying `wrap-content` function
|
|
||||||
/// for the bottom content. `x` alignment is kept (left/right), but `y` alignment is
|
|
||||||
/// overridden to `bottom`.
|
|
||||||
///
|
|
||||||
#let wrap-top-bottom(
|
|
||||||
top-fixed,
|
|
||||||
bottom-fixed,
|
|
||||||
body,
|
|
||||||
top-kwargs: (:),
|
|
||||||
bottom-kwargs: (:),
|
|
||||||
) = {
|
|
||||||
top-kwargs = top-kwargs + (
|
|
||||||
align: top-kwargs.at("align", default: top + left).x + top,
|
|
||||||
)
|
|
||||||
bottom-kwargs = bottom-kwargs + (
|
|
||||||
align: bottom-kwargs.at("align", default: bottom + right).x + bottom,
|
|
||||||
)
|
|
||||||
layout(size => {
|
|
||||||
let wrapfig(..args) = wrap-content(size: size, ..args)
|
|
||||||
wrapfig(top-fixed, ..top-kwargs)[
|
|
||||||
#wrapfig(bottom-fixed, ..bottom-kwargs)[
|
|
||||||
#body
|
|
||||||
]
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
171
resume.typ
171
resume.typ
|
|
@ -1,3 +1,170 @@
|
||||||
#import "lib/resume.typ": resume
|
#import "lib.typ": *
|
||||||
|
|
||||||
#resume.with()(yaml("content.yml"))
|
#let resume(contents, main: ("experience_by_type", "education"), sidebar: ("volunteering", "languages", "skills")) = {
|
||||||
|
show: style
|
||||||
|
set text(lang: lang)
|
||||||
|
|
||||||
|
let date_formatting = {
|
||||||
|
if lang == "de" {
|
||||||
|
"[day]. [month repr:long] [year]"
|
||||||
|
} else {
|
||||||
|
"[month repr:long] [day], [year]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set page(
|
||||||
|
paper: "a4",
|
||||||
|
margin: (x: 0.9cm, y: 1.3cm),
|
||||||
|
footer: [
|
||||||
|
#set text(
|
||||||
|
fill: luma(200),
|
||||||
|
size: 8pt,
|
||||||
|
)
|
||||||
|
#_columns_3[
|
||||||
|
#smallcaps[#datetime.today().display(date_formatting)]
|
||||||
|
][
|
||||||
|
#smallcaps[#contents.about.fullname]
|
||||||
|
][
|
||||||
|
#context counter(page).display()
|
||||||
|
]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
set par(justify: true)
|
||||||
|
|
||||||
|
header(contents.about)
|
||||||
|
|
||||||
|
let body = {
|
||||||
|
for item in main {
|
||||||
|
if item == "summary" and "summary" in contents {
|
||||||
|
section(
|
||||||
|
title: "",
|
||||||
|
{
|
||||||
|
contents.summary.at(lang)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if item == "experience_by_type" and "experience" in contents {
|
||||||
|
let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang)
|
||||||
|
section(title: title)[]
|
||||||
|
by_experience_type(experience: contents.experience, type: contents.experience_types)
|
||||||
|
}
|
||||||
|
if item == "experience_by_client" and "experience" in contents {
|
||||||
|
let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang)
|
||||||
|
section(title: title)[]
|
||||||
|
by_client(experience: contents.experience)
|
||||||
|
}
|
||||||
|
if item == "experience" and "experience" in contents {
|
||||||
|
let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang)
|
||||||
|
section(title: title, entries: contents.experience)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
if item == "education" and "education" in contents {
|
||||||
|
let title = (en: "Education", de: "Ausbildung").at(lang)
|
||||||
|
section(title: title, entries: contents.thesis + contents.education)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
if item == "volunteering" and "volunteering" in contents {
|
||||||
|
let title = (en: "Volunteer Work", de: "Ehrenamt").at(lang)
|
||||||
|
section(title: title, entries: contents.volunteering)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
if item == "skills" and "skills" in contents {
|
||||||
|
let title = (en: "Qualifications", de: "Qualifikationen").at(lang)
|
||||||
|
section(
|
||||||
|
title: title,
|
||||||
|
{
|
||||||
|
sidebar_entry(item: contents.skills)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if item == "languages" and "languages" in contents {
|
||||||
|
let title = (en: "Languages", de: "Sprachen").at(lang)
|
||||||
|
section(
|
||||||
|
title: title,
|
||||||
|
{
|
||||||
|
sidebar_entry(item: contents.languages)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sidebar = {
|
||||||
|
for item in sidebar {
|
||||||
|
if item == "volunteering" and "volunteering" in contents {
|
||||||
|
let title = (en: "Volunteer Work", de: "Ehrenamt").at(lang)
|
||||||
|
[== #title]
|
||||||
|
for e in contents.at("volunteering") {
|
||||||
|
[
|
||||||
|
- *#e.title.at(lang)* (#e.date.at(lang))
|
||||||
|
#par(e.bullets.at(0).at(lang)) \
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if item == "languages" and "languages" in contents {
|
||||||
|
let title = (en: "Languages", de: "Sprachen").at(lang)
|
||||||
|
[== #title]
|
||||||
|
sidebar_entry(item: contents.languages, is_sidebar: true)
|
||||||
|
[\ ]
|
||||||
|
}
|
||||||
|
|
||||||
|
if item == "skills" and "skills" in contents {
|
||||||
|
let title = (en: "Qualifications", de: "Kenntnisse").at(lang)
|
||||||
|
[== #title]
|
||||||
|
sidebar_entry(item: contents.skills, is_sidebar: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let margin = 1pt
|
||||||
|
grid(
|
||||||
|
columns: (2fr, 1fr),
|
||||||
|
block(
|
||||||
|
outset: 0pt,
|
||||||
|
inset: (top: 0.4 * margin, right: 0pt, rest: margin),
|
||||||
|
stroke: none,
|
||||||
|
width: 100%,
|
||||||
|
{
|
||||||
|
set block(above: 10pt)
|
||||||
|
show heading.where(level: 1): it => style(s => {
|
||||||
|
let h = text(size: 18pt, upper(it))
|
||||||
|
let dim = measure(h, s)
|
||||||
|
stack(
|
||||||
|
dir: ltr,
|
||||||
|
h,
|
||||||
|
place(
|
||||||
|
dy: 7pt,
|
||||||
|
dx: 10pt,
|
||||||
|
horizon + left,
|
||||||
|
line(stroke: accent-color, length: 100% - dim.width - 10pt),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
body
|
||||||
|
},
|
||||||
|
),
|
||||||
|
align(
|
||||||
|
right,
|
||||||
|
block(
|
||||||
|
width: 90%,
|
||||||
|
{
|
||||||
|
v(15pt)
|
||||||
|
set block(inset: (left: 5 * margin, right: 5 * margin))
|
||||||
|
show heading: it => align(right, upper(it))
|
||||||
|
set list(marker: "")
|
||||||
|
show list: it => {
|
||||||
|
set par(justify: false)
|
||||||
|
align(right, block(it))
|
||||||
|
}
|
||||||
|
sidebar
|
||||||
|
v(15pt)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#resume(yaml("content.yml"))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue