diff --git a/README.md b/README.md index 5dccec5..b5b878d 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,8 @@ Many companies and recruiters use [ATS](https://wikipedia.org/wiki/Applicant_tra You can use inline Markdown in the following properties to make text bold, italic, or link them to external pages: -* `summary` -* `highlights` +- `summary` +- `highlights` ### Open Graph Protocol @@ -58,6 +58,6 @@ Includes a dark mode, and uses the [`prefers-color-scheme`](https://developer.mo This theme makes no external connections, doesn't embed scripts, and is lightweight by design. Both HTML and PDF exports will be minimal. -## Preview +## Preview ![Two screenshots of the Class theme for JSON Resume side-by-side. On the left-side, we see the light mode variant, while on the right-side is the dark mode variant.](./assets/preview.png) diff --git a/index.js b/index.js index 40a83e5..48702fb 100644 --- a/index.js +++ b/index.js @@ -75,6 +75,112 @@ Handlebars.registerHelper('link', (body) => { return `${host}`; }); +/** + * Many users still have JSON Resumes written against old versions of the + * schema. We detect this and upgrade them to the latest version behind the + * scenes. + * + * Writes to the object directly. + * + * @param {any} resume + * @returns {boolean} + * If the JSON Resume was modified. (i.e. was using outdated property names) + * + * @see https://github.com/jsonresume/resume-schema/releases/tag/v0.0.17 + * @see https://github.com/jsonresume/resume-schema/releases/tag/v0.0.12 + */ +function upgradeOutdatedResume(resume) { + let upgraded = false; + + if (resume.bio && !resume.basics) { + resume.basics = resume.bio; + upgraded = true; + } + + if ((resume.basics?.firstName || resume.basics?.lastName) && !resume.basics.name) { + const names = []; + + if (resume.basics.firstName) { + names.push(resume.basics.firstName); + } + + if (resume.basics.lastName) { + names.push(resume.basics.lastName); + } + + resume.basics.name = names.join(' '); + upgraded = true; + } + + if (resume.basics?.picture && !resume.basics.image) { + resume.basics.image = resume.basics.picture; + upgraded = true; + } + + if (resume.basics?.website && !resume.basics.url) { + resume.basics.url = resume.basics.website; + upgraded = true; + } + + if (resume.basics?.state && !resume.basics?.region) { + resume.basics.region = resume.basics.state; + upgraded = true; + } + + if (Array.isArray(resume.work)) { + for (const work of resume.work) { + if (work?.company && !work.name) { + work.name = work.company; + upgraded = true; + } + + if (work?.website && !work.url) { + work.url = work.website; + upgraded = true; + } + } + } + + if (Array.isArray(resume.volunteer)) { + for (const volunteer of resume.volunteer) { + if (volunteer?.website && !volunteer.url) { + volunteer.url = volunteer.website; + upgraded = true; + } + } + } + + if (Array.isArray(resume.publications)) { + for (const publication of resume.publications) { + if (publication?.website && !publication.url) { + publication.url = publication.website; + upgraded = true; + } + } + } + + if (resume.hobbies && !resume.interests) { + resume.interests = resume.hobbies; + upgraded = true; + } + + if (Array.isArray(resume.languages)) { + for (const language of resume.languages) { + if (language?.name && !language.language) { + language.language = language.name; + upgraded = true; + } + + if (language?.level && !language.fluency) { + language.fluency = language.level; + upgraded = true; + } + } + } + + return upgraded; +} + /** * @param {any} resume * @returns {Promise} @@ -85,9 +191,12 @@ export async function render(resume) { fs.readFile(import.meta.dirname + '/resume.handlebars', 'utf-8'), ]); - const { profiles } = resume.basics; + if (upgradeOutdatedResume(resume)) { + console.warn('⚠️ Resume is written against an outdated version of the JSON Resume schema.\n⚠️ This will still work, but you should consider updating your resume.\n⚠️ See: https://jsonresume.org/schema'); + } - if (Array.isArray(profiles)) { + if (Array.isArray(resume.basics?.profiles)) { + const { profiles } = resume.basics; const xTwitter = profiles.find((profile) => { const name = profile.network.toLowerCase(); return name === 'x' || name === 'twitter'; @@ -114,7 +223,7 @@ export async function render(resume) { } } - const [ css, template ] = await loading; + const [css, template] = await loading; const html = Handlebars.compile(template)({ css, resume diff --git a/resume.handlebars b/resume.handlebars index 47725ef..2acc6d0 100644 --- a/resume.handlebars +++ b/resume.handlebars @@ -4,372 +4,360 @@ {{else}} {{/if}} - - {{#if resume.basics.name}} - {{resume.basics.name}} - CV - - {{else}} - CV - {{/if}} - {{#if resume.basics.summary}} - - - {{/if}} + + {{#if resume.basics.name}} + {{resume.basics.name}} - CV + + {{else}} + CV + {{/if}} - {{#if resume.basics.label}} - - {{/if}} + {{#if resume.basics.summary}} + + + {{/if}} - {{#if resume.basics.image}} - - - {{#if resume.basics.name}} - - {{/if}} - {{/if}} + {{#if resume.basics.label}} + + {{/if}} - {{#if resume.custom.xTwitterHandle}} - - {{/if}} + {{#if resume.basics.image}} + + + {{#if resume.basics.name}} + + {{/if}} + {{/if}} - - - - + {{#if resume.custom.xTwitterHandle}} + + {{/if}} + + + + + + + + - - - + {{#resume.basics}}
-

{{name}}

- {{#if label}} -

{{label}}

- {{/if}} +

{{name}}

+ {{#if label}} +

{{label}}

+ {{/if}}
-
-
-

Contact

-
- {{#if url}} -
- Website - - {{{link url}}} -
- {{else if website}} -
- Website - {{website}} -
- {{/if}} - {{#if email}} - - {{/if}} - {{#if phone}} -
- Phone - {{phone}} -
- {{/if}} -
-
- {{#if summary}} -
-

About

-

{{{markdown summary}}}

-
- {{/if}} - {{#if profiles.length}} -
- {{#each profiles}} -
- {{#if network}} - - {{network}} - - {{/if}} -
+
+
+

Contact

+
{{#if url}} - - {{#if username}} - {{username}} - {{else}} - {{url}} - {{/if}} - - {{else}} - {{username}} +
+ Website + + {{{link url}}} +
+ {{/if}} + {{#if email}} + + {{/if}} + {{#if phone}} +
+ Phone + {{phone}} +
{{/if}}
- {{/each}} -
- {{/if}} -
- {{/resume.basics}} - - {{#if resume.work.length}} -
-

Work Experience

- {{#each resume.work}} -
- {{#if name}} -

- {{name}} -

- {{else if company}} -

- {{company}} -

- {{/if}} - -
- {{#if position}} -
- {{position}} -
- {{/if}} - -
- {{#if startDate}} - {{{date startDate}}} - {{{date endDate}}} - {{/if}} -
- - {{#if url}} -
- {{{link url}}} -
- {{else if website}} - - {{/if}} -
- {{#if summary}}
+

About

{{{markdown summary}}}

{{/if}} - {{#if highlights.length}} - +
{{/if}} -
- {{/each}} - - {{/if}} + + {{/resume.basics}} - {{#if resume.volunteer.length}} -
-

Volunteer

- {{#each resume.volunteer}} -
- {{#if organization}} -

- {{organization}} -

- {{/if}} + {{#if resume.work.length}} +
+

Work Experience

+ {{#each resume.work}} +
+ {{#if name}} +

+ {{name}} +

+ {{/if}} -
- {{#if position}} -
- {{position}} -
- {{/if}} +
+ {{#if position}} +
+ {{position}} +
+ {{/if}} + +
+ {{#if startDate}} + {{{date startDate}}} - {{{date endDate}}} + {{/if}} +
+ + {{#if url}} +
+ {{{link url}}} +
+ {{/if}} +
-
- {{#if startDate}} - - {{{date startDate}}} - - - - {{{date endDate}}} - + {{#if summary}} +
+

{{{markdown summary}}}

+
+ {{/if}} + {{#if highlights.length}} +
    + {{#each highlights}} +
  • {{{markdown .}}}
  • + {{/each}} +
{{/if}}
+ {{/each}} +
+ {{/if}} - {{#if url}} -
- {{{link url}}} -
- {{else if website}} - - {{/if}} -
+ {{#if resume.volunteer.length}} +
+

Volunteer

+ {{#each resume.volunteer}} +
+ {{#if organization}} +

+ {{organization}} +

+ {{/if}} - {{#if summary}} -
-

{{{markdown summary}}}

-
- {{/if}} - {{#if highlights.length}} -
    - {{#each highlights}} -
  • {{{markdown .}}}
  • - {{/each}} -
- {{/if}} -
- {{/each}} -
- {{/if}} +
+ {{#if position}} +
+ {{position}} +
+ {{/if}} - {{#if resume.education.length}} -
-

Education

- {{#each resume.education}} -
+
+ {{#if startDate}} + + {{{date startDate}}} + + + - {{{date endDate}}} + + {{/if}} +
-
- {{#if institution}} -
- {{institution}} -
- {{/if}} + {{#if url}} +
+ {{{link url}}} +
+ {{/if}} +
-
- {{#if startDate}} - - {{{date startDate}}} - - - - {{{date endDate}}} - + {{#if summary}} +
+

{{{markdown summary}}}

+
+ {{/if}} + {{#if highlights.length}} +
    + {{#each highlights}} +
  • {{{markdown .}}}
  • + {{/each}} +
{{/if}}
+ {{/each}} +
+ {{/if}} - {{#if url}} -
- {{{link url}}} -
- {{/if}} -
+ {{#if resume.education.length}} +
+

Education

+ {{#each resume.education}} +
- {{#if qualification}} -
- {{qualification}} -
- {{/if}} +
+ {{#if institution}} +
+ {{institution}} +
+ {{/if}} - {{#if courses.length}} -
    - {{#each courses}} -
  • {{.}}
  • - {{/each}} -
- {{/if}} +
+ {{#if startDate}} + + {{{date startDate}}} + + + - {{{date endDate}}} + + {{/if}} +
- {{#if dissertation}} -
- Dissertation: {{dissertation}} -
- {{/if}} -
- {{/each}} -
- {{/if}} + {{#if url}} +
+ {{{link url}}} +
+ {{/if}} + + + {{#if qualification}} +
+ {{qualification}} +
+ {{/if}} + + {{#if courses.length}} + + {{/if}} - {{#if resume.skills.length}} -
-

Skills

- {{#each resume.skills}} -
- {{#if name}} -
- {{name}} + {{#if dissertation}} +
+ Dissertation: {{dissertation}} +
+ {{/if}}
- {{/if}} - {{#if level}} -
- {{level}} + {{/each}} +
+ {{/if}} + + {{#if resume.skills.length}} +
+

Skills

+ {{#each resume.skills}} +
+ {{#if name}} +
+ {{name}} +
+ {{/if}} + {{#if level}} +
+ {{level}} +
+ {{/if}} + {{#if keywords.length}} +
    + {{#each keywords}} +
  • {{.}}
  • + {{/each}} +
+ {{/if}}
- {{/if}} - {{#if keywords.length}} -
    - {{#each keywords}} -
  • {{.}}
  • - {{/each}} -
- {{/if}} - - {{/each}} -
- {{/if}} + {{/each}} +
+ {{/if}} - {{#if resume.languages.length}} -
-

Languages

- {{#each resume.languages}} -
- {{#if language}} -
{{language}}
- {{/if}} - {{#if fluency}} -
- {{fluency}} + {{#if resume.languages.length}} +
+

Languages

+ {{#each resume.languages}} +
+ {{#if language}} +
{{language}}
+ {{/if}} + {{#if fluency}} +
+ {{fluency}} +
+ {{/if}}
- {{/if}} -
- {{/each}} -
- {{/if}} + {{/each}} + + {{/if}} - {{#if resume.interests.length}} -
-

Interests

- {{#each resume.interests}} -
- {{#if name}} -
- {{name}} + {{#if resume.interests.length}} +
+

Interests

+ {{#each resume.interests}} +
+ {{#if name}} +
+ {{name}} +
+ {{/if}} + {{#if keywords.length}} +
    + {{#each keywords}} +
  • {{.}}
  • + {{/each}} +
+ {{/if}}
- {{/if}} - {{#if keywords.length}} -
    - {{#each keywords}} -
  • {{.}}
  • - {{/each}} -
- {{/if}} -
- {{/each}} -
- {{/if}} + {{/each}} + + {{/if}} - {{#if resume.references.length}} -
-

References

- {{#each resume.references}} -
- {{#if reference}} -
- {{reference}} -
- {{/if}} - {{#if name}} -
- — {{name}} + {{#if resume.references.length}} +
+

References

+ {{#each resume.references}} +
+ {{#if reference}} +
+ {{reference}} +
+ {{/if}} + {{#if name}} +
+ — {{name}} +
+ {{/if}}
- {{/if}} -
- {{/each}} -
- {{/if}} + {{/each}} + + {{/if}} - + + diff --git a/test/fixture.resume.json b/test/fixture.resume.json index 6c5603f..766868b 100644 --- a/test/fixture.resume.json +++ b/test/fixture.resume.json @@ -27,7 +27,7 @@ }, "work": [ { - "name": "JSON Resume", + "company": "JSON Resume", "location": "Remote", "position": "Open Sourcerer 🧙‍♂️", "url": "https://jsonresume.org", diff --git a/tsconfig.json b/tsconfig.json index 52ccfca..41d565f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,12 +3,15 @@ "noEmit": true, "module": "preserve", "target": "es2021", - "lib": ["es2021"], + "lib": [ + "es2021" + ], "allowJs": true, "checkJs": true, "strict": true, "resolveJsonModule": true, - "skipLibCheck": true + "skipLibCheck": true, + "noImplicitAny": false }, "exclude": [ ".yarn/",