Compare commits

..

No commits in common. "main" and "2025-02-05" have entirely different histories.

8 changed files with 416 additions and 725 deletions

2
.gitignore vendored
View file

@ -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

View file

@ -18,54 +18,28 @@ Is called like the following:
``` ```
This is the default invocation, though sidebar and main body sections can be exchanged at will. This is the default invocation, though sidebar and main body sections can be exchanged at will.
The following sections currently exist:
- education TODO:
- experience, subdivided in "experience", "experience_by_client" and "experience_by_type"
- languages
- skills
- summary
- volunteering
Sections in the main body or sidebar can be reordered at will. - [x] move double-resume sources (per langauge) to a variable or similar
- [x] separate volunteering from skills section
- [_] one function per skill section
- [_] resume prep:
- [ ] make experience groupable by client / short version
- [ ] enable display/hiding of sections/entries by tags?
Any individual entry can be hidden by adding a simple `hidden: true` to the relevant YAML entry - [x] migrate to typst template?
(works for all sections). - [x] ! Fix german summary to be like English summary
- [_] generalized entry?
Would have 'title', 'place', 'date', 'type'/'tags' + potential
'publication', 'bullets'
A simple example education entry: ## Typst-driven branch
```yaml - [x] Fix publication '\&'s
education: - [x] Fix en-dash/em-dash (e.g. in years)
- place: - [x] Producable from yaml content
de: Universität der Welt, Planet Erde - [x] Can be switched between Ger/En
en: University of the World, Planet Earth - [x] Try sidebar version
title: - [ ] Generalize sidebar version through abstraction/extractions
de: Globale Friedensstiftung, MA - [ ] unify items: experience/education/(thesis?)
en: Global Peace Foundations, MA
date:
de: 2016
en: 2016
hidden: false
```
## Advanced experience presentation settings
The experience section has 3 forms:
Purely chronological ("experience"), which is the default;
separated by client worked for ("experience_by_client");
separated by the type of work undertaken, then further separated by client worked for ("experience_by_client").
These options are intended especially for self-employed / entrepreneurial CVs.
They let you subdivide your work experience whichever way works best,
and additionally divide work undertaken for your own employ or salaried positions for example.
## Data-driven uses
Since the CV _data_ is separated from the CV _presentation_, it is possible to create different
presentations for the same underlying data structures.
One fun example is my self-presentation as a Linux [manual page](https://martyoeh.me/blog/2025-05-11-manpage-resume).
Other, more reasonable, uses are encouraged, such as presentation embedded within an HTML page,
different print and screen-reading documents,
or short calling cards.

View file

@ -1,7 +1,7 @@
about: about:
fullname: Marty Oehme fullname: Marty Oehme
contact: contact:
- text: Berlin, Germany - text: Pichelsdorfer Str. 133, 13595 Berlin, Germany
icon: icon:
- text: contact@martyoeh.me - text: contact@martyoeh.me
icon: icon:
@ -20,23 +20,19 @@ summary:
de: | de: |
Seit dem Abschluss des EU-geförderten European Master of Global Studies Erasmus Mundus Programms im Jahr 2021 habe ich als selbstständiger Forschungsberater gearbeitet und mich auf Prozesse der Ungleichheit und der Armutsreduktion, sowie deren räumliche Dimensionierung und kollektive Organisation konzentriert, hauptsächlich betrachtet durch das Prisma der Arbeitsmarktpolitik. Seit dem Abschluss des EU-geförderten European Master of Global Studies Erasmus Mundus Programms im Jahr 2021 habe ich als selbstständiger Forschungsberater gearbeitet und mich auf Prozesse der Ungleichheit und der Armutsreduktion, sowie deren räumliche Dimensionierung und kollektive Organisation konzentriert, hauptsächlich betrachtet durch das Prisma der Arbeitsmarktpolitik.
Durch die Erstellung der entwickelnden Forschungsliteratur habe ich ein breites Spektrum an Fähigkeiten im Bereich der Datenerfassung, Organisation und Visualisierung entwickelt, sowie Erfahrungen in Manuskriptbearbeitung und Referenzmanagement erworben. Zusätzlich habe ich Einblicke in Event Management, Lehrassistentenarbeit, Content Creation, Systemadministration und Website-Management gewonnen. In all meinen Tätigkeiten bemühe ich mich, die Entwicklung sowohl freier und offener Software als auch einer offenen Wissenschaft ohne Barrieren zu fördern. Durch die Erstellung der entwickelnden Forschungsliteratur habe ich ein breites Spektrum an Fähigkeiten im Bereich der Datenerfassung, Organisation und Visualisierung entwickelt, sowie Erfahrungen in Manuskriptbearbeitung und Referenzmanagement erworben. Zusätzlich habe ich Einblicke in Event Management, Lehrassistentenarbeit, Content Creation, Systemadministration und Website-Management gewonnen. Jenseits meines beruflichen Engagements fördere ich den Aufbau freier und offener Software und Offener Wissenschaft ohne Barrieren.
Neben meines beruflichen Werdegangs engagiere ich mich ehrenamtlich in der technischen Bildung älterer Menschen und Kinder, sowie bei der Erstellung und Pflege einer Linux-basierten Hosting-Umgebung um anderen eine Plattform zur Erkundung des Nutzens offener Software zu ermöglichen. Ich bin offen für Gelegenheiten, die meine Expertise in diesen Bereichen vertiefen, jedoch auch für solche, die meine Kenntnisbereiche erweitern.
Ich begrüße Gelegenheiten, meine Expertise in diesen Bereichen zu erweitern, jedoch auch solche, die meine Kenntnisbereiche weiter vergrößern.
en: | en: |
Since completing the EU-funded European Master of Global Studies Erasmus Mundus programme in 2021, I have acted as a research consultant focused on processes of inequality and poverty reduction, spatialization and collective organization, primarily through the lens of labour market policies. Since completing the EU-funded European Master of Global Studies Erasmus Mundus programme in 2021, I have acted as a research consultant focused on processes of inequality and poverty reduction, spatialization and collective organization, primarily through the lens of labour market policies.
Through producing the development research literature I have developed a broad range of skills in data acquisition, analysis and visualization, as well as manuscript editing and reference management. Additionally, I have gained insights into event management, teaching assistance, content creation, system and web administration. In all my work I strive to encourage the development both of free and open software and open science without barriers. Through producing the development research literature I have developed a broad range of skills in data acquisition, organization and visualization, as well as manuscript editing and reference management. Additionally, I have gained insights into event management, teaching assistance, content creation, system and web administration. Beyond professional work I strive to foster the development of free and open software and open science without barriers.
Aside from professional work, I have been engaging in technical education for elderly people and children on a voluntary basis, as well as creating and maintaining a Linux based hosting environment for other people to make use of an open software platform. I welcome opportunities to advance my expertise in these topics, in addition to those expanding my range of applicable skills.
I welcome opportunities to advance my expertise in these topics, in addition to those further expanding my range of applicable skills.
experience_types: experience_types:
1: 1:
de: Selbstständiger Consultant Forschung de: Selbstständiger Schriftsteller Forschung
en: Independent research consultant en: Independent research consultant
2: 2:
de: Honorararbeit de: Honorararbeit
@ -64,25 +60,8 @@ experience:
en: Quantitative analysis of connections between social protection and formalization en: Quantitative analysis of connections between social protection and formalization
- de: "Durchführung eines 'Scoping Review': Umfassende Quellenrecherche im Umfang von 2000 Überprüfungskandidaten" - de: "Durchführung eines 'Scoping Review': Umfassende Quellenrecherche im Umfang von 2000 Überprüfungskandidaten"
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 zur 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 functional elements at various locations
- date: - date:
de: 2023--2024 de: 2023--2024
en: 2023--2024 en: 2023--2024
@ -131,8 +110,8 @@ experience:
de: UNU-WIDER de: UNU-WIDER
en: UNU-WIDER en: UNU-WIDER
publication: publication:
de: "Morabito, C., Niño-Zarazúa, M., Coordination: David, A. and Yasser, R. (2025). Assessing the Distributional Impacts of Development Interventions - the Inequality Marker. Éditions AFD." de: Niño-Zarazúa, M., \& Morabito, C. (angenommen). Assessing the potential distributional impacts of development interventions. UNU-WIDER.
en: "Morabito, C., Niño-Zarazúa, M., Coordination: David, A. and Yasser, R. (2025). Assessing the Distributional Impacts of Development Interventions - the Inequality Marker. Éditions AFD." en: Niño-Zarazúa, M., \& Morabito, C. (forthcoming). Assessing the potential distributional impacts of development interventions. UNU-WIDER.
bullets: # TODO: add numerical description of quantity of data (>2mil. datapoints) bullets: # TODO: add numerical description of quantity of data (>2mil. datapoints)
- de: Sammlung, Verarbeitung und Bereinigung von 4 quantitativen Datensätzen, u.a. der UN Ungleichheitstrends - de: Sammlung, Verarbeitung und Bereinigung von 4 quantitativen Datensätzen, u.a. der UN Ungleichheitstrends
en: Collected, processed, and cleaned 4 datasets, including UN World inequality trends en: Collected, processed, and cleaned 4 datasets, including UN World inequality trends
@ -164,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
@ -376,23 +355,10 @@ 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
modules:
- de: Anwendungsorientierte Programmierung
en: Applied Programming
- de: Digitale Signalverarbeitung
en: Digital Signal Processing
- de: Betriebssysteme und Rechnernetze
en: Operating Systems and Computer Networks
- de: Datenbanken & Multimedia-Datenbanken
en: Database Systems & Multimedia Databases
- de: Wissenschaftskommunikation in der Informatik
en: Writing and Presenting in Computer Science
- de: IT Sicherheit
en: IT Security
thesis: thesis:
- title: - title:
@ -420,8 +386,8 @@ volunteering:
en: Transferring Digital Competence in Aging en: Transferring Digital Competence in Aging
- de: Wöchentlicher Workshop zur Entwicklung von Selbstvertrauen und Kompetenz mit Smartphones - de: Wöchentlicher Workshop zur Entwicklung von Selbstvertrauen und Kompetenz mit Smartphones
en: Weekly workshop on developing confidence and competence with smartphones en: Weekly workshop on developing confidence and competence with smartphones
- de: Personalisierte technische Unterstützung und personalisierte Einzelberatungen - de: Personalisierte technische Hilfsmeetings und technische Support-Beratungen
en: Personalized tech assistance appointments and individual technical support consultations en: Personalized tech assistance appointments and technical support consultations
- title: - title:
de: Verpixelt de: Verpixelt
en: Verpixelt en: Verpixelt
@ -432,45 +398,11 @@ volunteering:
de: Urban Souls e.V. de: Urban Souls e.V.
en: Urban Souls association en: Urban Souls association
bullets: bullets:
- de: Technische Assistenz zur Vermittlung digitaler Kompetenzen an Kinder und Jugendliche - de: Technische Assistenz zur Vermittlung digitaler Kompetenzen
en: Technical assistance for conveying digital competencies for children and youth en: Technical assistance for conveying digital competencies
- de: IT und Event Organisationsunterstützung - de: IT und Event Organisationsunterstützung
en: IT and event management services en: IT and event management services
digital:
- title:
de: Serverinfrastruktur Erstellung und Wartung
en: Server infrastructure creation and maintenance
date:
de: 2017--
en: 2017--
place:
de: martyoeh.me
en: martyoeh.me
bullets:
- de: Infrastruktur vollständig isoliert über Docker Swarm und idempotente Ansible Playbooks
en: Infrastructure running fully isolated on Docker swarm and idempotent Ansible playbooks
- de: Hochverfügbare S3-gestützte Nextcloud-Kollaborations- und Groupware-Umgebung
en: Highly-available S3-enabled Nextcloud Collaboration and Groupware environment
- de: Skalierbare und kontinuierliche Bereitstellungsoperation mit Grafana-Fehlerüberwachung
en: Scalable and continuous deployment operation with Grafana fault monitoring
- title:
de: Virtualisiertes Homelab Hypervisor
en: Virtualized Homelab hypervisor
date:
de: 2022--
en: 2022--
place:
de: Direkthardware 1U Intel-Heimserver
en: Bare-metal 1U Intel Homeserver
bullets:
- de: LXC/LXD (incus) Hypervisor
en: LXC/LXD (incus) hypervisor
- de: Internes Docker Compose Netzwerk mit virtueller LAN-Trennung
en: Nested Docker Compose network with virtual LAN separation
- de: Separierte VMs auf Debian/RHEL/Windows mit vollem Hardware- und Grafik-Passthrough
en: Separate VMs running Debian/RHEL/Windows with full hardware and graphics passthrough
skills: skills:
- name: - name:
de: Office-Suite de: Office-Suite
@ -511,7 +443,7 @@ skills:
- de: Seaborn - de: Seaborn
en: Seaborn en: Seaborn
- name: - name:
de: Programmierung de: Programmieren
en: Programming en: Programming
items: items:
- de: Python - de: Python
@ -570,8 +502,8 @@ languages:
de: Englisch de: Englisch
en: English en: English
items: items:
- de: verhandlungssicher - de: fließend
en: business fluent en: fluent
- name: - name:
de: Französisch de: Französisch
en: French en: French

160
cv.typ
View file

@ -1,7 +1,157 @@
#import "lib/resume.typ": resume #import "lib.typ": *
#resume.with( #let cv(contents, use_sidebar: false) = {
main: ("experience", "education", "volunteering", "digital", "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,
{
skill_item(item: contents.skills)
},
)
}
if "languages" in contents {
let title = (en: "Languages", de: "Sprachen").at(lang)
section(
title: title,
{
skill_item(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]
skill_item(item: contents.languages, is_sidebar: true)
[\ ]
}
if "skills" in contents {
let title = (en: "Qualifications", de: "Kenntnisse").at(lang)
[== #title]
skill_item(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"))

View file

@ -1,4 +1,11 @@
// set some styles
#let style(it) = {
show heading: set text(font: "New Computer Modern")
show link: underline
it
}
// transform md-similes to actual symbols // transform md-similes to actual symbols
#let smartypants(it) = { #let smartypants(it) = {
// smartypants and latex compatibility // smartypants and latex compatibility
@ -8,14 +15,6 @@
it it
} }
// set some styles
#let style(it) = {
show heading: set text(font: "New Computer Modern")
show link: underline
show: smartypants
it
}
// Choose the compiled language through cli by doing // Choose the compiled language through cli by doing
// //
// $ typst compile --input lang=de cv.typ // $ typst compile --input lang=de cv.typ
@ -64,10 +63,7 @@
block(inset: 5%, width: 85%, text(fill: luma(150), body)) block(inset: 5%, width: 85%, text(fill: luma(150), body))
} }
#let entry(item: (), show_sublists: true) = { #let entry(item: ()) = {
if "hidden" in item and item.hidden == true {
return
}
if "title" in item { if "title" in item {
[*#item.title.at(lang)*] [*#item.title.at(lang)*]
} }
@ -81,17 +77,9 @@
if "date" in item { if "date" in item {
[ _#item.date.at(lang)_ \ ] [ _#item.date.at(lang)_ \ ]
} }
if show_sublists == true { if "bullets" in item {
if "bullets" in item { for bullet in item.bullets {
for bullet in item.bullets { [- #bullet.at(lang)]
[- #bullet.at(lang)]
}
}
if "modules" in item {
subdued[Relevante Module:]
for bullet in item.modules {
subdued[ - #bullet.at(lang)]
}
} }
} }
if "publication" in item { if "publication" in item {
@ -102,24 +90,6 @@
} }
} }
#let sidebar_entry(item: (), is_sidebar: false) = {
let side_list(body) = if is_sidebar { list(body) } else { par(body) }
for skill in item {
side_list({
[*#skill.name.at(lang)*]
if is_sidebar [\ ] else [ (]
for (i, v) in skill.items.enumerate() {
[#v.at(lang)]
if i < skill.items.len() - 1 {
[, ]
}
}
if not is_sidebar [)]
})
}
}
#let horizon_line() = { #let horizon_line() = {
v(-3pt) v(-3pt)
line(length: 100%) line(length: 100%)
@ -131,50 +101,17 @@
horizon_line() horizon_line()
}; };
#let section(title: "Section", entries: (), longform: true, body) = { #let section(title: "Section", entries: (), body) = {
section_header(title) section_header(title)
if body == none or body == [] { if body == none or body == [] {
for e in entries { for e in entries {
entry(item: e, show_sublists: longform) entry(item: e)
} }
} else { } else {
body body
} }
}; };
// TODO: make it _return_ the data, not display it on its own
#let by_client(experience: ()) = {
let by_client = (:)
for item in experience {
let client = item.place.at(lang)
if client not in by_client {
by_client.insert(client, ())
}
by_client.at(client).push((title: item.title.at(lang), date: item.date.at(lang)))
}
for (client, jobs) in by_client {
[*#client*:]
for j in jobs {
[- #j.title #h(1fr) #j.date]
}
}
}
#let by_experience_type(type: (), experience: ()) = {
let by_ty = (:)
for (id, desc) in type {
let matching_exp_items = experience.filter(item => int(item.typeid) == int(id))
if matching_exp_items.len() == 0 {
return
}
[=== _#desc.at(lang)_]
by_client(experience: matching_exp_items)
}
}
// Slightly re-styled entry with PLACE first and TITLE second // Slightly re-styled entry with PLACE first and TITLE second
#let education_entry(item: ()) = { #let education_entry(item: ()) = {
assert( assert(
@ -191,3 +128,21 @@
[*#item.title.at(lang)* #item.place.at(lang) #h(1fr)] [*#item.title.at(lang)* #item.place.at(lang) #h(1fr)]
[#par(item.abstract.at(lang))] [#par(item.abstract.at(lang))]
} }
// skill-specific entry, changing its style for sidebar
#let skill_item(item: (), is_sidebar: false) = {
let side_list(body) = if is_sidebar { list(body) } else { par(body) }
for skill in item {
side_list({
[*#skill.name.at(lang)*]
if is_sidebar [\ ] else [ (]
for (i, v) in skill.items.enumerate() {
[#v.at(lang)]
if i < skill.items.len() - 1 {
[, ]
}
}
if not is_sidebar [)]
})
}
}

View file

@ -1,169 +0,0 @@
#import "lib.typ": *
#import "wrapit.typ": *
#let create_body(main: (), contents: (:), longform: true) = {
for item in main {
if item == "summary" and "summary" in contents {
section(
title: "",
{
contents.summary.at(lang)
},
longform: longform
)
}
if item == "experience_by_type" and "experience" in contents {
let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang)
section(title: title, longform: longform)[]
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, longform: longform)[]
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, longform: longform)[]
}
if item == "education" and "education" in contents {
let title = (en: "Education", de: "Ausbildung").at(lang)
section(title: title, entries: contents.thesis + contents.education, longform: longform)[]
}
if item == "volunteering" and "volunteering" in contents {
let title = (en: "Volunteer Work", de: "Ehrenamt").at(lang)
section(title: title, entries: contents.volunteering, longform: longform)[]
}
if item == "digital" and "digital" in contents {
let title = (en: "Digital Organization", de: "Digitales Schaffen").at(lang)
section(title: title, entries: contents.digital, longform: longform)[]
}
if item == "skills" and "skills" in contents {
let title = (en: "Qualifications", de: "Qualifikationen").at(lang)
section(
title: title,
{
sidebar_entry(item: contents.skills)
},
longform: longform
)
}
if item == "languages" and "languages" in contents {
let title = (en: "Languages", de: "Sprachen").at(lang)
section(
title: title,
{
sidebar_entry(item: contents.languages)
},
longform: longform
)
}
}
}
#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"), longform:true) = {
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, longform: longform),
align: top + right,
columns: (auto, 30%),
)
}

View file

@ -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
]
]
})
}

View file

@ -1,3 +1,192 @@
#import "lib/resume.typ": resume #import "lib.typ": *
#resume.with(longform: false)(yaml("content.yml")) // TODO: make it _return_ the data, not display it on its own
#let by_client(experience: ()) = {
let by_client = (:)
for item in experience {
let client = item.place.at(lang)
if client not in by_client {
by_client.insert(client, ())
}
by_client.at(client).push((item.title.at(lang), item.date.at(lang)))
}
for (client, jobs) in by_client {
[*#client*:]
for j in jobs {
[- #j.at(0) #h(1fr) #j.at(1)]
}
}
}
#let by_experience_type(type: (), experience: ()) = {
let by_ty = (:)
for (id, desc) in type {
let matching_exp_items = experience.filter(item => int(item.typeid) == int(id))
if matching_exp_items.len() == 0 {
return
}
[=== _#desc.at(lang)_]
by_client(experience: matching_exp_items)
}
}
#let resume(contents, main: ("experience", "education"), sidebar: ("volunteering", "languages", "skills")) = {
show: style
show: smartypants
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 main and "summary" in contents {
section(
title: "",
{
contents.summary.at(lang)
},
)
}
if "experience" in main 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 "education" in main and "education" in contents {
let title = (en: "Education", de: "Ausbildung").at(lang)
section(title: title, entries: contents.thesis + contents.education)[]
}
if "volunteering" in main and "volunteering" in contents {
let title = (en: "Volunteer Work", de: "Ehrenamt").at(lang)
section(title: title, entries: contents.volunteering)[]
}
if "skills" in main and "skills" in contents {
let title = (en: "Qualifications", de: "Qualifikationen").at(lang)
section(
title: title,
{
skill_item(item: contents.skills)
},
)
}
if "languages" in main and "languages" in contents {
let title = (en: "Languages", de: "Sprachen").at(lang)
section(
title: title,
{
skill_item(item: contents.languages)
},
)
}
}
let sidebar = {
if "volunteering" in sidebar 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 "languages" in sidebar and "languages" in contents {
let title = (en: "Languages", de: "Sprachen").at(lang)
[== #title]
skill_item(item: contents.languages, is_sidebar: true)
[\ ]
}
if "skills" in sidebar and "skills" in contents {
let title = (en: "Qualifications", de: "Kenntnisse").at(lang)
[== #title]
skill_item(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(
fill: luma(250),
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"))