UNPKG

jsonresume-theme-relaxed

Version:
148 lines (143 loc) 4.38 kB
const {existsSync, readFileSync} = require('node:fs'); const htmls = require('@chronobserver/htmls'); const {name, version} = require('./package.json'); /** * @typedef {'beginner' | 'intermediate' | 'advanced' | 'master'} SkillLevel * @typedef {'entry' | 'junior' | 'mid' | 'senior'} SkillLevelAlt */ /** @type {Intl.DateTimeFormatOptions} */ const dateFmt = {month: 'long', year: 'numeric'}; const lib = { /** * Strip the scheme & path from a URL. * @param {string} url a URL * @returns {string} */ strip: (url) => /^(?:https?:\/\/)?([^/]+)/.exec(url)[1], /** * Check if an object contains any of certain keys. * @param {any} obj an object * @param {string[]} arr a list of keys * @returns {boolean} */ anyOf: (obj, arr) => Object.keys(obj).some(k => arr.includes(k)), /** * Get the URL of an icon from heroicons. * @param {'outline' | 'solid'} style the icon style * @param {string} icon the icon name * @returns {string} */ icon: (style, icon) => `https://cdn.statically.io/gh/tailwindlabs/heroicons/master/src/24/${style}/${icon}.svg`, /** * Get the URL of an icon from simple-icons. * @param {string} name the icon name * @returns {string} */ brand: (name) => { const id = name.toLowerCase().replace(' ', ''); switch (id) { case 'linkedin': // simple-icons/simple-icons#11380 return 'https://upload.wikimedia.org/wikipedia/commons/8/81/LinkedIn_icon.svg' default: return `https://cdn.simpleicons.org/${id}` } }, /** * Guess the mime type of an image by its extension. * @param {string} url the URL of the image. * @returns {string} */ mime: (url) => { switch (url.toLowerCase().split('.').at(-1)) { case 'png': return 'image/png'; case 'jpg': case 'jpeg': return 'image/jpeg' case 'webp': return 'image/webp'; case 'avif': return 'image/avif'; case 'ico': return 'image/vnd.microsoft.icon'; default: return 'image/png'; } }, /** * Map the skill level to a percentage. * @param {SkillLevel | SkillLevelAlt} level the skill level as a string * @returns {25 | 50 | 75 | 100} * @throws if the skill level is invalid */ skill: (level) => { switch (level.toLowerCase()) { case 'beginner': case 'entry': return 25; case 'intermediate': case 'junior': return 50; case 'advanced': case 'mid': return 75; case 'master': case 'senior': return 100; default: throw Error(`Unexpected skill level: ${level}`); } }, /** * Format `YYYY-MM-DD` date as `MMMM YYYY`. * @param {string} date a date in ISO 8601 format * @param {string?} locale an optional locale * @param {Intl.DateTimeFormatOptions} format an optional date format * @returns {string} */ format: (date, locale, format = {}) => new Intl.DateTimeFormat(locale || 'en', {...dateFmt, ...format}).format(Date.parse(date)), /** * Format location data as an address. * @param {ResumeSchema['basics']['location']} location the location data * @param {string?} locale an optional locale * @returns {string} */ address: (location, locale) => [ location.address, location.city, location.postalCode, location.region, location.countryCode && new Intl.DisplayNames(locale || 'en', {type: 'region'}).of(location.countryCode) ].filter(e => e).join(', '), /** * Return an object with translation strings. * @param {string} lang a language code * @returns {Object.<string, string>} */ i18n: (lang) => existsSync(`i18n/${lang}.json`) ? JSON.parse(readFileSync(`${__dirname}/i18n/${lang}.json`, 'utf-8')) : JSON.parse(readFileSync(`${__dirname}/i18n/en.json`, 'utf-8')) } module.exports = { /** * Render the resume. * @param {ResumeSchema} resume the JSON resume * @returns {string} */ render: (resume) => { const template = readFileSync(`${__dirname}/src/resume.htmls`, 'utf-8'); const style = readFileSync(`${__dirname}/src/resume.css`, 'utf-8'); return htmls(template)({resume, lib, style, name, version}); }, pdfViewport: { width: 1240, height: 1754, deviceScaleFactor: 2 }, pdfRenderOptions: { format: 'A4', mediaType: 'screen' } };