Compare commits

..

10 commits

Author SHA1 Message Date
1e6c9059e8 feat(content): Add event logistics experience 2025-03-26 18:07:25 +01:00
434cb0717e fix(cv): Remove summary from CV 2025-03-19 16:21:22 +01:00
b061d853bc fix(cv): Fix CV creation 2025-03-19 14:17:28 +01:00
51b43f4d9c
ref(lib): Move all logic to lib
The *.typ files in root will only make use of the backing library,
invoking the bare mininum.
2025-03-19 13:50:14 +01:00
21d2010762
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.
2025-03-19 12:53:42 +01:00
eace89411b
fix(content): Remove duplicate Roskilde University 2025-03-19 12:53:42 +01:00
1afc51a857
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.
2025-03-19 12:53:41 +01:00
bb9606b2db
fix(content): Rephrase partial completion of BA 2025-03-19 12:53:41 +01:00
2970745b7d
ref(resume): Extract column creation from resume func
Extracted into their own functions called 'create_main' and
'create_sidebar' for now.
2025-03-19 12:53:40 +01:00
f404bf3c55
feat(sidebar): Make sidebar visible backgrounded
Just add a grey background for now.
2025-03-19 12:53:40 +01:00
7 changed files with 529 additions and 327 deletions

2
.gitignore vendored
View file

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

View file

@ -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
@ -143,7 +160,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
@ -355,7 +372,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

160
cv.typ
View file

@ -1,157 +1,7 @@
#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(
main: ("experience", "education", "volunteering", "skills", "languages"),
sidebar:()
)(yaml("content.yml"))

162
lib/resume.typ Normal file
View file

@ -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%),
)
}

340
lib/wrapit.typ Normal file
View file

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

View file

@ -1,170 +1,3 @@
#import "lib.typ": *
#import "lib/resume.typ": resume
#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"))
#resume.with()(yaml("content.yml"))