From 9d4c195ce9dc22b6b56accdb7c915cc7dbc70857 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 28 Jan 2025 09:59:42 +0100 Subject: [PATCH 01/11] Fix spelling issue --- content.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content.yml b/content.yml index c862e1a..b9d98ec 100644 --- a/content.yml +++ b/content.yml @@ -26,7 +26,7 @@ summary: 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. - 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 administration and website management. Beyond professional work I foster the development 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. I welcome opportunities to deepen my expertise in these topics, in addition to those expanding my range of applicable skills. @@ -55,7 +55,7 @@ experience: en: 2023--2024 title: de: Externer Forscher, Ungleichheiten auf dem Arbeitsmarkt - en: External researcher, Inequalities on the Labour market + en: External researcher, Inequalities on the labour market place: de: ILO en: ILO From 1ea7afdaa5c2dd9b5f5e193c53d3656ee5003822 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 28 Jan 2025 10:00:25 +0100 Subject: [PATCH 02/11] Fix page counter for typst update --- cv.typ | 2 +- resume.typ | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cv.typ b/cv.typ index 89bb34c..b56bc08 100644 --- a/cv.typ +++ b/cv.typ @@ -156,7 +156,7 @@ ][ #smallcaps[#contents.about.fullname] ][ - #counter(page).display() + #context counter(page).display() ] ], ) diff --git a/resume.typ b/resume.typ index 6babff7..f878a17 100644 --- a/resume.typ +++ b/resume.typ @@ -174,7 +174,7 @@ ][ #smallcaps[#contents.about.fullname] ][ - #counter(page).display() + #context counter(page).display() ] ], ) From 484ea7512eca1d2d48a0a3f47406f87fc4650037 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 28 Jan 2025 10:01:13 +0100 Subject: [PATCH 03/11] Update Makefile for typst letter --- Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index de980ec..365449f 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,9 @@ resume: resume.typ typst compile --input lang=en resume.typ build/resume_en.pdf typst compile --input lang=de resume.typ build/resume_de.pdf -letter: letter.qmd templates/letter.latex - poetry run quarto render letter.qmd +letter: letter.typ + typst compile --input lang=en letter.typ build/letter_en.pdf + typst compile --input lang=de letter.typ build/letter_de.pdf clean: rm -f *CV.aux *CV.bcf *CV.log *CV.out *CV.run.xml *CV.pdf short_CV.tex long_CV.tex *CV.bbl *CV.blg *yaml_CV.md From a81276f89bfbff2f3ceb9f5620f13a36c608708c Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 28 Jan 2025 21:06:17 +0100 Subject: [PATCH 04/11] Add separation by experience type --- resume.typ | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/resume.typ b/resume.typ index f878a17..9c006fd 100644 --- a/resume.typ +++ b/resume.typ @@ -59,14 +59,16 @@ block(inset: 5%, width: 85%, text(fill:luma(150), body)) } -#let freelance_by_client(experience:()) = { +#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) )) + by_client.at(client).push( (item.title.at(lang), item.date.at(lang)) ) } for (client, jobs) in by_client { @@ -77,6 +79,23 @@ } } +#let by_experience_type(type:(), experience:()) = { + let by_ty = (:) + for (id, desc) in type { + let matching_exp_items = (); + // for item in experience { + // [itemtypeid: #item.typeid, id: #id] + // if item.typeid == id { + // matching_exp_items.push(item) + // } + // } + let matching_exp_items = experience.filter(item => + int(item.typeid) == int(id)) + [=== _#desc.at(lang)_] + by_client(experience: matching_exp_items) + } +} + #let entry(item: ()) = { if "title" in item { [*#item.title.at(lang)*] @@ -193,8 +212,7 @@ if "experience" in contents { let title = sel_word_lang(en:"Professional Experience", de:"Berufserfahrung") section(title: title)[] - freelance_by_client(experience:contents.experience) - + by_experience_type(experience: contents.experience, type: contents.experience_types) } if "education" in contents { @@ -229,7 +247,7 @@ for e in contents.volunteering { [ - *#e.title.at(lang)* (#e.date.at(lang)) - #par(e.bullets.at(0).at(lang)) \ + #par(e.bullets.at(0).at(lang)) \ ] } } From 03acad7a44f0e06b2201d13910a7d556626cd5b9 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 28 Jan 2025 21:07:14 +0100 Subject: [PATCH 05/11] Add experience types to data --- content.yml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/content.yml b/content.yml index b9d98ec..7c5889f 100644 --- a/content.yml +++ b/content.yml @@ -30,8 +30,17 @@ summary: I welcome opportunities to deepen my expertise in these topics, in addition to those expanding my range of applicable skills. +experience_types: + 1: + de: Selbstständiger Schriftsteller Forschung + en: Independent research consultant + 2: + de: Honorararbeit + en: Salaried work + experience: - - date: + - typeid: 1 + date: de: 2024 en: 2024 title: @@ -53,6 +62,7 @@ experience: - date: de: 2023--2024 en: 2023--2024 + typeid: 1 title: de: Externer Forscher, Ungleichheiten auf dem Arbeitsmarkt en: External researcher, Inequalities on the labour market @@ -72,6 +82,7 @@ experience: - date: de: 2023 en: 2023 + typeid: 1 title: de: Consultant, Forschungsarbeit für nachhaltige Beschaffung in internationaler Logistik en: Consultant, Research on sustainable procurement in international logistics @@ -88,6 +99,7 @@ experience: - date: de: 2022 en: 2022 + typeid: 1 title: de: Consultant, Datenbankforschung internationale Hilfsgelder en: Consultant, Database research international aid funds @@ -104,6 +116,7 @@ experience: - date: de: 2022 en: 2022 + typeid: 1 title: de: Redaktionsarbeit, Soziale Absicherung und Widerstandsfähigkeit en: Editorial work, Social Protection and Resilience, Roskilde University @@ -123,6 +136,7 @@ experience: - date: de: 2022 en: 2022 + typeid: 1 title: de: Forschungsassistenz, Entwicklungsprojekte zur Reduzierung Ungleichheitstrends en: Research Assistant, Development projects to reduce inequality trends @@ -143,6 +157,7 @@ experience: - date: de: 2022 en: 2022 + typeid: 1 title: de: Consultant, Review Verknüpfung sozialer Schutz, Produktivität und Formalisierung en: Consultant, Social Protection, Productivity and Formalization Nexus Review @@ -162,6 +177,7 @@ experience: - date: de: 2022 en: 2022 + typeid: 1 title: de: Consultant, Review Arbeitsmarktpolitiken in Asien und dem Pazifik en: Consultant, Labour Market Policies Review in Asia and the Pacific @@ -179,6 +195,7 @@ experience: - date: de: 2021 en: 2021 + typeid: 1 title: de: Forschungsassistenz, Informelle Organisierung und Absicherung en: Research Assistant, Informal Organization and Social Security @@ -198,6 +215,7 @@ experience: - date: de: 2021 en: 2021 + typeid: 1 title: de: Redakteur, Soziale Absicherung informeller Arbeiter (SPIWORK) en: Editorial Assistant, Social Protection of Informal Workers (SPIWORK) @@ -217,6 +235,7 @@ experience: - date: de: 2018--2019 en: 2018--2019 + typeid: 2 title: de: Akademische Hilfskraft, Institut für Amerikastudien en: Academic Assistant, Institute of American Studies @@ -233,6 +252,7 @@ experience: - date: de: 2017--2019 en: 2017--2019 + typeid: 2 title: de: Studentische Hilfskraft, Professor Crister S. Garrett en: Student Assistant, Professor Crister S. Garrett @@ -249,6 +269,7 @@ experience: - date: de: 2018 en: 2018 + typeid: 2 title: de: Lehrassistenz, Transatlantische Sommerschule Cultures of Security en: Teaching Assistant, Trans Atlantic Summer School Cultures of Security @@ -265,6 +286,7 @@ experience: - date: de: 2017--2018 en: 2017--2018 + typeid: 2 title: de: Content-Management, Bachelor Plus/Alumni-System en: Content Management, Bachelor Plus/Alumni System @@ -281,6 +303,7 @@ experience: - date: de: 2014--2018 en: 2014--2018 + typeid: 2 title: de: Verkaufsassistent und Eventhelfer für historische Märkte en: Sales Assistant and Event Support for Historical Markets From cfa3e8a625a7d7659ec6fb536eade332a6a062f3 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 28 Jan 2025 21:27:42 +0100 Subject: [PATCH 06/11] Allow manually overriding main or sidebar sections --- README.md | 45 ++++++++++++++++++++++++++++++++++++++++ resume.typ | 61 +++++++++++++++++++++++++++--------------------------- 2 files changed, 75 insertions(+), 31 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae62ba2 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Data-driven CV & resume + +My personal CV and resume files, automatically generated from a multi-lingual +yaml data file. + +The CV contains a full run-down of my educational and job experience to date, +while the resume is more compact, reduced to a single page +and can be tailored for a specific job area or expertise. + +Is called like the following: + +```typst +#resume( + yaml("content.yml"), + main: ("experience", "education", "volunteering", "skills", "languages"), + sidebar: ("volunteering", "skills") +) +``` + +This is the default invocation, though sidebar and main body sections can be exchanged at will. + +TODO: + +- [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? + +- [x] migrate to typst template? +- [x] ! Fix german summary to be like English summary +- [_] generalized entry? + Would have 'title', 'place', 'date', 'type'/'tags' + potential + 'publication', 'bullets' + +## Typst-driven branch + +- [x] Fix publication '\&'s +- [x] Fix en-dash/em-dash (e.g. in years) +- [x] Producable from yaml content +- [x] Can be switched between Ger/En +- [x] Try sidebar version + - [ ] Generalize sidebar version through abstraction/extractions +- [ ] unify items: experience/education/(thesis?) diff --git a/resume.typ b/resume.typ index 9c006fd..ce523d0 100644 --- a/resume.typ +++ b/resume.typ @@ -170,7 +170,7 @@ } } -#let resume(contents) = { +#let resume(contents, main: ("experience", "education"), sidebar: ("volunteering", "languages", "skills")) = { set text(lang: lang) let date_formatting = { @@ -203,48 +203,48 @@ header(contents.about) let body = { -// if "summary" in contents { -// section(title:"", { -// contents.summary.at(lang) -// }) -// }; + if "summary" in main and "summary" in contents { + section(title:"", { + contents.summary.at(lang) + }) + } - if "experience" in contents { + if "experience" in main and "experience" in contents { let title = sel_word_lang(en:"Professional Experience", de:"Berufserfahrung") section(title: title)[] by_experience_type(experience: contents.experience, type: contents.experience_types) } - if "education" in contents { + if "education" in main and "education" in contents { let title = sel_word_lang(en:"Education", de:"Ausbildung") section(title: title, entries:contents.thesis + contents.education)[] } -// if "volunteering" in contents { -// let title = sel_word_lang(en:"Volunteer Work", de:"Ehrenamt") -// section(title: title, entries:contents.volunteering)[] -// } -// -// if "skills" in contents { -// let title = sel_word_lang(en:"Qualifications", de:"Qualifikationen") -// section(title: title, { -// skill_item(item:contents.skills) -// }) -// } -// -// if "languages" in contents { -// let title = sel_word_lang(en:"Languages", de:"Sprachen") -// section(title: title, { -// skill_item(item:contents.languages) -// }) -// } + if "volunteering" in main and "volunteering" in contents { + let title = sel_word_lang(en:"Volunteer Work", de:"Ehrenamt") + section(title: title, entries:contents.volunteering)[] + } + + if "skills" in main and "skills" in contents { + let title = sel_word_lang(en:"Qualifications", de:"Qualifikationen") + section(title: title, { + skill_item(item:contents.skills) + }) + } + + if "languages" in main and "languages" in contents { + let title = sel_word_lang(en:"Languages", de:"Sprachen") + section(title: title, { + skill_item(item:contents.languages) + }) + } } let sidebar = { - if "volunteering" in contents { + if "volunteering" in sidebar and "volunteering" in contents { let title = sel_word_lang(en:"Volunteer Work", de:"Ehrenamt") [== #title] - for e in contents.volunteering { + for e in contents.at("volunteering") { [ - *#e.title.at(lang)* (#e.date.at(lang)) #par(e.bullets.at(0).at(lang)) \ @@ -252,14 +252,14 @@ } } - if "languages" in contents { + if "languages" in sidebar and "languages" in contents { let title = sel_word_lang(en:"Languages", de:"Sprachen") [== #title] skill_item(item:contents.languages, is_sidebar: true) [\ ] } - if "skills" in contents { + if "skills" in sidebar and "skills" in contents { let title = sel_word_lang(en:"Qualifications", de:"Kenntnisse") [== #title] skill_item(item:contents.skills, is_sidebar: true) @@ -307,4 +307,3 @@ #resume( yaml("content.yml") ) - From 64dffb43d659439d0ea9ba2ac94bd25c0554957a Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 28 Jan 2025 21:29:21 +0100 Subject: [PATCH 07/11] Fix english spellink HTWK country --- content.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content.yml b/content.yml index 7c5889f..0086afc 100644 --- a/content.yml +++ b/content.yml @@ -348,7 +348,7 @@ education: en: 2018 - place: de: HTWK Leipzig, Deutschland - en: HTWK Leipzig, Deutschland + en: HTWK Leipzig, Germany title: de: Medieninformatik, BSc (nicht abgeschlossen) en: Media Computer Science, BSc (not completed) From 18df7051e7b054e24f1ff5a747293978b625e2f7 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 28 Jan 2025 21:30:12 +0100 Subject: [PATCH 08/11] Format with typstyle --- cv.typ | 444 +++++++++++++++++++++++++----------------------- resume.typ | 486 ++++++++++++++++++++++++++++------------------------- 2 files changed, 491 insertions(+), 439 deletions(-) diff --git a/cv.typ b/cv.typ index b56bc08..518cd71 100644 --- a/cv.typ +++ b/cv.typ @@ -11,266 +11,290 @@ // $ typst compile --input lang=de cv.typ // #let lang = { - if "lang" in sys.inputs and sys.inputs.lang == "de" { - "de" - } else { - "en" - } + if "lang" in sys.inputs and sys.inputs.lang == "de" { + "de" + } else { + "en" + } } -#let sel_word_lang(de: "", en:"") = { - if lang == "de" { - de - } else { - en - } +#let sel_word_lang(de: "", en: "") = { + if lang == "de" { + de + } else { + en + } } #let _columns_3(left_body, center_body, right_body) = { - block[ - #box(width: 1fr)[ - #align(left)[#left_body] - ] - #box(width: 1fr)[ - #align(center)[#center_body] - ] - #box(width: 1fr)[ - #align(right)[#right_body] - ] + block[ + #box(width: 1fr)[ + #align(left)[#left_body] ] + #box(width: 1fr)[ + #align(center)[#center_body] + ] + #box(width: 1fr)[ + #align(right)[#right_body] + ] + ] } #let header(about, columns: (1.5fr, 1fr, 1fr)) = { - [= #about.fullname]; - let contact_fields = (for c in about.contact { - if "link" in c { - ([#c.icon ~ #link(c.link)[#c.text]],) - } else { - ([#c.icon ~ #c.text],) - } - }) - grid( - columns: columns, - gutter: 5pt, - ..contact_fields - ); + [= #about.fullname] + let contact_fields = ( + for c in about.contact { + if "link" in c { + ([#c.icon ~ #link(c.link)[#c.text]],) + } else { + ([#c.icon ~ #c.text],) + } + } + ) + grid( + columns: columns, + gutter: 5pt, + ..contact_fields + ) } #let subdued(body) = { - block(inset: 5%, width: 85%, text(fill:luma(150), body)) + block(inset: 5%, width: 85%, text(fill: luma(150), body)) } #let entry(item: ()) = { + if "title" in item { + [*#item.title.at(lang)*] + } + if "place" in item { if "title" in item { - [*#item.title.at(lang)*] + [, ] } - if "place" in item { - if "title" in item { - [, ] - } - [_#item.place.at(lang)_] - } - [#h(1fr)] - if "date" in item { - [ _#item.date.at(lang)_ \ ] - } - if "bullets" in item { - for bullet in item.bullets { - [- #bullet.at(lang)] - } - } - if "publication" in item { - subdued[#item.publication.at(lang) \ ] - }; - if "abstract" in item { - subdued[#item.abstract.at(lang) \ ] + [_#item.place.at(lang)_] + } + [#h(1fr)] + if "date" in item { + [ _#item.date.at(lang)_ \ ] + } + if "bullets" in item { + for bullet in item.bullets { + [- #bullet.at(lang)] } + } + if "publication" in item { + subdued[#item.publication.at(lang) \ ] + } + if "abstract" in item { + subdued[#item.abstract.at(lang) \ ] + } } -#let horizon_line() = {v(-3pt); line(length: 100%); v(-5pt)} +#let horizon_line() = { + v(-3pt) + line(length: 100%) + v(-5pt) +} -#let section_header(title) = {[== #title]; horizon_line()}; +#let section_header(title) = { + [== #title] + horizon_line() +}; #let section(title: "Section", entries: (), body) = { - section_header(title); - if body == none or body == [] { - for e in entries { - entry(item:e) - } - } else { - body + section_header(title) + if body == none or body == [] { + for e in entries { + entry(item: e) } + } else { + body + } }; // Slightly re-styled entry with PLACE first and TITLE second #let education_entry(item: ()) = { - assert("place" in item and "title" in item and "date" in item, message: "Education items require place, program and date."); - [*#item.place.at(lang)*, #item.title.at(lang) #h(1fr)]; - [ _#item.date.at(lang)_ \ ]; + assert( + "place" in item and "title" in item and "date" in item, + message: "Education items require place, program and date.", + ) + [*#item.place.at(lang)*, #item.title.at(lang) #h(1fr)] + [ _#item.date.at(lang)_ \ ] } // Restyled entry with PLACE not emphasized like usual, and no date but an abstract #let thesis_entry(item: ()) = { - assert("title" in item and "place" in item, message: "Thesis items require type and title."); - [*#item.title.at(lang)* #item.place.at(lang) #h(1fr)]; - [#par(item.abstract.at(lang))] + assert("title" in item and "place" in item, message: "Thesis items require type and title.") + [*#item.title.at(lang)* #item.place.at(lang) #h(1fr)] + [#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 [)] - }) - } + 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 cv(contents, use_sidebar: false) = { - set text(lang: lang) + set text(lang: lang) - let date_formatting = { - if lang == "de" { - "[day]. [month repr:long] [year]" - } else { - "[month repr:long] [day], [year]" - } + 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 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) + set par(justify: true) - header(contents.about) + header(contents.about) - let body = { - if "summary" in contents { - section(title:"", { - contents.summary.at(lang) - }) - }; - - if "experience" in contents { - let title = sel_word_lang(en:"Professional Experience", de:"Berufserfahrung") - section(title: title, entries:contents.experience)[] - } - - if "education" in contents { - let title = sel_word_lang(en:"Education", de:"Ausbildung") - section(title: title, entries:contents.thesis + contents.education)[] - } - - if not use_sidebar { - if "volunteering" in contents { - let title = sel_word_lang(en:"Volunteer Work", de:"Ehrenamt") - section(title: title, entries:contents.volunteering)[] - } - - if "skills" in contents { - let title = sel_word_lang(en:"Qualifications", de:"Qualifikationen") - section(title: title, { - skill_item(item:contents.skills) - }) - } - - if "languages" in contents { - let title = sel_word_lang(en:"Languages", de:"Sprachen") - section(title: title, { - skill_item(item:contents.languages) - }) - } - } + let body = { + if "summary" in contents { + section( + title: "", + { + contents.summary.at(lang) + }, + ) } - let sidebar = { - if "volunteering" in contents { - let title = sel_word_lang(en:"Volunteer Work", de:"Ehrenamt") - [== #title] - for e in contents.volunteering { - [ - - *#e.title.at(lang)* (#e.date.at(lang)) - #par(e.bullets.at(0).at(lang)) \ - ] - } - } + if "experience" in contents { + let title = sel_word_lang(en: "Professional Experience", de: "Berufserfahrung") + section(title: title, entries: contents.experience)[] + } - if "languages" in contents { - let title = sel_word_lang(en:"Languages", de:"Sprachen") - [== #title] - skill_item(item:contents.languages, is_sidebar: true) - [\ ] - } - - if "skills" in contents { - let title = sel_word_lang(en:"Qualifications", de:"Kenntnisse") - [== #title] - skill_item(item:contents.skills, is_sidebar: true) - } + if "education" in contents { + let title = sel_word_lang(en: "Education", de: "Ausbildung") + section(title: title, entries: contents.thesis + contents.education)[] } 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 - } - ) + if "volunteering" in contents { + let title = sel_word_lang(en: "Volunteer Work", de: "Ehrenamt") + section(title: title, entries: contents.volunteering)[] + } + if "skills" in contents { + let title = sel_word_lang(en: "Qualifications", de: "Qualifikationen") + section( + title: title, + { + skill_item(item: contents.skills) + }, + ) + } + + if "languages" in contents { + let title = sel_word_lang(en: "Languages", de: "Sprachen") + section( + title: title, + { + skill_item(item: contents.languages) + }, + ) + } + } + } + + let sidebar = { + if "volunteering" in contents { + let title = sel_word_lang(en: "Volunteer Work", de: "Ehrenamt") + [== #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 = sel_word_lang(en: "Languages", de: "Sprachen") + [== #title] + skill_item(item: contents.languages, is_sidebar: true) + [\ ] + } + + if "skills" in contents { + let title = sel_word_lang(en: "Qualifications", de: "Kenntnisse") + [== #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") -) +#cv.with(use_sidebar: false)(yaml("content.yml")) diff --git a/resume.typ b/resume.typ index ce523d0..27938a0 100644 --- a/resume.typ +++ b/resume.typ @@ -11,283 +11,312 @@ // $ typst compile --input lang=de cv.typ // #let lang = { - if "lang" in sys.inputs and sys.inputs.lang == "de" { - "de" - } else { - "en" - } + if "lang" in sys.inputs and sys.inputs.lang == "de" { + "de" + } else { + "en" + } } -#let sel_word_lang(de: "", en:"") = { - if lang == "de" { - de - } else { - en - } +#let sel_word_lang(de: "", en: "") = { + if lang == "de" { + de + } else { + en + } } #let _columns_3(left_body, center_body, right_body) = { - block[ - #box(width: 1fr)[ - #align(left)[#left_body] - ] - #box(width: 1fr)[ - #align(center)[#center_body] - ] - #box(width: 1fr)[ - #align(right)[#right_body] - ] + block[ + #box(width: 1fr)[ + #align(left)[#left_body] ] + #box(width: 1fr)[ + #align(center)[#center_body] + ] + #box(width: 1fr)[ + #align(right)[#right_body] + ] + ] } #let header(about, columns: (1.5fr, 1fr, 1fr)) = { - [= #about.fullname]; - let contact_fields = (for c in about.contact { - if "link" in c { - ([#c.icon ~ #link(c.link)[#c.text]],) - } else { - ([#c.icon ~ #c.text],) - } - }) - grid( - columns: columns, - gutter: 5pt, - ..contact_fields - ); + [= #about.fullname] + let contact_fields = ( + for c in about.contact { + if "link" in c { + ([#c.icon ~ #link(c.link)[#c.text]],) + } else { + ([#c.icon ~ #c.text],) + } + } + ) + grid( + columns: columns, + gutter: 5pt, + ..contact_fields + ) } #let subdued(body) = { - block(inset: 5%, width: 85%, text(fill:luma(150), body)) + block(inset: 5%, width: 85%, text(fill: luma(150), body)) } -#let by_client(experience:()) = { - let by_client = (:) +#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 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)] - } + 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 = (); - // for item in experience { - // [itemtypeid: #item.typeid, id: #id] - // if item.typeid == id { - // matching_exp_items.push(item) - // } - // } - let matching_exp_items = experience.filter(item => - int(item.typeid) == int(id)) - [=== _#desc.at(lang)_] - by_client(experience: matching_exp_items) - } +#let by_experience_type(type: (), experience: ()) = { + let by_ty = (:) + for (id, desc) in type { + let matching_exp_items = () + // for item in experience { + // [itemtypeid: #item.typeid, id: #id] + // if item.typeid == id { + // matching_exp_items.push(item) + // } + // } + let matching_exp_items = experience.filter(item => int(item.typeid) == int(id)) + [=== _#desc.at(lang)_] + by_client(experience: matching_exp_items) + } } #let entry(item: ()) = { + if "title" in item { + [*#item.title.at(lang)*] + } + if "place" in item { if "title" in item { - [*#item.title.at(lang)*] + [, ] } - if "place" in item { - if "title" in item { - [, ] - } - [_#item.place.at(lang)_] - } - [#h(1fr)] - if "date" in item { - [ _#item.date.at(lang)_ \ ] - } - if "bullets" in item { - for bullet in item.bullets { - [- #bullet.at(lang)] - } - } - if "publication" in item { - subdued[#item.publication.at(lang) \ ] - }; - if "abstract" in item { - subdued[#item.abstract.at(lang) \ ] + [_#item.place.at(lang)_] + } + [#h(1fr)] + if "date" in item { + [ _#item.date.at(lang)_ \ ] + } + if "bullets" in item { + for bullet in item.bullets { + [- #bullet.at(lang)] } + } + if "publication" in item { + subdued[#item.publication.at(lang) \ ] + } + if "abstract" in item { + subdued[#item.abstract.at(lang) \ ] + } } -#let horizon_line() = {v(-3pt); line(length: 100%); v(-5pt)} +#let horizon_line() = { + v(-3pt) + line(length: 100%) + v(-5pt) +} -#let section_header(title) = {[== #title]; horizon_line()}; +#let section_header(title) = { + [== #title] + horizon_line() +}; #let section(title: "Section", entries: (), body) = { - section_header(title); - if body == none or body == [] { - for e in entries { - entry(item:e) - } - } else { - body + section_header(title) + if body == none or body == [] { + for e in entries { + entry(item: e) } + } else { + body + } }; // Slightly re-styled entry with PLACE first and TITLE second #let education_entry(item: ()) = { - assert("place" in item and "title" in item and "date" in item, message: "Education items require place, program and date."); - [*#item.place.at(lang)*, #item.title.at(lang) #h(1fr)]; - [ _#item.date.at(lang)_ \ ]; + assert( + "place" in item and "title" in item and "date" in item, + message: "Education items require place, program and date.", + ) + [*#item.place.at(lang)*, #item.title.at(lang) #h(1fr)] + [ _#item.date.at(lang)_ \ ] } // Restyled entry with PLACE not emphasized like usual, and no date but an abstract #let thesis_entry(item: ()) = { - assert("title" in item and "place" in item, message: "Thesis items require type and title."); - [*#item.title.at(lang)* #item.place.at(lang) #h(1fr)]; - [#par(item.abstract.at(lang))] + assert("title" in item and "place" in item, message: "Thesis items require type and title.") + [*#item.title.at(lang)* #item.place.at(lang) #h(1fr)] + [#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 [)] - }) - } + 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 resume(contents, main: ("experience", "education"), sidebar: ("volunteering", "languages", "skills")) = { - set text(lang: lang) + set text(lang: lang) - let date_formatting = { - if lang == "de" { - "[day]. [month repr:long] [year]" - } else { - "[month repr:long] [day], [year]" - } + 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 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) + set par(justify: true) - header(contents.about) + 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 = sel_word_lang(en:"Professional Experience", de:"Berufserfahrung") - section(title: title)[] - by_experience_type(experience: contents.experience, type: contents.experience_types) - } - - if "education" in main and "education" in contents { - let title = sel_word_lang(en:"Education", de:"Ausbildung") - section(title: title, entries:contents.thesis + contents.education)[] - } - - if "volunteering" in main and "volunteering" in contents { - let title = sel_word_lang(en:"Volunteer Work", de:"Ehrenamt") - section(title: title, entries:contents.volunteering)[] - } - - if "skills" in main and "skills" in contents { - let title = sel_word_lang(en:"Qualifications", de:"Qualifikationen") - section(title: title, { - skill_item(item:contents.skills) - }) - } - - if "languages" in main and "languages" in contents { - let title = sel_word_lang(en:"Languages", de:"Sprachen") - section(title: title, { - skill_item(item:contents.languages) - }) - } + let body = { + if "summary" in main and "summary" in contents { + section( + title: "", + { + contents.summary.at(lang) + }, + ) } - let sidebar = { - if "volunteering" in sidebar and "volunteering" in contents { - let title = sel_word_lang(en:"Volunteer Work", de:"Ehrenamt") - [== #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 = sel_word_lang(en:"Languages", de:"Sprachen") - [== #title] - skill_item(item:contents.languages, is_sidebar: true) - [\ ] - } - - if "skills" in sidebar and "skills" in contents { - let title = sel_word_lang(en:"Qualifications", de:"Kenntnisse") - [== #title] - skill_item(item:contents.skills, is_sidebar: true) - } + if "experience" in main and "experience" in contents { + let title = sel_word_lang(en: "Professional Experience", de: "Berufserfahrung") + section(title: title)[] + by_experience_type(experience: contents.experience, type: contents.experience_types) } - 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%, + if "education" in main and "education" in contents { + let title = sel_word_lang(en: "Education", de: "Ausbildung") + section(title: title, entries: contents.thesis + contents.education)[] + } + + if "volunteering" in main and "volunteering" in contents { + let title = sel_word_lang(en: "Volunteer Work", de: "Ehrenamt") + section(title: title, entries: contents.volunteering)[] + } + + if "skills" in main and "skills" in contents { + let title = sel_word_lang(en: "Qualifications", de: "Qualifikationen") + section( + title: title, + { + skill_item(item: contents.skills) + }, + ) + } + + if "languages" in main and "languages" in contents { + let title = sel_word_lang(en: "Languages", de: "Sprachen") + section( + title: title, + { + skill_item(item: contents.languages) + }, + ) + } + } + + let sidebar = { + if "volunteering" in sidebar and "volunteering" in contents { + let title = sel_word_lang(en: "Volunteer Work", de: "Ehrenamt") + [== #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 = sel_word_lang(en: "Languages", de: "Sprachen") + [== #title] + skill_item(item: contents.languages, is_sidebar: true) + [\ ] + } + + if "skills" in sidebar and "skills" in contents { + let title = sel_word_lang(en: "Qualifications", de: "Kenntnisse") + [== #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: 20 * margin, right: 20 * margin)) @@ -297,13 +326,12 @@ set par(justify: false) align(right, block(it)) } - sidebar + sidebar v(15pt) - })) - ) - + }, + ), + ), + ) } -#resume( - yaml("content.yml") -) +#resume(yaml("content.yml")) From 0beef3934b28a0eb7019a6627a986599b944073b Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 28 Jan 2025 21:39:28 +0100 Subject: [PATCH 09/11] Hide unused experience types --- resume.typ | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/resume.typ b/resume.typ index 27938a0..873fd28 100644 --- a/resume.typ +++ b/resume.typ @@ -83,14 +83,11 @@ #let by_experience_type(type: (), experience: ()) = { let by_ty = (:) for (id, desc) in type { - let matching_exp_items = () - // for item in experience { - // [itemtypeid: #item.typeid, id: #id] - // if item.typeid == id { - // matching_exp_items.push(item) - // } - // } 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) } From c36a5cde83ab3beb667e871264885b1d9fa0c3fd Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 28 Jan 2025 21:39:43 +0100 Subject: [PATCH 10/11] Remove sel_word_lang function --- resume.typ | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/resume.typ b/resume.typ index 873fd28..6176dc6 100644 --- a/resume.typ +++ b/resume.typ @@ -17,13 +17,6 @@ "en" } } -#let sel_word_lang(de: "", en: "") = { - if lang == "de" { - de - } else { - en - } -} #let _columns_3(left_body, center_body, right_body) = { block[ @@ -220,23 +213,23 @@ } if "experience" in main and "experience" in contents { - let title = sel_word_lang(en: "Professional Experience", de: "Berufserfahrung") + 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 = sel_word_lang(en: "Education", de: "Ausbildung") + 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 = sel_word_lang(en: "Volunteer Work", de: "Ehrenamt") + 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 = sel_word_lang(en: "Qualifications", de: "Qualifikationen") + let title = (en: "Qualifications", de: "Qualifikationen").at(lang) section( title: title, { @@ -246,7 +239,7 @@ } if "languages" in main and "languages" in contents { - let title = sel_word_lang(en: "Languages", de: "Sprachen") + let title = (en: "Languages", de: "Sprachen").at(lang) section( title: title, { @@ -258,7 +251,7 @@ let sidebar = { if "volunteering" in sidebar and "volunteering" in contents { - let title = sel_word_lang(en: "Volunteer Work", de: "Ehrenamt") + let title = (en: "Volunteer Work", de: "Ehrenamt").at(lang) [== #title] for e in contents.at("volunteering") { [ @@ -269,14 +262,14 @@ } if "languages" in sidebar and "languages" in contents { - let title = sel_word_lang(en: "Languages", de: "Sprachen") + 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 = sel_word_lang(en: "Qualifications", de: "Kenntnisse") + let title = (en: "Qualifications", de: "Kenntnisse").at(lang) [== #title] skill_item(item: contents.skills, is_sidebar: true) } From 8788c0a647412e586addf242822dd214be4a9d17 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 28 Jan 2025 21:43:33 +0100 Subject: [PATCH 11/11] Update content slightly --- content.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/content.yml b/content.yml index 0086afc..93b6526 100644 --- a/content.yml +++ b/content.yml @@ -37,6 +37,9 @@ experience_types: 2: de: Honorararbeit en: Salaried work + 3: + de: Gewerbeschein + en: Trade license experience: - typeid: 1 @@ -304,6 +307,7 @@ experience: de: 2014--2018 en: 2014--2018 typeid: 2 + hidden: true # TODO: Allow hiding entries like this? title: de: Verkaufsassistent und Eventhelfer für historische Märkte en: Sales Assistant and Event Support for Historical Markets @@ -350,8 +354,8 @@ education: de: HTWK Leipzig, Deutschland en: HTWK Leipzig, Germany title: - de: Medieninformatik, BSc (nicht abgeschlossen) - en: Media Computer Science, BSc (not completed) + de: Medieninformatik, BSc (nicht abg.) + en: Media Computer Science, BSc (not compl.) date: de: 2015 en: 2015