#import "@preview/fontawesome:0.5.0": * #import "@preview/linguify:0.4.2": * // const color #let color-darknight = rgb("#131A28") #let color-darkgray = rgb("#333333") #let color-gray = rgb("#5d5d5d") #let default-accent-color = rgb("#262F99") #let default-location-color = rgb("#333333") // const icons #let linkedin-icon = box(fa-icon("linkedin", fill: color-darknight)) #let github-icon = box(fa-icon("github", fill: color-darknight)) #let gitlab-icon = box(fa-icon("gitlab", fill: color-darknight)) #let bitbucket-icon = box(fa-icon("bitbucket", fill: color-darknight)) #let twitter-icon = box(fa-icon("twitter", fill: color-darknight)) #let google-scholar-icon = box(fa-icon("google-scholar", fill: color-darknight)) #let orcid-icon = box(fa-icon("orcid", fill: color-darknight)) #let phone-icon = box(fa-icon("square-phone", fill: color-darknight)) #let email-icon = box(fa-icon("envelope", fill: color-darknight)) #let birth-icon = box(fa-icon("cake", fill: color-darknight)) #let homepage-icon = box(fa-icon("home", fill: color-darknight)) #let website-icon = box(fa-icon("globe", fill: color-darknight)) #let address-icon = box(fa-icon("location-crosshairs", fill: color-darknight)) /// Helpers // Common helper functions #let __format_author_name(author, language) = { if language == "zh" or language == "ja" { str(author.firstname) + str(author.lastname) } else { str(author.firstname) + " " + str(author.lastname) } } #let __apply_smallcaps(content, use-smallcaps) = { if use-smallcaps { smallcaps(content) } else { content } } // layout utility #let __justify_align(left_body, right_body) = { block[ #left_body #box(width: 1fr)[ #align(right)[ #right_body ] ] ] } #let __justify_align_3(left_body, mid_body, right_body) = { block[ #box(width: 1fr)[ #align(left)[ #left_body ] ] #box(width: 1fr)[ #align(center)[ #mid_body ] ] #box(width: 1fr)[ #align(right)[ #right_body ] ] ] } #let __coverletter_footer( author, language, date, lang_data, use-smallcaps: true, ) = { set text(fill: gray, size: 8pt) __justify_align_3[ #__apply_smallcaps(date, use-smallcaps) ][ #__apply_smallcaps( { let name = __format_author_name(author, language) name + " · " + linguify("cover-letter", from: lang_data) }, use-smallcaps, ) ][ #context { counter(page).display() } ] } #let __resume_footer(author, language, lang_data, date, use-smallcaps: true) = { set text(fill: gray, size: 8pt) __justify_align_3[ #__apply_smallcaps(date, use-smallcaps) ][ #__apply_smallcaps( { let name = __format_author_name(author, language) name + " · " + linguify("resume", from: lang_data) }, use-smallcaps, ) ][ #context { counter(page).display() } ] } /// Show a link with an icon, specifically for Github projects /// *Example* /// #example(`resume.github-link("DeveloperPaul123/awesome-resume")`) /// - github-path (string): The path to the Github project (e.g. "DeveloperPaul123/awesome-resume") /// -> none #let github-link(github-path) = { set box(height: 11pt) align(right + horizon)[ #fa-icon("github", fill: color-darkgray) #link( "https://github.com/" + github-path, github-path, ) ] } /// Right section for the justified headers /// - body (content): The body of the right header #let secondary-right-header(body) = { set text(size: 11pt, weight: "medium") body } /// Right section of a tertiaty headers. /// - body (content): The body of the right header #let tertiary-right-header(body) = { set text(weight: "light", size: 9pt) body } /// Justified header that takes a primary section and a secondary section. The primary section is on the left and the secondary section is on the right. /// - primary (content): The primary section of the header /// - secondary (content): The secondary section of the header #let justified-header(primary, secondary) = { set block(above: 0.7em, below: 0.7em) pad[ #__justify_align[ == #primary ][ #secondary-right-header[#secondary] ] ] } /// Justified header that takes a primary section and a secondary section. The primary section is on the left and the secondary section is on the right. This is a smaller header compared to the `justified-header`. /// - primary (content): The primary section of the header /// - secondary (content): The secondary section of the header #let secondary-justified-header(primary, secondary) = { __justify_align[ === #primary ][ #tertiary-right-header[#secondary] ] } /// --- End of Helpers /// ---- Resume Template ---- /// Resume template that is inspired by the Awesome CV Latex template by posquit0. This template can loosely be considered a port of the original Latex template. /// /// The original template: https://github.com/posquit0/Awesome-CV /// /// - author (content): Structure that takes in all the author's information /// - profile-picture (image): The profile picture of the author. This will be cropped to a circle and should be square in nature. /// - date (string): The date the resume was created /// - accent-color (color): The accent color of the resume /// - colored-headers (boolean): Whether the headers should be colored or not /// - language (string): The language of the resume, defaults to "en". See lang.toml for available languages /// - use-smallcaps (boolean): Whether to use small caps formatting throughout the template /// - body (content): The body of the resume /// -> none #let resume( author: (:), profile-picture: image, date: datetime.today().display("[month repr:long] [day], [year]"), accent-color: default-accent-color, colored-headers: true, show-footer: true, language: "en", font: ("Source Sans Pro", "Source Sans 3"), header-font: "Roboto", paper-size: "a4", use-smallcaps: true, show-address-icon: false, body, ) = { if type(accent-color) == str { accent-color = rgb(accent-color) } let lang_data = toml("lang.toml") show: body => context { set document( author: author.firstname + " " + author.lastname, title: lflib._linguify("resume", lang: language, from: lang_data).ok, ) body } set text( font: font, lang: language, size: 11pt, fill: color-darkgray, fallback: true, ) set page( paper: paper-size, margin: (left: 15mm, right: 15mm, top: 10mm, bottom: 10mm), footer: if show-footer [#__resume_footer( author, language, lang_data, date, use-smallcaps: use-smallcaps, )] else [], footer-descent: 0pt, ) // set paragraph spacing set par(spacing: 0.75em, justify: true) set heading(numbering: none, outlined: false) show heading.where(level: 1): it => [ #set text(size: 16pt, weight: "regular") #set align(left) #set block(above: 1em) #let color = if colored-headers { accent-color } else { color-darkgray } #text[#strong[#text(color)[#it.body]]] #box(width: 1fr, line(length: 100%)) ] show heading.where(level: 2): it => { set text(color-darkgray, size: 12pt, style: "normal", weight: "bold") it.body } show heading.where(level: 3): it => { set text(size: 10pt, weight: "regular") __apply_smallcaps(it.body, use-smallcaps) } let name = { align(center)[ #pad(bottom: 5pt)[ #block[ #set text(size: 32pt, style: "normal", font: header-font) #if language == "zh" or language == "ja" [ #text(accent-color, weight: "thin")[#author.firstname]#text(weight: "bold")[#author.lastname] ] else [ #text(accent-color, weight: "thin")[#author.firstname] #text(weight: "bold")[#author.lastname] ] ] ] ] } let positions = { set text(accent-color, size: 9pt, weight: "regular") align(center)[ #__apply_smallcaps( author.positions.join(text[#" "#sym.dot.c#" "]), use-smallcaps, ) ] } let address = { set text(size: 9pt, weight: "regular") align(center)[ #if ("address" in author) [ #if show-address-icon [ #address-icon #box[#text(author.address)] ] else [ #text(author.address) ] ] ] } let contacts = { set box(height: 9pt) let separator = box(width: 5pt) align(center)[ #set text(size: 9pt, weight: "regular", style: "normal") #block[ #align(horizon)[ #if ("birth" in author) [ #birth-icon #box[#text(author.birth)] #separator ] #if ("phone" in author) [ #phone-icon #box[#text(author.phone)] #separator ] #if ("email" in author) [ #email-icon #box[#link("mailto:" + author.email)[#author.email]] ] #if ("homepage" in author) [ #separator #homepage-icon #box[#link(author.homepage)[#author.homepage]] ] #if ("github" in author) [ #separator #github-icon #box[#link("https://github.com/" + author.github)[#author.github]] ] #if ("gitlab" in author) [ #separator #gitlab-icon #box[#link("https://gitlab.com/" + author.gitlab)[#author.gitlab]] ] #if ("bitbucket" in author) [ #separator #bitbucket-icon #box[#link("https://bitbucket.org/" + author.bitbucket)[#author.bitbucket]] ] #if ("linkedin" in author) [ #separator #linkedin-icon #box[ #link("https://www.linkedin.com/in/" + author.linkedin)[#author.firstname #author.lastname] ] ] #if ("twitter" in author) [ #separator #twitter-icon #box[#link("https://twitter.com/" + author.twitter)[\@#author.twitter]] ] #if ("scholar" in author) [ #let fullname = str(author.firstname + " " + author.lastname) #separator #google-scholar-icon #box[#link("https://scholar.google.com/citations?user=" + author.scholar)[#fullname]] ] #if ("orcid" in author) [ #separator #orcid-icon #box[#link("https://orcid.org/" + author.orcid)[#author.orcid]] ] #if ("website" in author) [ #separator #website-icon #box[#link(author.website)[#author.website]] ] ] ] ] } if profile-picture != none { grid( columns: (100% - 4cm, 4cm), rows: 100pt, gutter: 10pt, [ #name #positions #address #contacts ], align(left + horizon)[ #block( clip: true, stroke: 0pt, radius: 2cm, width: 4cm, height: 4cm, profile-picture, ) ], ) } else { name positions address contacts } body } /// The base item for resume entries. /// This formats the item for the resume entries. Typically your body would be a bullet list of items. Could be your responsibilities at a company or your academic achievements in an educational background section. /// - body (content): The body of the resume entry #let resume-item(body) = { set text(size: 10pt, style: "normal", weight: "light", fill: color-darknight) set block(above: 0.75em, below: 1.25em) set par(leading: 0.65em) block(above: 0.5em)[ #body ] } /// The base item for resume entries. This formats the item for the resume entries. Typically your body would be a bullet list of items. Could be your responsibilities at a company or your academic achievements in an educational background section. /// - title (string): The title of the resume entry /// - location (string): The location of the resume entry /// - date (string): The date of the resume entry, this can be a range (e.g. "Jan 2020 - Dec 2020") /// - description (content): The body of the resume entry /// - title-link (string): The link to use for the title (can be none) /// - accent-color (color): Override the accent color of the resume-entry /// - location-color (color): Override the default color of the "location" for a resume entry. #let resume-entry( title: none, location: "", date: "", description: "", title-link: none, accent-color: default-accent-color, location-color: default-location-color, ) = { let title-content if type(title-link) == str { title-content = link(title-link)[#title] } else { title-content = title } block(above: 1em, below: 0.65em)[ #pad[ #justified-header(title-content, location) #if description != "" or date != "" [ #secondary-justified-header(description, date) ] ] ] } /// Show cumulative GPA. /// *Example:* /// #example(`resume.resume-gpa("3.5", "4.0")`) #let resume-gpa(numerator, denominator) = { set text(size: 12pt, style: "italic", weight: "light") text[Cumulative GPA: #box[#strong[#numerator] / #denominator]] } /// Show a certification in the resume. /// *Example:* /// #example(`resume.resume-certification("AWS Certified Solutions Architect - Associate", "Jan 2020")`) /// - certification (content): The certification /// - date (content): The date the certification was achieved #let resume-certification(certification, date) = { justified-header(certification, date) } /// Styling for resume skill categories. /// - category (string): The category #let resume-skill-category(category) = { align(left)[ #set text(hyphenate: false) == #category ] } /// Styling for resume skill values/items /// - values (array): The skills to display #let resume-skill-values(values) = { align(left)[ #set text(size: 11pt, style: "normal", weight: "light") // This is a list so join by comma (,) #values.join(", ") ] } /// Show a list of skills in the resume under a given category. /// - category (string): The category of the skills /// - items (list): The list of skills. This can be a list of strings but you can also emphasize certain skills by using the `strong` function. #let resume-skill-item(category, items) = { set block(below: 0.65em) set pad(top: 2pt) pad[ #grid( columns: (3fr, 8fr), gutter: 10pt, resume-skill-category(category), resume-skill-values(items), ) ] } /// Show a grid of skill lists with each row corresponding to a category of skills, followed by the skills themselves. The dictionary given to this function should have the skill categories as the dictionary keys and the values should be an array of values for the corresponding key. /// - categories_with_values (dictionary): key value pairs of skill categories and it's corresponding values (skills) #let resume-skill-grid(categories_with_values: (:)) = { set block(below: 1.25em) set pad(top: 2pt) pad[ #grid( columns: (auto, auto), gutter: 10pt, ..categories_with_values .pairs() .map(((key, value)) => ( resume-skill-category(key), resume-skill-values(value), )) .flatten() ) ] } /// ---- End of Resume Template ---- /// ---- Coverletter ---- #let default-closing(lang_data) = { align(bottom)[ #text(weight: "light", style: "italic")[ #linguify("attached", from: lang_data)#sym.colon #linguify( "curriculum-vitae", from: lang_data, )] ] } /// Cover letter template that is inspired by the Awesome CV Latex template by posquit0. This template can loosely be considered a port of the original Latex template. /// This coverletter template is designed to be used with the resume template. /// - author (content): Structure that takes in all the author's information. The following fields are required: firstname, lastname, positions. The following fields are used if available: email, phone, github, linkedin, orcid, address, website. /// - profile-picture (image): The profile picture of the author. This will be cropped to a circle and should be square in nature. /// - date (datetime): The date the cover letter was created. This will default to the current date. /// - accent-color (color): The accent color of the cover letter /// - language (string): The language of the cover letter, defaults to "en". See lang.toml for available languages /// - font (array): The font families of the cover letter /// - header-font (array): The font families of the cover letter header /// - show-footer (boolean): Whether to show the footer or not /// - closing (content): The closing of the cover letter. This defaults to "Attached Curriculum Vitae". You can set this to `none` to show the default closing or remove it completely. /// - use-smallcaps (boolean): Whether to use small caps formatting throughout the template /// - body (content): The body of the cover letter #let coverletter( author: (:), profile-picture: image, date: datetime.today().display("[month repr:long] [day], [year]"), accent-color: default-accent-color, language: "en", font: ("Source Sans Pro", "Source Sans 3"), header-font: "Roboto", show-footer: true, closing: none, paper-size: "a4", use-smallcaps: true, show-address-icon: false, body, ) = { if type(accent-color) == str { accent-color = rgb(accent-color) } // language data let lang_data = toml("lang.toml") if closing == none { closing = default-closing(lang_data) } show: body => context { set document( author: author.firstname + " " + author.lastname, title: lflib._linguify("cover-letter", lang: language, from: lang_data).ok, ) body } set text( font: font, lang: language, size: 11pt, fill: color-darkgray, fallback: true, ) set page( paper: paper-size, margin: (left: 15mm, right: 15mm, top: 10mm, bottom: 10mm), footer: if show-footer [#__coverletter_footer( author, language, date, lang_data, use-smallcaps: use-smallcaps, )] else [], footer-descent: 2mm, ) // set paragraph spacing set par(spacing: 0.75em, justify: true) set heading(numbering: none, outlined: false) show heading: it => [ #set block(above: 1em, below: 1em) #set text(size: 16pt, weight: "regular") #align(left)[ #text[#strong[#text(accent-color)[#it.body]]] #box(width: 1fr, line(length: 100%)) ] ] let name = { align(right)[ #pad(bottom: 5pt)[ #block[ #set text(size: 32pt, style: "normal", font: header-font) #if language == "zh" or language == "ja" [ #text(accent-color, weight: "thin")[#author.firstname]#text(weight: "bold")[#author.lastname] ] else [ #text(accent-color, weight: "thin")[#author.firstname] #text(weight: "bold")[#author.lastname] ] ] ] ] } let positions = { set text(accent-color, size: 9pt, weight: "regular") align(right)[ #__apply_smallcaps( author.positions.join(text[#" "#sym.dot.c#" "]), use-smallcaps, ) ] } let address = { set text(size: 9pt, weight: "bold", fill: color-gray) align(right)[ #if ("address" in author) [ #if show-address-icon [ #address-icon #box[#text(author.address)] ] else [ #text(author.address) ] ] ] } let contacts = { set box(height: 9pt) let separator = [ #box(sym.bar.v) ] let author_list = () if ("phone" in author) { author_list.push[ #phone-icon #box[#text(author.phone)] ] } if ("email" in author) { author_list.push[ #email-icon #box[#link("mailto:" + author.email)[#author.email]] ] } if ("github" in author) { author_list.push[ #github-icon #box[#link("https://github.com/" + author.github)[#author.github]] ] } if ("linkedin" in author) { author_list.push[ #linkedin-icon #box[ #link("https://www.linkedin.com/in/" + author.linkedin)[#author.firstname #author.lastname] ] ] } if ("orcid" in author) { author_list.push[ #orcid-icon #box[#link("https://orcid.org/" + author.orcid)[#author.orcid]] ] } if ("website" in author) { author_list.push[ #website-icon #box[#link(author.website)[#author.website]] ] } align(right)[ #set text(size: 8pt, weight: "light", style: "normal") #author_list.join(separator) ] } let letter-heading = { grid( columns: (1fr, 2fr), rows: 100pt, align(left + horizon)[ #block( clip: true, stroke: 0pt, radius: 2cm, width: 4cm, height: auto, profile-picture, ) ], [ #name #positions #address #contacts ], ) } let signature = { align(bottom)[ #pad(bottom: 2em)[ #text(weight: "light")[#linguify("sincerely", from: lang_data)#if ( language != "de" ) [#sym.comma]] \ #text(weight: "bold")[#author.firstname #author.lastname] \ \ ] ] } // actual content letter-heading body linebreak() signature closing } /// Cover letter heading that takes in the information for the hiring company and formats it properly. /// - entity-info (content): The information of the hiring entity including the company name, the target (who's attention to), street address, and city /// - date (date): The date the letter was written (defaults to the current date) #let hiring-entity-info( entity-info: (:), date: datetime.today().display("[month repr:long] [day], [year]"), use-smallcaps: true, ) = { set par(leading: 1em) pad(top: 1.5em, bottom: 1.5em)[ #__justify_align[ #text(weight: "bold", size: 12pt)[#entity-info.target] ][ #text(weight: "light", style: "italic", size: 9pt)[#date] ] #pad(top: 0.65em, bottom: 0.65em)[ #text(weight: "regular", fill: color-gray, size: 9pt)[ #__apply_smallcaps(entity-info.name, use-smallcaps) \ #entity-info.street-address \ #entity-info.city \ ] ] ] } /// Letter heading for a given job position and addressee. /// - job-position (string): The job position you are applying for /// - addressee (string): The person you are addressing the letter to /// - dear (string): optional field for redefining the "dear" variable #let letter-heading(job-position: "", addressee: "", dear: "") = { let lang_data = toml("lang.toml") // TODO: Make this adaptable to content underline(evade: false, stroke: 0.5pt, offset: 0.3em)[ #text(weight: "bold", size: 12pt)[#linguify( "letter-position-pretext", from: lang_data, ) #job-position] ] pad(top: 1em, bottom: 1em)[ #text(weight: "light", fill: color-gray)[ #if dear == "" [ #linguify("dear", from: lang_data) ] else [ #dear ] #addressee, ] ] } /// Cover letter content paragraph. This is the main content of the cover letter. /// - content (content): The content of the cover letter #let coverletter-content(content) = { pad(top: 1em, bottom: 1em)[ #set par(first-line-indent: 3em) #set text(weight: "light") #content ] } /// ---- End of Coverletter ----