From 0dd715d8069e33b3298c713e69d1cbc9541d8267 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 18 Mar 2025 14:43:43 +0100 Subject: [PATCH 01/30] fix(content): Rephrase ZeitRaum support consultations --- content.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content.yml b/content.yml index 2afb449..0cd575d 100644 --- a/content.yml +++ b/content.yml @@ -386,8 +386,8 @@ volunteering: en: Transferring Digital Competence in Aging - de: Wöchentlicher Workshop zur Entwicklung von Selbstvertrauen und Kompetenz mit Smartphones en: Weekly workshop on developing confidence and competence with smartphones - - de: Personalisierte technische Hilfsmeetings und technische Support-Beratungen - en: Personalized tech assistance appointments and technical support consultations + - de: Personalisierte technische Unterstützung und personalisierte Einzelberatungen + en: Personalized tech assistance appointments and individual technical support consultations - title: de: Verpixelt en: Verpixelt From f60991344db7d8212db2d926bb18d9e2017aaf18 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 18 Mar 2025 14:43:43 +0100 Subject: [PATCH 02/30] feat(resume): Choose experience display by client,type,chronological Can be set by setting the point in 'main' array to 'experience_by_client', 'experience_by_type' or just 'experience' respectively. --- resume.typ | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/resume.typ b/resume.typ index 789e029..16119f4 100644 --- a/resume.typ +++ b/resume.typ @@ -33,7 +33,7 @@ } } -#let resume(contents, main: ("experience", "education"), sidebar: ("volunteering", "languages", "skills")) = { +#let resume(contents, main: ("experience_by_type", "education"), sidebar: ("volunteering", "languages", "skills")) = { show: style show: smartypants set text(lang: lang) @@ -77,11 +77,20 @@ ) } - if "experience" in main and "experience" in contents { + if "experience_by_type" 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 "experience_by_client" in main and "experience" in contents { + let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang) + section(title: title)[] + by_client(experience: contents.experience) + } + if "experience" in main and "experience" in contents { + let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang) + section(title: title, entries: contents.experience)[] + } if "education" in main and "education" in contents { let title = (en: "Education", de: "Ausbildung").at(lang) From 1a62b90df77a38ba20d5409f97482777baaa4002 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 18 Mar 2025 14:57:57 +0100 Subject: [PATCH 03/30] feat(resume): Add ability to reorder sections Both in main and in sidebar, sections can be reordered simply by changing the order they appar in the array adding them. `#resume.with(main:("education", "experience"))` displays education before experience items. --- resume.typ | 136 +++++++++++++++++++++++++++-------------------------- 1 file changed, 70 insertions(+), 66 deletions(-) diff --git a/resume.typ b/resume.typ index 16119f4..8309611 100644 --- a/resume.typ +++ b/resume.typ @@ -68,84 +68,88 @@ header(contents.about) let body = { - if "summary" in main and "summary" in contents { - section( - title: "", - { - contents.summary.at(lang) - }, - ) - } + for item in main { + if item == "summary" and "summary" in contents { + section( + title: "", + { + contents.summary.at(lang) + }, + ) + } - if "experience_by_type" 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 "experience_by_client" in main and "experience" in contents { - let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang) - section(title: title)[] - by_client(experience: contents.experience) - } - if "experience" in main and "experience" in contents { - let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang) - section(title: title, entries: contents.experience)[] - } + 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 "education" in main and "education" in contents { - let title = (en: "Education", de: "Ausbildung").at(lang) - section(title: title, entries: contents.thesis + contents.education)[] - } + if item == "education" 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 item == "volunteering" 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 item == "skills" 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) - }, - ) + if item == "languages" 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)) \ - ] + 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 "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 item == "languages" 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) + if item == "skills" and "skills" in contents { + let title = (en: "Qualifications", de: "Kenntnisse").at(lang) + [== #title] + skill_item(item: contents.skills, is_sidebar: true) + } } } From 37c59e71db0e32849c0dc07616a04ce06ab09d67 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 18 Mar 2025 14:57:57 +0100 Subject: [PATCH 04/30] docs: Update README Give light hints to how the section arrays work. --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ae62ba2..29a97f8 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,28 @@ Is called like the following: ``` This is the default invocation, though sidebar and main body sections can be exchanged at will. +The following sections currently exist: + +- education +- 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. + +## Advanced experience 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. TODO: @@ -42,4 +64,4 @@ TODO: - [x] Can be switched between Ger/En - [x] Try sidebar version - [ ] Generalize sidebar version through abstraction/extractions -- [ ] unify items: experience/education/(thesis?) +- [x] unify items: experience/education/(thesis?) From c831b008b93b9875646c7dd7c22b253eb4bc9b51 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 18 Mar 2025 14:57:57 +0100 Subject: [PATCH 05/30] ref(resume): Move by_client and by_experience_type to lib In the continuous process of moving everything away from the main resume body, move these experience functions away. --- lib.typ | 33 +++++++++++++++++++++++++++++++++ resume.typ | 33 --------------------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/lib.typ b/lib.typ index 09b33e1..c27d018 100644 --- a/lib.typ +++ b/lib.typ @@ -112,6 +112,39 @@ } }; +// 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 #let education_entry(item: ()) = { assert( diff --git a/resume.typ b/resume.typ index 8309611..df2715a 100644 --- a/resume.typ +++ b/resume.typ @@ -1,38 +1,5 @@ #import "lib.typ": * -// 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_by_type", "education"), sidebar: ("volunteering", "languages", "skills")) = { show: style show: smartypants From df480875df8eb088ff4c1e2a17d7d803a3b978bc Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 18 Mar 2025 14:57:57 +0100 Subject: [PATCH 06/30] ref(lib): Rename skill_item to sidebar_entry We use the function for all entries in the sidebar (volunteering, languages, etc) not just skills. --- cv.typ | 8 ++++---- lib.typ | 36 ++++++++++++++++++------------------ resume.typ | 9 ++++----- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/cv.typ b/cv.typ index 227584a..19a5459 100644 --- a/cv.typ +++ b/cv.typ @@ -65,7 +65,7 @@ section( title: title, { - skill_item(item: contents.skills) + sidebar_entry(item: contents.skills) }, ) } @@ -75,7 +75,7 @@ section( title: title, { - skill_item(item: contents.languages) + sidebar_entry(item: contents.languages) }, ) } @@ -97,14 +97,14 @@ if "languages" in contents { let title = (en: "Languages", de: "Sprachen").at(lang) [== #title] - skill_item(item: contents.languages, is_sidebar: true) + sidebar_entry(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) + sidebar_entry(item: contents.skills, is_sidebar: true) } } diff --git a/lib.typ b/lib.typ index c27d018..d82a0a1 100644 --- a/lib.typ +++ b/lib.typ @@ -90,6 +90,24 @@ } } + +#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() = { v(-3pt) line(length: 100%) @@ -161,21 +179,3 @@ [*#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 [)] - }) - } -} diff --git a/resume.typ b/resume.typ index df2715a..42764df 100644 --- a/resume.typ +++ b/resume.typ @@ -75,7 +75,7 @@ section( title: title, { - skill_item(item: contents.skills) + sidebar_entry(item: contents.skills) }, ) } @@ -85,7 +85,7 @@ section( title: title, { - skill_item(item: contents.languages) + sidebar_entry(item: contents.languages) }, ) } @@ -108,14 +108,14 @@ if item == "languages" and "languages" in contents { let title = (en: "Languages", de: "Sprachen").at(lang) [== #title] - skill_item(item: contents.languages, is_sidebar: true) + sidebar_entry(item: contents.languages, is_sidebar: true) [\ ] } if item == "skills" and "skills" in contents { let title = (en: "Qualifications", de: "Kenntnisse").at(lang) [== #title] - skill_item(item: contents.skills, is_sidebar: true) + sidebar_entry(item: contents.skills, is_sidebar: true) } } } @@ -150,7 +150,6 @@ align( right, block( - fill: luma(250), width: 90%, { v(15pt) From 72d40e509447c8ebd8f8db5859b5b10221cf9e2a Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 18 Mar 2025 14:57:57 +0100 Subject: [PATCH 07/30] ref(smartypants): Move smartypants application to lib --- lib.typ | 15 ++++++++------- resume.typ | 1 - 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib.typ b/lib.typ index d82a0a1..ff4c0ed 100644 --- a/lib.typ +++ b/lib.typ @@ -1,11 +1,4 @@ -// set some styles -#let style(it) = { - show heading: set text(font: "New Computer Modern") - show link: underline - it -} - // transform md-similes to actual symbols #let smartypants(it) = { // smartypants and latex compatibility @@ -15,6 +8,14 @@ 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 // // $ typst compile --input lang=de cv.typ diff --git a/resume.typ b/resume.typ index 42764df..2930ed1 100644 --- a/resume.typ +++ b/resume.typ @@ -2,7 +2,6 @@ #let resume(contents, main: ("experience_by_type", "education"), sidebar: ("volunteering", "languages", "skills")) = { show: style - show: smartypants set text(lang: lang) let date_formatting = { From f404bf3c55cb7b9665c4293849a54768b68f68fe Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 18 Mar 2025 14:57:57 +0100 Subject: [PATCH 08/30] feat(sidebar): Make sidebar visible backgrounded Just add a grey background for now. --- resume.typ | 1 + 1 file changed, 1 insertion(+) diff --git a/resume.typ b/resume.typ index 2930ed1..41715dd 100644 --- a/resume.typ +++ b/resume.typ @@ -149,6 +149,7 @@ align( right, block( + fill: luma(230), width: 90%, { v(15pt) From 2970745b7dc4ad5e9a2106dde379b5b3e560b863 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 18 Mar 2025 14:57:57 +0100 Subject: [PATCH 09/30] ref(resume): Extract column creation from resume func Extracted into their own functions called 'create_main' and 'create_sidebar' for now. --- resume.typ | 177 +++++++++++++++++++++++++++-------------------------- 1 file changed, 89 insertions(+), 88 deletions(-) diff --git a/resume.typ b/resume.typ index 41715dd..dcc4ca9 100644 --- a/resume.typ +++ b/resume.typ @@ -1,6 +1,92 @@ #import "lib.typ": * -#let resume(contents, main: ("experience_by_type", "education"), sidebar: ("volunteering", "languages", "skills")) = { +#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", "education"), sidebar: ("volunteering", "languages", "skills")) = { show: style set text(lang: lang) @@ -33,91 +119,6 @@ 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( @@ -143,7 +144,7 @@ ), ) }) - body + create_body(main: main, contents: contents) }, ), align( @@ -160,7 +161,7 @@ set par(justify: false) align(right, block(it)) } - sidebar + create_sidebar(sidebar: sidebar, contents: contents) v(15pt) }, ), From bb9606b2db1d3f249a002feba14dc79cf0ca75ba Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 18 Mar 2025 14:57:57 +0100 Subject: [PATCH 10/30] fix(content): Rephrase partial completion of BA --- content.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content.yml b/content.yml index 0cd575d..af23b8b 100644 --- a/content.yml +++ b/content.yml @@ -355,7 +355,7 @@ education: en: HTWK Leipzig, Germany title: de: Medieninformatik, BSc (nicht abg.) - en: Media Computer Science, BSc (not compl.) + en: Media Computer Science, BSc (incompl.) date: de: 2015 en: 2015 From 1afc51a857c27695a750fd3e3594d76083b3bff8 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 18 Mar 2025 14:57:57 +0100 Subject: [PATCH 11/30] feat(resume): Add wrapit library to dynamically add sidebar Removes issue that grid will be static throughout all pages and thus empty space where the sidebar is on page 1. Now, the sidebar aligns nicely along the first page but then we can use the full width for the next few pages. --- resume.typ | 77 ++++++------ wrapit.typ | 340 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 374 insertions(+), 43 deletions(-) create mode 100644 wrapit.typ diff --git a/resume.typ b/resume.typ index dcc4ca9..4c9f55c 100644 --- a/resume.typ +++ b/resume.typ @@ -1,4 +1,5 @@ #import "lib.typ": * +#import "wrapit.typ": * #let create_body(main: (), contents: (:)) = { for item in main { @@ -86,7 +87,7 @@ } } -#let resume(contents, main: ( "experience", "education"), sidebar: ("volunteering", "languages", "skills")) = { +#let resume(contents, main: ("experience", "education"), sidebar: ("volunteering", "languages", "skills")) = { show: style set text(lang: lang) @@ -119,53 +120,43 @@ 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 - grid( - columns: (2fr, 1fr), + let sb = if sidebar.len() > 0 { block( - outset: 0pt, - inset: (top: 0.4 * margin, right: 0pt, rest: margin), - stroke: none, - width: 100%, + fill: luma(230), + inset: (top: 15 * margin, left: 10 * margin, right: 15 * margin, bottom: 15 * margin), { - 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), - ), - ) - }) - create_body(main: main, contents: contents) + 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) }, - ), - align( - right, - block( - fill: luma(230), - 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)) - } - create_sidebar(sidebar: sidebar, contents: contents) - v(15pt) - }, - ), - ), + ) + } else { [] } + wrap-content( + sb, + create_body(main: main, contents: contents), + align: top + right, + columns: (auto, 30%), ) } diff --git a/wrapit.typ b/wrapit.typ new file mode 100644 index 0000000..12114c0 --- /dev/null +++ b/wrapit.typ @@ -0,0 +1,340 @@ +// 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 + ] + ] + }) +} From eace89411b2fd92bf82c7f3f856565e4fdcffbec Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 18 Mar 2025 16:52:51 +0100 Subject: [PATCH 12/30] fix(content): Remove duplicate Roskilde University --- content.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content.yml b/content.yml index af23b8b..b647213 100644 --- a/content.yml +++ b/content.yml @@ -143,7 +143,7 @@ experience: typeid: 1 title: de: Redaktionsarbeit, Soziale Absicherung und Widerstandsfähigkeit - en: Editorial work, Social Protection and Resilience, Roskilde University + en: Editorial work, Social Protection and Resilience place: de: Universität Roskilde en: Roskilde University From 21d201076243e3025145cb26a1f8532f5f84db66 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 18 Mar 2025 17:00:01 +0100 Subject: [PATCH 13/30] ref(repo): Refactor resume into single importable function Moved all 'behind-the-scenes' structure to 'lib/' folder and made resume importable as the main utility. --- .gitignore | 2 +- cv.typ | 161 ++-------------------------------- lib.typ => lib/lib.typ | 0 lib/resume.typ | 162 ++++++++++++++++++++++++++++++++++ wrapit.typ => lib/wrapit.typ | 0 resume.typ | 164 +---------------------------------- 6 files changed, 171 insertions(+), 318 deletions(-) rename lib.typ => lib/lib.typ (100%) create mode 100644 lib/resume.typ rename wrapit.typ => lib/wrapit.typ (100%) diff --git a/.gitignore b/.gitignore index c5bc47a..0dd50d0 100644 --- a/.gitignore +++ b/.gitignore @@ -51,7 +51,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ lib64/ parts/ sdist/ @@ -62,6 +61,7 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +# lib/ # PyInstaller # Usually these files are written by a python script from a template diff --git a/cv.typ b/cv.typ index 19a5459..643ee39 100644 --- a/cv.typ +++ b/cv.typ @@ -1,157 +1,8 @@ -#import "lib.typ": * +#import "lib/resume.typ": resume -#let cv(contents, use_sidebar: false) = { - 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 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")) +#resume.with( + content: yaml("content.yml"), + main: ("summary", "experience", "education", "volunteering", "skills", "languages"), + sidebar:(), +) diff --git a/lib.typ b/lib/lib.typ similarity index 100% rename from lib.typ rename to lib/lib.typ diff --git a/lib/resume.typ b/lib/resume.typ new file mode 100644 index 0000000..e01dcf2 --- /dev/null +++ b/lib/resume.typ @@ -0,0 +1,162 @@ +#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%), + ) +} + diff --git a/wrapit.typ b/lib/wrapit.typ similarity index 100% rename from wrapit.typ rename to lib/wrapit.typ diff --git a/resume.typ b/resume.typ index 4c9f55c..f94862c 100644 --- a/resume.typ +++ b/resume.typ @@ -1,163 +1,3 @@ -#import "lib.typ": * -#import "wrapit.typ": * +#import "lib/resume.typ": resume -#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", "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%), - ) -} - -#resume(yaml("content.yml")) +#resume.with()(yaml("content.yml")) From 51b43f4d9ce7bba34a15ac184468deef35384b7b Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 19 Mar 2025 12:53:51 +0100 Subject: [PATCH 14/30] ref(lib): Move all logic to lib The *.typ files in root will only make use of the backing library, invoking the bare mininum. From b061d853bc6c9288e4c924216c9a1e86191cce4f Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 19 Mar 2025 14:15:43 +0100 Subject: [PATCH 15/30] fix(cv): Fix CV creation --- cv.typ | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cv.typ b/cv.typ index 643ee39..4f8333e 100644 --- a/cv.typ +++ b/cv.typ @@ -1,8 +1,7 @@ #import "lib/resume.typ": resume #resume.with( - content: yaml("content.yml"), main: ("summary", "experience", "education", "volunteering", "skills", "languages"), - sidebar:(), -) + sidebar:() +)(yaml("content.yml")) From 434cb0717e988440d622bdaad34a22e7bef988ae Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 19 Mar 2025 14:19:26 +0100 Subject: [PATCH 16/30] fix(cv): Remove summary from CV --- cv.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cv.typ b/cv.typ index 4f8333e..c1f6971 100644 --- a/cv.typ +++ b/cv.typ @@ -1,7 +1,7 @@ #import "lib/resume.typ": resume #resume.with( - main: ("summary", "experience", "education", "volunteering", "skills", "languages"), + main: ("experience", "education", "volunteering", "skills", "languages"), sidebar:() )(yaml("content.yml")) From 1e6c9059e8bb9e0fad6e21f5d05315e9478ddd19 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 26 Mar 2025 17:54:23 +0100 Subject: [PATCH 17/30] feat(content): Add event logistics experience --- content.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/content.yml b/content.yml index b647213..4c29388 100644 --- a/content.yml +++ b/content.yml @@ -62,6 +62,23 @@ experience: 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 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: de: 2023--2024 en: 2023--2024 From 82965954bb065b09f4bce7ee3a80fe82c181b47c Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 21 May 2025 16:27:17 +0200 Subject: [PATCH 18/30] feat(CV): Add digital work section --- content.yml | 34 ++++++++++++++++++++++++++++++++++ cv.typ | 2 +- lib/resume.typ | 4 ++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/content.yml b/content.yml index 4c29388..cb7a4f1 100644 --- a/content.yml +++ b/content.yml @@ -420,6 +420,40 @@ volunteering: - de: IT und Event Organisationsunterstützung 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/Rocky Linux/Windows mit vollem Hardware- und Grafik-Passthrough + en: Separate VMs running Debian/Rocky Linux/Windows with full hardware and graphics passthrough + skills: - name: de: Office-Suite diff --git a/cv.typ b/cv.typ index c1f6971..e43f917 100644 --- a/cv.typ +++ b/cv.typ @@ -1,7 +1,7 @@ #import "lib/resume.typ": resume #resume.with( - main: ("experience", "education", "volunteering", "skills", "languages"), + main: ("experience", "education", "volunteering", "digital", "skills", "languages"), sidebar:() )(yaml("content.yml")) diff --git a/lib/resume.typ b/lib/resume.typ index e01dcf2..3870bef 100644 --- a/lib/resume.typ +++ b/lib/resume.typ @@ -36,6 +36,10 @@ let title = (en: "Volunteer Work", de: "Ehrenamt").at(lang) section(title: title, entries: contents.volunteering)[] } + if item == "digital" and "digital" in contents { + let title = (en: "Digital Organization", de: "Digitales Schaffen").at(lang) + section(title: title, entries: contents.digital)[] + } if item == "skills" and "skills" in contents { let title = (en: "Qualifications", de: "Qualifikationen").at(lang) From 0399bed2de4af654bd3506c32ab3ce720cd9e465 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Wed, 21 May 2025 16:27:17 +0200 Subject: [PATCH 19/30] fix(CV): Improve English summary --- content.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/content.yml b/content.yml index cb7a4f1..e6ef213 100644 --- a/content.yml +++ b/content.yml @@ -20,15 +20,19 @@ summary: 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. - 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. + 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. - Ich bin offen für Gelegenheiten, die meine Expertise in diesen Bereichen vertiefen, jedoch auch für solche, die meine Kenntnisbereiche erweitern. + 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 begrüße Gelegenheiten, meine Expertise in diesen Bereichen zu erweitern, jedoch auch solche, die meine Kenntnisbereiche weiter vergrößern. 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 and web administration. Beyond professional work I strive to 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, 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. - I welcome opportunities to advance my expertise in these topics, in addition to those expanding my range of applicable skills. + 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 further expanding my range of applicable skills. experience_types: 1: From 54d58e104aeb761123ddf58096e7497a5fbd81c2 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Mon, 7 Apr 2025 14:46:19 +0200 Subject: [PATCH 20/30] feat(content): Update lang --- content.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/content.yml b/content.yml index e6ef213..5760d39 100644 --- a/content.yml +++ b/content.yml @@ -64,7 +64,7 @@ experience: en: Quantitative analysis of connections between social protection and formalization - 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" - - de: Editorielle Vorbereitung eines Arbeitspapiers auf eine Veröffentlichung durch wissenschaftlichen Verlag + - de: Editorielle Vorbereitung eines Arbeitspapiers zur Veröffentlichung durch wissenschaftlichen Verlag en: Editorial adaptation from a working paper towards a journal article ready for publishing - date: de: 2023--2024 @@ -419,8 +419,8 @@ volunteering: de: Urban Souls e.V. en: Urban Souls association bullets: - - de: Technische Assistenz zur Vermittlung digitaler Kompetenzen - en: Technical assistance for conveying digital competencies + - de: Technische Assistenz zur Vermittlung digitaler Kompetenzen an Kinder und Jugendliche + en: Technical assistance for conveying digital competencies for children and youth - de: IT und Event Organisationsunterstützung en: IT and event management services @@ -557,8 +557,8 @@ languages: de: Englisch en: English items: - - de: fließend - en: fluent + - de: verhandlungssicher + en: business fluent - name: de: Französisch en: French From f2dff448f8b12b24d7903370c49b2b04ada7bd04 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Mon, 19 May 2025 12:46:01 +0200 Subject: [PATCH 21/30] fix(content): Fix publication and spelling mistake --- content.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/content.yml b/content.yml index 5760d39..e9a1852 100644 --- a/content.yml +++ b/content.yml @@ -82,7 +82,7 @@ experience: - 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 + en: Relocation and placement of decorative and functional elements at various locations - date: de: 2023--2024 en: 2023--2024 @@ -131,8 +131,8 @@ experience: de: UNU-WIDER en: UNU-WIDER publication: - de: Niño-Zarazúa, M., \& Morabito, C. (angenommen). Assessing the potential distributional impacts of development interventions. UNU-WIDER. - en: Niño-Zarazúa, M., \& Morabito, C. (forthcoming). Assessing the potential distributional impacts of development interventions. UNU-WIDER. + 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." + 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." 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 en: Collected, processed, and cleaned 4 datasets, including UN World inequality trends From 5f18397212b0e021630382bc3e009797aec6c400 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Thu, 20 Mar 2025 09:57:38 +0100 Subject: [PATCH 22/30] fix(content): Fix German wording programming --- content.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content.yml b/content.yml index e9a1852..0f3d93a 100644 --- a/content.yml +++ b/content.yml @@ -498,7 +498,7 @@ skills: - de: Seaborn en: Seaborn - name: - de: Programmieren + de: Programmierung en: Programming items: - de: Python From 641cf03bbcdbb86c1618071aca1961e33bce51c8 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Mon, 25 Aug 2025 11:33:22 +0200 Subject: [PATCH 23/30] feat(lib): Print relevant education modules --- lib/lib.typ | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/lib.typ b/lib/lib.typ index ff4c0ed..d0000fc 100644 --- a/lib/lib.typ +++ b/lib/lib.typ @@ -83,6 +83,12 @@ [- #bullet.at(lang)] } } + if "modules" in item { + subdued[Relevante Module:] + for bullet in item.modules { + subdued[ - #bullet.at(lang)] + } + } if "publication" in item { subdued[#item.publication.at(lang) \ ] } From 37c5f0ec0c4c6cb3cc30544094e0055b99c76e1d Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Mon, 25 Aug 2025 11:33:22 +0200 Subject: [PATCH 24/30] feat(content): Add relevant IT modules --- content.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/content.yml b/content.yml index 0f3d93a..c3d0781 100644 --- a/content.yml +++ b/content.yml @@ -380,6 +380,19 @@ education: date: de: 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: - title: From ab25f44b58c826d4301c72839d82314b82e76beb Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Mon, 25 Aug 2025 11:49:42 +0200 Subject: [PATCH 25/30] docs: Update README --- README.md | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 29a97f8..0fdead1 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,26 @@ The following sections currently exist: Sections in the main body or sidebar can be reordered at will. -## Advanced experience settings +Any individual entry can be hidden by adding a simple `hidden: true` to the relevant YAML entry +(works for all sections). + +A simple example education entry: + +```yaml +education: + - place: + de: Universität der Welt, Planet Erde + en: University of the World, Planet Earth + title: + de: Globale Friedensstiftung, MA + en: Global Peace Foundations, MA + date: + de: 2016 + en: 2016 + hidden: false +``` + +## Advanced experience presentation settings The experience section has 3 forms: @@ -41,27 +60,12 @@ 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. -TODO: +## Data-driven uses -- [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? +Since the CV _data_ is separated from the CV _presentation_, it is possible to create different +presentations for the same underlying data structures. -- [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 -- [x] unify items: experience/education/(thesis?) +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. From febdcb9796965b6a9e4a262bb16dfb13a54447ce Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Mon, 25 Aug 2025 11:33:22 +0200 Subject: [PATCH 26/30] feat(lib): Add entry hiding with content boolean Add the simple value `hidden: true` to any entry (experience, education, volunteering, skills, language, etc) and it will not be displayed on the final output. --- lib/lib.typ | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/lib.typ b/lib/lib.typ index d0000fc..661248d 100644 --- a/lib/lib.typ +++ b/lib/lib.typ @@ -64,7 +64,10 @@ block(inset: 5%, width: 85%, text(fill: luma(150), body)) } -#let entry(item: ()) = { +#let entry(item: (), show_sublists: true) = { + if "hidden" in item and item.hidden == true { + return + } if "title" in item { [*#item.title.at(lang)*] } @@ -78,15 +81,17 @@ if "date" in item { [ _#item.date.at(lang)_ \ ] } - if "bullets" in item { - for bullet in item.bullets { - [- #bullet.at(lang)] + if show_sublists == true { + if "bullets" in item { + for bullet in item.bullets { + [- #bullet.at(lang)] + } } - } - if "modules" in item { - subdued[Relevante Module:] - for bullet in item.modules { - subdued[ - #bullet.at(lang)] + if "modules" in item { + subdued[Relevante Module:] + for bullet in item.modules { + subdued[ - #bullet.at(lang)] + } } } if "publication" in item { From 8972f3c687f1720141557d9fbb3ab3e25e7093ad Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Mon, 15 Sep 2025 10:19:49 +0200 Subject: [PATCH 27/30] fix(content): Generalize mainbranch location to Berlin --- content.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content.yml b/content.yml index c3d0781..c84c49a 100644 --- a/content.yml +++ b/content.yml @@ -1,7 +1,7 @@ about: fullname: Marty Oehme contact: - - text: Pichelsdorfer Str. 133, 13595 Berlin, Germany + - text: Berlin, Germany icon:  - text: contact@martyoeh.me icon:  From 16b561ff86aa49400c6452a751f13e19ce409ce9 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Mon, 15 Sep 2025 10:19:49 +0200 Subject: [PATCH 28/30] fix(content): Refine German Consultancy description While the official title was accurate, this provides a more fitting description of my day-to-day activities. --- content.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content.yml b/content.yml index c84c49a..c33ebeb 100644 --- a/content.yml +++ b/content.yml @@ -36,7 +36,7 @@ summary: experience_types: 1: - de: Selbstständiger Schriftsteller Forschung + de: Selbstständiger Consultant Forschung en: Independent research consultant 2: de: Honorararbeit From 2684de9cc0db5466791adbe92287e6c0070cadd1 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Mon, 15 Sep 2025 10:33:01 +0200 Subject: [PATCH 29/30] fix(content): Classify RHEL-class Linux under single umbrella Instead of deciding between CentOS, Rocky Linux and AlmaLinux, just mention the RHEL family to abstract from individual implementations. --- content.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content.yml b/content.yml index c33ebeb..6ecd9bf 100644 --- a/content.yml +++ b/content.yml @@ -468,8 +468,8 @@ digital: 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/Rocky Linux/Windows mit vollem Hardware- und Grafik-Passthrough - en: Separate VMs running Debian/Rocky Linux/Windows with full hardware and graphics passthrough + - 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: - name: From e8d9f678c2ce953f1acd6d16b91307d1d9ff43e9 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Mon, 15 Sep 2025 10:19:49 +0200 Subject: [PATCH 30/30] feat(resume): Allow toggling longform for main body contents --- lib/lib.typ | 4 ++-- lib/resume.typ | 21 ++++++++++++--------- resume.typ | 2 +- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/lib.typ b/lib/lib.typ index 661248d..4eea33b 100644 --- a/lib/lib.typ +++ b/lib/lib.typ @@ -131,11 +131,11 @@ horizon_line() }; -#let section(title: "Section", entries: (), body) = { +#let section(title: "Section", entries: (), longform: true, body) = { section_header(title) if body == none or body == [] { for e in entries { - entry(item: e) + entry(item: e, show_sublists: longform) } } else { body diff --git a/lib/resume.typ b/lib/resume.typ index 3870bef..2a7a087 100644 --- a/lib/resume.typ +++ b/lib/resume.typ @@ -1,7 +1,7 @@ #import "lib.typ": * #import "wrapit.typ": * -#let create_body(main: (), contents: (:)) = { +#let create_body(main: (), contents: (:), longform: true) = { for item in main { if item == "summary" and "summary" in contents { section( @@ -9,36 +9,37 @@ { 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)[] + 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)[] + 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)[] + 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)[] + 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)[] + 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)[] + section(title: title, entries: contents.digital, longform: longform)[] } if item == "skills" and "skills" in contents { @@ -48,6 +49,7 @@ { sidebar_entry(item: contents.skills) }, + longform: longform ) } @@ -58,6 +60,7 @@ { sidebar_entry(item: contents.languages) }, + longform: longform ) } } @@ -91,7 +94,7 @@ } } -#let resume(contents, main: ("experience_by_type", "education"), sidebar: ("volunteering", "languages", "skills")) = { +#let resume(contents, main: ("experience_by_type", "education"), sidebar: ("volunteering", "languages", "skills"), longform:true) = { show: style set text(lang: lang) @@ -158,7 +161,7 @@ } else { [] } wrap-content( sb, - create_body(main: main, contents: contents), + create_body(main: main, contents: contents, longform: longform), align: top + right, columns: (auto, 30%), ) diff --git a/resume.typ b/resume.typ index f94862c..66ed9e5 100644 --- a/resume.typ +++ b/resume.typ @@ -1,3 +1,3 @@ #import "lib/resume.typ": resume -#resume.with()(yaml("content.yml")) +#resume.with(longform: false)(yaml("content.yml"))