diff --git a/.gitignore b/.gitignore index 0dd50d0..c5bc47a 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ dist/ downloads/ eggs/ .eggs/ +lib/ lib64/ parts/ sdist/ @@ -61,7 +62,6 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST -# lib/ # PyInstaller # Usually these files are written by a python script from a template diff --git a/content.yml b/content.yml index 4c29388..0cd575d 100644 --- a/content.yml +++ b/content.yml @@ -62,23 +62,6 @@ 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 @@ -160,7 +143,7 @@ experience: typeid: 1 title: de: Redaktionsarbeit, Soziale Absicherung und Widerstandsfähigkeit - en: Editorial work, Social Protection and Resilience + en: Editorial work, Social Protection and Resilience, Roskilde University place: de: Universität Roskilde en: Roskilde University @@ -372,7 +355,7 @@ education: en: HTWK Leipzig, Germany title: de: Medieninformatik, BSc (nicht abg.) - en: Media Computer Science, BSc (incompl.) + en: Media Computer Science, BSc (not compl.) date: de: 2015 en: 2015 diff --git a/cv.typ b/cv.typ index c1f6971..19a5459 100644 --- a/cv.typ +++ b/cv.typ @@ -1,7 +1,157 @@ -#import "lib/resume.typ": resume +#import "lib.typ": * -#resume.with( - main: ("experience", "education", "volunteering", "skills", "languages"), - sidebar:() -)(yaml("content.yml")) +#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")) diff --git a/lib/lib.typ b/lib.typ similarity index 100% rename from lib/lib.typ rename to lib.typ diff --git a/lib/resume.typ b/lib/resume.typ deleted file mode 100644 index e01dcf2..0000000 --- a/lib/resume.typ +++ /dev/null @@ -1,162 +0,0 @@ -#import "lib.typ": * -#import "wrapit.typ": * - -#let create_body(main: (), contents: (:)) = { - for item in main { - if item == "summary" and "summary" in contents { - section( - title: "", - { - contents.summary.at(lang) - }, - ) - } - - if item == "experience_by_type" and "experience" in contents { - let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang) - section(title: title)[] - by_experience_type(experience: contents.experience, type: contents.experience_types) - } - if item == "experience_by_client" and "experience" in contents { - let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang) - section(title: title)[] - by_client(experience: contents.experience) - } - if item == "experience" and "experience" in contents { - let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang) - section(title: title, entries: contents.experience)[] - } - - if item == "education" and "education" in contents { - let title = (en: "Education", de: "Ausbildung").at(lang) - section(title: title, entries: contents.thesis + contents.education)[] - } - - if item == "volunteering" and "volunteering" in contents { - let title = (en: "Volunteer Work", de: "Ehrenamt").at(lang) - section(title: title, entries: contents.volunteering)[] - } - - if item == "skills" and "skills" in contents { - let title = (en: "Qualifications", de: "Qualifikationen").at(lang) - section( - title: title, - { - sidebar_entry(item: contents.skills) - }, - ) - } - - if item == "languages" and "languages" in contents { - let title = (en: "Languages", de: "Sprachen").at(lang) - section( - title: title, - { - sidebar_entry(item: contents.languages) - }, - ) - } - } -} - -#let create_sidebar(sidebar: (), contents: (:)) = { - for item in sidebar { - if item == "volunteering" and "volunteering" in contents { - let title = (en: "Volunteer Work", de: "Ehrenamt").at(lang) - [== #title] - for e in contents.at("volunteering") { - [ - - *#e.title.at(lang)* (#e.date.at(lang)) - #par(e.bullets.at(0).at(lang)) \ - ] - } - } - - if item == "languages" and "languages" in contents { - let title = (en: "Languages", de: "Sprachen").at(lang) - [== #title] - sidebar_entry(item: contents.languages, is_sidebar: true) - [\ ] - } - - if item == "skills" and "skills" in contents { - let title = (en: "Qualifications", de: "Kenntnisse").at(lang) - [== #title] - sidebar_entry(item: contents.skills, is_sidebar: true) - } - } -} - -#let resume(contents, main: ("experience_by_type", "education"), sidebar: ("volunteering", "languages", "skills")) = { - show: style - set text(lang: lang) - - let date_formatting = { - if lang == "de" { - "[day]. [month repr:long] [year]" - } else { - "[month repr:long] [day], [year]" - } - } - set page( - paper: "a4", - margin: (x: 0.9cm, y: 1.3cm), - footer: [ - #set text( - fill: luma(200), - size: 8pt, - ) - #_columns_3[ - #smallcaps[#datetime.today().display(date_formatting)] - ][ - #smallcaps[#contents.about.fullname] - ][ - #context counter(page).display() - ] - ], - ) - - set par(justify: true) - - header(contents.about) - - set block(above: 10pt) - show heading.where(level: 1): it => style(s => { - let h = text(size: 18pt, upper(it)) - let dim = measure(h, s) - stack( - dir: ltr, - h, - place( - dy: 7pt, - dx: 10pt, - horizon + left, - line(stroke: accent-color, length: 100% - dim.width - 10pt), - ), - ) - }) - - let margin = 1pt - let sb = if sidebar.len() > 0 { - block( - fill: luma(230), - inset: (top: 15 * margin, left: 10 * margin, right: 15 * margin, bottom: 15 * margin), - { - show heading: it => align(right, upper(it)) - set list(marker: "") - show list: it => { - set par(justify: false) - align(right, block(it)) - } - create_sidebar(sidebar: sidebar, contents: contents) - }, - ) - } else { [] } - wrap-content( - sb, - create_body(main: main, contents: contents), - align: top + right, - columns: (auto, 30%), - ) -} - diff --git a/lib/wrapit.typ b/lib/wrapit.typ deleted file mode 100644 index 12114c0..0000000 --- a/lib/wrapit.typ +++ /dev/null @@ -1,340 +0,0 @@ -// https://github.com/ntjess/wrap-it?tab=readme-ov-file -#let styled = text(red)[lorem].func() - -#let _gridded(dir, fixed, to-wrap, ..kwargs) = { - let dir-kwargs = (:) - if dir not in (ltr, rtl) { - panic("Specify either `rtl` or `ltr` as the wrap direction") - } - let args = if dir == rtl { - (to-wrap, fixed) - } else { - (fixed, to-wrap) - } - grid(..args, columns: 2, rows: 2, column-gutter: 1em, ..kwargs) -} - -#let _grid-height(content, container-size) = { - measure(box(width: container-size.width, content)).height -} - -#let _get-chunk(words, end, reverse, start: 0) = { - if end < 0 { - return words.join(" ") - } - if reverse { - words = words.rev() - } - let subset = words.slice(start, end) - if reverse { - subset = subset.rev() - } - subset.join(" ") -} - -#let _get-wrap-index(height-func, words, goal-height, reverse) = { - for index in range(1, words.len(), step: 1) { - let cur-height = height-func(_get-chunk(words, index, reverse)) - if cur-height > goal-height { - return index - 1 - } - } - return -1 -} - -#let _rewrap(element, new-content) = { - let fields = element.fields() - for key in ("body", "text", "children", "child") { - if key in fields { - let _ = fields.remove(key) - } - } - let positional = (new-content,) - if "styles" in fields { - positional.push(fields.remove("styles")) - } - element.func()(..fields, ..positional) -} - -#let split-other(body, height-func, goal-height, align, splitter-func) = { - (wrapped: none, rest: body) -} - -#let split-has-text(body, height-func, goal-height, align, splitter-func) = { - let words = body.text.split(" ") - let reverse = align.y == bottom - let wrap-index = _get-wrap-index(height-func, words, goal-height, reverse) - let _rewrap = _rewrap.with(body) - if wrap-index > 0 { - let chunk = _rewrap(_get-chunk(words, wrap-index, reverse)) - let end-chunk = _rewrap(_get-chunk(words, words.len(), reverse, start: wrap-index)) - ( - wrapped: context { - chunk - linebreak(justify: par.justify) - }, - rest: end-chunk, - ) - } else { - (wrapped: none, rest: body) - } -} - -#let split-has-children(body, height-func, goal-height, align, splitter-func) = { - let reverse = align.y == bottom - let children = if reverse { - body.children.rev() - } else { - body.children - } - for (ii, child) in children.enumerate() { - let prev-children = children.slice(0, ii).join() - let new-height-func(child) = { - height-func((prev-children, child).join()) - } - let height = new-height-func(child) - if height <= goal-height { - continue - } - // height func calculator should now account for prior children - let split = splitter-func(child, new-height-func, goal-height, align) - let new-children = (..children.slice(0, ii), split.wrapped) - let new-rest = children.slice(ii + 1) - if split.rest != none { - new-rest.insert(0, split.rest) - } - if reverse { - new-children = new-children.rev() - new-rest = new-rest.rev() - } - return ( - wrapped: _rewrap(body, new-children), - rest: _rewrap(body, new-rest), - ) - } - panic("This function should only be called if the seq child should be split") -} - -#let split-has-body(body, height-func, goal-height, align, splitter-func) = { - // Elements that can be split and have a 'body' field. - let splittable = (strong, emph, underline, stroke, overline, highlight, list.item, styled) - - let new-height-func(content) = { - height-func(_rewrap(body, content)) - } - let args = (new-height-func, goal-height, align, splitter-func) - let body-text = body.at("body", default: body.at("child", default: none)) - if body.func() in splittable { - let result = splitter-func(body-text, new-height-func, goal-height, align) - if result.wrapped != none { - return (wrapped: _rewrap(body, result.wrapped), rest: _rewrap(body, result.rest)) - } else { - return split-other(body, ..args) - } - } - // Shape doesn't split nicely, so treat it as unwrappable - return split-other(body, ..args) -} - -#let splitter(body, height-func, goal-height, align) = { - let self-height = height-func(body) - if self-height <= goal-height { - return (wrapped: body, rest: none) - } - if type(body) == str { - body = text(body) - } - let body-splitter = if body.has("text") { - split-has-text - } else if body.has("body") or body.has("child") { - split-has-body - } else if body.has("children") { - split-has-children - } else { - split-other - } - return body-splitter(body, height-func, goal-height, align, splitter) -} - -#let _inner-wrap-content(to-wrap, y-align, grid-func, container-size, ..grid-kwargs) = { - let height-func(txt) = _grid-height(grid-func(txt), container-size) - let goal-height = height-func([]) - if y-align == top { - goal-height += measure(v(1em)).height - } - let result = splitter(to-wrap, height-func, goal-height, y-align) - if y-align == top { - grid-func(result.wrapped) - result.rest - } else { - result.rest - grid-func(result.wrapped) - } -} - -/// Places `to-wrap` next to `fixed`, wrapping `to-wrap` as its height overflows `fixed`. -/// -/// *Basic Use:* -/// ```typ -/// #let body = lorem(40) -/// #wrap-content(rect(fill: teal), body) -/// ``` -/// -/// *Something More Fun:* -/// ```typ -/// #set par(justify: true) -/// // Helpers; not required -/// #let grad(map) = { -/// gradient.linear( -/// ..eval("color.map." + map) -/// ) -/// } -/// #let make-fig(fill) = { -/// set figure.caption(separator: "") -/// fill = grad(fill) -/// figure( -/// rect(fill: fill, radius: 0.5em), -/// caption: [], -/// ) -/// } -/// #let (fig1, fig2) = { -/// ("viridis", "plasma").map(make-fig) -/// } -/// #wrap-content(fig1, body, align: right) -/// #wrap-content(fig2, [#body #body], align: bottom) -/// ``` -/// -/// Note that you can increase the distance between a figure's bottom and the wrapped -/// text by boxing it with an inset: -/// ```typ -/// #let spaced = box( -/// make-fig("rocket"), -/// inset: (bottom: 0.3em) -/// ) -/// #wrap-content(spaced, body) -/// ``` -/// -/// - fixed (content): Content that will not be wrapped, (i.e., a figure). -/// -/// - to-wrap (content): Content that will be wrapped, (i.e., text). Currently, logic -/// works best with pure-text content, but hypothetically will work with any `content`. -/// -/// - align (alignment): Alignment of `fixed` relative to `to-wrap`. `top` will align -/// the top of `fixed` with the top of `to-wrap`, and `bottom` will align the bottom of -/// `fixed` with the bottom of `to-wrap`. `left` and `right` alignments determine -/// horizontal alignment of `fixed` relative to `to-wrap`. Alignments can be combined, -/// i.e., `bottom + right` will align the bottom-right corner of `fixed` with the -/// bottom-right corner of `to-wrap`. -/// ```typ -/// #wrap-content( -/// make-fig("turbo"), -/// body, -/// align: bottom + right -/// ) -/// ``` -/// -/// - size (size, auto): Size of the wrapping container. If `auto`, this will be set to -/// the current container size. Otherwise, wrapping logic will attempt to stay within -/// the provided constraints. -/// -/// - ..grid-kwargs (any): Keyword arguments to pass to the underlying `grid` function. -/// Of note: -/// - `column-gutter` controls horizontal margin between `fixed` and `to-wrap`. Or, -/// you can surround the fixed content in a box with `(inset: ...)` for more -/// fine-grained control. -/// - `columns` can be set to force sizing of `fixed` and `to-wrap`. For instance, -/// `columns: (50%, 50%)` will force `fixed` and `to-wrap` to each take up half -/// of the available space. If content isn't this big, the fill will be blank -/// margin. -/// ```typ -/// #let spaced = box( -/// make-fig("mako"), inset: 0.5em -/// ) -/// #wrap-content(spaced, body) -/// ``` -/// ```typ -/// #wrap-content( -/// make-fig("spectral"), -/// body, -/// align: bottom, -/// columns: (50%, 50%), -/// ) -/// ``` -/// -#let wrap-content( - fixed, - to-wrap, - align: top + left, - size: auto, - ..grid-kwargs, -) = { - if center in (align.x, align.y) { - panic("Center alignment is not supported") - } - - // "none" x alignment defaults to left - let dir = if align.x == right { - rtl - } else { - ltr - } - let gridded(..args) = box(_gridded(dir, fixed, ..grid-kwargs, ..args)) - // "none" y alignment defaults to top - let y-align = if align.y == bottom { - bottom - } else { - top - } - - if size != auto { - _inner-wrap-content(to-wrap, y-align, gridded, size, ..grid-kwargs) - } else { - layout(container-size => { - _inner-wrap-content(to-wrap, y-align, gridded, container-size, ..grid-kwargs) - }) - } -} - -/// Wrap a body of text around two pieces of content. The logic only works if enough text -/// exists to overflow both the top and bottom content. Use this instead of 2 separate -/// `wrap-content` calls if you want to avoid a paragraph break between the top and bottom -/// content. -/// -/// *Example:* -/// ```typ -/// #let fig1 = make-fig("inferno") -/// #let fig2 = make-fig("rainbow") -/// #wrap-top-bottom(fig1, fig2, lorem(60)) -/// ``` -/// - top-fixed (content): Content that will not be wrapped, (i.e., a figure). -/// - bottom-fixed (content): Content that will not be wrapped, (i.e., a figure). -/// - body (content): Content that will be wrapped, (i.e., text) -/// - top-kwargs (any): Keyword arguments to pass to the underlying `wrap-content` function -/// for the top content. `x` alignment is kept (left/right), but `y` alignment is -/// overridden to `top`. -/// - bottom-kwargs (any): Keyword arguments to pass to the underlying `wrap-content` function -/// for the bottom content. `x` alignment is kept (left/right), but `y` alignment is -/// overridden to `bottom`. -/// -#let wrap-top-bottom( - top-fixed, - bottom-fixed, - body, - top-kwargs: (:), - bottom-kwargs: (:), -) = { - top-kwargs = top-kwargs + ( - align: top-kwargs.at("align", default: top + left).x + top, - ) - bottom-kwargs = bottom-kwargs + ( - align: bottom-kwargs.at("align", default: bottom + right).x + bottom, - ) - layout(size => { - let wrapfig(..args) = wrap-content(size: size, ..args) - wrapfig(top-fixed, ..top-kwargs)[ - #wrapfig(bottom-fixed, ..bottom-kwargs)[ - #body - ] - ] - }) -} diff --git a/resume.typ b/resume.typ index f94862c..2930ed1 100644 --- a/resume.typ +++ b/resume.typ @@ -1,3 +1,170 @@ -#import "lib/resume.typ": resume +#import "lib.typ": * -#resume.with()(yaml("content.yml")) +#let resume(contents, main: ("experience_by_type", "education"), sidebar: ("volunteering", "languages", "skills")) = { + show: style + set text(lang: lang) + + let date_formatting = { + if lang == "de" { + "[day]. [month repr:long] [year]" + } else { + "[month repr:long] [day], [year]" + } + } + set page( + paper: "a4", + margin: (x: 0.9cm, y: 1.3cm), + footer: [ + #set text( + fill: luma(200), + size: 8pt, + ) + #_columns_3[ + #smallcaps[#datetime.today().display(date_formatting)] + ][ + #smallcaps[#contents.about.fullname] + ][ + #context counter(page).display() + ] + ], + ) + + set par(justify: true) + + header(contents.about) + + let body = { + for item in main { + if item == "summary" and "summary" in contents { + section( + title: "", + { + contents.summary.at(lang) + }, + ) + } + + if item == "experience_by_type" and "experience" in contents { + let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang) + section(title: title)[] + by_experience_type(experience: contents.experience, type: contents.experience_types) + } + if item == "experience_by_client" and "experience" in contents { + let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang) + section(title: title)[] + by_client(experience: contents.experience) + } + if item == "experience" and "experience" in contents { + let title = (en: "Professional Experience", de: "Berufserfahrung").at(lang) + section(title: title, entries: contents.experience)[] + } + + if item == "education" and "education" in contents { + let title = (en: "Education", de: "Ausbildung").at(lang) + section(title: title, entries: contents.thesis + contents.education)[] + } + + if item == "volunteering" and "volunteering" in contents { + let title = (en: "Volunteer Work", de: "Ehrenamt").at(lang) + section(title: title, entries: contents.volunteering)[] + } + + if item == "skills" and "skills" in contents { + let title = (en: "Qualifications", de: "Qualifikationen").at(lang) + section( + title: title, + { + sidebar_entry(item: contents.skills) + }, + ) + } + + if item == "languages" and "languages" in contents { + let title = (en: "Languages", de: "Sprachen").at(lang) + section( + title: title, + { + sidebar_entry(item: contents.languages) + }, + ) + } + } + } + + let sidebar = { + for item in sidebar { + if item == "volunteering" and "volunteering" in contents { + let title = (en: "Volunteer Work", de: "Ehrenamt").at(lang) + [== #title] + for e in contents.at("volunteering") { + [ + - *#e.title.at(lang)* (#e.date.at(lang)) + #par(e.bullets.at(0).at(lang)) \ + ] + } + } + + if item == "languages" and "languages" in contents { + let title = (en: "Languages", de: "Sprachen").at(lang) + [== #title] + sidebar_entry(item: contents.languages, is_sidebar: true) + [\ ] + } + + if item == "skills" and "skills" in contents { + let title = (en: "Qualifications", de: "Kenntnisse").at(lang) + [== #title] + sidebar_entry(item: contents.skills, is_sidebar: true) + } + } + } + + let margin = 1pt + grid( + columns: (2fr, 1fr), + block( + outset: 0pt, + inset: (top: 0.4 * margin, right: 0pt, rest: margin), + stroke: none, + width: 100%, + { + set block(above: 10pt) + show heading.where(level: 1): it => style(s => { + let h = text(size: 18pt, upper(it)) + let dim = measure(h, s) + stack( + dir: ltr, + h, + place( + dy: 7pt, + dx: 10pt, + horizon + left, + line(stroke: accent-color, length: 100% - dim.width - 10pt), + ), + ) + }) + body + }, + ), + align( + right, + block( + width: 90%, + { + v(15pt) + set block(inset: (left: 5 * margin, right: 5 * margin)) + show heading: it => align(right, upper(it)) + set list(marker: "") + show list: it => { + set par(justify: false) + align(right, block(it)) + } + sidebar + v(15pt) + }, + ), + ), + ) +} + +#resume(yaml("content.yml"))