From f404bf3c55cb7b9665c4293849a54768b68f68fe Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Tue, 18 Mar 2025 14:57:57 +0100 Subject: [PATCH 01/10] 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 02/10] 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 03/10] 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 04/10] 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 05/10] 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 06/10] 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 07/10] 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 08/10] 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 09/10] 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 10/10] 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